使用memset()函数还是值初始化来将struct清零?

99

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

STRUCT theStruct;
memset( &theStruct, 0, sizeof( STRUCT ) );
或者
STRUCT theStruct = {};

第二种变体看起来更简洁 - 它只有一行代码,没有可能被输入错误的参数,并导致错误的情况。

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


这个后来的问题的答案看起来更加有用和容易。 - TheMatto
8个回答

118

这两个构造在含义上非常不同。第一个使用了memset函数,该函数旨在将内存缓冲区设置为特定值。第二个则用于初始化对象。让我举个例子:

假设您有一个只包含POD类型成员的结构体("Plain Old Data" - 参见C++中的POD类型是什么?

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)并没有太大的区别,因为在使用memset时唯一的区别是将对齐字节设置为零值。由于通常你无法访问这些字节,所以对你来说没有区别。
另一方面,既然你已经标记了你的问题为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,并在所有其他情况下使用默认初始化x = {}


嗨,迪米蒂!我有一个结构体,其中有一些成员,我尝试了memsetting的第一种选项:“struct stVar = {}”。但是我收到了“-Wmissing-field-initializers”警告。这是个问题吗? - MayurK
2
在这种情况下,您所说的 POD 是否实际上指的是一个 可以平凡构造 的对象(即没有用户提供的构造函数的对象)?我认为它不应该被限制为 POD。 - Al.G.
这不会崩溃:http://coliru.stacked-crooked.com/a/4b3dbf0b8761bc9b。从技术上讲,这是未定义的行为,因为结构体不是平凡可分配的(因此编译器会发出警告)。但是,我怀疑在任何常见的平台上,零字节都不是“std::string”的无效值。 - Kyle Strand
3
我认为这个答案已经过时了。在C++11中,保证填充位被零初始化:如果T是(可能带有cv限定符的)非联合类类型,则每个非静态数据成员和每个基类子对象都将进行零初始化并且填充将被初始化为零位; - Clément

42

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

此外,一些API要求将结构实际设置为所有位为零。例如,伯克利套接字API在多态使用结构时非常重要,因此确实需要将整个结构设置为零,而不仅仅是显然的值。 API文档应说明是否真正需要将结构设置为所有位为零,但可能存在缺陷。

但是,如果没有这两种情况或类似情况适用,则由您决定。当定义结构时,我更喜欢值初始化,因为这样可以更清晰地传达意图。当然,如果需要将现有结构归零,则memset是唯一的选择(除手动将每个成员初始化为零之外,但通常不会执行此操作,特别是对于大型结构)。


3
一些旧的IEEE-754标准之前的CPU存在奇怪的浮点数零值。非754标准的数学运算可能会重新出现,所以最好不要写那些错误。 - Andrew McGregor
1
不要紧。C标准没有规定使用哪种浮点格式。因此,即使它现在适用于IEEE 754,它也可能无法在不同的浮点实现(未来或过去)上工作。 - Toad
3
现在可能不多了,因为IEEE非常普遍,但它们曾经更加普遍。我知道软件浮点实现是零不全部是零的典型例子。所以你可能不会遇到麻烦,但是C语言并没有强制使用IEEE标准,所以除非零初始化是一个瓶颈,否则“更安全”的方法实际上不会有任何成本。 - JaakkoK
1
将每个成员初始化为零不会使每个成员都为零,但您会错过填充字节。因此,memset是您唯一的选择。 - fmuecke
通过 Berkeley socket API 我吃了苦头,从中吸取了教训... - mab0189
显示剩余3条评论

13
如果您的结构体包含以下内容:
int a;
char b;
int c;

那么在bc之间将会插入填充字节。使用memset函数会把这些字节清零,而其他方法则不会,因此会有3个垃圾字节(如果您的整数是32位)。如果您打算使用结构体从文件中读取/写入数据,则这可能很重要。


3
这似乎不是真的。根据CppReference,如果T是一个非联合类类型,所有基类和非静态数据成员都会被零初始化,并且所有填充位都会被初始化为零位。如果有构造函数,则这些构造函数会被忽略。https://zh.cppreference.com/w/cpp/language/zero_initialization - Kyle Strand
1
可能只适用于C而不是C++。 - syockit

9
我会使用值初始化,因为它看起来更干净,而且如你所述, less error prone。我认为这样做没有任何不利影响。
但是,在使用结构体后,你可能需要依赖于 memset 来将其清零。

6

虽然较少见,但我猜第二种方法还有一个好处,就是将浮点数初始化为零,而使用 memset 则不一定能做到这一点。


在执行memset时并非完全不可能。实际上,在x86和x64上将float/double置零会使它变为零。当然,这不符合C/C++标准,但它可以在大多数流行的平台上运行。 - sbk
3
目前来说,谁知道他们将使用什么浮点数实现。IEEE 754 标准并未为编译器定义。因此,即使现在可能有效,但对你来说只是侥幸,后续可能会出现问题。 - Toad

6

值初始化是首选的,因为它可以在编译时完成。
此外,它正确地将所有POD类型初始化为0。

memset在运行时完成。
如果结构体不是POD,则使用memset是可疑的。
不会正确地初始化(为零)非int类型。


4
数值在编译时不会被初始化。编译器生成启动代码,在程序开始时初始化所有全局变量,因此在运行时进行。对于堆栈变量,初始化在函数进入时进行,同样是在运行时进行。 - qrdl
@qrdl,这取决于编译器和目标。对于可存储的代码,有时会在编译时设置值。 - Prof. Falken
3
让我重新表述一下。在某些情况下,值初始化可以使编译器在编译时进行初始化(而不是运行时)。因此,只有 POD 类型的全局变量可以在编译时进行初始化。 - Martin York
@qrdl:在许多平台上,如果“foo”是具有静态存储类的Int32_t类型,运行时语句“foo=0x12345678;”将生成代码将0x12345678存储在foo中;该代码可能至少为10个字节长,一些微控制器可能需要多达32个字节。声明“Int32_t foo=0x12345678;”会导致变量链接到初始化数据段,并将4个字节添加到初始化列表中,在许多平台上,“Int32_t foo;”比“Int32_t foo=0;”便宜四个字节,后者会强制将foo放入初始化数据段。 - supercat
理论上是这样的,但大多数编译器(包括所有主流的编译器如GCC和Clang)都知道如何内联 memset,因此零值可以在编译时可见,并传播到使用该值的代码中。就优化而言,我不会在这里期望有任何区别或者真实的 libc 中运行时调用 memset。(除了非常大的对象以外,编译器可能通过调用 memset 而不是内联 SIMD 循环来实现“T foo = {};”) - Peter Cordes
显示剩余2条评论

4
在一些编译器中,STRUCT theStruct = {}; 可以被翻译成 memset( &theStruct, 0, sizeof( STRUCT ) ); 在可执行文件中。一些 C 函数已经被链接到运行时环境中,因此编译器可以使用这些库函数,例如 memset/memcpy。

4
最近我遇到了一个严重的问题。我正在编写一段自定义压缩代码,并在声明时使用struct something foo = { x,y,z }来初始化一些大型结构体。然而 cachegrind 显示,我的程序中 70% 的“工作”都花在了 memset 上,因为每次函数调用时结构体都会被清零。 - Jody Bruchon

-1
如果有许多指针成员并且您可能在将来添加更多指针成员,请使用memset可能会有所帮助。结合适当的 assert(struct->member)调用,您可以避免因尝试引用您忘记初始化的坏指针而导致的随机崩溃。但是,如果您不像我这样健忘,那么成员初始化可能是最好的选择! 然而,如果您的结构体作为公共API的一部分被使用,您应该要求客户端代码使用memset。这有助于未来的保护,因为您可以添加新的成员,并且客户端代码将自动在memset调用中将其归零,而不是将其保留在(可能危险的)未初始化状态中。例如,在使用套接字结构时就是这样做的。

它如何帮助未来的保护?如果您假设客户端代码没有重新编译,那么它最终会使用错误的结构大小调用memset。如果客户端代码被重新编译,则需要访问包含memset或值初始化的结构定义的更新标头文件。(但是,客户端和库确实需要具有关于空指针表示方式的一致概念,因此,如果API建议使用memset,则应该针对所有位零进行检查,而不是针对NULL。) - jamesdlin
此外,如果这个结构体是公共API的一部分,那么也许应该考虑使用一个具有初始化函数的不透明结构体来代替。 - jamesdlin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接