Set to nil – Release

I was checking the consequences of setting a pointer to “nil” and calling “release” on object, in Objective-C when developing an iPhone application.

Here is my understanding.

CASE 1

Person * p = [[Person alloc] init];

NSLog(@"%d", [p retainCount]); // 1

Here, a Person object instance is created in a memory address.
Pointer p points to the memory address of the Person instance.
As set by the memory management in Obj-C. calling [p retainCount] gives 1.

CASE 2

Person * p = [[Person alloc] init];
[p release];

NSLog(@"%d", [p retainCount]); // Error

When [p release] is called, the retain count of the Person instance decreases by one and becomes 0 (As set by Obj-C’s memory management). As the retain count reaches 0 the Person instance is destroyed and its memory address can be claimed by other.

How about the pointer p?

Pointer p still points to the memory address of the destroyed Person instance.
Thus, calling [p retainCount] gives error because p points to memory address of a destroyed object.

CASE 3

Person * p = [[Person alloc] init];
p = nil;

NSLog(@"%d", [p retainCount]); // 0

First, a Person instance is created in a memory address.
Second, by setting pointer p to nil, the following happens: the Person instance is destroyed from its memory address and then pointer p points to nothing.
Why is the Person instance destroyed? It is the way Obj-C is, that is when a pointer pointing to a memory address of an object is set to nil, the object is destroyed, unless case 5 below occurs.

Calling [p retainCount] returns 0. The 0 here is not because the retain count decreases by one, but because a call by a nil pointer is just ignored although no error occurs. We can see the evidence in case 4.

The memory management, however, will detect a leak, because setting p to nil does not decrease the Person instance’s retain counter, and hence the retain counter remains 1, although the Person instance is destroyed (But I can’t seem to get the evidence for this in Leaks instrument, as it does not show the leak).

CONCLUDING CASE 2 AND 3

Person * p = [[Person alloc] init];
[p release];
p = nil;

This concludes case 2 and 3.

Some people suggests to set pointer p to nil after [p release]. The purpose is to prevent error when there is an unexpected method call on p later on.

In dealloc method, however, setting p=nil is not necessary, because in dealloc method such pointer p will be cleaned.

CASE 4

Person * p = [[Person alloc] init];
[p retain];

NSLog(@"%d", [p retainCount]); // 2

p = nil;

NSLog(@"%d", [p retainCount]); // 0

First, a Person instance is created in a memory address.
Second, by calling [p retain], the retain count of the Person instance increases by one, so [p retainCount] gives 2.

Third, by setting p to nil, the Person instance is destroyed from its memory address, and then pointer p points to nothing. Calling [p retainCount] now gives 0 because the call is just ignored (as that in case 3).

The memory management still records that the retain count is 2, however, and so a memory leak will be reported, although the Person instance is destroyed.

CASE 5

Person * p1 = [[Person alloc] init];
Person * p2 = p1;
p1 = nil;

NSLog(@"%d", [p2 retainCount]); // 1

Pointer p2 points to the memory address of the Person instance, which is the same memory address that pointer p1 points to.

When p1 points to nil, here the Person instance is not destroyed. Why? Because the memory address of the Person instance is still pointed by pointer p2. Therefore calling [p2 retainCount] gives 1.

If the intention is to destroy the Person instance, we need to call “release”, as in case 6 below.

CASE 6

Person * p1 = [[Person alloc] init];
Person * p2 = p1;
[p1 release];

NSLog(@"%d", [p2 retainCount]); // Error

As in the previous scenario, pointer p2 points to the same memory address of the Person instance as that pointed by pointer p1.

Calling [p1 release] causes the Person instance to be destroyed, because the retain count of the Person instance reaches 0 after being decreased by one.

But pointer p2, unfortunately, points to memory address of the destroyed object. Therefore, unlike in case 5, calling [p2 retainCount] gives error.

CASE 7

Person * p1 = [[Person alloc] init];
Person * p2 = p1;
[p2 retain];
[p1 release];

NSLog(@"%d", [p1 retainCount]); // 1
NSLog(@"%d", [p2 retainCount]); // 1

Now, modified from case 6, [p2 retain] is called before [p1 release]. When [p2 retain] is called, the retain counter of the Person instance increases by one, and becomes 2.

Then when [p1 release] is called, the retain counter of the Person instance decreases by one, and becomes 1. Because it does not reach 0, the Person instance is not destroyed, so that at this point both [p1 retainCounter] and [p2 retainCounter] give 1.

CASE 8

Person * p1 = [[Person alloc] init];
People * people = [[NSMutableArray alloc] init];
[people addObject:p1];

NSLog(@"%d", [p1 retainCount]); // 2

[p1 release];

NSLog(@"%d", [p1 retainCount]); // 1

Person * p = (Person *) [people objectAtIndex:0];

NSLog(@"%d", [p retainCount]); // 1

This case seems familiar: a Person instance is created, passed into an array, and then it is released.

When an array adds an object, the object automatically calls “retain”, increasing the retain count of the object by one.

Calling “retain” before [p1 release] is similar to that in case 7, the retain count of the Person instance becomes 2.

When [p1 release] is called, the retain count decreases by one, and becomes 1.

As the evidence for the above, get the object from the array and let it be pointed by a pointer p, then call [p retainCount]. Consistently, it gives 1.

Slightly out of topic here, we may ask when the retain count will reach 0, because otherwise a memory leak occurs. The objectAtIndex automatically calls “autorelease” on the object, so that the object will be destroyed eventually by an autorelease pool.

CASE 9

Person * p1 = [[Person alloc] init];
People * people = [[NSMutableArray alloc] init];
[people addObject:p1];
p1 = nil;

Person * p = (Person *) [people objectAtIndex:0];

NSLog(@"%d", [p retainCount]); // 2

Now, what if p1 is set to nil, instead of released (i.e. calling [p1 release] as in case 8)?

The Person instance is now being used by the People array. Its memory address is being pointed by a pointer somewhere (implemented by array). Therefore, similar to case 4, when setting p1=nil the Person instance is not destroyed. In addition, the retain count of the Person instance does not decrease by one, but remains 2. Thus, [p retainCount] gives 2.

When eventually the object is released (because objectAtIndex automatically calls autorelease), its retain count will become 1. Thus, here a memory leak will occur, because of not only the retain count being more than 0 but also the Person instance being not destroyed.

CASE 10

Person * p1 = [[Person alloc] init];
People * people = [[NSMutableArray alloc] init];
[people addObject:p1];

NSLog(@"%d", [p1 retainCount]); // 2

[p1 release];

NSLog(@"%d", [p1 retainCount]); // 1

[people replaceObjectAtIndex:0 withObject:[NSNull null]];

NSLog(@"%d", [p1 retainCount]); // Error

This case is slightly out of topic here. It just checks replaceObjectAtIndex method on the array.

With replaceObjectAtIndex, the object is firstly removed from the location in the array before being replaced. When removing an object from an array, “release” is automatically called on the object. Therefore, the “release” decreases the Person instance’s retain count by one, making it reach 0, leading to the Person instance being destroyed.

Therefore the last [p1 retainCount] call gives error, because pointer p1 points to the memory address of the destroyed Person instance.

Conclusion
Only when the object, to which a pointer points, has been destroyed in its memory address, can the pointer be set to nil safely. Otherwise, there are consequences, as described in case 3, 4, 5 and 9.

Leave a Reply

Your email address will not be published. Required fields are marked *