【问题标题】:SetLength / Move - causes memory corruptionSetLength / Move - 导致内存损坏
【发布时间】:2012-12-20 23:22:58
【问题描述】:

今天我偶然发现了一个导致我的阵列损坏的问题。这是一个可重现的测试用例:

unit Unit40;

interface

type
  TVertex = record
    X, Y: Double;
  end;

  TEdge = record
    V1, V2: TVertex;
  end;
  TEdges = array of TEdge;

type
  TBoundryInfo = array of TEdges;

procedure MemoryCorrupt;

implementation

procedure MemoryCorrupt;
var
  BoundryInfo: TBoundryInfo;
  i, PointIndex, BoundryLength: Integer;
begin
  BoundryLength := 57;
  PointIndex := 0;
  SetLength(BoundryInfo, BoundryLength);
  for i := 0 to BoundryLength - 1 do
  begin
    if i <> 17 then
    begin
      SetLength(BoundryInfo[i], 1);
      BoundryInfo[i][0].V1.X := 1;
      BoundryInfo[i][0].V2.X := 1;
      BoundryInfo[i][0].V1.Y := 1;
      BoundryInfo[i][0].V2.Y := 1;
    end else
    begin
      SetLength(BoundryInfo[i], 2);
      BoundryInfo[i][0].V1.X := 1;
      BoundryInfo[i][0].V2.X := 1;
      BoundryInfo[i][0].V1.Y := 1;
      BoundryInfo[i][0].V2.Y := 1;
      BoundryInfo[i][1].V1.X := 1;
      BoundryInfo[i][1].V2.X := 1;
      BoundryInfo[i][1].V1.Y := 1;
      BoundryInfo[i][1].V2.Y := 1;
    end;
  end;
  BoundryLength := 9;
  SetLength(BoundryInfo, BoundryLength);
  Move(BoundryInfo[PointIndex+1], BoundryInfo[PointIndex],
    ((BoundryLength - 1) - PointIndex) * SizeOf(BoundryInfo[PointIndex]));
  Dec(BoundryLength);
  Finalize(BoundryInfo[BoundryLength]);
  SetLength(BoundryInfo, BoundryLength); //After this, arrays contains garbage
  BoundryInfo[0][0].V1.X := 3;
end;

end.

我猜最后一个SetLength 之后的内存损坏只是Move 使用不当的症状。 有人可以向我解释我做错了什么以及在这种情况下如何正确使用Move

在原始问题中,我在循环中从 BoundryInfo 中删除元素,这就是为什么我调用 Finalize(BoundryInfo[BoundryLength])

【问题讨论】:

  • 天啊!我成为水平滚动的受害者。如果看不到整个代码,则无法诊断代码问题。
  • 简单的解决方案是停止使用 Move。
  • @DavidHeffernan 我害怕这样的答案,这就是为什么我写了“......在这种情况下如何正确使用 Move?”
  • 嗯,根据我的经验,在这里移动通常是错误的方法。

标签: delphi memory-management multidimensional-array delphi-2009


【解决方案1】:

在您的代码中,

Move(BoundryInfo[PointIndex+1], BoundryInfo[PointIndex], 
  ((BoundryLength - 1) - PointIndex) * SizeOf(BoundryInfo[PointIndex]));

BoundryInfo[PointIndex+1]的指针复制到BoundryInfo[PointIndex]。这个指针是另一个动态数组,你必须注意引用计数。

即:

SetLength(BoundryInfo[PointIndex],0); // release memory
Move(BoundryInfo[PointIndex+1], BoundryInfo[PointIndex], 
  ((BoundryLength - 1) - PointIndex) * SizeOf(BoundryInfo[PointIndex]));
PPointerArray(BoundryInfo)^[BoundryLength-1] := nil; // avoid GPF

简而言之:

  • 完成move()期间将被覆盖的项目;
  • 将 nil 写入与 move() 重复的最新项目。

【讨论】:

  • @Wodzu - 仅当您了解数据的内存布局以及引用计数的工作原理时才使用此招式技巧。否则算了。
  • 我展示了与my article on how to delete items from a dynamic array中类似的代码。
  • Move 可以安全地使用交换习语:MyPointer := TheArray[Itemindex]; Move(TheArray[ItemIndex + 1], TheArray[ItemIndex], NumOfItems); Ponter(TheArray[High(TheArray)]) := MyPointer; Setlength(TheArray, High(TheArray));。这样,项目只被交换,没有消失,所以所有引用计数仍然有效,并且 Setlength 将在必要时负责完成。
【解决方案2】:

通过使用 Move 并颠覆动态数组引用计数机制,您只是为自己设置了一个陷阱。我强烈建议您坚持标准机制,让编译器担心细节。它每次都会让他们正确。

for i := 0 to high(BoundaryInfo)-1 do
  BoundaryInfo[i] := BoundaryInfo[i+1];
SetLength(BoundaryInfo, Length(BoundaryInfo)-1);

【讨论】:

  • 只有在删除第一项 ISTM 时才有效。 i 应该从 PointIndex 开始。但即使在这种情况下,如果您使用交换习语,也可以使用 Move(请参阅我上面的评论)。
  • @Rudy 好吧,您可以在要删除的项目处开始循环。在 Q 中的代码中,PointIndex=0。当然可以使用 Move,但为什么要这样做呢?为什么比作业好?
  • David 的代码更简单,更易读,即使 BoundaryInfo 记录变得更复杂,For 循环也可能继续工作,尽管 Move() 可能更快。但是当谈到更快与可读性时,我总是会选择更具可读性的,除非真的有必要。如果我是首席开发人员,我会坚持将代码重写为尽可能简单易读,并避免使用 Move 来复制整个数组。这种东西很脆弱。我个人会使用 Move 来复制单个记录、字节或字符缓冲区,而不是记录数组。怪癖太多。
  • @David:如果必须移动多个,移动比循环快得多。所以一般来说,如果我写一个像数组列表这样的东西,它可以从除顶部以外的任何地方删除项目,我会使用 Move。
  • @RudyVelthuis 只有在项目删除成为瓶颈时才有意义。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-02-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-06-03
  • 2018-03-07
  • 1970-01-01
相关资源
最近更新 更多