参考资料
“浅谈Object Pascal的指针”
“Delphi 指针入门”
基本知识
指针
指针是一个特殊的变量,它里面存储的数值被解释成为内存里的一个地址。
注意:
(1)、指针对应着一个数据在内存中的地址,得到了指针就可以*地修改该数据。
(2)、一个指针变量仅仅是存储一个内存的地址,为指针所指向的内容分配空间是程序员要干的工作。
(3)、如果一个指针没有指向任何数据,它的值是n i l,它就被称为是零( n i l )指针或空(null) 指针。
(4)、要访问一个指针所指向的内容,在指针变量名字的后面跟上^运算符。这种方法称为对指针取内容。
(5)、指针的指针就是用来存放指针所在的内存地址的。
要搞清一个指针,需要了解以下内容:
(1)、指针的类型。
(2)、指针所指向的类型。
(3)、指针的值(即指针所指向的内存区)。
(4)、指针本身所占据的内存区。
指针大小
指针是一个无符号整数(unsigned int),它是一个以当前系统寻址范围为取值范围的整数。指针类型变量本身要占内存,占用内存的大小与机器硬件、操作系统以及编译器都有关系,最直接的关系就是编译器,现在的编译器大都是32位(4B)的,即使你的机器和操作系统都是是64位的,所以指针类型变量一般占用4B空间(也就是可表示2^32次方的地址空间)。
指针类型
一个指针变量指示了内存的位置。PASCAL通用指针类型的名称是Pointer,Pointer有时又被称为无类型指针,因为它只指向内存地址,但编译器并不管指针所指向的数据,所以建议你在大部分情况下用有类型的指针。
任何对象、结构、变量什么的,在内存里面,实质上就是字节流,那么很有可能某一个字节数组array of char的内容刚好和某一个对象的字节流内容一样,如果一个pointer指向的内容为上述字节内容,你能区分是那个对象还是array of char的字节数组?Pointer 作为一个无类型指针,可以指向任何元素。强制转换时,Delphi 并不知道 Pointer 指向的数据是什么类型。例如TObject(p) 就是一种强制转换,用于告诉编译器指针指向的数据是TObject的实例。也就是说:编译器不能确定类型转换的正确性!你必须自己负责该指针的实际指向!总得说来,无类型指针的转换是没有安全性的,你必须明确指针的用途才可以使用。
有类型指针在你的应用程序的Type部分用^ (或Pointer)运算符声明。对于有类型指针来说,编译器能准确地跟踪指针所指内容的数据类型,这样用指针变量,编译器就能跟踪正在进行的工作。
对于编译器来说,指针的类型可以用来标明地址所指向内存区域的大小、所指向的数据类型(整型、对象、方法),以及进行指针运算时指针偏移的长度。对于有类型指针的操作最终反映到编译器的可执行代码中,如果不参考可执行代码,单凭一个内存地址,我们无法判断地址的实际用途。
数据类型
从数据类型的简单或复杂程序上来说分为二种:
(1)、简单数据类型
对于简单数据类型(或叫基础数据类型),编译器分配内存时直接为其分配存储单元!
(2)、复杂数据类型
对于复杂数据类型,编译器分配内存时先在栈中分配一个指针,该指针占4个存储单元,然后再在堆中创建对象的实体!并且使栈上的指针关联到堆中对象实体的首址处!
说明:可以使用SizeOf区别简单数据类型与复杂数据类型。
指针运算
指针位移的字节数是以指针的类型决定的。
比如:
procedure TForm1.Button2Click(Sender: TObject);
var
a:array[0..4] of Integer;
p1: ^integer;
b:array[0..1] of char;
p2: ^char;
begin
p1 := @a[0];
Inc(p1); //--初始指针向后偏移4个字节,指向a[1]
Inc(p1,2); //--指针向后偏移2*4个字节,指向a[3]
//---
p2 := @b[0];
Inc(p2); //--初始指针向后偏移1个字节,指向b[1]
Inc(p1,2); //--指针向后偏移2*1个字节,指向b[3]
end;
当执行Inc(p1)时,编译器会产生让p1前进sizeof(integer)步长的汇编代码,之后p1将指向a[1]。Inc(p1,2)这句使得p1前进2个sizeof(integer)大小的步长,之后p1将指向a[3]。
操作符@
操作符@用来返回变量的内存中的存储地址, 或者是返回过程、函数和方法的入口地址。
说明:
(1)、对于变量X,@X返回的是X的地址。如果编译选项{$T-}没有打开,则返回的是一个通用的指针,如果编译选项打开,则返回的是X的类型对应的指针。
(2)、对于例程F (过程/函数),@F返回的是F的入口点,@F的类型是一个指针。
(3)、当@用在类的方法中时,则方法的名称必须有类名,例如@TMyclass.Dosomething指针指向TMyclass的dosomething方法。
(4)、对于过程变量P, @P把P转换成一个包含地址的无类型的指针变量(即@P等价于无类型指针P),此时可以把一个无类型的指针值赋给过程变量P。获得一个过程变量的内存地址使用@@。例如,@@P返回P的地址。
操作符^
操作符^有两个目标:
(1)、当它出现在类型定义的前面时如 ^typename 表示指向这种类型的指针;
(2)、当它出现在指针变量后边时,如 point^ 返回指针指向的变量的值;
记录指针
Record记录变量和记录指针的用法有类似的地方,不过记录变量不是由指针来实现的,所以大家在某些时候要小心记录指针的使用。
过程/函数/方法指针
说明:
(1)、当一个过程变量在赋值语句的左边时,编译器期望一个过程值在赋值语句的右边。这种赋值使得左边的变量可以指向右边定义的过程或者函数入口点。换句话说,可以通过该变量来引用声明的过程或者函数,可以直接使用参数的引用。
(2)、在赋值语句“过程变量 := 过程值”中,左边变量的类型决定了右边的过程或者方法指针解释。
(3)、无论何时一个过程变量(procedural variable)出现在一个表达式中,它表示调用所指向的函数或者过程。
(4)、任何过程变量可以赋成nil,表示指证什么也不指向。但是试图调用一个nil值的过程变量导致一个错误,为了测试一个过程变量是否可以赋值,用标准的赋值函数Assigned。如下所示:
if Assigned(OnClick) then OnClick(X);
结构:
(1)、过程/函数指针是一个32位的指针。
(2)、方法指针是指向对象方法的指针,实现上是一个对象指针加上一个过程/函数指针组成。方法指针的结构如下:
TMethod = record
Code: Pointer;//指向过程/函数的指针
Data: Pointer;//指向对象的指针
end;
类指针
DELPHI中的类是一个指针,这个指针指向类在内存中所占据的一块空间。类指针与VMT指针地址相同。
类的类型
在DELPHI中我们用TObject、TComponent等等标识符表示类,它们在DELPHI的内部实现为各自的VMT数据。而用class of保留字定义的类的类型,实际就是指向相关类的指针。例如“ TClass = class of TObject”,从概念上说,TClass是TObject类的类型,实际上TClass就是指向TObject的指针类型!
有了类的类型,我们就可以将类赋值给使用“类的类型”声明的变量(即类变量),从而将类作为变量来使用。
对象指针
DELPHI中的对象是一个指针,这个指针指向该对象在内存中所占据的一块空间。我们可以试着用sizeof函数获取对象的大小,结果是4字节,这正是一个32位指针的大小。而对象的真正大小应该用MyObject.InstanceSize获得。
注意:
对象虽然是用指针实现,具有指针所有特性,但毕竟定义为Class,所以只能用“对象.成员、对象.方法”的方式来调用,而不能用“对象^.成员、对象^.方法”的方式来调用。
例如我们最熟悉的Form,当我们调用Form1.Edit1.Text时,其实Form1、Edit1都是指针,但是只能用Form1.Edit1.Text来调用,而不能用Form1^.Edit^.Text。