Delphi 中的内存管理遵循两条简单规则:必须销毁自己创建和分配的每个对象和内存块,而且必须销毁每个对象并释放每个内存块仅一次。Delphi支持三个动态元素内存管理(即不在堆栈和全局内存区域),下面分别介绍:
-
每次创建对象时,用完就应该释放它。如果未能做到这一点,那么该对象使用的内存不会释放给其他对象(一直占用内存),直到程序终止运行了。
-
·创建组件时,可以指定组件的所有者(拥有者),并传递组件构造函数的所有者。所有者组件(通常是表单或数据模块)负责销毁其拥有的所有对象。换句话说,当您释放表单或数据模块时(所有者组件),它会释放它所包含的所有组件。所以,如果你创建一个组件并给它一个所有者,你就不需担心自己再去销毁(所有者会自动销毁)。
-
为字符串、动态数组和引用的对象分配内存时通过接口变量,Delphi 自动释放引用超出范围时的内存。你不需要释放一个字符串:当它变得不可访问时,它的内存会被自动释放。
销毁自己创建的对象
比较简单的情况下,假如我们在桌面编译器上创建临时对象,就必须销毁它。任何非临时对象都应该有所有者,所有者要么可以收集,或者可以访问一些数据结构,总之所有者有理由和条件在适当的时候销毁它。用于创建和销毁临时对象的代码通常封装在try finally代码块中,这样即使出现问题,对象也会被最后销毁:
MyObj := TMyClass.Create;
try
MyObj.DoSomething;
finally
MyObj.Free;
end;
另一种常见的情况是,一个对象被另一个对象使用,这将成为其所有者:
constructor TMyOwner.Create;
begin
FSubObj := TSubObject.Create;
end;
destructor TMyOnwer.Destroy;
begin
FSubObj.Free;
end;
还有一些常见的更复杂的场景,就是对象延迟创建(在需要时才创建)或者在所有者销毁前就销毁。例如:
function TMyOwner.GetSubObject: TSubObject
begin
if not Assigned (FSubObj) then
FSubObj := TSubObject.Create;
Result := FSubObj;
end;
destructor TMyOnwer.Destroy;
begin
FSubObj.Free;
end;
请注意,在释放对象之前,不需要测试对象是否已指定,因为这正是Free所做的,下一节中我们将讨论。
对象只需要销毁一次
另一个问题是,如果调用一个对象的析构函数两次,就会得到一个错误。析构函数(destructor)的作用是用来释放对象内存的。可以在析构函数中增加一些我们自己的代码,以便对象在销毁前执行我们的程序。
Destroy是TObject(对象)类的虚拟析构函数。大多数时候,需要我们书写自己的清理代码来覆盖此虚拟方法。永远不要定义新的析构函数,因为对象是通过调用Free方法销毁,该方法调Destroy virtual析构函数(但是可以重写版本)。
正如前面提到的,Free只是TObject类的一种方法,由其他类继承的。Free方法在调用销毁虚拟析构函数(Destroy) 会检查当前对象(Self)是否为nil。
注释:
您可能有疑问,如果对象引用为nil,为什么可以安全地调用Free,但不能调用摧毁(Destroy)原因?因为Free是给定内存位置的已知方法,而虚拟函数销毁(Destroy)是在运行时通过查看对象的类型来确定的,这是一个比较复杂的过程。如果对象都不存在了,再来销毁(Destroy),那就危险了(会出现错误)。
代码示例如下:
procedure TObject.Free;
begin
if Self <> nil then
Destroy;
end;
接下来,我们可以将讨论 Assigned 函数功能。当我们将指针传递到这个函数只测试指针是否为你nil。以下两种状态是相同的,至少在大多数情况下,这些条件是相当的:
if Assigned (MyObj) then
...
if MyObj <> nil then
...
注意,这些语句只测试指针是否为nil;并没有检查它是否是有效的指针。如果您编写以下代码:
MyObj.Free;
if MyObj <> nil then
MyObj.DoSomething;
测试结果将为True,那么将调用对象的方法(DoSomething)。需要注意的是,Free 只是释放了对象,并没有把对象代表指针(MyObj)设置成nil,所以执行将会出错。
自动设置对象指针(引用)为nil是不可能的。因为同一个对象可能有好几个引用,delphi并不跟踪这些引用。同时,在对象的方法中(例如Free)可以操作对象,但是并不知道有多少对象引用,也就是我们调用方法的对象内存地址。
换句话说,在Free方法或类的任何其他方法中,我们知道对象(self)的内存地址,但我们不知道对象的内存位置引用对象的变量,例如MyObj。所以,Free方法不能影响MyObj变量。
(结论:Free 方法可以释放对象,但是不能影响对象自身指针位置)
当然,我们可以通过调用外部函数,把对象作为参数传递,这样函数将能修改参数原有的值。这个典型的例子就是 FreeAndNil 过程函数。代码如下:
procedure FreeAndNil( const [ref] Obj: TObject); inline;
var
Temp: TObject;
begin
Temp := Obj;
TObject(Pointer(@Obj)^) := nil;
Temp.Free;
end;
在 Delphi 10.4 以前,FreeAndNil 参数就是一个普通的指针,你传递的参数可能是一个普通的指针、或者是一个接口引用、或者是一个不兼容的数据结构,这样调用 FreeAndNil很容易出现错误并且很难查找bug。但是10.4之后,FreeAndNil 的参数限制为一个常量的对象,只能是对象,就没有问题了。
备注:
不少 Delphi 专家会争辩说,FreeAndNil 永远不应该被使用。因为一个对象变量的可见引用应该匹配他的生命周期。 如果一个对象拥有另外一个对象,同时在destructor中释放了拥有的对象,就没有必要设置被引用对象为nil了,因为再也不需要了。同时如果使用 try finally 代码块释放了对象,也没必要将其设置成nil了。
另外,除了Free方法之外,TObject还有一个DisposeOf方法,该方法是Delphi几年来对ARC支持的遗留。目前DisposeOf方法只是调用Free方法。
总结一下这些清理对象内存操作的使用,这里有几个指导方针:
- 总是调用对象的Free来销毁对象,而不是调用destroy destructor。
-
使用FreeAndNil,或者在调用Free后将对象引用设置为nil,除非这个对象在Free之后再也不用了(自己需要明确知道不用了)。