5.5 什么是指针?
指针是 Object Pascal 语言的另一种基本数据类型。一些面向对象的语言在很大程度上隐藏了指针这种强大但危险的语言结构,而 Object Pascal 则允许程序员在需要时使用指针(一般情况下并不经常使用)。
那么什么是指针,这个名字又从何而来呢?与大多数其他数据类型不同的是,指针并不保存实际值,而是保存对变量的间接引用,而变量反过来又有一个值。一种更专业的表述方式是,指针类型定义了一个变量,该变量持有给定数据类型(或未定义类型)的另一个变量的内存地址。
注解:这是本书的一个高级章节,放在这里是因为指针是 Object Pascal 语言的一部分,应该成为任何开发人员的核心知识的一部分,尽管指针不是一个基础的主题,如果你是语言的新手,你可能想在第一次阅读本书时跳过这一部分。同样,你也有可能使用过没有(显式)指针的编程语言,所以这一小部分可能是一次有趣的阅读。
指针类型的定义不是基于特定的关键字,而是使用一个特殊符号—插入符(^)。例如,你可以用下面的声明定义一个表示指向 Integer 类型变量的指针的类型:
type
TPointerToInt = ^Integer;
一旦您定义了指针变量,可以使用 @ 运算符将另一个变量的地址进行赋值给指针变量:
var
P: ^Integer;
X: Integer;
begin
X := 10;
P := @X;
// 使用指针更改X的值
P^ := 20;
Show('X: ' + X.ToString);
Show('P^: ' + P^.ToString);
Show('P: ' + UIntPtr(P).ToHexString(8));
这段代码是 PointersTest 示例的一部分。在指针 P 指向变量 X 的情况下,您可以使用 P^ 指向变量的值,并读取或更改它。通过使用特殊类型 UIntPtr 将指针转换为数字,还可以显示指针本身的值,即 X 的内存地址(更多信息,请参阅下面的注释)。代码没有显示简单的数值,而是显示了十六进制表示法,这在内存地址中更为常见。这就是输出结果(指针地址可能取决于具体的编译):
X: 20
P^: 20
P: 0018FC18
警告:只有在限制为 2GB 的 32 位平台上,将指针转换为整数才是正确的。如果要使用更大的内存空间,就必须使用 Cardinal 类型。对于 64 位平台,更好的选择是使用 NativeUInt。不过,这种类型有一个别名,专门用于指针,称为 UIntPtr,它是这种情况下的最佳选择,因为使用它可以向开发人员和编译器清楚地表明你的意图。
为了清晰起见,让我总结一下。当您有一个指针 P 时:
- 通过直接使用指针(使用表达式 P)可以引用指针所指向的内存的地址。
- 通过解引用指针(使用表达式 P^)可以引用该内存位置的实际内容。
指针也可以不引用现有的内存位置,而是引用通过 New 过程在堆上动态分配的新的特定内存块。在这种情况下,当你不再需要指针访问的值时,你也必须通过调用 Dispose 来删除动态分配的内存。
注解:内存管理和堆的工作方式在第13章中有详细介绍。简而言之,堆是一块(很大的)内存区域,在堆中你不用按指定顺序分配和释放内存块。除了 New 和 Dispose 之外,还可以使用 GetMem 和 FreeMem,它们要求开发人员提供分配的大小(而在 New 和 Dispose 的情况下,编译器会自动确定分配的大小)。在编译时不知道分配大小的情况下、 GetMem 和 FreeMem 就变得非常方便。
下面是一段动态分配内存的代码片段:
var
P: ^Integer;
begin
// 初始化
New(P);
// 操作
P^ := 20;
Show(P^.ToString);
// 终止
Dispose(P);
如果在使用内存后为释放,程序最终可能会耗尽所有可用内存并崩溃。未释放不再需要的内存被称为内存泄漏。
警告:为了更安全起见,上面的代码实际上应该使用 try-finally 块,我决定在本书的这个部分不介绍这个主题,但我会在后面的第 9 章中介绍。
如果指针没有值,可以为其赋值为 nil。您可以通过直接相等测试或使用 Assigned 函数(如下所示)来测试指针是否为 nil,以确定它当前是否指向某个值。
这种测试经常使用,因为解引用无效指针会导致内存访问违规(根据操作系统的不同,影响也略有不同):
var
P: ^Integer;
begin
P := nil;
Show(P^.ToString);
您可以通过运行PointersTest
示例来查看代码的运行效果。您将看到的错误(在Windows上)应该类似于:
Access violation at address 0080B14E in module 'PointersTest.exe'. Read
of address 00000000.
使指针数据访问更安全的方法之一是添加 "指针不为空 "安全检查,例如下面的方法:
if P <> nil then
Show(P^.ToString);
正如我前面提到的,出于可读性的考虑,另一种通常更可取的方法是使用 Assigned 伪函数:
if Assigned(P) then
Writeln(P^.ToString);
注解: Assigned 并不是一个真正的函数,因为它是由编译器 "解析 "并生成正确代码的。此外,它还可以用于过程类型变量(或方法引用),而不实际调用它,只是检查它是否被赋值。
Object Pascal 还定义了指针数据类型,它表示无类型的指针(如 C 语言中的 void*)。如果使用无类型指针,则应使用 GetMem 而不是 New,并指出要分配的字节数,因为从类型本身无法推断出该值。每次分配的内存变量大小未定义时,都需要使用 GetMem 过程。
在 Object Pascal 中很少需要指针,这是该语言的一个有趣的优点。不过,指针功能在实现一些极其高效的底层函数和调用操作系统的 API 时,还是有所帮助的。无论如何,了解指针对于高级编程和全面理解 Delphi 的对象模型(在幕后使用指针(通常称为引用))都非常重要。
警告: 当一个变量持有指向第二个变量的指针,而第二个变量离开作用域或被释放(如果是动态分配)时,指针将指向未定义或持有其他数据的内存位置。这会导致很难发现的错误。