代码之家  ›  专栏  ›  技术社区  ›  Rob

这个C++结构初始化技巧是安全的吗?

  •  31
  • Rob  · 技术社区  · 16 年前

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

    struct MY_STRUCT
    {
        int n1;
        int n2;
    };
    
    class CMyStruct : public MY_STRUCT
    {
    public:
        CMyStruct()
        {
            memset(this, 0, sizeof(MY_STRUCT));
        }
    };
    

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

    现在,只要memset调用没有用于销毁的虚拟函数表,这是一个安全的实践吗?

    16 回复  |  直到 13 年前
        1
  •  19
  •   Community George Stocker    7 年前

    序言:

    当我的回答还可以的时候,我发现 litb's answer 比我的好,因为:

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

    所以,请在我之前考虑一下李特的回答。事实上,我建议这个问题的作者把李特的回答看作是正确的答案。

    原始答案

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

    使用初始化列表对G++不起作用(我很惊讶…)。在cmystruct构造函数体中初始化它。它将是C++友好的:

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

    P.S.:我想你有 当然,控制我的结构。有了控件,您就可以直接在我的结构中添加构造函数,而忘记了继承。请注意,您可以将非虚拟方法添加到类C结构中,但它的行为仍像结构一样。

    编辑:在LouFranco的评论后添加了缺失的括号。谢谢!

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

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

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

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

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

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

    编辑4:请看一下弗兰克·克鲁格的解决方案。我不能保证它是可移植的(我想是),但从技术的角度来看,它仍然是有趣的,因为它显示了一个情况,在C++中,“这个”指针“地址”从它的基类移动到它的继承类。

        2
  •  84
  •   Johannes Schaub - litb    15 年前

    您可以简单地对基值进行初始化,它的所有成员都将为零。这是有保证的

    struct MY_STRUCT
    {
        int n1;
        int n2;
    };
    
    class CMyStruct : public MY_STRUCT
    {
    public:
        CMyStruct():MY_STRUCT() { }
    };
    

    为了使这项工作正常,在基类中不应该有用户声明的构造函数,就像在您的示例中一样。

    不讨厌 memset 为此。不能保证 清零 在您的代码中工作,即使它应该在实践中工作。

        3
  •  12
  •   Drealmer    16 年前

    比记忆集好得多,你可以用这个小技巧来代替:

    MY_STRUCT foo = { 0 };
    

    这将把所有成员初始化为0(或其默认值IIRC),无需为每个成员指定一个值。

        4
  •  9
  •   Frank Krueger    16 年前

    这会让我觉得更安全,因为即使有一个 vtable (否则编译器会尖叫)。

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

    我相信你的解决方案会奏效,但我怀疑在混合时有什么保证 memset 和类。

        5
  •  4
  •   Benoit    16 年前

    这是一个将C习语移植到C++的完美例子(以及为什么它不一定能正常工作……)

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

    因此,如果以后某个善意的程序员像这样改变了我的结构:

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

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

        6
  •  3
  •   Richard Corden    16 年前

    这些例子有“未指明的行为”。

    对于非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)可能与同一类型的最派生对象的布局不同。”

    因此,i编译器可以使用不同的成员执行空间优化:

    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”的值。

        7
  •  2
  •   Florian Bösch    16 年前

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

    也许它可以工作,直到你找到一个编译器,它不在上面,或者你把一些vtable放到混合中。

        8
  •  1
  •   Lou Franco    16 年前

    如果您已经有了一个构造函数,那么为什么不在这里用n1=0;n2=0;初始化它呢--这当然是更多的 正常的 方式。

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

        9
  •  1
  •   kervin    16 年前

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

    随着CMystruct定义的更改和成员的添加/删除,这可能会导致错误。很容易。

    为采用mystruct的cmystruct创建一个具有参数的构造函数。

    CMyStruct::CMyStruct(MyStruct &)
    

    或是其他的东西。然后可以初始化公共或私有的“mystrut”成员。

        10
  •  1
  •   MSalters    16 年前

    从ISO C++的观点来看,有两个问题:

    (1)物体是否为吊舱?首字母缩略词代表简单的旧数据,标准列举了POD中不能包含的内容(维基百科有一个很好的摘要)。如果它不是一个吊舱,你就不能记忆它。

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

    FrankKruegers提示通过将memset限制在非pod类的pod库中来解决您的问题。

        11
  •  1
  •   Thanatopsis    16 年前

    试试这个-超载新的。

    编辑:我应该补充-这是安全的,因为之前的内存是零的 任何 调用构造函数。大缺陷-只有在动态分配对象时才有效。

    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);
        }
    
    };
    
        12
  •  1
  •   jwhitlock    15 年前

    如果MyOxStutt是您的代码,并且使用C++编译器很高兴,您可以将构造函数放置在一个类中,而不需要包装。

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

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

        13
  •  1
  •   Community George Stocker    7 年前

    评论 litb's answer (似乎还不允许我直接评论):

    即使有了这个漂亮的C++风格的解决方案,你也必须非常小心,不要把这个天真地应用到包含非POD成员的结构中。

    有些编译器不再正确初始化。

    this answer to a similar question . 我个人在VC2008上有过不好的体验,另外还有一个std::string。

        14
  •  0
  •   fizzer    16 年前

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

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

    其余的成员将初始化为适当类型的零(如Drealmer的post)。在这里,您相信微软不会通过在中间添加新的结构成员(一个合理的假设)而毫无理由地破坏兼容性。这个解决方案让我觉得是最优的——一条语句,没有类,没有内存集,没有关于浮点零或空指针的内部表示的假设。

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

        15
  •  0
  •   grob    15 年前

    我假设结构是提供给您的,不能修改。如果您可以更改结构,那么显而易见的解决方案是添加一个构造函数。

    当你所需要的只是一个简单的宏来初始化你的结构时,不要用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;
    }
    
        16
  •  0
  •   Salman A    15 年前

    这是一点代码,但它是可重用的;包括它一次,它应该适用于任何POD。您可以将此类的实例传递给任何需要my结构的函数,或者使用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;
    

    这样,您就不必担心nulls都是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。