31

不必记住初始化一个简单的“C”结构,我可以从它派生并在构造函数中将其归零,如下所示:

struct MY_STRUCT
{
    int n1;
    int n2;
};

class CMyStruct : public MY_STRUCT
{
public:
    CMyStruct()
    {
        memset(this, 0, sizeof(MY_STRUCT));
    }
};

这个技巧通常用于初始化 Win32 结构,有时可以设置无处不在的cbSize成员。

现在,只要没有用于 memset 调用销毁的虚函数表,这是一种安全的做法吗?

4

16 回答 16

84

您可以简单地对基础进行值初始化,并且其所有成员都将被清零。这是有保证的

struct MY_STRUCT
{
    int n1;
    int n2;
};

class CMyStruct : public MY_STRUCT
{
public:
    CMyStruct():MY_STRUCT() { }
};

为此,基类中不应有用户声明的构造函数,就像您的示例中一样。

没有讨厌memset的。不能保证memset在您的代码中有效,即使它应该在实践中有效。

于 2009-08-22T03:15:07.087 回答
19

前言:

虽然我的答案仍然是好的,但我发现litb 的答案比我的要好,因为:

  1. 它教会了我一个我不知道的技巧(litb的答案通常有这个效果,但这是我第一次写下来)
  2. 它准确地回答了这个问题(即,将原始结构的部分初始化为零)

所以,请在我之前考虑一下 litb 的回答。事实上,我建议问题的作者将 litb 的答案视为正确的答案。

原始答案

将一个真正的对象(即std::string)等放入其中会破坏,因为真正的对象将在memset之前初始化,然后被零覆盖。

使用初始化列表对 g++ 不起作用(我很惊讶......)。而是在 CMyStruct 构造函数主体中对其进行初始化。它将是 C++ 友好的:

class CMyStruct : public MY_STRUCT
{
public:
    CMyStruct() { n1 = 0 ; n2 = 0 ; }
};

PS:当然,我假设您确实无法控制 MY_STRUCT。通过控制,您将直接在 MY_STRUCT 中添加构造函数而忘记了继承。请注意,您可以将非虚拟方法添加到类似 C 的结构中,并且仍然让它像结构一样运行。

编辑:在 Lou Franco 的评论之后添加了缺少的括号。谢谢!

编辑 2:我在 g++ 上尝试了代码,由于某种原因,使用初始化列表不起作用。我使用主体构造函数更正了代码。不过,该解决方案仍然有效。

请重新评估我的帖子,因为原始代码已更改(有关更多信息,请参阅更改日志)。

编辑 3:阅读 Rob 的评论后,我想他有一个值得讨论的观点:“同意,但这可能是一个巨大的 Win32 结构,可能会随着新的 SDK 而改变,所以 memset 是未来的证明。”

我不同意:了解微软,它不会因为他们需要完美的向后兼容性而改变。他们将创建一个扩展的 MY_STRUCT Ex结构,其初始布局与 MY_STRUCT 相同,末尾有附加成员,并且可以通过“大小”成员变量(如用于 RegisterWindow、IIRC 的结构)来识别。

因此,Rob 的评论中剩下的唯一有效点是“巨大的”结构。在这种情况下,也许 memset 更方便,但您必须使 MY_STRUCT 成为 CMyStruct 的变量成员,而不是从它继承。

我看到另一个 hack,但我想这会因为可能的结构对齐问题而中断。

编辑 4:请查看 Frank Krueger 的解决方案。我不能保证它是可移植的(我猜是),但从技术角度来看它仍然很有趣,因为它展示了一种情况,在 C++ 中,“this”指针“地址”从其基类移动到其继承类.

于 2008-09-21T20:51:02.990 回答
12

比 memset 好得多,你可以使用这个小技巧:

MY_STRUCT foo = { 0 };

这会将所有成员初始化为 0(或它们的默认值 iirc),无需为每个成员指定值。

于 2008-09-21T20:56:34.020 回答
9

这会让我感觉更安全,因为即使有一个vtable(或者编译器会尖叫)它也应该工作。

memset(static_cast<MY_STRUCT*>(this), 0, sizeof(MY_STRUCT));

我确信您的解决方案会起作用,但我怀疑在混合memset和上课时可以做出任何保证。

于 2008-09-21T20:52:48.647 回答
4

这是将 C 习语移植到 C++ 的完美示例(以及为什么它可能并不总是有效......)

使用 memset 时会遇到的问题是,在 C++ 中,结构和类完全相同,只是默认情况下,结构具有公共可见性,而类具有私有可见性。

因此,如果稍后,一些善意的程序员像这样更改 MY_STRUCT 怎么办:


struct MY_STRUCT
{
    int n1;
    int n2;

   // Provide a default implementation...
   virtual int add() {return n1 + n2;}  
};

通过添加该单一功能,您的 memset 现在可能会造成严重破坏。comp.lang.c+中有详细的讨论

于 2008-09-21T21:06:43.873 回答
3

这些示例具有“未指定的行为”。

对于非 POD,编译器布置对象(所有基类和成员)的顺序是未指定的 (ISO C++ 10/3)。考虑以下:

struct A {
  int i;
};

class B : public A {       // 'B' is not a POD
public:
  B ();

private:
  int j;
};

这可以布置为:

[ int i ][ int j ]

或者作为:

[ int j ][ int i ]

因此,直接在“this”的地址上使用 memset 是非常不确定的行为。上面的答案之一,乍一看似乎更安全:

 memset(static_cast<MY_STRUCT*>(this), 0, sizeof(MY_STRUCT));

但是,我相信,严格来说,这也会导致未指定的行为。我找不到规范文本,但是 10/5 中的注释说:“基类子对象的布局(3.7)可能与相同类型的最派生对象的布局不同”。

结果,我的编译器可以使用不同的成员执行空间优化:

struct A {
  char c1;
};

struct B {
  char c2;
  char c3;
  char c4;
  int i;
};

class C : public A, public B
{
public:
  C () 
  :  c1 (10);
  {
    memset(static_cast<B*>(this), 0, sizeof(B));      
  }
};

可以布置为:

[ char c1 ] [ char c2, char c3, char c4, int i ]

在 32 位系统上,由于“B”的对齐等原因,sizeof(B) 很可能是 8 个字节。但是,如果编译器打包数据成员,sizeof(C) 也可以是“8”字节。因此,对 memset 的调用可能会覆盖赋予“c1”的值。

于 2008-09-22T09:14:01.877 回答
2

C++ 中不能保证类或结构的精确布局,这就是为什么你不应该从外部假设它的大小(这意味着如果你不是编译器)。

可能它可以工作,直到你找到一个它不支持的编译器,或者你将一些 vtable 加入其中。

于 2008-09-21T20:50:52.320 回答
1

如果你已经有一个构造函数,为什么不直接用 n1=0 初始化它呢?n2=0;——这当然是更正常的方式。

编辑:实际上,正如 paercebal 所示,ctor 初始化更好。

于 2008-09-21T20:51:38.043 回答
1

我的意见是否定的。我也不确定它会得到什么。

随着您对 CMyStruct 的定义发生更改并添加/删除成员,这可能会导致错误。容易地。

为 CMyStruct 创建一个构造函数,它接受一个 MyStruct 有一个参数。

CMyStruct::CMyStruct(MyStruct &)

或寻求的东西。然后,您可以初始化公共或私有“MyStruct”成员。

于 2008-09-21T20:54:00.283 回答
1

从 ISO C++ 的角度来看,有两个问题:

(1) 对象是 POD 吗?首字母缩略词代表普通旧数据,标准列举了您在 POD 中不能拥有的内容(维基百科有一个很好的总结)。如果它不是 POD,则无法对其进行 memset。

(2) 是否存在所有位为零无效的成员?在 Windows 和 Unix 上,NULL 指针全为零;它不需要。浮点 0 在 IEEE754(这很常见)和 x86 中的所有位都为零。

Frank Kruegers 提示通过将 memset 限制为非 POD 类的 POD 基础来解决您的问题。

于 2008-09-21T22:14:08.563 回答
1

试试这个 - 重载新的。

编辑:我应该添加 - 这是安全的,因为在调用任何构造函数之前内存被清零。大缺陷 - 仅在对象是动态分配的情况下才有效。

struct MY_STRUCT
{
    int n1;
    int n2;
};

class CMyStruct : public MY_STRUCT
{
public:
    CMyStruct()
    {
        // whatever
    }
    void* new(size_t size)
    {
        // dangerous
        return memset(malloc(size),0,size);
        // better
        if (void *p = malloc(size))
        {
            return (memset(p, 0, size));
        }
        else
        {
            throw bad_alloc();
        }
    }
    void delete(void *p, size_t size)
    {
        free(p);
    }

};
于 2008-09-21T22:25:07.163 回答
1

如果 MY_STRUCT 是您的代码,并且您乐于使用 C++ 编译器,则可以将构造函数放在那里而不用包装在类中:

struct MY_STRUCT
{
  int n1;
  int n2;
  MY_STRUCT(): n1(0), n2(0) {}
};

我不确定效率,但我讨厌在你没有证明效率是必要的时候耍花招。

于 2009-08-21T16:29:15.233 回答
1

评论litb 的回答(似乎我还不能直接评论):

即使使用这个不错的 C++ 风格的解决方案,您也必须非常小心,不要将它天真地应用于包含非 POD 成员的结构。

然后一些编译器不再正确初始化。

请参阅此对类似问题的回答。我个人在 VC2008 上使用额外的 std::string 时体验不佳。

于 2011-05-31T07:48:31.590 回答
0

这是一些代码,但它是可重用的;包含一次,它应该适用于任何 POD。您可以将此类的实例传递给任何需要 MY_STRUCT 的函数,或者使用 GetPointer 函数将其传递给将修改结构的函数。

template <typename STR>
class CStructWrapper
{
private:
    STR MyStruct;

public:
    CStructWrapper() { STR temp = {}; MyStruct = temp;}
    CStructWrapper(const STR &myStruct) : MyStruct(myStruct) {}

    operator STR &() { return MyStruct; }
    operator const STR &() const { return MyStruct; }

    STR *GetPointer() { return &MyStruct; }
};

CStructWrapper<MY_STRUCT> myStruct;
CStructWrapper<ANOTHER_STRUCT> anotherStruct;

这样,您不必担心 NULL 是否全为 0 或浮点表示。只要 STR 是一个简单的聚合类型,一切都会奏效。当 STR 不是简单的聚合类型时,您会收到编译时错误,因此您不必担心意外误用它。此外,如果类型包含更复杂的内容,只要它具有默认构造函数,就可以:

struct MY_STRUCT2
{
    int n1;
    std::string s1;
};

CStructWrapper<MY_STRUCT2> myStruct2; // n1 is set to 0, s1 is set to "";

不利的一面是,由于您要制作额外的临时副本,因此速度较慢,并且编译器会将每个成员单独分配给 0,而不是一个 memset。

于 2008-09-22T16:23:32.420 回答
0

我所做的是使用聚合初始化,但只为我关心的成员指定初始化器,例如:

STARTUPINFO si = {
    sizeof si,      /*cb*/
    0,              /*lpReserved*/
    0,              /*lpDesktop*/
    "my window"     /*lpTitle*/
};

其余成员将被初始化为适当类型的零(如 Drealmer 的帖子中所示)。在这里,您相信微软不会通过在中间添加新的结构成员来无缘无故地破坏兼容性(一个合理的假设)。这个解决方案给我的印象是最优的——一个语句,没有类,没有 memset,没有关于浮点零或空指针的内部表示的假设。

我认为涉及继承的黑客是可怕的风格。对于大多数读者来说,公共继承意味着 IS-A。另请注意,您从一个并非设计为基础的类继承。由于没有虚拟析构函数,通过指向基的指针删除派生类实例的客户端将调用未定义的行为。

于 2008-09-22T19:01:50.670 回答
0

我假设该结构已提供给您并且无法修改。如果您可以更改结构,那么显而易见的解决方案是添加构造函数。

当您只需要一个简单的宏来初始化您的结构时,不要使用 C++ 包装器过度设计您的代码。

#include <stdio.h>

#define MY_STRUCT(x) MY_STRUCT x = {0}

struct MY_STRUCT
{
    int n1;
    int n2;
};

int main(int argc, char *argv[])
{
    MY_STRUCT(s);

    printf("n1(%d),n2(%d)\n", s.n1, s.n2);

    return 0;
}
于 2009-10-26T05:44:48.630 回答