【问题标题】:C++ zero initialization - Why is `b` in this program uninitialized, but `a` is initialized?C++ 零初始化 - 为什么这个程序中的 `b` 未初始化,但 `a` 已初始化?
【发布时间】:2019-06-18 09:26:29
【问题描述】:

根据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. 如果 POD 成员未在构造函数中或通过 C++11 初始化 类内初始化,默认初始化。

  2. 无论栈还是堆,答案都是一样的。

  3. 在 C++98(而不是之后)中,new int() 被指定为执行 零初始化。

尽管我试图将我(尽管 )的脑袋绕在 default constructorsdefault initialization 周围,但我无法给出解释。

【问题讨论】:

  • 有趣的是,我什至收到 b 的警告:main.cpp:18:34:警告:'b.bar::b' 在此函数中未初始化 [-Wuninitialized] coliru.stacked-crooked.com/a/d1b08a4d6fb4ca7e
  • bar 的构造函数是用户提供的,而foo 的构造函数是默认的。
  • @JoeyMallone 关于“它是如何由用户提供的”:不能保证bar::bar() 的定义在main() 中可见-它可能在单独的编译单元中定义并执行某些操作在main() 中只有声明可见。我想你会同意这种行为不应该根据你是否将 bar::bar() 的定义放在单独的编译单元中而改变(即使整个情况不直观)。
  • @balki 或int a = 0; 是不是你想明确表达。
  • 语言应该包含的特质的好例子......

标签: c++ initialization language-lawyer


【解决方案1】:

嗯,我尝试通过 gcc 和 clang 以及多个优化级别运行您提供为 test.cpp 的 sn-p:

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;
}

敲响叮当,哎呀:

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
}

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

【讨论】:

    【解决方案2】:

    来自cppreference

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

    聚合是以下类型之一:

    [剪辑]

    • 类类型 [snip],具有

      • [snip](不同标准版本存在差异)

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

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

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

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

    聚合初始化的效果是:

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

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

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

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

    对于 pod-types,默认初始化是零初始化。

    没有。这是错误的。


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

    为此,我在发布之前可能运行了 5~6 次,现在大约运行了 10 次,a 始终为零。 b 略有变化。

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

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

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

    我怎么能以某种方式摇动我的 RAM,如果那里是零,现在应该是别的东西了

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

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

    【讨论】:

    • “你定义的默认构造函数 default 初始化成员(由于没有成员初始化器),在 int 的情况下它留下一个不确定的值。” - -> 嗯! “对于 pod 类型,默认初始化是零初始化。”还是我错了?
    • @JoeyMallone POD 类型的默认初始化是不初始化。
    • @NathanOliver,那我就更糊涂了。那么a是怎么初始化的。我在想a 是默认初始化的,而成员 POD 的默认初始化是零初始化。是a 那么幸运的是,无论我运行这个程序多少次,它总是为零。
    • @JoeyMallone Then how come a is initialized. 因为它是值初始化的。 I was thinking a is default initialized不是。
    • @JoeyMallone 别担心。你可以用 C++ 的初始化写一本书。如果你有机会在 youtube 上的 CppCon 有一些关于初始化的视频,其中最令人失望(指出它有多糟糕)是 youtube.com/watch?v=7DTlWPgX6zs
    【解决方案3】:

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

    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]) 或用户提供或删除的默认构造函数,则对象为默认初始化;

    【讨论】:

    • 我的意思是(*_*) .... 如果要使用语言的基本结构,我需要阅读语言草案的细则,然后哈利路亚!但它可能似乎是你所说的。
    • @balki 是的,在行外执行bar::bar() = default 与行内执行bar::bar(){} 相同。
    • @JoeyMallone 是的,C++ 可能相当复杂。我不确定这是什么原因。
    • 如果有先前的声明,那么使用 default 关键字的后续定义将不会零初始化成员。对吗? 这是正确的。这就是这里发生的事情。
    • 原因就在您的引用中:离线默认值的要点是“提供高效执行和简洁的定义,同时为不断发展的代码库提供稳定的二进制接口”,在换句话说,如果需要,您可以稍后切换到用户编写的正文,而不会破坏 ABI。注意,外联定义并不是隐式内联的,所以默认只能出现在一个TU中;单独看到类定义的另一个 TU 无法知道它是否被明确定义为默认值。
    【解决方案4】:

    行为的不同在于,根据[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 是没有默认构造函数 ([class.ctor]) 或用户提供或删除的默认构造函数的(可能是 cv 限定的)类类型,则该对象是默认初始化的;

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

    • ...

    来自[dcl.init.list]

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

    • ...

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

    来自Vittorio Romeo's answer

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-12-29
      • 2018-07-26
      • 2021-11-11
      • 2020-12-25
      • 1970-01-01
      • 2010-12-09
      相关资源
      最近更新 更多