C++零初始化 - 为什么在这个程序中'b'未初始化,而'a'已经初始化了?

141

根据这个Stack Overflow问题的唯一被接受的答案,

Defining the constructor with

MyTest() = default;

will instead zero-initialize the object.

那么下面的内容为什么会出现,
#include <iostream>

struct foo {
    foo() = default;
    int a;
};

struct bar {
    bar();
    int b;
};

bar::bar() = default;

int main() {
    foo a{};
    bar b{};
    std::cout << a.a << ' ' << b.b;
}

生成此输出:

0 32766

两个构造函数都是默认的?对吗?对于POD类型,默认初始化是零初始化。
根据这个问题的接受答案,
1. 如果POD成员在构造函数中没有被初始化,也没有通过C++11中类内初始化,则会默认初始化。 2. 无论是堆还是栈,答案都是相同的。 3. 在C++98(以后不再支持),new int()被指定为执行零初始化。
尽管我试图理解默认构造函数默认初始化,但我无法得出解释。

3
有趣的是,即使是在b变量上我也收到了警告:main.cpp:18:34: 警告:'b.bar::b' 在此函数中未初始化 [-Wuninitialized] http://coliru.stacked-crooked.com/a/d1b08a4d6fb4ca7e - tkausl
9
bar的构造函数是用户自定义的,而foo的构造函数是默认的。 - Jarod42
3
关于“how is it user-provided”的问题:不能保证在main()中可见bar::bar()的定义——它可能是在单独的编译单元中定义并且执行非常不平凡的操作,而在main()中只有声明是可见的。我认为您会同意,这种行为不应因将bar::bar()的定义放在单独的编译单元中与否而改变(即使整个情况令人费解)。 - Max Langhof
3
如果你想要更加明确,可以使用int a = 0; - NathanOliver
3
一个语言不应该包含的个性化特征的绝佳例子... - cmaster - reinstate monica
显示剩余15条评论
4个回答

112
这里的问题相当微妙。你可能会认为{{...}}。
bar::bar() = default;

你将会得到一个编译器生成的默认构造函数,但现在它被视为用户提供的。[dcl.fct.def.default]/5规定:显式默认函数和隐式声明函数统称为默认函数,实现应为它们提供隐式定义([class.ctor] [class.dtor],[class.copy.ctor],[class.copy.assign]),这可能意味着将它们定义为已删除。如果第一次声明时用户已声明且未明确默认或删除,则函数是由用户提供的。用户提供的显式默认函数(即在第一次声明后显式默认)在显式默认的点上定义;如果此类函数被隐式定义为已删除,则程序无效。因此,由于您没有在首次声明时默认bar(),所以它现在被视为用户提供的。由于这个原因,[dcl.init]/8.2规定:如果T是一个(可能是cv限定的)类类型,没有用户提供或已删除的默认构造函数,则对象被零初始化,并检查默认初始化的语义约束,如果T有一个非平凡的默认构造函数,则对象被默认初始化。

不再适用,我们不会使用值初始化b,而是按照[dcl.init]/8.1的规定进行默认初始化。

如果T是一个(可能带有cv限定符的)类类型([class]),且没有默认构造函数([class.default.ctor])或者其默认构造函数是用户提供的或已删除,则对象将被默认初始化;


53
我理解 (*_*) 的意思是惊讶或者困惑。如果我甚至需要阅读语言草案的详细说明才能使用基本结构, 那就太难了!但这可能就是你所说的情况。 - Duck Dodgers
14
是的,将bar::bar() = default放在类外与内部定义bar::bar(){}是一样的。 - NathanOliver
15
是的,C++可能会非常复杂。我不确定这是什么原因。 - NathanOliver
3
如果有先前的声明,那么随后使用默认关键字进行定义将不会对成员进行零初始化。对吗?是的,这是正确的。这就是正在发生的事情。 - NathanOliver
6
你引用的原话已经说明了原因:离线默认值的目的是“提供高效执行和简洁定义,同时使演变的代码库具有稳定的二进制接口”,也就是说,使你能够在必要时切换到用户编写的主体而不会破坏ABI。请注意,离线定义不是隐式内联的,因此默认情况下只能出现在一个TU中;另一个TU仅看到类定义本身时,无法知道它是否明确定义为默认值。 - T.C.
显示剩余13条评论

24
行为上的差异源于根据 [dcl.fct.def.default]/5bar::bar用户提供的,而 foo::foo 不是1。因此,foo::foo值初始化其成员(即零初始化foo::a),但bar::bar将保持未初始化状态2

1) [dcl.fct.def.default]/5

如果函数是用户声明的并且在其第一次声明时没有显式地默认或删除,则该函数是用户提供的。

2)

来自[dcl.init#6]:

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

  • 如果T是一个(可能是cv限定的)类类型,它没有默认构造函数([class.ctor])或默认构造函数是用户提供的或已删除的,则对象是默认初始化的;

  • 如果T是一个(可能是cv限定的)类类型,没有用户提供或已删除的默认构造函数,则对象将被零初始化,并检查默认初始化的语义约束,如果T有一个非平凡的默认构造函数,则对象将被默认初始化;

  • ...

来自[dcl.init.list]:

类型为T的对象或引用的列表初始化定义如下:

  • ...

  • 否则,如果初始化器列表没有元素并且T是具有默认构造函数的类类型,则对象将进行值初始化。

来自Vittorio Romeo的回答


10

来自cppreference

聚合初始化用于初始化聚合体。它是一种列表初始化形式。

聚合体包括以下类型之一:

[省略]

  • 类类型[省略],具有

    • [省略](不同标准版本有不同的变化)

    • 没有用户提供的、继承的或显式构造函数(显式默认或删除的构造函数允许)

    • [省略](还有更多规则,适用于两个类)

根据这个定义,foo 是一个聚合体,而 bar 不是(它有用户提供的非默认构造函数)。

因此对于 fooT object {arg1, arg2, ...}; 是聚合初始化的语法。

聚合初始化的效果包括:

  • [省略](一些与此情况无关的细节)

  • 如果初始化程序的数量少于成员数量或初始化列表完全为空,则剩余的成员将被值初始化

因此,a.a 被值初始化,对于 int,这意味着零初始化。

对于 barT object {}; 是值初始化(类实例的值初始化,而不是成员变量的值初始化!)。由于它是一个带有默认构造函数的类类型,因此会调用默认构造函数。您定义的默认构造函数通过没有成员初始值设定项来默认初始化成员(通过没有成员初始值设定项来默认初始化成员),在 int 的情况下(具有非静态存储),这将使 b.b 具有不确定的值。

而对于 pod-types,其默认初始化为零初始化。

不,这是错误的。


P.S. 关于你的实验和结论:看到输出为零并不一定意味着变量被初始化为零。零也可能是垃圾值的一种。

因此,在发布之前,我运行了大约5~6次程序,现在已经运行了大约10次,a总是为零。b会稍微变化。

同样的值出现多次并不一定意味着它被初始化了。

我还尝试使用set(CMAKE_CXX_STANDARD 14)。结果是相同的。

多个编译器选项得到相同的结果并不意味着变量被初始化了。(尽管在某些情况下,更改标准版本可能会改变是否初始化)。

我该如何让我的RAM稍微晃动一下,以便如果有零存在,它现在应该是其他值

C++中没有保证使未初始化的值出现非零的方法。

唯一知道变量是否初始化的方法是将程序与语言规则进行比较,并验证规则是否表明它被初始化。在这种情况下,a.a确实被初始化了。


2
@JoeyMallone POD类型的默认初始化是不进行初始化。 - NathanOliver
@JoeyMallone 默认 的 POD 初始化不是初始化,但这里不是这种情况。在这里,您正在进行初始化,它会执行零初始化。 - NathanOliver
哎呀,好的(你们两个),谢谢你们澄清了这个问题。显然,我不知道我的值初始化和默认初始化有什么区别。谢谢。 - Duck Dodgers
3
别担心。你可以写一本关于C++初始化的书。如果你有机会,可以在Youtube上看CppCon,有一些关于初始化的视频,其中最令人失望(指出它有多糟糕)的是https://www.youtube.com/watch?v=7DTlWPgX6zs。 - NathanOliver
我稍后会去阅读有关默认初始化与值初始化的内容。 - Duck Dodgers
显示剩余4条评论

0

我试着运行你提供的代码片段 test.cpp,通过gcc和clang以及多个优化级别:

steve@steve-pc /tmp> g++ -o test.gcc.O0 test.cpp
                                                                              [ 0s828 | Jan 27 01:16PM ]
steve@steve-pc /tmp> g++ -o test.gcc.O2 -O2 test.cpp
                                                                              [ 0s901 | Jan 27 01:16PM ]
steve@steve-pc /tmp> g++ -o test.gcc.Os -Os test.cpp
                                                                              [ 0s875 | Jan 27 01:16PM ]
steve@steve-pc /tmp> ./test.gcc.O0
0 32764                                                                       [ 0s004 | Jan 27 01:16PM ]
steve@steve-pc /tmp> ./test.gcc.O2
0 0                                                                           [ 0s004 | Jan 27 01:16PM ]
steve@steve-pc /tmp> ./test.gcc.Os
0 0                                                                           [ 0s003 | Jan 27 01:16PM ]
steve@steve-pc /tmp> clang++ -o test.clang.O0 test.cpp
                                                                              [ 1s089 | Jan 27 01:17PM ]
steve@steve-pc /tmp> clang++ -o test.clang.Os -Os test.cpp
                                                                              [ 1s058 | Jan 27 01:17PM ]
steve@steve-pc /tmp> clang++ -o test.clang.O2 -O2 test.cpp
                                                                              [ 1s109 | Jan 27 01:17PM ]
steve@steve-pc /tmp> ./test.clang.O0
0 274247888                                                                   [ 0s004 | Jan 27 01:17PM ]
steve@steve-pc /tmp> ./test.clang.Os
0 0                                                                           [ 0s004 | Jan 27 01:17PM ]
steve@steve-pc /tmp> ./test.clang.O2
0 0                                                                           [ 0s004 | Jan 27 01:17PM ]
steve@steve-pc /tmp> ./test.clang.O0
0 2127532240                                                                  [ 0s002 | Jan 27 01:18PM ]
steve@steve-pc /tmp> ./test.clang.O0
0 344211664                                                                   [ 0s004 | Jan 27 01:18PM ]
steve@steve-pc /tmp> ./test.clang.O0
0 1694408912                                                                  [ 0s004 | Jan 27 01:18PM ]

这就是有趣的地方,它清楚地显示出clang O0构建正在读取随机数字,可能是堆栈空间。

我立刻打开我的IDA看看发生了什么:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  __int64 v3; // rax
  __int64 v4; // rax
  int result; // eax
  unsigned int v6; // [rsp+8h] [rbp-18h]
  unsigned int v7; // [rsp+10h] [rbp-10h]
  unsigned __int64 v8; // [rsp+18h] [rbp-8h]

  v8 = __readfsqword(0x28u); // alloca of 0x28
  v7 = 0; // this is foo a{}
  bar::bar((bar *)&v6); // this is bar b{}
  v3 = std::ostream::operator<<(&std::cout, v7); // this is clearly 0
  v4 = std::operator<<<std::char_traits<char>>(v3, 32LL); // 32 = 0x20 = ' '
  result = std::ostream::operator<<(v4, v6); // joined as cout << a.a << ' ' << b.b, so this is reading random values!!
  if ( __readfsqword(0x28u) == v8 ) // stack align check
    result = 0;
  return result;
}

现在,bar::bar(bar *this) 是做什么的?
void __fastcall bar::bar(bar *this)
{
  ;
}

嗯,什么也没有。我们不得不使用汇编语言:

.text:00000000000011D0                               ; __int64 __fastcall bar::bar(bar *__hidden this)
.text:00000000000011D0                                               public _ZN3barC2Ev
.text:00000000000011D0                               _ZN3barC2Ev     proc near               ; CODE XREF: main+20↓p
.text:00000000000011D0
.text:00000000000011D0                               var_8           = qword ptr -8
.text:00000000000011D0
.text:00000000000011D0                               ; __unwind {
.text:00000000000011D0 55                                            push    rbp
.text:00000000000011D1 48 89 E5                                      mov     rbp, rsp
.text:00000000000011D4 48 89 7D F8                                   mov     [rbp+var_8], rdi
.text:00000000000011D8 5D                                            pop     rbp
.text:00000000000011D9 C3                                            retn
.text:00000000000011D9                               ; } // starts at 11D0
.text:00000000000011D9                               _ZN3barC2Ev     endp

所以,这只是一个无用的构造函数,它基本上做的就是this = this。但我们知道它实际上正在加载随机未初始化的堆栈地址并打印它。

如果我们明确为这两个结构提供值会怎样呢?

#include <iostream>

struct foo {
    foo() = default;
    int a;
};

struct bar {
    bar();
    int b;
};

bar::bar() = default;

int main() {
    foo a{0};
    bar b{0};
    std::cout << a.a << ' ' << b.b;
}

运行clang,出了点小问题:

steve@steve-pc /tmp> clang++ -o test.clang.O0 test.cpp
test.cpp:17:9: error: no matching constructor for initialization of 'bar'
    bar b{0};
        ^~~~
test.cpp:8:8: note: candidate constructor (the implicit copy constructor) not viable: no known conversion
      from 'int' to 'const bar' for 1st argument
struct bar {
       ^
test.cpp:8:8: note: candidate constructor (the implicit move constructor) not viable: no known conversion
      from 'int' to 'bar' for 1st argument
struct bar {
       ^
test.cpp:13:6: note: candidate constructor not viable: requires 0 arguments, but 1 was provided
bar::bar() = default;
     ^
1 error generated.
                                                                              [ 0s930 | Jan 27 01:35PM ]

g++也有类似的命运:

steve@steve-pc /tmp> g++ test.cpp
test.cpp: In function ‘int main()’:
test.cpp:17:12: error: no matching function for call to ‘bar::bar(<brace-enclosed initializer list>)’
     bar b{0};
            ^
test.cpp:8:8: note: candidate: ‘bar::bar()’
 struct bar {
        ^~~
test.cpp:8:8: note:   candidate expects 0 arguments, 1 provided
test.cpp:8:8: note: candidate: ‘constexpr bar::bar(const bar&)’
test.cpp:8:8: note:   no known conversion for argument 1 from ‘int’ to ‘const bar&’
test.cpp:8:8: note: candidate: ‘constexpr bar::bar(bar&&)’
test.cpp:8:8: note:   no known conversion for argument 1 from ‘int’ to ‘bar&&’
                                                                              [ 0s718 | Jan 27 01:35PM ]

这意味着它实际上是直接初始化bar b(0),而不是聚合初始化。

这可能是因为如果您没有提供显式构造函数实现,这可能会成为一个外部符号,例如:

bar::bar() {
  this.b = 1337; // whoa
}

编译器在非优化阶段无法将其推断为无操作/内联调用。

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