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

时间:2019-01-24 15:29:05

标签: c++ initialization language-lawyer

根据this Stack Overflow question接受的(也是唯一的)答案,

  

使用

定义构造函数
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类型,默认初始化为零初始化。

然后根据接受的this question答案,

  
      
  1. 如果未在构造函数中也不通过C ++ 11初始化POD成员   类初始化,它是默认初始化的。

  2.   
  3. 答案是相同的,无论是堆栈还是堆。

  4.   
  5. 在C ++ 98中(而非之后),新的int()被指定为执行   零初始化。

  6.   

尽管试图将我的(尽管是 tiny )头缠在default constructorsdefault initialization上,但我无法给出解释。

4 个答案:

答案 0 :(得分:104)

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

bar::bar() = default;

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

  

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

强调我的

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

  

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

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

  

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

答案 1 :(得分:24)

行为上的差异来自以下事实:根据[dcl.fct.def.default]/5bar::bar是用户提供的 ,其中foo::foo不是 1 < / sup>。因此,foo::foo将对其成员进行 value-initialize (意思是: zero-initialize foo::a),但是bar::bar将保持未初始化 2


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

  

如果函数是用户声明的,并且不是在其第一个声明中显式默认或删除的,则由用户提供。

2)

  

来自[dcl.init#6]

     
    

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

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

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

  •     
  • ...

  •     
  
     

来自[dcl.init.list]

     
    

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

         
        
  • ...

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

  •     
  

来自Vittorio Romeo's answer

答案 2 :(得分:10)

来自cppreference

  

聚集初始化将初始化聚集。这是列表初始化的一种形式。

     

聚集是以下类型之一:

     

[snip]

     
      
  • 具有

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

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

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

    •   
  •   

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

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

  

聚合初始化的效果是:

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

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

  •   

因此,a.a是值初始化的,对于int来说是零初始化。

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

  

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

不。这是错误的。


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

  

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

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

  

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

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

  

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

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

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

答案 3 :(得分:0)

嗯,我尝试通过gcc和clang以及多个优化级别来运行您作为test.cpp提供的代码段:

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 ]

这就是有趣的地方,它清楚地显示出c ++ 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;
}

哎呀,H一声:

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
}

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