I guess you might not know what the term “dangling pointer” means, but if you have ever done some more complex programming in Delphi (where you do not only put controls on a form and handle a few events – rather you create and use objects at run-time) you might have experienced weird Access Violations when you tried accessing properties of an object you though is “Assigned” (or not nil).
The term dangling pointer refers to a pointer that is not nil, but does not point to valid data (i.e. points to an invalid memory address).
Still confused? You’re not alone!
Take a look at the following piece of code:
TMyClass = class (TObject);
…
var
mc_1 : TMyClass;
begin
mc_1 := TMyClass.Create;
mc_1.Free;
// is Assigned(mc_1) = TRUE?
end;
You might expect that Assigned(mc_1) would return false, but this is not the case.
When you create an instance of a class, an object, the memory required is allocated on the heap. Calling mc_1.Free will free the memory occupied for the object referenced by mc_1. The variable mc_1 is not cleared out, it is not nil (i.e. is still Assigned) but the memory it points to is no longer valid.
The Assigned function tests if the pointer to the memory (the variable reference) is nil. In case of variable references, Assigned(mc_1) is equal to “mc_1
nil”. mc_1 after the call to mc_1.Free is a dangling pointer.
Certainly, since the above is “your” code, you know you can no longer operate on mc_1 after you called Free on it. In case you want to be sure Assigned(mc_1) would return false, you would also need to do mc_1 := nil, or call FreeAndNil(mc_1). Case closed.
How about the next example:
var
mc_1, mc_2 : TMyClass;
….
begin
mc_1 := TMyClass.Create;
mc_2 := mc_1;
mc_1.Free;
mc_1 := nil;
//mc_2 usable ?
end;
Now, what is the state (i.e. can you use it) of the variable mc_2 after we have freed mc_1?
What if at some stage in the life of your application you need to do something with mc_2? Would you expect that Assigned(mc_2) would return false? Should mc_2 = nil be true?
The truth is that a call to “Assigned(mc_2)” as well as “mc_2
nil” will return true.
The problem here is that freeing mc_1 marks the memory block used by mc_1 as available – but all the mc_1 data is still there. Since mc_2 points to the same memory location where mc_1 used to be, the data is still there, Assigned(mc_2) returns true.
Therefore, mc_2 is a dangling pointer. But this one is a more complex one to handle! There is no way for you to check if mc_2 is usable!
The way how Delphi memory manager works, after freeing an object, is to mark the memory space occupied by the object as available – not clear it out in any way.
As you create new instances of your objects (various types), memory manager will reuse blocks of memory from the heap. Your dangling pointer points to the same address but this time, quite possibly, to a different object (different class instantiated at the same memory block).
What’s worst, if the new object is of the same type you could end up with this dangling pointer working perfectly! Consider where a newly instantiated mc_3 gets the memory space where mc_1 used to be. Using mc_2 you are not aware that mc_1 is dead, mc_2 works normally but operates on the mc_3 instance. Catastrophe.
Of course, it is more likely that some other object (of some other type) will reuse the memory space of mc_1 and you could start experiencing weird Access Violations when using mc_2. And still, Assigned(mc_2) returns true.
While there’s a lot discussion on this problem on the Internet, and while there are some posted solutions to check if a pointer points to a valid object instance, I am strong in my belief that there’s no way to check this with 100% certainty (am not considering third-party memory managers).
Is there a solution? No Or, yes: be careful when you have such code constructs.
Rewrite the usage of mc_2 in a way that you somehow signal to mc_2 when mc_1 is freed (no longer really available).
In one of my programs I have the next setup (I guess common): an object has a property that is a list of objects of the same type. Something like:
//PSEUDO CODE:
TMyClass = class
public
property Relatives : TObjectList<TMyClass>;
end;
Now consider the following:
procedure TDanglingPointersForm.FormCreate(Sender: TObject);
var
mc : TMyClass;
begin
mc1 := TMyClass.Create;
mc2 := TMyClass.Create;
mc3 := TMyClass.Create;
mc1.Relatives.Add(mc2);
mc1.Relatives.Add(mc3);
for mc in mc1.Relatives do
begin
//there are 2 that are ok (mc2 and mc3)
end;
FreeAndNil(mc2);
for mc in mc1.Relatives do
begin
//mc that is mc2 will not be usable here !!
//how to remove it from mc1.Relatives when mc2.Free ?
end;
end;
Of course, in a real world application all the code is not contained in one procedure!
My solution was to signal (using events) to the list when an object is freed – so that the list can remove it from itself.
Here’s the full source:
TMyClass = class;
TMyClassList = class(TObjectList<TMyClass>)
procedure Destroyed(sender : TObject);
function Add(myObject : TMyClass): Integer;
end;
TMyClass = class
private
fRelatives : TMyClassList;
fOnDestroyed: TNotifyEvent;
function GetRelatives: TMyClassList;
public
destructor Destroy; override;
property OnDestroyed : TNotifyEvent read fOnDestroyed write fOnDestroyed;
property Relatives : TMyClassList read GetRelatives;
end;
The TMyClass has a lazy-instantiated property “Relatives” which is actually a list of (other) TMyClass instances. Relatives does not own the instances added, they will be freed by other means.
Here’s the implementation part:
{ TMyClass }
function TMyClass.GetRelatives: TMyClassList;
begin
if fRelatives = nil then fRelatives := TMyClassList.Create(false);
result:= fRelatives;
end;
destructor TMyClass.Destroy;
begin
if Assigned(fOnDestroyed) then OnDestroyed(self);
fRelatives.Free;
inherited;
end;
{ TMyClassList }
function TMyClassList.Add(myObject: TMyClass): Integer;
begin
myObject.OnDestroyed := Destroyed;
result := inherited Add(myObject);
end;
procedure TMyClassList.Destroyed(sender: TObject);
begin
if self.Contains(TMyClass(Sender)) then self.Remove(TMyClass(Sender));
end;
This time:
procedure TDanglingPointersForm.FormCreate(Sender: TObject);
var
mc : TMyClass;
begin
mc1 := TMyClass.Create;
mc2 := TMyClass.Create;
mc3 := TMyClass.Create;
mc1.Relatives.Add(mc2);
mc1.Relatives.Add(mc3);
for mc in mc1.Relatives do
begin
//there are 2 that are ok (mc2 and mc3)
end;
FreeAndNil(mc2);
for mc in mc1.Relatives do
begin
//only mc3 is here, as expected !
end;
end;
Am eager to hear your ideas and solutions to this problem…