【问题标题】:memset() or value initialization to zero out a struct?memset() 或值初始化以将结构清零?
【发布时间】:2011-01-01 05:00:43
【问题描述】:

在 Win32 API 编程中,通常使用具有多个字段的 C structs。通常它们中只有几个具有有意义的值,而所有其他值都必须归零。这可以通过以下两种方式之一实现:

STRUCT theStruct;
memset( &theStruct, 0, sizeof( STRUCT ) );

STRUCT theStruct = {};

第二个变体看起来更干净 - 它是单行的,它没有任何可能输入错误并导致错误的参数。

与第一个变体相比,它有什么缺点吗?使用哪种变体?为什么?

【问题讨论】:

标签: c++ c visual-c++ struct initialization


【解决方案1】:

这两个结构的含义非常不同。第一个使用memset 函数,该函数旨在将内存缓冲区设置为某个值。第二个初始化对象。让我用一点代码解释一下:

假设您的结构只有 POD 类型的成员(“Plain Old Data” - 请参阅 What are POD types in C++?

struct POD_OnlyStruct
{
    int a;
    char b;
};

POD_OnlyStruct t = {};  // OK

POD_OnlyStruct t;
memset(&t, 0, sizeof t);  // OK as well

在这种情况下,编写 POD_OnlyStruct t = {}POD_OnlyStruct t; memset(&t, 0, sizeof t) 并没有太大区别,因为我们这里唯一的区别是 alignment 字节在 @ 的情况下被设置为零值987654327@ 已使用。由于您通常无法访问这些字节,因此对您来说没有区别。

另一方面,由于您已将问题标记为 C++,让我们尝试另一个示例,其中成员 类型不同于 POD

struct TestStruct
{
    int a;
    std::string b;
};

TestStruct t = {};  // OK

{
    TestStruct t1;
    memset(&t1, 0, sizeof t1);  // ruins member 'b' of our struct
}  // Application crashes here

在这种情况下,使用TestStruct t = {} 之类的表达式很好,在其上使用memset 会导致崩溃。如果您使用memset,会发生以下情况 - 创建TestStruct 类型的对象,从而创建std::string 类型的对象,因为它是我们结构的成员。接下来,memset 将对象b 所在的内存设置为某个值,比如零。现在,一旦我们的 TestStruct 对象超出范围,它将被销毁,当轮到它的成员 std::string b 时,您会看到崩溃,因为该对象的所有内部结构都被 memset 破坏了。

所以,现实情况是,那些事情是非常不同的,尽管在​​某些情况下您有时需要将整个结构 memset 归零,但确保您了解您的内容始终很重要'正在做,而不是像我们的第二个例子那样犯错误。

我的投票 - 如果需要,对对象使用memset,在所有其他情况下使用默认初始化x = {}

【讨论】:

  • 嗨,Dimity!我有一个包含一些成员的结构,我尝试了 memsetting 的第一个选项:“struct stVar={}”。但我收到“-Wmissing-field-initializers”警告。有问题吗?
  • 在这种情况下,您所说的 POD 是指实际上是一个 普通可构造 对象(即没有用户提供的 c-tor 的对象)吗?我认为它不应该局限于 POD。
  • 这不会崩溃:coliru.stacked-crooked.com/a/4b3dbf0b8761bc9b 从技术上讲,这是未定义的行为,因为该结构不是可以简单分配的(因此会出现编译器警告)。但是,我怀疑是否存在任何常见的平台,其中零字节是 std::string 的无效值。
  • 我认为这个答案已经过时了。在 C++11 中,填充位保证为零初始化:if T is a (possibly cv-qualified) non-union class type, each non-static data member and each base-class subobject is zero-initialized and padding is initialized to zero bits;
【解决方案2】:

根据结构成员的不同,这两个变体不一定是等价的。 memset 会将结构设置为所有位为零,而值初始化会将所有成员初始化为零值。 C 标准保证这些仅对整数类型相同,而不是浮点值或指针。

此外,某些 API 要求将结构真正设置为全位为零。例如,Berkeley 套接字 API 以多态方式使用结构,因此将整个结构真正设置为零很重要,而不仅仅是明显的值。 API 文档应该说明结构是否真的需要全位为零,但它可能有缺陷。

但如果这些或类似情况均不适用,则由您决定。在定义结构时,我会更喜欢值初始化,因为这样可以更清楚地传达意图。当然,如果您需要将现有结构归零,memset 是唯一的选择(嗯,除了手动将每个成员初始化为零,但通常不会这样做,尤其是对于大型结构)。

【讨论】:

  • 出于好奇,在哪个平台上所有位为零的浮点数不是正零?
  • 几个旧的 pre-IEEE-754 CPU 有奇怪的浮点零。非 754 数学可能会回来,你永远不知道,所以最好不要写那些错误。
  • 没关系。 C 标准没有指定使用什么浮点格式。因此,即使它现在适用于 IEEE 754,它也可能不适用于不同的浮点实现(未来或过去)
  • 我猜现在并不多,IEEE 如此普遍,但它们过去更为流行。我了解软件 FP 实现是典型的例子,其中零不是所有位为零。所以你可能不会遇到麻烦,但 C 语言并没有强制要求 IEEE,所以除非零初始化是一个瓶颈,否则“更安全”的方式并不需要任何成本。
  • 将每个成员初始化为零不会使每个成员都为零,但您会错过填充字节。因此 memset 是你唯一的选择。
【解决方案3】:

如果您的结构包含以下内容:

int a;
char b;
int c;

然后将在“b”和“c”之间插入填充字节。 memset() 会将这些归零,否则不会,因此会有 3 个字节的垃圾(如果您的整数是 32 位)。如果您打算使用您的结构来读取/写入文件,这可能很重要。

【讨论】:

  • 这似乎不是真的。来自 CppReference:“如果 T 是非联合类类型,则所有基类和非静态数据成员都初始化为零,并且所有填充都初始化为零位。构造函数(如果有)将被忽略。” en.cppreference.com/w/cpp/language/zero_initialization
  • 可能只适用于 C 而不是 C++。
【解决方案4】:

我会使用值初始化,因为它看起来很干净并且更不容易出错,正如您所提到的。我认为这样做没有任何缺点。

您可能会依赖 memset 在结构使用后将其归零。

【讨论】:

    【解决方案5】:

    这不是很常见,但我猜第二种方法也有将浮点数初始化为零的好处。虽然做 memset 肯定不会

    【讨论】:

    • while doing a memset would certainly not - 不完全正确。实际上,在 x86 和 x64 mem 上,将 float/double 设置为零会将其设置为零。当然,这不在 C/C++ 标准中,但它适用于最流行的平台。
    • sbk:现在......谁知道他们可能会开始使用什么浮点实现。没有为编译器定义 IEEE 754。所以即使它现在可以工作,对你来说只是幸运,但以后可能会出现问题。
    【解决方案6】:

    在某些编译器中,STRUCT theStruct = {}; 会在可执行文件中转换为 memset( &theStruct, 0, sizeof( STRUCT ) );。一些 C 函数已经链接到执行运行时设置,因此编译器可以使用这些库函数,如 memset/memcpy。

    【讨论】:

    • 这实际上让我最近很难受。我正在编写一段自定义压缩代码,并在声明时使用struct something foo = { x, y, z } 初始化一些大型结构,cachegrind 显示我的程序的 70% 的“工作”在memset 中,因为结构在每个函数调用中都归零。
    【解决方案7】:

    值初始化,因为它可以在编译时完成。
    它也正确地 0 初始化所有 POD 类型。

    memset() 在运行时完成。
    如果结构不是 POD,也可以使用 memset()。
    未正确初始化(为零)非 int 类型。

    【讨论】:

    • 值在编译时未初始化。编译器生成启动代码,在程序启动期间初始化所有全局变量,从而在运行时初始化。对于堆栈变量,初始化是在函数入口处执行的 - 再次在运行时。
    • @qrdl,取决于编译器和目标。对于支持 ROM 的代码,有时会在编译时设置值。
    • @qrdl:让我重新表述一下。值初始化可能允许(在某些情况下)编译器在编译时(而不是运行时)进行初始化。所以只能在编译时初始化 POD 全局变量。
    • @qrdl:在许多平台上,如果“foo”是静态存储类的 Int32_t,则运行时语句“foo=0x12345678;”将生成代码以将 0x12345678 存储在 foo 中;该代码可能至少有 10 个字节长,某些微控制器可能需要多达 32 个字节。声明“Int32_t foo=0x12345678;”在许多平台上会导致变量在初始化数据段中链接,并向初始化列表添加 4 个字节。在某些系统上,“Int32_t foo;”将比“Int32_t foo=0;”便宜四个字节,后者将 foo 强制到初始化数据段。
    【解决方案8】:

    如果有很多指针成员,并且您将来可能会添加更多,那么使用 memset 会有所帮助。结合适当的assert(struct->member) 调用,您可以避免随机崩溃,因为您试图遵守您忘记初始化的错误指针。但如果你不像我那么健忘,那么成员初始化可能是最好的!

    然而,如果你的结构被用作公共 API 的一部分,你应该让客户端代码使用 memset 作为要求。这有助于未来的验证,因为您可以添加新成员,并且客户端代码会在 memset 调用中自动将它们清空,而不是让它们处于(可能是危险的)未初始化状态。例如,这就是您在使用套接字结构时所做的。

    【讨论】:

    • 它对未来的验证有何帮助?如果您假设客户端代码没有重新编译,它最终会以错误的结构大小调用memset。如果客户端代码被重新编译,它需要使用结构定义访问更新的头文件,以便 memset 或值初始化工作。 (但是,客户端和库确实需要对如何表示空指针有一致的概念,因此如果 API 推荐 memset,它应该检查全位为零,而不是检查 NULL。)
    • 另外,如果结构是公共 API 的一部分,那么也许应该考虑使用带有初始化函数的不透明结构。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-03-24
    • 2023-03-11
    • 2022-01-23
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多