【发布时间】:2009-02-27 15:17:00
【问题描述】:
我已经开发了一段时间了,到目前为止我还没有在我的开发中使用过指针。
那么指针有什么好处呢?应用程序运行速度更快还是使用的资源更少?
因为我确信指针很重要,你能“指点”我一些文章吗?基本但很好地开始在 Delphi 中使用指针? Google 给了我太多、太特别的结果。
【问题讨论】:
我已经开发了一段时间了,到目前为止我还没有在我的开发中使用过指针。
那么指针有什么好处呢?应用程序运行速度更快还是使用的资源更少?
因为我确信指针很重要,你能“指点”我一些文章吗?基本但很好地开始在 Delphi 中使用指针? Google 给了我太多、太特别的结果。
【问题讨论】:
指针是指向一块内存的变量。优点是:
Delphi 使用了很多隐藏指针。例如,如果您正在使用:
var
myClass : TMyClass;
begin
myClass := TMyClass.Create;
myClass 是一个指向对象的指针。
另一个例子是动态数组。这也是一个指针。
要更多地了解指针,您需要更多地了解内存。每条数据可以存在于不同的数据中。
例如全局变量:
unit X;
interface
var
MyVar: Integer;
在数据段中定义了一个全局变量。数据段是固定的。并且在程序的生命周期中,这些变量都是可用的。这意味着内存不能用于其他用途。
局部变量:
procedure Test;
var
MyVar: Integer;
堆栈上存在局部变量。这是一块用于管理的内存。它包含函数的参数(好的,有些放在寄存器中,但现在不重要了)。它包含返回地址,因此如果程序结束,cpu 知道返回到哪里。它包含函数中使用的局部变量。 局部变量只存在于函数的生命周期中。如果函数结束,则无法以可靠的方式访问局部变量。
堆变量:
procedure Test2;
var
MyClass: TMyClass;
begin
MyClass := TMyClass.Create;
变量 MyClass 是一个指针(它是在堆栈上定义的局部变量)。通过构造一个对象,您可以在堆上分配一块内存(不用于程序和堆栈的大块“其他”内存)。变量 MyClass 包含这块内存的地址。 堆变量在您释放它们之前一直存在。这意味着如果您退出函数 Test2 而不释放对象,则该对象仍然存在于堆中。但是您将无法访问它,因为地址(变量 MyClass)已消失。
最佳做法
几乎总是最好在同一级别分配和释放指针变量。
例如:
var
myClass: TMyClass;
begin
myClass := TMyClass.Create;
try
DoSomething(myClass);
DoSomeOtherthing(myClass);
finally
myClass.Free;
end;
end;
如果可以,尽量避免使用返回对象实例的函数。调用者是否需要处理对象永远无法确定。这会造成内存泄漏或崩溃。
【讨论】:
到目前为止,您已经得到了很多很好的答案,但是从您在使用长字符串、动态数组和对象引用时已经在处理指针的答案开始,您应该开始想知道为什么您会 使用指针,而不是长字符串、动态数组和对象引用。考虑到 Delphi 在很多情况下都能很好地隐藏指针,还有什么理由继续使用指针?
让我举两个在 Delphi 中使用指针的例子。如果您主要编写业务应用程序,您会发现这可能与您完全无关。但是,如果您需要使用任何标准 Delphi 单元未导入的 Windows 或第三方 API 函数,并且在(例如)JEDI 库中找不到导入单元,则它可能变得很重要。它可能是在字符串处理代码中实现必要的最后一点速度的关键。
指针可用于处理不同大小的数据类型(编译时未知)
考虑 Windows 位图数据类型。每个图像可以有不同的宽度和高度,并且有不同的格式,从黑白(每像素 1 位)到 2^4、2^8、2^16、2^24 甚至 2^32 灰度值或颜色.这意味着在编译时不知道位图将占用多少内存。
在 windows.pas 中有 TBitmapInfo 类型:
type
PBitmapInfo = ^TBitmapInfo;
tagBITMAPINFO = packed record
bmiHeader: TBitmapInfoHeader;
bmiColors: array[0..0] of TRGBQuad;
end;
TBitmapInfo = tagBITMAPINFO;
TRGBQuad 元素描述单个像素,但位图当然包含多个像素。因此,永远不会使用 TBitmapInfo 类型的局部变量,而总是使用指向它的指针:
var
BmpInfo: PBitmapInfo;
begin
// some other code determines width and height...
...
BmpInfo := AllocMem(SizeOf(TBitmapInfoHeader)
+ BmpWidth * BmpHeight * SizeOf(TRGBQuad));
...
end;
现在使用指针可以访问所有像素,即使 TBitmapInfo 确实只有一个像素。请注意,对于此类代码,您必须禁用范围检查。
这样的事情当然也可以用 TMemoryStream 类来处理,它基本上是一个友好的封装指向内存块的指针。
当然,简单地创建一个TBitmap 并指定它的宽度、高度和像素格式要容易得多。再说一遍,Delphi VCL 确实消除了大多数需要指针的情况。
指向字符的指针可以用来加速字符串操作
与大多数微优化一样,这是仅在极端情况下使用的东西,在您分析并发现使用字符串的代码会消耗大量时间之后。
字符串的一个很好的特性是它们是引用计数的。复制它们不会复制它们占用的内存,只会增加引用计数。只有当代码尝试修改引用计数大于 1 的字符串时,才会复制内存,创建引用计数为 1 的字符串,然后可以安全地对其进行修改。
字符串的一个不太好的属性是它们是引用计数的。每个可能修改字符串的操作都必须确保引用计数为 1,否则对字符串的修改将是危险的。替换字符串中的字符就是这样的修改。为了确保引用计数为 1,每当写入字符串中的字符时,编译器都会添加对 UniqueString() 的调用。现在在循环中写入字符串的 n 个字符将导致 UniqueString() 被调用 n 次,即使在第一次之后是确保引用计数为 1。这意味着基本上 n - 1 次调用 UniqueString() 是不必要的。
使用指向字符的指针是加速涉及循环的字符串操作的常用方法。想象一下,您希望(出于显示目的)用一个小点替换字符串中的所有空格。使用调试器的 CPU 视图并比较为此代码执行的代码
procedure MakeSpacesVisible(const AValue: AnsiString): AnsiString;
var
i: integer;
begin
Result := AValue;
for i := 1 to Length(Result) do begin
if Result[i] = ' ' then
Result[i] := $B7;
end;
end;
使用此代码
procedure MakeSpacesVisible(const AValue: AnsiString): AnsiString;
var
P: PAnsiChar;
begin
Result := AValue;
P := PAnsiChar(Result);
while P[0] <> #0 do begin
if P[0] = ' ' then
P[0] := $B7;
Inc(P);
end;
end;
在第二个函数中,只有一次调用 UniqueString(),此时第一个字符串字符的地址被分配给 char 指针。
【讨论】:
您可能已经使用过指针,但您只是不知道。类变量是指针,字符串是指针,动态数组是指针,Delphi 只是为你隐藏了它。当您执行 API 调用(将字符串转换为 PChar)时,您会看到它们,但即使这样,Delphi 也可以隐藏很多。
有关指针的优势,请参阅 Gamecats 的答案。
在About.com article 中,您可以找到 Delphi 中指针的基本解释。
【讨论】:
某些数据结构需要指针。最简单的例子是链表。这种结构的优点是您可以重组元素而无需在内存中移动它们。例如,您可以拥有一个大型复杂对象的链表并快速交换其中任意两个,因为您确实必须调整两个指针而不是移动这些对象。
这适用于许多语言,包括 Object Pascal (Delphi)。
【讨论】: