【问题标题】:Why most Delphi examples use FillChar() to initialize records?为什么大多数 Delphi 示例使用 FillChar() 来初始化记录?
【发布时间】:2010-10-21 11:51:55
【问题描述】:

我只是想知道,为什么大多数 Delphi 示例使用 FillChar() 来初始化记录。

type
  TFoo = record
    i: Integer;
    s: string; // not safe in record, better use PChar instead
  end;

const
  EmptyFoo: TFoo = (i: 0; s: '');

procedure Test;
var
  Foo: TFoo;
  s2: string;
begin
  Foo := EmptyFoo; // initialize a record

  // Danger code starts
  FillChar(Foo, SizeOf(Foo), #0);
  s2 := Copy("Leak Test", 1, MaxInt); // The refcount of the string buffer = 1
  Foo.s = s2; // The refcount of s2 = 2
  FillChar(Foo, SizeOf(Foo), #0); // The refcount is expected to be 1, but it is still 2
end;
// After exiting the procedure, the string buffer still has 1 reference. This string buffer is regarded as a memory leak.

这里 (http://stanleyxu2005.blogspot.com/2008/01/potential-memory-leak-by-initializing.html) 是我对此主题的说明。 IMO,用默认值声明一个常量是更好的方法。

【问题讨论】:

    标签: delphi initialization record


    【解决方案1】:

    主要是历史原因。 FillChar() 可以追溯到 Turbo Pascal 时代并用于此类目的。这个名字确实有点用词不当,因为它虽然说 FillChar(),但实际上是 FillByte()。原因是最后一个参数可以带一个字符一个字节。所以 FillChar(Foo, SizeOf(Foo), #0) 和 FillChar(Foo, SizeOf(Foo), 0) 是等价的。另一个令人困惑的原因是,从 Delphi 2009 开始,FillChar 仍然只填充字节,即使 Char 等效于 WideChar。在查看 FillChar 的最常见用途以确定大多数人是使用 FillChar 实际用字符数据填充内存还是仅使用它用给定字节值初始化内存时,我们发现后一种情况主导了它的使用而不是前者。因此,我们决定让 FillChar 以字节为中心。

    确实,如果没有在适当的上下文中使用,使用 FillChar 清除包含使用“托管”类型(字符串、变体、接口、动态数组)之一声明的字段的记录可能是不安全的。然而,在您给出的示例中,在本地声明的记录变量上调用 FillChar 实际上是安全的,只要它是您对该范围内的记录所做的第一件事。原因是编译器已经生成了代码来初始化记录中的字符串字段。这已经将字符串字段设置为 0 (nil)。调用 FillChar(Foo, SizeOf(Foo), 0) 只会用 0 个字节覆盖整个记录,包括已经为 0 的字符串字段。在记录变量 after 上使用 FillChar 一个值被分配给不推荐使用字符串字段。使用你的初始化常量技术是一个很好的解决这个问题的方法,因为编译器可以生成正确的代码来确保现有的记录值在赋值过程中被正确地完成。

    【讨论】:

    • >>> 在将值分配给字符串字段后,不建议在记录变量上使用 FillChar 在这种情况下,您应该使用 Finalize(V); FillChar(V, SizeOf(V), 0);如果你经常这样做,你可能会写一个新的函数,它会完成 Finalize + FillChar。我认为使用 FillChar 会比分配给常量更快,但今天可能不相关。更重要的是 - 您不需要为每个记录的类型声明一个常量。
    • >> 我认为使用 FillChar 会比分配给常量更快,但今天可能不相关。更重要的是 - 您不需要为每个记录的类型声明一个常量。
    • 我尽可能使用它,因为它帮助我遵循开放/封闭原则。那么当一条记录添加了一个新的(非字符串)成员时,就不需要进一步初始化了。
    【解决方案2】:

    如果您有 Delphi 2009 及更高版本,请使用 Default 调用来初始化记录。

    Foo := Default(TFoo); 
    

    请参阅David's answer 问题How to properly free records that contain various types in Delphi at once?

    编辑:

    使用Default(TSomeType) 调用的优点是记录在被清除之前就已完成。没有内存泄漏,也没有对 FillChar 或 ZeroMem 的显式危险低级调用。当记录很复杂时,可能包含嵌套记录等,就消除了出错的风险。

    您的记录初始化方法可以变得更加简单:

    const EmptyFoo : TFoo = ();
    ...
    Foo := EmptyFoo; // Initialize Foo
    

    有时您希望参数具有非默认值,然后这样做:

    const PresetFoo : TFoo = (s : 'Non-Default'); // Only s has a non-default value
    

    这将节省一些打字,并将重点放在重要的东西上。

    【讨论】:

    • 感谢您的信息。 IMO,对于旧的 delphi 编译器,我们应该定义一些默认记录常量,而不是使用魔法 FillChar 函数。对于现代 Delphi 编译器,我们也应该为记录类型声明构造函数。如果您有兴趣,可以从我的个人资料页面查看开源项目。我根据我在评论中指出的原则对这些项目进行编码。
    • 添加了一些简化来定义 const 记录。
    • 感谢精彩的 () 构造,我不知道 Delphi 可以做到。
    【解决方案3】:

    FillChar 可以确保您在新的、未初始化的结构(记录、缓冲区、数组...)中没有任何垃圾。
    在不知道您正在重置什么的情况下,不应使用它来“重置”这些值。
    不只是写MyObject := nil 并期望避免内存泄漏。
    特别要仔细观察所有托管类型。
    请参阅 Finalize 函数。

    当你有能力直接摆弄记忆时,总有办法让自己在脚下开枪。

    【讨论】:

    • 它发生在我身上,以前,有时我没有使用 fillchar 作为我的记录,并且该记录的一些 should_be_empty 成员得到了垃圾字符串,从那时起我总是使用 fillchar 或 zeromemory。
    • 致弗朗索瓦:我没有要求确认这个问题。我知道如果使用/小心/没有问题。我只是想知道,为什么 FillChar 多年来一直用于初始化记录。
    【解决方案4】:

    FillChar 通常用于仅填充数字类型和数组的数组记录。当 record 中有 strings(或任何引用计数的变量)时,您是正确的。

    虽然您建议使用 const 来初始化它会起作用,但当我有一个 可变长度 数组 时,就会出现问题我想初始化。

    【讨论】:

      【解决方案5】:

      问题也可能在问:

      Windows 中没有 ZeroMemory 功能。在头文件 (winbase.h) 中,它是一个宏,在 C 世界中,它转身调用 memset:

      memset(Destination, 0, Length);
      

      ZeroMemory 是“您的平台可用于零内存的功能”的语言中性术语

      memset 的 Delphi 等效项是 FillChar

      由于 Delphi 没有宏(并且在内联之前),调用 ZeroMemory 意味着您必须在实际到达 FillChar。

      所以在很多方面,调用 FillChar 是一种性能微优化 - 现在 ZeroMemory 已内联:

      procedure ZeroMemory(Destination: Pointer; Length: NativeUInt); inline;
      

      阅读奖励

      Windows 还包含SecureZeroMemory 函数。它的作用与 ZeroMemory 完全相同。如果它和 ZeroMemory 做同样的事情,它为什么存在?

      因为一些智能 C/C++ 编译器可能会认识到,在清除内存之前将内存设置为 0 是浪费时间 - 并优化了对 ZeroMemory 的调用。

      我不认为 Delphi 的编译器像许多其他编译器一样聪明;所以不需要 SecureFillChar

      【讨论】:

        【解决方案6】:

        传统上,字符是单个字节(Delphi 2009 不再适用),因此使用带有 #0 的 fillchar 将初始化分配的内存,使其仅包含空值、字节 0 或 bin 00000000。

        为了兼容性,您应该改用 ZeroMemory 函数,它的调用参数与旧的 fillchar 相同。

        【讨论】:

        • 为什么你总是和我同时回答同样的问题?这次你比我快 35 秒。
        • 一定是水里的东西。
        • FillChar 的工作方式与往常一样。它的名字已经不太合适了,但除此之外,没有任何区别。
        • ZeroMemory() 没有相同的参数。首先,FillChar() 需要 3 个参数,而 ZeroMemory() 只需要 2 个参数。接下来,FillChar() 需要一个变量引用,ZeroMemory() 需要一个指针(至少在 XE 和原始 API 中是这样)。我对“相同”有不同的定义。
        【解决方案7】:

        这个问题具有更广泛的含义,多年来我一直在脑海中。我也是,在使用 FillChar 记录时长大的。这很好,因为我们经常向(数据)记录添加新字段,当然 FillChar( Rec, SizeOf( Rec), #0 ) 会处理这些新字段。如果我们“正确地做”,我们必须遍历记录的所有字段,其中一些是枚举类型,其中一些可能是记录本身,如果我们不添加新代码,结果代码的可读性较差,也可能是错误的认真记录字段。字符串字段很常见,因此 FillChar 现在是禁忌。几个月前,我四处走动,将带有字符串字段的记录上的所有 FillChars 转换为迭代清除,但我对解决方案不满意,想知道是否有一种简洁的方法可以对简单类型(序数 / float) 和“Finalize”变体和字符串?

        【讨论】:

        • 函数“Finalize”确实有帮助。但我仍然建议通过将其分配给 const 空记录来正确执行此操作。一旦你改变了记录的结构,编译器也会提醒你更新这个空记录。
        • 布莱恩,请查看我的回答并点击链接。这是您正在寻找的简洁答案:)
        • Finalize + ZeroMemory/FillChar 将涵盖所有情况并满足所有需求。而且它可能比分配给常量空记录更快。
        【解决方案8】:

        这是一种不使用 FillChar 来初始化内容的更好方法:

        Record in record (Cannot initialize)
        How to initialize a static array?

        【讨论】:

          猜你喜欢
          • 2012-08-07
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2018-03-06
          • 1970-01-01
          • 2011-04-28
          相关资源
          最近更新 更多