变量的内存分配和释放


变量的内存分配和释放

从作用范围的角度,变量可以分为两大类:全局(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)如何人工分配和释放变量内存。