使用复杂记录时“无效指针操作”的建议

时间:2010-12-22 01:32:10

标签: delphi delphi-2007 records

环境:德尔福2007

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

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;

好的,所以我在记录中的记录中有记录,这接近于初始级别的混乱,我是第一个承认并不是真正好的模型。显然,我将不得不对其进行重组。我希望你从大师那里更好地理解为什么它会破坏内存(与创建的字符串对象有关,然后释放...)以便我可以避免将来出现这类错误。

由于

3 个答案:

答案 0 :(得分:9)

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

仅此一点可能引起关注。与引用类型相比,传递记录类型变量可以从程序中榨取生命。你的代码可以轻松地花费更多的时间从这里到那里复制东西,而不是实际完成工作。

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

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

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

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

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

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

在将此报告为编译器错误之前,请在调试器反汇编视图中使用细齿梳查看代码。 有很多方法可以自行解决这个问题。查看编译器生成的代码分配,编写和处理中间结果的位置,以及代码与代码的交互方式图案。

当您看到一个临时记录变量的字符串字段被覆盖而没有调用减少对这些字符串的引用时,将会出现吸烟枪。它可能是由您的代码引起的,或者它可能是由编译器生成的代码中的某些内容引起的,但唯一可以找到的方法是见证该行为并从中指出指针。

答案 1 :(得分:2)

从你提供的腐败发生的代码中看不出来,所以这里有一些建议。尝试不同的字段链组合,看看是否可以重现它。

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

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

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

答案 2 :(得分:-1)

我的代码提取不能很好。 TProfile是创纪录的吗?因此使用函数SomeName:TProfile会将记录内容的副本复制到结果中,这是非常低效的。即使使用optimized version of the record copy function,它仍然很耗时。

您应该使用PProfile = ^ TProfile类型通过引用/指针获取它。 在这种情况下,您将防止大部分内存问题,即访问记录中的字符串。

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

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