構造体 - C++ゼロ初期化-このプログラムの `b`が初期化されていないのに、` a`が初期化されているのはなぜですか?



ポインタ 初期化 null (3)

この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()がゼロ初期化を実行するように指定されていました。

(とはいえ)私の デフォルトのコンストラクタ デフォルトの初期化 をラップしようとしたにもかかわらず、私は説明を思いつくことができませんでした。


Answer #1

ええと、私はあなたが test.cpp として提供したスニペットをgccとclangと複数の最適化レベルを通して走らせてみました:

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

だからそれはそれが面白くなるところです、それは明らかにクラン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 です。 しかし、我々はそれが実際にランダムな未初期化スタックアドレスをロードしてそれを印刷していることを知っています。

2つの構造体に明示的に値を指定した場合はどうなりますか?

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

こんにちは。

[email protected]-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 ++でも同様の運命:

[email protected]-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
}

コンパイラは、これを最適化されていない段階での無操作/インライン呼び出しとして推論するほど賢くはありません。


Answer #2

ここの問題はかなり微妙です。 あなたはそれを考えるだろう

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修飾の)クラス型である場合、オブジェクトはゼロで初期化され、デフォルト初期化のための意味上の制約がチェックされます。オブジェクトはデフォルトで初期化されています。

適用されなくなり、 b 初期化する値ではなく、代わりに [dcl.init]/8.1 に従ってデフォルトで初期化されます。

Tがデフォルトコンストラクタ([class.default.ctor])を持たない(またはcv修飾可能な)クラス型([class.default.ctor])またはユーザ指定または削除されたデフォルトコンストラクタのいずれかである場合、オブジェクトはデフォルトで初期化されます。 ;


Answer #3

cppreference から:

集合体の初期化は集合体を初期化します。 これはリスト初期化の形式です。

集約は、以下のいずれかのタイプです。

[スニップ]

  • クラス型[snip]

    • [snip](標準バージョンごとにバリエーションがあります)

    • ユーザー提供、継承、または明示的コンストラクター(明示的にデフォルトまたは削除されたコンストラクターは許可されません)

    • [スニップ](両方のクラスに適用されるより多くの規則があります)

この定義では、 foo は集約ですが、 bar はそうではありません(ユーザ指定のデフォルトではないコンストラクタがあります)。

したがって、 foo では、 T object {arg1, arg2, ...}; 集約初期化の構文です。

集約初期化の効果は次のとおりです。

  • [snip](この場合には関係のない詳細がいくつかあります)

  • 初期化子句の数がメンバの数より少ない場合、または初期化子リストが完全に空の場合、残りの メンバは値で初期化されます

したがって、 aa は初期化された値です。これは、 int 場合、ゼロの初期化を意味します。

bar 場合、 T object {}; 一方、(クラスインスタンスの値の初期化であり、メンバの値の初期化ではありません。) これはデフォルトコンストラクタを持つクラス型なので、デフォルトコンストラクタが呼び出されます。 defaultを定義したデフォルトコンストラクタは、メンバを初期化します(メンバ初期化子を持たないため)。これは、 int 場合(非静的記憶領域を含む)、 bb に不定の値を残します。

ポッドタイプの場合、デフォルトの初期化はゼロ初期化です。

いいえ、違います。

追伸あなたの実験とあなたの結論についての言葉:出力がゼロであるということは、変数がゼロで初期化されたことを必ずしも意味しません。 ゼロは、ゴミの値として完全に可能な数です。

そのため、私はプログラムを投稿する前にたぶん5〜6回、今では約10回実行しました。aは常にゼロです。 bは少し変わります。

値が複数回同じであったという事実は、必ずしもそれが初期化されたという意味でもありません。

私もset(CMAKE_CXX_STANDARD 14)で試しました。 結果は同じでした。

結果が複数のコンパイラオプションで同じであるという事実は、変数が初期化されることを意味するのではありません。 (場合によっては、標準バージョンを変更すると初期化されるかどうかが変わる可能性があります)。

どういうわけか私のRAMを少し振ることができたのでそこにゼロがあった場合、それは今他の何かになるはずです

C ++には、初期化されていない値をゼロ以外の値にするための保証された方法はありません。

変数が初期化されたことを知る唯一の方法は、プログラムを言語の規則と比較し、その規則が初期化されたことを示すことを検証することです。 この場合、 aa は確かに初期化されています。





language-lawyer