处理指向复杂记录的指针

时间:2011-08-05 21:15:48

标签: delphi pointers dispose

我有一些指向一些复杂记录的指针。有时当我尝试处理它们时,我得到无效的指针操作错误。我不确定我是否正确地创建和处理它们。 记录如下:

type
  PFILEDATA = ^TFILEDATA;
  TFILEDATA = record
    Description80: TFileType80;  // that's array[0..80] of WideChar
    pFullPath: PVeryLongPath;    // this is pointer to array of WideChar
    pNext: PFILEDATA;            // this is pointer to the next TFILEDATA record
  end;

据我所知,当我需要指向此类记录的指针时,我需要初始化指针和动态数组,如下所示:

function GimmeNewData(): PFILEDATA;
begin
  New(Result);
  New(Result^.pFullPath);
end;

现在处理这些记录的系列我写道:

procedure DisposeData(var pData: PFILEDATA);
var pNextData: PFILEDATA;
begin
  while pData^.pNext <> nil do begin
    pNextData := pData^.pNext;          // Save pointer to the next record
    Finalize(pData^.pFullPath);         // Free dynamic array
    Dispose(pData);                     // Free the record
    pData := pNextData;
  end;
  Finalize(pData^.pFullPath);
  Dispose(pData);
  pData := nil;
end;

当我在Delphi 2010 IDE中以调试模式(F9)运行我的程序时,会发生一些奇怪的事情。当我通过F8步进DisposeData代码时,似乎程序跳过Finalize(pData ^ .pFullPath)行并跳转到Dispose(pData)。这是正常的吗?此外,当执行Dispose(pData)时,显示指针内容的“局部变量”窗口不会更改。这是否意味着处置失败?

编辑: PVeryLongPath是:

type
  TVeryLongPath = array of WideChar;
  PVeryLongPath = ^TVeryLongPath;

EDIT2

所以我创建了两个TFILEDATA记录然后我处理它们。然后我再次创建相同的2条记录。由于某种原因,第二次记录中的这个时间pNext不是零。它指向第一记录。处理这个奇怪的事情会导致无效的指针操作错误。 我随机地在DisposeData过程中插入了pData ^ .pNext:= nil。 现在代码看起来像这样:

procedure DisposeData(var pData: PFILEDATA);
var pNextData: PFILEDATA;
begin
  while pData <> nil do begin
    pNextData := pData^.pNext;
    pData^.pNext := nil;          // <----
    Dispose(pData^.pFullPath);
    Dispose(pData);
    pData := pNextData;
  end;
end;

错误消失了。 我将尝试将PVeryLongPath更改为TVeryLongPath。

5 个答案:

答案 0 :(得分:5)

首先,如果你释放了某些内容,指针的内容就不会改变。这就是为什么你没有看到局部变量显示的变化。

编辑:声明 pFullPath TVeryLongPath这是一种引用类型,您不应该使用指向这种类型的指针。在这种情况下,New()不会按照您的想法执行。

如果你把它声明为UnicodeString,或者如果你的Delphi没有那个,那么可能会更好,WideString。

如果pFullPath被声明为动态“WideChar数组”,那么就不应该使用New()。对于动态数组,请使用SetLength()而不使用其他内容。 Dispose()将正确处理记录中的所有项目,所以只需:

New(Result);
SetLength(Result^.pFullPath, size_you_need);

以后:

Dispose(pData);

在普通代码中,您永远不必调用Finalize()。只要您将正确类型的指针传递给Dispose(),Dispose就会完成这一切。

FWIW,我会推荐thisthis我的文章。

答案 1 :(得分:4)

我建议您避免使用WideChar的动态数组,这对使用来说并不方便。如果你有Delphi 2009或更高版本,则使用string,或者对于早期的Delphi版本,使用WideString。这两个都是具有WideChar元素的动态字符串类型。您可以分配给他们,Delphi处理所有分配。

所以,假设你现在有以下记录:

TFILEDATA = record
  Description80: TFileType80;
  pFullPath: WideString; 
  pNext: PFILEDATA;          
end;

你可以大大简化事情。

function GimmeNewData(): PFILEDATA;
begin
  New(Result);
end;

procedure DisposeData(var pData: PFILEDATA);
var pNextData: PFILEDATA;
begin
  while pData <> nil do begin
    pNextData := pData^.pNext;          
    Dispose(pData);                     
    pData := pNextData;
  end;
end;

答案 2 :(得分:4)

您接受Serg的回答这一事实表明您的节点创建代码存在问题。您对该答案的评论证实了这一点。

我将此添加为新答案,因为对该问题的编辑会显着改变它。

链接列表代码应如下所示:

var
  Head: PNode=nil;
  //this may be a global variable, or better, a field in a class, 
  //in which case it would be initialized to nil on creation

function AddNode(var Head: PNode): PNode;
begin
  New(Result);
  Result.Next := Head;
  Head := Result;
end;

请注意,我们正在将节点添加到列表的头部。我们无需在任何地方将Next初始化为nil,因为我们始终将另一个节点指针指定给Next。这条规则很重要。

我把它写成一个返回新节点的函数。由于新节点始终添加在头部,因此这有点多余。因为你可以忽略函数返回值,所以它实际上不会造成任何伤害。

有时,您可能希望在添加新节点时初始化节点的内容。例如:

function AddNode(var Head: PNode; const Caption: string): PNode;
begin
  New(Result);
  Result.Caption := Caption;
  Result.Next := Head;
  Head := Result;
end;

我更喜欢这种方法。始终确保您的字段已初始化。如果零初始化对您没问题,那么您可以使用AllocMem来创建节点。

这是使用这种方法的更具体的例子:

type
  PNode = ^TNode;
  TNode = record
    Caption: string;
    Next: PNode;
  end;

procedure PopulateList(Items: TStrings);
var
  Item: string;
begin
  for Item in Items do
    AddNode(Head, Item);
end;

要销毁列表,代码运行如下:

procedure DestroyList(var Head: PNode);
var
  Next: PNode;
begin
  while Assigned(Head) do begin
    Next := Head.Next;
    Dispose(Head);
    Head := Next;
  end;
end;

您可以清楚地看到此方法只能在Headnil时返回。

如果将链接列表封装在类中,则可以将头指针作为类的成员,并避免传递它。

我想说的是手动内存分配代码很精细。在细节上很容易犯错误。在这样的情况下,将精巧的代码放在辅助函数或方法中是值得的,因此您只需要编写一次。链接列表是一个很好的例子,可以解决泛型问题。您可以编写一次内存管理代码,并将其重用于各种不同的节点类型。

答案 3 :(得分:1)

您还应将pNext字段初始化为nil - 如果没有它,您最终会获得访问权限违规。考虑到之前答案中已经说过的内容,您可以将代码更改为

type
  TFileType80 = array[0..80] of WideChar;

  PFILEDATA = ^TFILEDATA;
  TFILEDATA = record
    Description80: TFileType80;
    FullPath: WideString;
    pNext: PFILEDATA;
  end;

function GimmeNewData: PFILEDATA;
begin
  New(Result);
  Result^.pNext:= nil;
end;

答案 4 :(得分:1)

我认为你的大多数问题都是由New()给你的内存归零的假设引起的。我很确定(如果我错的话,我也肯定有人会纠正我),但德尔福并不能保证是这样的。这可以通过将代码更改为:

来解决
function GimmeNewData(): PFILEDATA;
begin
  New(Result);
  ZeroMemory(Result, SizeOf(TFILEDATA));
end;

您应该始终将为记录分配的内存归零,或者至少用其他相关内容填充所有字段。此行为与对象不同,对象保证在分配时归零。

希望这有帮助。