C ++零初始化-为什么该程序中的`b`未初始化,而`a`已初始化?


135

根据此Stack Overflow问题的公认的(唯一的)答案,

用以下方法定义构造函数

MyTest() = default;

而是将对象初始化为零。

那为什么下面这样

#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中(而不是之后),新的int()被指定为执行零初始化。

尽管试图将我的头(尽管很小)包装在默认构造函数默认初始化周围,但我无法给出一个解释。


3
有趣的是,我甚至得到对于b警告:main.cpp中:18:34:警告: 'b.bar::b'在此功能被使用的未初始化[-Wuninitialized] coliru.stacked-crooked.com/a/d1b08a4d6fb4ca7e
tkausl19年

8
bar的构造函数是用户提供的,而foo的构造函数是默认值。
Jarod42 '19

2
@PeteBecker,我知道。我如何以某种方式摇晃我的RAM,以便如果那里有零,现在应该是其他东西了。;)ps我运行了十次程序。这不是一个大程序。您可以运行它并在系统上对其进行测试。a是零。b不是。似乎a已初始化。
Duck Dodgers

2
@JoeyMallone关于“它是如何由用户提供的”:不能保证在中bar::bar()可以看到main()它的定义-它可以在单独的编译单元中定义并且在main()仅可见声明的情况下做一些非常简单的事情。我想您会同意,这种行为不应根据您bar::bar()的定义是否放在单独的编译单元中而改变(即使整个情况是不直观的)。
Max Langhof

2
@balki还是int a = 0;您想明确一点?
NathanOliver

Answers:


109

这里的问题非常微妙。你会认为

bar::bar() = default;

会为您提供编译器生成的默认构造函数,并且确实可以,但是现在认为它是用户提供的。 [dcl.fct.def.default] / 5状态:

显式默认函数和隐式声明函数统称为默认函数,实现应为其提供隐式定义([class.ctor] [class.dtor],[class.copy.ctor],[class.copy.assign ]),这可能意味着将它们定义为已删除。如果函数是由用户声明的,并且未在其第一个声明中显式默认或删除,则由用户提供。用户提供的显式默认函数(即,在其第一次声明后显式默认)在显式默认的点定义。如果将此类函数隐式定义为Delete,则程序格式错误。[注意:在第一个声明之后将函数声明为默认函数可以提供有效的执行和简洁的定义,同时还可以为不断发展的代码库提供稳定的二进制接口。—尾注]

重点矿

因此我们可以看到,由于您bar()在首次声明它时没有默认值,因此现在将其视为用户提供的。因此,[dcl.init] /8.2

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

不再适用,我们不是值初始化b,而是根据[dcl.init] /8.1对其进行默认初始化

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


52
我的意思是(*_*)....如果甚至要使用语言的基本结构,我需要阅读语言草案的精美文字,然后是哈利路亚!但这似乎就是您所说的。
Duck Dodgers

12
@balki是的,执行bar::bar() = default脱节与执行bar::bar(){}内联相同。
NathanOliver

15
@JoeyMallone是的,C ++可能非常复杂。我不确定这是什么原因。
NathanOliver

3
如果存在先前的声明,则后续的带有default关键字的定义将不会对成员进行零初始化。对?这是对的。这就是这里发生的事情。
NathanOliver

6
原因就在您的报价中:脱机默认值的目的是“提供有效的执行和简洁的定义,同时使稳定的二进制接口与不断发展的代码库兼容”,换句话说,使您可以切换到稍后在没有破坏ABI的情况下由用户编写的正文。注意,离线定义不是隐式内联的,因此默认情况下只能出现在一个TU中。另一个仅看到类定义的TU无法知道是否将其明确定义为默认值。
TC

25

在行为的差异来自这一事实,即根据[dcl.fct.def.default]/5bar::bar用户提供的,其中foo::foo1。结果,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

聚合初始化将初始化聚合。它是列表初始化的一种形式。

聚合是以下类型之一:

[片段]

  • 类类型[snip],具有

    • [snip](不同的标准版本有所不同)

    • 不允许用户提供,继承或显式的构造函数(允许使用显式默认或删除的构造函数)

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

给定此定义,它foo是一个集合,而bar不是(它具有用户提供的,非默认的构造函数)。

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

聚合初始化的效果是:

  • [snip](一些与本案无关的细节)

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

因此a.a,值已初始化,这int意味着初始化为零。

另一方面bar,对于,T object {};是值初始化(类实例,而不是成员的值初始化!)。由于它是具有默认构造函数的类类型,因此将调用默认构造函数。您定义的默认构造函数会默认初始化成员(由于没有成员初始化程序),在使用int(非静态存储)情况下,这些成员将b.b具有不确定的值。

对于pod类型,默认初始化为零初始化。

不,这是错误的。


PS关于您的实验和结论的一句话:看到输出为零并不一定意味着变量被初始化为零。零是垃圾值的完美可能数字。

为此,我可能在发布前运行了5〜6次程序,而现在大约运行了10次,所以a始终为零。b略有变化。

该值多次相同的事实并不一定意味着它也已被初始化。

我也尝试了set(CMAKE_CXX_STANDARD 14)。结果是一样的。

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

我如何以某种方式摇晃我的RAM,以便如果那里有零,现在应该是其他东西

C ++中没有保证使未初始化的值显示为非零的方法。

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


“您定义的默认构造函数默认初始化成员(由于没有成员初始化程序),如果int的话,它将为它保留不确定的值。” ->嗯!“对于pod类型,默认初始化为零初始化。” 还是我错了?
Duck Dodgers

2
@JoeyMallone POD类型的默认初始化为不初始化。
NathanOliver

@NathanOliver,那让我更加困惑。然后如何a初始化。我当时在想a默认初始化,而成员POD的默认初始化是零初始化。难道a那么就幸运总会来找零,不管我多少次运行此程序。
Duck Dodgers

@JoeyMallone Then how come a is initialized.因为它是值初始化的。I was thinking a is default initialized它不是。
eerorika

3
@JoeyMallone不用担心。您可以通过C ++的初始化来制作一本书。如果您有机会在youtube上观看CppCon,则有一些初始化视频(最令人失望的是),
网址

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+20p
.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;
}

打c,哎呀:

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
}

编译器不够聪明,无法在非优化阶段将其推论为无操作/内联调用。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.