默认初始化、值初始化和零初始化混淆问题

95
我非常困惑于值初始化、默认初始化和零初始化。特别是当它们在不同的标准(C++03、C++11和C++14)中启用时。我在引用并扩展一个真正好的答案Value-/Default-/Zero- Init C++98 and C++03,以使其更加通用,因为如果有人可以帮助填补所需的空白,那么它将对许多用户有所帮助,让我们了解发生了什么?
简而言之,有时候new运算符返回的内存将被初始化,有时候不会,这取决于您要新建的类型是否为POD(旧式数据),或者它是否是包含POD成员并使用编译器生成的默认构造函数的类。
  • C++1998有两种初始化类型:零初始化默认初始化
  • C++2003添加了第三种初始化类型,值初始化
  • C++2011/C++2014只添加了列表初始化,并且值/默认/零初始化的规则有所改变。

假设:

struct A { int m; };                     
struct B { ~B(); int m; };               
struct C { C() : m(){}; ~C(); int m; };  
struct D { D(){}; int m; };             
struct E { E() = default; int m;}; /** only possible in c++11/14 */  
struct F {F(); int m;};  F::F() = default; /** only possible in c++11/14 */

在C++98编译器中,应该出现以下情况:
  • new A - 不确定的值(A是POD)
  • new A()- 零初始化
  • new B - 默认构造(B::m未初始化,B是非POD类型)
  • new B() - 默认构造(B::m未初始化)
  • new C - 默认构造(C::m零初始化,C是非POD类型)
  • new C() - 默认构造(C::m零初始化)
  • new D - 默认构造(D::m未初始化,D是非POD类型)
  • new D() - 默认构造?D::m未初始化)
在符合C++03标准的编译器中,应该按照以下方式工作:
  • new A - 不确定的值 (A 是 POD)
  • new A() - 值初始化 A,因为它是一个 POD,所以它是零初始化。
  • new B - 默认初始化(B::m 未初始化,B 是非 POD)
  • new B() - 值初始化 B,因为它的默认构造函数是编译器生成的而不是用户定义的,所以所有字段都被零初始化。
  • new C - 默认初始化 C,调用默认构造函数。(C::m 被零初始化,C 是非 POD)
  • new C() - 值初始化 C,调用默认构造函数。(C::m 被零初始化)
  • new D - 默认构造(D::m 未初始化,D 是非 POD)
  • new D() - 值初始化 D?,调用默认构造函数(D::m 未初始化)
Italic值和?表示不确定性,请帮忙纠正 :-)
在符合C++11标准的编译器中,应该按照以下方式工作:
??? (如果我从这里开始,无论如何都会出错,请帮忙)
在符合C++14标准的编译器中,应该按照以下方式工作:
??? (如果我从这里开始,无论如何都会出错,请帮忙) (基于答案的草稿)
  • new A - 默认初始化A,编译器生成构造函数,(A::m未初始化)(A是POD)

  • new A() - 值初始化A,由于第2点在/8中,因此它是零初始化

  • new B - 默认初始化B,编译器生成构造函数,(B::m未初始化)(B是非POD)

  • new B() - 值初始化B,由于其默认构造函数是编译器生成的而不是用户定义的,因此会将所有字段都进行零初始化。

  • new C - 默认初始化C,调用默认构造函数。 (C::m为零初始化,C是非POD)

  • new C() - 值初始化C,调用默认构造函数。 (C::m为零初始化)

  • new D - 默认初始化DD::m未初始化,D是非POD)

  • new D() - 值初始化D,调用默认构造函数(D::m未初始化)

  • new E - 默认初始化E,调用编译器生成的构造函数。(E::m未初始化,E是非POD)

  • new E() - 值初始化E,由于/8中的第2点,会将E进行零初始化。

  • new F - 默认初始化F,调用编译器生成的构造函数。(F::m未初始化,F是非POD)

  • new F() - 值初始化F,由于/8中的第1点,F构造函数如果在其第一次声明中是用户提供的且未明确默认化或删除,则为用户提供。(Link


这里有一个很好的解释:http://en.cppreference.com/w/cpp/language/default_constructor - Richard Hodges
1
据我所知,这些示例中只有C++98和C++03之间存在差异。该问题似乎在N1161(该文档有更新版本)和CWG DR#178中有所描述。由于新功能和POD的新规范,C++11中需要更改措辞,并且由于C++11措辞中的缺陷,它在C++14中再次更改,但在这些情况下影响不会改变。 - dyp
3
虽然有些无聊,但是 struct D { D() {}; int m; }; 可能值得包含在你的列表中。 - Yakk - Adam Nevraumont
有一张很好但也很令人不安的海报将这个混乱的问题点明了:http://randomcat.org/cpp_initialization/initialization.png - Gabriel
3个回答

26
C++14规定了使用new创建的对象的初始化方式在[expr.new]/17中进行说明(在C++11中,该内容在[expr.new]/15中描述,当时的注释不是注释而是规范性文本):

创建类型为Tnew-expression将按以下方式初始化该对象:

  • 如果省略了new-initializer,则对象将被default-initialized(8.5)。[注:如果没有执行初始化,则对象具有不确定值。— 结束注释]
  • 否则,new-initializer将根据直接初始化的8.5的初始化规则进行解释。
默认初始化在[dcl.init]/7(在C++11中为/6,措辞本身的效果相同)中定义:

对于类型为T的对象进行default-initialize意味着:

  • 如果T是(可能是cv-qualified的)类类型(第9条),则调用T的默认构造函数(12.1)(如果T没有默认构造函数或者重载解析(13.3)导致模棱两可或者函数被删除或从初始化的上下文中不可访问,则该初始化无效);
  • 如果T是数组类型,则每个元素都将被进行default-initialized
  • 否则,不执行任何初始化。
因此,
  • new A仅调用A的默认构造函数,不会初始化m。值是不确定的。对于new B应该是相同的。
  • new A()根据[dcl.init]/11(在C ++ 11中为/10)进行解释:

    其初始化器为空括号集,即()的对象将被值初始化。

    现在考虑[dcl.init]/8(在C ++ 11中为/7):

    将类型为T的对象进行值初始化意味着:

    • 如果T是一个(可能带有cv限定符的)类类型(第9条款),没有默认构造函数(12.1)或者是用户提供的或删除的,则对象将被默认初始化;
    • 如果T是一个(可能带有cv限定符的)类类型,没有用户提供的或删除的默认构造函数,则对象将被零初始化,并且检查默认初始化的语义约束,如果T具有非平凡的默认构造函数,则对象将被默认初始化;
    • 如果T是数组类型,则每个元素都将被值初始化;
    • 否则,对象将被零初始化。

    因此,new A()将对m进行零初始化。这应该对于AB是等效的。

  • new Cnew C()将再次默认初始化对象,因为最后一段引用中的第一个要点适用(C具有用户提供的默认构造函数!)。但是,显然,现在在两种情况下都在构造函数中初始化了m


† 好吧,这段话在C ++ 11中有略微不同的措辞,但并不改变结果:

对于类型为 T 的对象进行 值初始化 意味着:
  • 如果 T 是一个(可能是 cv-限定的)类类型(第 9 条),且具有用户提供的构造函数(12.1),则调用 T 的默认构造函数(如果 T 没有可访问的默认构造函数,则该初始化是非法的);
  • 如果 T 是一个(可能是 cv-限定的)非联合类类型且没有用户提供的构造函数,则对象进行零初始化。如果 T 的隐式声明的默认构造函数是非平凡的,则会调用该构造函数。
  • 如果 T 是一个数组类型,则每个元素都会被值初始化;
  • 否则,对象进行零初始化。

啊,你主要在谈论C++14,而C++11的参考资料在括号中给出。 - Gabriel
@Gabriel 正确。我的意思是,C++14是最新的标准,因此它处于领先地位。 - Columbo
1
尝试追踪不同标准下的初始化规则最让人烦恼的事情是,在已发布的C++14和C++11标准之间的很多变化(大部分或全部?)都是通过DRs进行的,因此它们实质上属于C++11。此外,还有一些C++14之后的DRs。 - T.C.
@Columbo 我仍然不明白为什么struct A { int m; }; struct C { C() : m(){}; int m; };会产生不同的结果,并且是什么导致了A中的m首先被初始化。我已经开了一个专门的线程来进行实验,希望你能在那里提供你的意见来澄清这个问题。谢谢。https://stackoverflow.com/questions/45290121/c-default-initialization-types - darkThoughts

12
以下答案扩展了答案https://dev59.com/_3RB5IYBdhLWcg3wcm6d#620402,可作为C++ 98和C++ 03的参考。
引用答案:
  1. 在C++1998中有两种初始化类型:零初始化和默认初始化
  2. 在C++2003中添加了第三种初始化类型:值初始化。
C++11(参考n3242)

初始化器

8.5 初始值设定 [dcl.init] 规定,变量 POD 或非 POD 可以初始化为 花括号或等号初始值,它可以是 花括号初始化列表初始值子句,统称为 花括号或等号初始值,或使用 ( 表达式列表 )。在 C++11 之前,只支持 (表达式列表)初始值子句,但 初始值子句 比 C++11 中支持的要受限。在 C++11 中,初始值子句 现在除了像 C++03 中那样支持 赋值表达式 外,还支持 花括号初始化列表。下面的语法总结了新支持的子句,其中加粗部分是 C++11 标准中新增的。

初始化器:
    花括号或等号初始化器
    ( 表达式列表 )
花括号或等号初始化器:
    = 初始化子句
    花括号初始化列表
初始化子句:
   &nbsp赋值表达式
    花括号初始化列表
初始化列表:
    初始化子句 ...opt
    初始化列表 , 初始化子句 ...opt**
花括号初始化列表:
    { 初始化列表 ,opt }
    { }

初始化

C++11仍然支持C++03的三种初始化形式。


注意

C++11 中添加了加粗部分,删除了被划掉的部分。

  1. 初始化类型:8.5.5 [dcl.init] _零初始化_

以下情况将执行零初始化

  • 具有静态或线程存储期的对象将被零初始化
  • 如果数组元素比初始化器少,则未明确初始化的每个元素将被零初始化
  • 在进行值初始化时,如果T是一个(可能是cv-限定的)非联合类类型且没有用户提供的构造函数,则对象将被零初始化。
零初始化类型为T的对象或引用意味着:
  • 如果T是标量类型(3.9),则将对象设置为值0(零),作为整数常量表达式,转换为T;
  • 如果T是(可能带有cv限定符的)非联合类类型,则每个非静态数据成员和每个基类子对象都被零初始化,并且填充位被初始化为零位;
  • 如果T是(可能带有cv限定符的)联合类型,则对象的第一个非静态命名数据成员被零初始化,并且填充位被初始化为零位;
  • 如果T是数组类型,则每个元素都被零初始化;
  • 如果T是引用类型,则不执行初始化。

2. 初始化器类型:8.5.6 [dcl.init] _default-initialize_

在以下情况下执行:

  • 如果省略了new-initializer,则对象将进行默认初始化;如果未执行任何初始化,则对象具有不确定的值。
  • 如果对象没有指定初始化程序,则对象将进行默认初始化,静态或线程存储期对象除外
  • 当基类或非静态数据成员在构造函数初始化列表中未被提及且调用该构造函数时。

对于类型 T 进行默认初始化意味着:

  • 如果 T 是一个(可能带有 cv 修饰的) 非 POD 类类型(第 9 条),则调用 T 的默认构造函数(如果 T 没有可访问的默认构造函数,则初始化无效);
  • 如果 T 是数组类型,则每个元素都进行默认初始化;
  • 否则,不执行任何初始化。

注意:直到C++11,只有具有自动存储期的非POD类类型才被认为是默认初始化的,当没有使用初始化程序时。

3. 初始化器类型: 8.5.7 [dcl.init] _value-initialize_

  1. 当一个对象(无名临时对象、命名变量、动态存储期或非静态数据成员)的初始化器为空括号集,即()或大括号{}时

对于类型T的对象进行值初始化意味着:

  • 如果T是一个(可能带有cv限定符的)类类型(Clause 9),并且具有用户提供的构造函数(12.1), 则调用T的默认构造函数(如果T没有可访问的默认构造函数,则初始化是非法的);
  • 如果T是一个(可能带有cv限定符的)非联合类类型而没有用户提供的构造函数,则该对象被零初始化,并且如果T的隐式声明的默认构造函数是非平凡的,则调用该构造函数。
  • 如果T是数组类型,则每个元素都进行值初始化;
  • 否则,对象将进行零初始化。

因此,总结一下:

注意 标准中相关引用已用粗体标出

  • new A : 默认初始化 (A::m 保持未初始化)
  • new A() : 对于没有用户提供或删除的默认构造函数的非联合类类型 T,零初始化 A。如果 T 的隐式声明默认构造函数是非平凡的,则调用该构造函数。
  • new B : 默认初始化 (B::m 保持未初始化)
  • new B() : 值初始化 B,这会将所有字段都设置为零;如果 T 是一个具有用户提供的构造函数的类类型,则调用 T 的默认构造函数
  • new C : 默认初始化 C,这会调用默认构造函数。如果 T 是一个类类型,那么将调用 T 的默认构造函数。此外,如果省略了 new-initializer,则对象将进行默认初始化
  • new C() : 值初始化 C,这会调用默认构造函数。如果 T 是一个具有用户提供的构造函数的类类型,则调用 T 的默认构造函数。此外,其初始化程序为空的对象,即 (),将进行值初始化

1
我可以确认,在C++11中,问题中提到的所有内容在C++14中都是正确的,至少根据编译器实现是这样的。
为了验证这一点,我在我的测试套件中添加了以下代码。我在GCC 7.4.0、GCC 5.4.0、Clang 10.0.1和VS 2017中使用-std=c++11 -O3进行测试,下面的所有测试都通过了。
#include <gtest/gtest.h>
#include <memory>

struct A { int m;                    };
struct B { int m;            ~B(){}; };
struct C { int m; C():m(){}; ~C(){}; };
struct D { int m; D(){};             };
struct E { int m; E() = default;     };
struct F { int m; F();               }; F::F() = default;

// We use this macro to fill stack memory with something else than 0.
// Subsequent calls to EXPECT_NE(a.m, 0) are undefined behavior in theory, but
// pass in practice, and help illustrate that `a.m` is indeed not initialized
// to zero. Note that we initially tried the more aggressive test
// EXPECT_EQ(a.m, 42), but it didn't pass on all compilers (a.m wasn't equal to
// 42, but was still equal to some garbage value, not zero).
//
// Update 2020-12-14: Even the less aggressive EXPECT_NE(a.m, 0) fails in some
// machines, so we comment them out. But this change in behavior does illustrate
// that, in fact, the behavior was undefined.
//
#define FILL { int m = 42; EXPECT_EQ(m, 42); }

// We use this macro to fill heap memory with something else than 0, before
// doing a placement new at that same exact location. Subsequent calls to
// EXPECT_EQ(a->m, 42) are undefined behavior in theory, but pass in practice,
// and help illustrate that `a->m` is indeed not initialized to zero.
//
#define FILLH(b) std::unique_ptr<int> bp(new int(42)); int* b = bp.get(); EXPECT_EQ(*b, 42)

TEST(TestZero, StackDefaultInitialization)
{
    //{ FILL; A a; EXPECT_NE(a.m, 0); } // UB!
    //{ FILL; B a; EXPECT_NE(a.m, 0); } // UB!
    { FILL; C a; EXPECT_EQ(a.m, 0); }
    //{ FILL; D a; EXPECT_NE(a.m, 0); } // UB!
    //{ FILL; E a; EXPECT_NE(a.m, 0); } // UB!
    //{ FILL; F a; EXPECT_NE(a.m, 0); } // UB!
}

TEST(TestZero, StackValueInitialization)
{
    { FILL; A a = A(); EXPECT_EQ(a.m, 0); }
    { FILL; B a = B(); EXPECT_EQ(a.m, 0); }
    { FILL; C a = C(); EXPECT_EQ(a.m, 0); }
    //{ FILL; D a = D(); EXPECT_NE(a.m, 0); } // UB!
    { FILL; E a = E(); EXPECT_EQ(a.m, 0); }
    //{ FILL; F a = F(); EXPECT_NE(a.m, 0); } // UB!
}

TEST(TestZero, StackListInitialization)
{
    { FILL; A a{}; EXPECT_EQ(a.m, 0); }
    { FILL; B a{}; EXPECT_EQ(a.m, 0); }
    { FILL; C a{}; EXPECT_EQ(a.m, 0); }
    //{ FILL; D a{}; EXPECT_NE(a.m, 0); } // UB!
    { FILL; E a{}; EXPECT_EQ(a.m, 0); }
    //{ FILL; F a{}; EXPECT_NE(a.m, 0); } // UB!
}

TEST(TestZero, HeapDefaultInitialization)
{
    { FILLH(b); A* a = new (b) A; EXPECT_EQ(a->m, 42); } // ~UB
    { FILLH(b); B* a = new (b) B; EXPECT_EQ(a->m, 42); } // ~UB
    { FILLH(b); C* a = new (b) C; EXPECT_EQ(a->m, 0);  }
    { FILLH(b); D* a = new (b) D; EXPECT_EQ(a->m, 42); } // ~UB
    { FILLH(b); E* a = new (b) E; EXPECT_EQ(a->m, 42); } // ~UB
    { FILLH(b); F* a = new (b) F; EXPECT_EQ(a->m, 42); } // ~UB
}

TEST(TestZero, HeapValueInitialization)
{
    { FILLH(b); A* a = new (b) A(); EXPECT_EQ(a->m, 0);  }
    { FILLH(b); B* a = new (b) B(); EXPECT_EQ(a->m, 0);  }
    { FILLH(b); C* a = new (b) C(); EXPECT_EQ(a->m, 0);  }
    { FILLH(b); D* a = new (b) D(); EXPECT_EQ(a->m, 42); } // ~UB
    { FILLH(b); E* a = new (b) E(); EXPECT_EQ(a->m, 0);  }
    { FILLH(b); F* a = new (b) F(); EXPECT_EQ(a->m, 42); } // ~UB
}

TEST(TestZero, HeapListInitialization)
{
    { FILLH(b); A* a = new (b) A{}; EXPECT_EQ(a->m, 0);  }
    { FILLH(b); B* a = new (b) B{}; EXPECT_EQ(a->m, 0);  }
    { FILLH(b); C* a = new (b) C{}; EXPECT_EQ(a->m, 0);  }
    { FILLH(b); D* a = new (b) D{}; EXPECT_EQ(a->m, 42); } // ~UB
    { FILLH(b); E* a = new (b) E{}; EXPECT_EQ(a->m, 0);  }
    { FILLH(b); F* a = new (b) F{}; EXPECT_EQ(a->m, 42); } // ~UB
}

int main(int argc, char **argv)
{
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

提到 UB! 的地方是未定义的行为,实际行为可能取决于许多因素(a.m 可能等于 42、0 或其他一些垃圾值)。提到 ~UB 的地方在理论上也是未定义的行为,但在实践中,由于使用了放置 new,a->m 很不可能等于除 42 以外的任何值。

有些测试在我使用GCC7.4时似乎失败了:https://godbolt.org/z/PocEGz6sq...可能是GTest版本的问题吗?这很奇怪,因为代码似乎是您在GitHub上的测试套件的一部分,在那里测试仍然通过(或者至少在5个月前通过了?) - Adomas Baliuka
@AdomasBaliuka 感谢您的提醒。我很快就会回到编码中,看看测试是否仍然通过。我记得在某些平台上有一些测试没有通过,所以我不确定这是在 SO 帖子之前还是之后进行的更改。请注意,我现在正在使用 C++17,因此结果可能也会因此而有所不同。 - Boris Dalstein
@AdomasBaliuka 我添加了一个 GitHub 问题来跟踪此事,如果您感兴趣:https://github.com/vgc/vgc/issues/566 - Boris Dalstein
@AdomasBaliuka 我刚刚有时间看了一下。你是对的,“UB!”标记的测试在某些机器/编译器上确实会失败。事实上,我已经在2020年12月14日的生产代码库中将它们注释掉了(https://github.com/vgc/vgc/commit/1d3f492e6),这就是为什么我的测试通过的原因。故事的寓意是:这些肯定是未定义的行为,所以,嗯,行为是未定义的。该值不应该被零初始化,但是由于“机会”或其他因素,它可能会被初始化为零。 - Boris Dalstein

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