C++零为什么这个程序中的"b"未初始化,而"a"已初始化

C++ zero Why is `b` in this program uninitialized, but `a` is initialized?

本文关键字:quot 初始化 程序 为什么 C++      更新时间:2023-10-16

根据此堆栈溢出问题的已接受(也是唯一)答案,

用定义构造函数

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

尽管我试图围绕默认构造函数和默认初始化进行思考(尽管很小),但我还是无法做出解释。

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

bar::bar() = default;

将为您提供一个编译器生成的默认构造函数,它确实如此,但现在它被认为是用户提供的。[dcl.fct.def.default]/5状态:

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

emphasis mine

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

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

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

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

行为的差异来自于这样一个事实,即根据[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的答案

来自cppreference:

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

聚合是以下类型之一:

[狙击]

  • 类类型[snip],具有

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

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

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

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

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

聚合初始化的效果是:

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

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

因此a.a是值初始化的,这对int来说意味着零初始化。

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

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

否。这是错误的。


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

为此,我在发布之前可能运行了5-6次程序,现在大约运行了10次,a总是零。b稍微改变一下。

值多次相同并不一定意味着它已经初始化。

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

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

我怎么能以某种方式稍微摇晃我的RAM,这样如果那里有零,它现在应该是其他

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

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

Meh,我尝试通过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;
}

击中叮当声,oopsie:

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
}

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