【问题标题】:Advice on "Invalid Pointer Operation" when using complex records使用复杂记录时“无效指针操作”的建议
【发布时间】:2011-05-29 04:13:01
【问题描述】:

环境:德尔福 2007

我倾向于经常使用复杂记录,因为它们提供了类的几乎所有优点,但处理起来更简单。

Anyhoo,我刚刚实现的一个特别复杂的记录是破坏内存(后来导致“无效指针操作”错误)。

这是内存垃圾代码的示例:

sSignature := gProfiles.Profile[_stPrimary].Signature.Formatted(True);

在我第二次调用它时,我得到“无效的指针操作”

如果我这样称呼它就可以了:

  AProfile    := gProfiles.Profile[_stPrimary];
  ASignature  := AProfile.Signature;
  sSignature  := ASignature.Formatted(True);

后台代码:

  gProfiles: TProfiles;

  TProfiles = Record
  private
    FPrimaryProfileID: Integer;
    FCachedProfile: TProfile;
    ...
  public
    < much code removed >

    property Profile[ProfileType: TProfileType]: TProfile Read GetProfile;
  end;


  function TProfiles.GetProfile(ProfileType: TProfileType): TProfile;
  begin        
    case ProfileType of
      _stPrimary        : Result := ProfileByID(FPrimaryProfileID);
      ...
    end;
  end;

  function TProfiles.ProfileByID(iID: Integer): TProfile;
  begin
    <snip>
    if LoadProfileOfID(iID, FCachedProfile)  then
    begin
      Result := FCachedProfile;
    end
    else
    ...
  end;


  TProfile = Record
  private     
    ...
  public
    ...
    Signature: TSignature;
    ...
  end;


  TSignature = Record
  private               
  public
    PlainTextFormat : string;
    HTMLFormat      : string;

    // The text to insert into a message when using this profile
    function Formatted(bHTML: boolean): string;
  end;

  function TSignature.Formatted(bHTML: boolean): string;
  begin
    if bHTML then
      result := HTMLFormat
    else
      result := PlainTextFormat;
    < SNIP MUCH CODE >
  end;

好的,所以我在记录中的记录中有一个记录,这接近于 Inception 级别的混乱,我首先承认这不是一个真正的好模型。显然,我将不得不对其进行重组。我想从各位大师那里更好地理解它为什么会破坏内存(与创建然后释放的字符串对象有关......)这样我就可以避免将来犯这些错误。

谢谢

【问题讨论】:

  • 记录类型的属性?我很确定那根本不应该编译。 “无效的指针操作”意味着你正在释放一些已经被释放的东西,或者释放的东西从来没有引用过动态分配的内存。
  • @Rob:为什么不应该编译?尽管无法分配给记录成员,但仍允许记录属性 (`Obj.Record.Member := 'blub')
  • +1 记录属性是完全合理的,例如使用运算符重载的复数记录呢?

标签: delphi delphi-2007 records


【解决方案1】:

从您提供的代码中看不到发生损坏的地方,因此这里有一些建议。尝试不同的字段链接组合,看看是否可以重现它。

AProfile := gProfiles.Profile[_stPrimary];
sSignature := AProfile.Signature.Formatted(True);

ASignature := gProfiles.Profile[_stPrimary].Signature;
sSignature := ASignature.Formatted(True);

如果您还没有打开范围检查和溢出检查。下载 FastMM4 并使用其 FullDebugMode。如果这些都没有导致答案,请学习如何使用内存断点。

【讨论】:

  • 这两种方法都可以正常工作而不会损坏。似乎 gProfiles.Profile[_stPrimary].Signature 在之后被释放了
  • 这两种方法都可以正常工作而不会损坏。似乎 gProfiles.Profile[_stPrimary].Signature 在第一次调用后退出该方法时被释放。 FullDebugMode 没有返回任何可以帮助我的东西。
  • 特别是 gProfiles.FCachedProfile.Signature 在方法退出后被释放
  • 我认为您在这里遗漏了一些要点。使用返回 TProfile 的方法会将内容的本地副本复制到本地 AProfile 变量中。然后可以安全地从 AProfile 中检索 sSignature。
【解决方案2】:

您在类上使用记录的理由似乎有缺陷。每次您将记录作为函数结果返回或将记录作为函数参数传递或从一个记录变量分配给另一个记录变量时,该记录结构的所有字段都将被复制到内存中。

仅此一项就令人担忧。与引用类型相比,传递记录类型变量会消耗程序的生命。与实际完成工作相比,您的代码很容易花更多的时间从这里复制到那里和从那里复制到这里。

在一个语句中串联调用三个函数与在单独语句中调用三个函数的区别在于中间结果的分配和生命周期。当您在单独的语句中调用函数时,您提供局部变量来保存调用之间的中间结果。变量是显式的,并且它们的生命周期是明确定义的。

当您在一条语句中调用函数时,编译器负责分配临时变量以保存调用之间的中间结果。这些隐式变量的生命周期分析可能会变得模糊 - 可以使用同一个局部变量来保存多个串联调用的中间结果吗?大多数时候,答案可能是肯定的,但如果涉及的记录类型包含编译器管理的数据类型(字符串、变体和接口)的字段,则不能用下一个数据块覆盖相同的局部变量。

包含编译器管理类型的记录必须以有序的方式处理以避免泄漏堆内存。如果这样的记录被垃圾数据覆盖,或者如果这样的记录在编译器不知情的情况下被复制,那么当记录超出范围时,编译器生成的代码来处理记录的编译器管理的字段可能会报告它遇到无效的指针和/或损坏的堆。

您的 TSignature 记录包含字符串字段,使其成为编译器管理的数据类型。只要你有一个 TSignature 类型的局部变量,编译器就必须在函数体中隐式生成 try..finally 帧,以确保在执行离开该范围时释放该局部变量结构中的字符串字段。

任何最终修改或覆盖 TSignature 记录中的字符串字段指针的操作都可能导致 Invalid Pointer Operation 错误。复制记录(通过将其分配给多个变量)应该自动增加引用计数,但是任何使用 MemCopy 将记录的内容批量复制到其他位置都会抛出引用计数并在清理时导致无效指针操作代码尝试释放这些字符串字段的次数比实际引用的次数多。将记录变量类型转换为错误的记录类型可能会导致字符串字段被垃圾覆盖,并导致无效的指针操作(当记录在范围结束时被清理时)

还有一种可能是编译器本身在单语句场景中丢失了对中间记录变量的跟踪,并且正在清理隐藏的中间记录太多次,或者在没有清理先前值的情况下覆盖它们。在 Delphi 3 时代的某个地方,这个区域有一个编译器错误,但我不记得我们在哪个产品版本中修复了它。我似乎记得我想到的错误涉及将记录类型函数结果传递给 const 类型参数,因此它与您的方案不完全匹配,但结果相似。

在将此作为编译器错误报告之前,请在调试器反汇编视图中使用细齿梳检查您的代码。 有很多方法可以让您自己搞砸。查看编译器生成的代码分配、写入和处置中间结果的位置,以及您的代码如何与之交互图案。

当您看到一个临时记录变量的字符串字段被覆盖而没有调用减少对这些字符串的引用时,确凿无疑。它可能是由您的代码引起的,也可能是由编译器生成的代码中的某些东西引起的,但唯一的方法是亲眼目睹这一行为并从那里找出问题所在。

【讨论】:

  • 感谢您的详细介绍(我已经阅读了三遍)。我个人并不关心指手画脚。只能找到解决方案。对于需要许多子类的大型“类”,您会提出什么更安全的模型?
  • +1(如果可以的话+10)花时间解释得如此透彻。
  • @A.Bouchez:是的,我知道 _LStrClr 中的联锁 Dec。我实现了它。 ;>
  • @A.Bouchez:我严重怀疑 Xaz 在这里运行多个线程。这种情况下的无效指针操作几乎可以肯定是由于覆盖单语句函数调用之间使用的中间记录值和丢弃签名记录中的字符串指针字段引起的。
  • 引发了如此热烈的辩论,我相信我在这里的工作已经完成(大步走入日落)。说真的,感谢大家的反馈。
【解决方案3】:

我对您的代码摘录不太了解。 TProfile 是记录吗?所以使用函数 SomeName:TProfile 会将记录内容复制到结果中,这是非常低效的。即使使用optimized version of the record copy function,它仍然很耗时。

您应该通过引用/指针获取它,使用 PProfile = ^TProfile 类型。 在这种情况下,您将避免大多数关于访问记录中的字符串的内存问题。

但您应该确保原始 TProfile 在 PProfile 指针的整个生命周期内将在内存中保持可用。

在某些(罕见的)情况下,使用记录可能比使用类更快/更容易,if you parse some binary content for example。但是您永远不应该使用普通的记录类型来操作带有函数/方法的记录,而是使用指向记录的指针(或 var 参数)。它会更安全、更快捷。

【讨论】:

  • @MisterDownVoter 请发表评论以分享您的意见! :)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-04-24
  • 2011-01-05
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多