small medium large xlarge

Generic-user-small
12 Jun 2011, 13:49
panos (2 posts)

Trying to slightly modify the chapter 8.2 code as an exercise i aimed to convert the displayed array into a string. Did it fairly easy but the probems lies elsewhere. Simply returning the sideBySide string seemed in my mind to create a memory leak (although instruments says otherwise). When i try to autorelease sideBySide i get a “double free” runtime error. Breaking with malloc_error_break and rerunning points me to NSApplicationMain. The code seems to autorelease sideBySide by itself but to be honest i don’t quiet understand why. Same goes if i try to autorelease finalString. Can I please have a brief explanation of what i don’t quiet understand about memory management? Am I thinking something wrong?

edit: It’s built in Xcode4 if it is of any relevance

#import "NotifyingClass.h"

@implementation NotifyingClass

- (IBAction)displaySomeText:(id)sender
{
    NSString *finalString = [[NSString alloc] initWithString:@""];
    
    NSString *firstObject = @"Milk ";
    NSString *secondObject = @"Eggs ";
    NSString *thirdObject = @"Butter ";
	
    NSArray *shoppingListArray = [NSArray arrayWithObjects:firstObject, secondObject, thirdObject, nil];
    
    NSLog(@"%i\n",[finalString retainCount]);
   
    finalString = [finalString stringByAppendingString:
                         [self stringFinal:shoppingListArray] ];
	
    [textView insertText:finalString];
    
    
}

- (NSString *) stringFinal:(NSArray *)array
{
    int i;
    int counter;

    NSString *sideBySide = [[NSString alloc] initWithString:@"shoppingListArray: "]; 
    
    counter = [array count];
    
    for (i=0; i<counter ; i++) 
    {
        sideBySide = [sideBySide stringByAppendingString:
                      [array objectAtIndex:i] ];
    }
    
    sideBySide = [sideBySide stringByAppendingString:@"\n"];
    
    return [sideBySide autorelease] ;
}


@end
Generic-user-small
12 Jun 2011, 14:53
Tim Isted (105 posts)

Hi there, excellent adding exercises!

But, firstly a quick note about debugging this sort of thing… The retainCount method is rarely of help. An object may be retained and released “behind the scenes” by the Cocoa framework, so the retainCount method won’t just return you a count relevant to when you’ve retained and released something. In an ideal world, the method wouldn’t be made public at all as it’s a common source of confusion about solving this sort of problem.

The problem you’ve run into is in the stringFinal: method.

You start by setting the value of sideBySide to an alloc] init]ed NSString object.

Each pass through the array, you then call the stringByAppendingString: method to append a string. This is where the problem lies—the stringByAppendingString: method returns a different string object, setting the sideBySide variable to point to a different object.

The first time through the loop, the original sideBySide string will be leaked, and replaced with the a new string object containing the characters from the original plus the appended letters.

Because the stringByAppendingString: method creates and returns a new string, but you haven’t used alloc] init], you can be sure it’s an autoreleased string by the time you get hold of it.

This means that when you return the final value of the sideBySide variable (the last object returned by the final call of stringByAppendingString:), but call autorelease on it, you’re autoreleasing a variable that’s already been autoreleased, which means it will be released twice, hence the memory error.

Rewrite the method like this:

- (NSString *) stringFinal:(NSArray *)array
{
    int i;
    int counter;

    NSString *sideBySide = @"shoppingListArray: "; // no need to allocate an NSString object, just use the @"" notation

    counter = [array count];

    for (i=0; i<counter; i++) 
    {
        sideBySide = [sideBySide stringByAppendingString:
                      [array objectAtIndex:i] ];
    }

    sideBySide = [sideBySide stringByAppendingString:@"\n"];

    return sideBySide; // sideBySide has already been autoreleased by the stringByAppendingString: method
}

The confusion over stringByAppendingString: is understandable. Remember that a plain NSString is immutable, or unchangeable. To add something it, you have to create a new string object.

To accomplish what I think you might have intended, you’d need to use an NSMutableString, on which you can then call appendString: or appendFormat: to append the characters to the same object.

One more thing… This is purely personal preference, but I dislike leaving variables unassigned, so if I were writing something similar, I would declare them only when used, like this:

- (NSString *) stringFinal:(NSArray *)array
{
    NSString *sideBySide = @"shoppingListArray: ";

    int counter = [array count];

    for (int i=0; i<counter; i++) 
    {
        sideBySide = [sideBySide stringByAppendingString:
                      [array objectAtIndex:i] ];
    }

    sideBySide = [sideBySide stringByAppendingString:@"\n"];

    return sideBySide;
}

There’s also a handy method provided by NSArray, which simplifies the code but accomplishes the same thing (actually adds a comma between each item as well):

- (NSString *) stringFinal:(NSArray *)array
{
    NSString *sideBySide = @"shoppingListArray: ";

    NSString *componentsString = [array componentsJoinedByString:@", "];

    sideBySide = [sideBySide stringByAppendingString:componentsString];

    return sideBySide;
}
Generic-user-small
12 Jun 2011, 16:25
panos (2 posts)

First off, i appreciate the fast response thank you very much. This thing has been bothering me for a while but by reading your explanation really cleared things up. Seems i’ve been chasing my tail for the past couple of days and for no good reason since i’ve already read about mutability. What i failed to see was the autorelease mechanism inside stringByAppendingString (factory methods right?).

Second, yes the retainCount was added at the last moment as a desperate attempt to check what’s going on. In fact retainCount at this line of code returns the uint_max value. Apple states “For objects that never get released (that is, their release method does nothing), this method should return UINT_MAX, as defined in . ".

Third, that’s the final code.

#import "NotifyingClass.h"

@implementation NotifyingClass

- (IBAction)displaySomeText:(id)sender
{
    NSString *firstObject = @"Milk ";
	NSString *secondObject = @"Eggs ";
	NSString *thirdObject = @"Butter ";
	
	NSArray *shoppingListArray = [NSArray arrayWithObjects:
			firstObject, secondObject, thirdObject, nil];
       
    [textView insertText:[self stringFinal:shoppingListArray]];
}

- (NSString *) stringFinal:(NSArray *)array
{
    NSString *sideBySide = @"shoppingListArray: "; 
        
    NSString *commaSep = [array componentsJoinedByString:@", "];
    
    sideBySide = [sideBySide stringByAppendingString:
                  [commaSep stringByAppendingString:@"\n"]];
    
    return sideBySide;
}

@end

Guess i’m ok i’ll double check this on instruments. Will write the mutable version too just for practice.

One last note:

“The first time through the loop, the original sideBySide string will be leaked, and replaced with the a new string object containing the characters from the original plus the appended letters.”

i’m pretty sure i’m running instruments leak check the right way but it didn’t catch it. For instance when i run wonderful number without autorelease i get feedback. Is this type of leak so cryptic or am I doing something wrong?

Generic-user-small
12 Jun 2011, 17:04
Tim Isted (105 posts)

Ah - yes, the leak isn’t really a leak… :)

When you use @"", you’re using a string literal. The value is held within the program code itself, rather than in the traditional area of memory used for objects allocated at runtime. Normally I would advise not worrying about this while you’re learning Cocoa and Objective-C, but this does have several side effects, so you may find the following of interest…

Firstly, if you use the same @"string" in multiple places throughout your code, you’ll find that an optimization occurs such you always use the same object:

NSString *firstString = @"Test";
NSLog(@"The address of the first string is: %p", firstString);

NSString *secondString = @"Test";
NSLog(@"The address of the second string is: %p", secondString);

If I run this, I get this output in the log:

The address of the first string is: 0x100002738
The address of the second string is: 0x100002738

Both objects have the same address, which means they are the same object. This means that you can in fact do this:

NSString *firstString = @"Test";
NSString *secondString = @"Test";
    
if( firstString == secondString ) {
    NSLog(@"They're the same object!");
}

when you would normally need to do this:

NSString *firstString = @"Test";
NSString *secondString = @"Test";

if( [firstString isEqualToString:secondString] ) {
    NSLog(@"The strings are the same!");
}

What’s more, if you do this:

NSString *firstString = @"Test";
NSLog(@"The address of the first string is: %p", firstString);

NSString *secondString = [[NSString alloc] initWithString:@"Test"];
NSLog(@"The address of the second string is: %p", secondString);

you’ll find that another optimization occurs. You’re asking to allocate and initialize a string object using a string literal, so rather than waste memory by allocating a new object to hold that string literal, you’re just returned the string literal. Once again, I get this:

The address of the first string is: 0x100002738
The address of the second string is: 0x100002738

So, the alloc] init] you’re using above doesn’t really allocate and initialize a new object, it uses a string literal.

You can’t release a string literal (hence the value of the retainCount you were seeing), and so the leak isn’t detected. But, there’s no guarantee that Apple won’t change the way this behaves in the future, such that alloc] init]ing using a string literal might return a different object, so I would still balance the call with a release, even though technically that will have no effect.

For example, this code:

NSString *firstString = @"Test";

[firstString release];
[firstString release];
[firstString release];

NSLog(@"The string is: %@", firstString);

makes me shudder with horror, but you’ll find it “works” because the release calls have no effect.

By contrast, this code:

NSString *firstString = [NSString stringWithFormat:@"%i", 42 * 17];

[firstString release];
[firstString release];
[firstString release];

NSLog(@"The result is: %@", firstString);

will crash. If you use stringWithFormat: to create a string object using some value at runtime, the object must obey traditional rules of memory management.

Even if not releasing an alloc] init]ed string appears to work, doesn’t crash, and doesn’t show up as a leak, I’d worry about a “code smell,” and call release anyway. The rules of memory management are clear :)

You must be logged in to comment