变量的内存分配和释放
变量的内存分配和释放
从作用范围的角度,变量可以分为两大类:全局(Global)变量和局部(Local)变量。
函数或者过程内部定义的变量为局部变量;其他的变量被声明在interface和implementation部分,称作全局变量,可以在整个单元中引用。对于在类中声明的变量,如果我将类比作单元,那么类中的变量可以比作单元中的全局变量;类的方法中声明的变量可以比作函数和过程中的局部变量。以下所讲的内存分配形式对于类中的变量也是适用的。
变量的内存分配形式有两种:自动和人工。所谓自动分配,是一个变量被声明后即被分配内存;而人工分配是指变量被声明后必须用代码显式地分配内存。
一般地,无论是全局变量还是局部变量,如果它是非指针类型的,则声明后被自动分配内存。如果是全局变量,还会被初始化为0:数值类型的为0,布尔类型的为False,字符的为”,等等。如果是局部变量,则不会被初始化,因此,它的值是不确定的(取决于别的程序对这块内存作过的操作)。对于非Variant和File类型的全局变量,还可以在声明时指定初始值(如:var I: Integer = 7;),但是对任何的局部变量都不可以这么做。
如果变量是指针类型的,则不会被自动分配内存。如果它是全局的,则其初始值是nil,表示还没有指向;如果是局部的,尽管没有被分配内存,但是会随机地指向一个地址,因此值不是nil。
为了验证上述内容,我需要举几个例子。
例1 验证全局变量的内存分配形式:
var
Global_Int: Integer; {声明非指针类型的全局变量Global_Int}
Global_P: PChar; {声明指针类型的全局变量Global_P}
procedure TForm1.Button1Click(Sender: TObject);
begin
if @Global_Int <> nil then
{用@Global_Int取得Global_Int的地址指针,然后和nil比较。此条件为True}
begin
ShowMessage('Global_Int已被分配内存');
ShowMessage(IntToStr(Global_Int)); {显示初始值0}
end;
if Global_P = nil then {此条件也为True}
begin
ShowMessage('Global_P还没被分配内存');
ShowMessage(Global_P);
{期望显示Global_P指向地址处保存的字符串,但因为指向不存在,所以返回空字符串''}
end;
end;
例2 验证局部变量的内存分配形式:
procedure TForm1.Button1Click(Sender: TObject);
var
Local_Int, Local_Int2: Integer;
OldAddr, NewAddr: Integer;
Local_P: PChar;
begin
OldAddr := Integer(@Local_Int); {取得Local_Int声明后的地址}
Local_Int := 7; {给Local_Int赋值}
NewAddr := Integer(@Local_Int); {取得Local_Int被赋值后的地址}
if OldAddr = NewAddr then {这个条件为True}
ShowMessage ('地址值没有变化,所以声明Local_Int时就分配内存');
ShowMessage(IntToStr(Local_Int2));{非指针局部变量的初始值不是0}
if Local_P <> nil then {这个条件也为True}
ShowMessage(Local_P); {显示的不是空字符串而很可能是乱码}
end;
综上所述,无论是全局变量还是局部变量,非指针类型的变量是被自动分配内存的,这个工作由编译器编译时完成,所以这种分配方式也被称作静态分配。静态分配时,全局变量的内存分配在全局变量区,局部变量分配在应用程序栈(Stack)。它们的内存释放工作也被自动管理,不需要程序员干预。
注意:应用程序可用的内存区分为三类:全局变量区(专门用来存放全局变量)、栈(Stack)和堆(Heap)。应用程序开始运行时,所有全局变量的内存被分配到全局变量区,应用程序结束时被释放;被分配在栈上的变量内存可被栈管理器自动释放;堆上的变量内存必须人工释放。
一般而言,对于指针类变量,则需要程序员使用一些代码来完成内存分配,通常,这样的分配方式也被称作动态分配。但是也有一些指针类型的变量是被动态分配内存的,它们是:
长字符串(AnsiString/String)、宽字符串(WideString)、动态数组(dynamic arrays)和接口(interface)。这些类型的变量也是被自动释放内存的。动态数组和接口也可以人工释放内存,方法是赋值nil。
完成具体分配的方法主要有下列一些:
(1)赋值。其原理是将变量指向一块已经存在的内存。这种方法适用于所有的指针类型。比如:
var
P1,P2: PChar;
begin
P1 := 'lxpbuaa'; {P1已经拥有一块内存}
P2 := P1; {将P2指向P1的内存,这样就间接完成了P2的内存分配}
end;
这种方法的本质是多个指针共享一块已有的内存,因此,通过操作任何一个指针都可以达到内存释放的目的。如果该块内存是被自动管理的,那么就不需要人工释放。
(2)对于类,则调用构造函数。比如:
var
Obj: TObject;
begin
Obj := TObject.Create; {调用构造函数创建对象,变量Obj指向该对象}
Obj.Free; {释放内存,Free内部调用析构函数Destroy;也可以使用
FreeAndNil(Obj);}
end;
对象变量所指向的内存是必须人工释放的,因为该块内存被分配在堆(Heap)而不是栈(Stack)上。释放对象内存时,应该调用析构函数(通常调用析构函数的包装方法Free或者全局过程FreeAndNil即可)。
(3)分配指定大小的内存块。主要用于创建缓冲区,一些函数和过程通过缓冲区返回一些执行结果。比如文件读写、流读写以及大量的API函数。我们看一个API函数使用缓冲区的例子,该函数可以取得计算机名字:
var
P: PChar;
Size: Cardinal;
begin
Size := MAX_COMPUTERNAME_LENGTH + 1;
GetMem(P, Size); {分配Size个字节的内存块(即缓冲区),并让P指向它}
GetComputerName(P, Size); {API函数GetComputerName将取得的计算机名放在P
中}
ShowMessage(P);
FreeMem(P); {释放缓冲区占用内存}
end;
在上例中,我使用了GetMem过程来创建内存块,并用FreeMem来释放它。总结一下,动态分配内存的函数和过程有以下一些,它们都是在堆中分配内存,所以必须释放:
(1)GetMem:
procedure GetMem(var P: Pointer; Size: Integer);
分配大小为Size字节的内存块,并让P指向它。
(2)AllocMem:
function AllocMem(Size: Cardinal): Pointer;
分配大小为Size字节的内存块并初始化为零,并返回地址指针。
如果希望在中途改变先前用GetMem或者AllocMem分配的内存大小,可以使用ReallocMem:
procedure ReallocMem(var P: Pointer; Size: Integer);
使用GetMem和AllocMem分配的内存都应该用FreeMem释放:
procedure FreeMem(var P: Pointer);
(3)New:
procedure New(var P: Pointer);
用New分配的内存块大小由参数P的类型确定,因此,不要使用它给无类型指针(即Pointer类型)变量分配内存。释放该内存块时使用Dispose:
procedure Dispose(var P: Pointer);
小结
本小节详细讨论了变量内存分配的两种形式。重点是:
(1)全局变量和局部变量的内存分配异同。
(2)变量内存分配和释放什么时候是自动/人工的。
(3)如何人工分配和释放变量内存。