Delphi 指针, 静态数组, 动态数组

指针 : 指针是一个特殊的变量, 它里面存储的数值被解释成为内存里的一个地址.

(1) 指针对应着一个数据在内存中的地址, 得到了指针就可以自由地修改该数据.

(2) 一个指针变量仅仅是存储一个内存的地址, 为指针所指向的内容分配空间是程序员要干的工作.

(3) 如果一个指针没有指向任何数据, 它的值是nil, 它就被称为是零( nil )指针或空(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区别简单数据类型与复杂数据类型.

操作符 = <>

P = Q : 相同的地址

P <> Q : 不同的地址

操作符 + - 指针运算 Inc

指针位移的字节数是以指针的类型决定的.

- : 计算两个PChar指针的Offset

对于“+/-”加来说,只支持 PChar 和整数相加,

也就是说对于“+”和“-”操作来说,步长是一个字节

不过Delphi提供的一个函数Inc能实现除了 PChar 类型指针的自加寻址,

这个就是你的指针类型是什么类型,步长便是此指针类型的大小

比如:

var
  a:array[0..4] of Integer;
  p1: ^integer;
  b:array[0..1] of char;
  p2: ^char;
begin
  p1 := @a[0];
  Inc(p1);      // 当执行Inc(p1)时, 编译器会产生让p1前进sizeof(integer)步长的汇编代码, 之后p1将指向a[1]. 
  Inc(p1,2);    // Inc(p1,2)这句使得p1前进2个sizeof(integer)大小的步长, 之后p1将指向a[3]. 
  p2 := @b[0];
  Inc(p2);    //--初始指针向后偏移1个字节,指向b[1]
  Inc(p1,2);   //--指针向后偏移2*1个字节,指向b[3]
end;

操作符@ : 用来返回变量的内存中的存储地址, 或者是返回过程、函数和方法的入口地址。

(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. Addr是个返回值类型为Pointer的函数
  2. 默认编译条件下的@运算符,在对任何变量、运行期常量、函数取地址后,与 Addr 的结果一样,返回值的类型为Pointer;
  3. Pointer类型的变量,可以与任何指针、对象、类类型变量自由转换,并且编译器不会给出任何警告。
  4. 通过Project Options -> Compiler,勾上“Typed @ operator”,或是通过预编译指令$T+或$TYPEDADDRESS ON,来改变@运算符的行为
  5. I :Integer;PC : PChar := @I; // 出现一个编译错误,提示 Char 与 Integer 是不同的类型

操作符^

(1) 当它出现在类型定义的前面时如 ^typename 表示指向这种类型的指针;

(2) 当它出现在指针变量后边时, 如 point^ 返回指针指向的变量的值;

过程\函数\方法指针

(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.

动态内存分配

动态分配内存的函数是GetMem(),与之对应的释放函数为FreeMem()

(传统 Pascal中获取内存的函数是New()和 Dispose(),

但New()只能获得对象的单个实体的内存大小,无法取得连续的存放多个对象的内存块)。

var ptr, ptr2 : ^integer; 
    i : integer; 
begin 
    GetMem(ptr, sizeof(integer) * 20); 
    //这句等价于C的 ptr = (int*) malloc(sizeof(int) * 20); 
    ptr2 := ptr; //保留原始指针位置 
    for i := 0 to 19 do 
    begin 
        ptr^ := i; 
        Inc(ptr); 
    end; 
    FreeMem(ptr2); 
end; 

数组指针 : Delphi 中可以定义静态数组指针,然后指向你要操作的内存区块首地址

取数组在内存中的首地址,应该使用 @MyArr[0] 这种形式,无论它是在堆中还是在栈中,

这样的写法能保证不会错,而且简单明了,因为它直接给出了内存地址值。

静态数组的数组名等同于取第一个元素

type
  TPMyArr = ^TMyArr;
  TMyArr = array [ 0 .. 100 ] of Integer;
var
  I : Integer;
  PMyArr : TPMyArr;
  p : Pointer;
begin
  p := @I;
  PMyArr := @I;
  PMyArr[ 0 ] := 100;  
  TPMyArr( p )[ 0 ] := 200;
end;

动态数组的数组名是指向该数组的指针

var
  MyArray : array [ 1 .. 100 ] of Char;
  MyDynamicArray : array of Byte;
begin
  FillChar( MyArray, sizeof( MyArray ), 0 );
  SetLength( MyDynamicArray, 100 );
  FillChar( Pointer( MyDynamicArray )^, length( MyDynamicArray ), 0 );
end;

静态数组 static_array:array[0...99] of char;

@static_array 和 @static_array[0]等价, 都是静态数组中第一个元素的指针.

静态数组名 static_array 是一个变量, 等价于静态数组中第一个元素

static_array = static_array[0]

@static_array = @static_array[0]

// procedure FillChar(var X; Count: Integer; Value: Ordinal);

// This function does not perform any range checking.

// This method has an untyped parameter, which can lead to memory corruption.

// To avoid this problem, use SizeOf to find the number of bytes appropriate

// to fill for the data type of the X parameter.

// 无类型参数需要传递一个变量, 而不是变量的地址

//

// function SizeOf(var X): Integer;

//

FillChar( static_array, sizeof( static_array ), 0 );

FillChar( static_array[0], sizeof( static_array ), 0 );

动态数组 dynamic_array:array of char;

dynamic_array 和 @dynamic_array[0]等价, 都是静态数组中第一个元素的指针.

动态数组名dynamic_array是一个指针, @dynamic_array 动态数组指针的指针

dynamic_array = @dynamic_array[0]

dynamic_array^ = dynamic_array[0]

// procedure FillChar(var X; Count: Integer; Value: Ordinal);

FillChar( dynamic_array[0], length( dynamic_array ), 0 );

FillChar( dynamic_array^, length( dynamic_array ), 0 );

FillChar( Pointer(dynamic_array)^, length( dynamic_array ), 0 );

动态数组第一个元素前面的2个Integer分别是引用计数和数组当前长度。

但是只有动态数组长度不为0时,这8个字节才可以访问,否则Access Violation。

动态数组的索引都是从0开始,所以请注意避免索引越界错误。

当动态数组长度为0时,虽然不可以读取或设置其第一个元素的值,但是却可以获取该元素地址。

比如@DynArr[0]不会产生运行时错误,它返回nil

可以用SetLength改变动态数组的长度, 释放动态数组的内存可以用SetLength将其长度设置为0,或者直接赋nil值。

当然,如果你不手动释放,编译器会为你处理的。

但是如果你存储的是对象或缓冲区的指针,你必须负责释放他们。