C ++ zero initialize-왜이 프로그램에서`b`가 초기화되지 않았는데`a`가 초기화 되었습니까?


135

이 스택 오버플로 질문에 대한 (그리고 유일한) 답변에 따르면 ,

로 생성자 정의

MyTest() = default;

대신 객체를 0으로 초기화합니다.

그렇다면 왜 다음을 수행합니까?

#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 유형의 경우 기본 초기화는 0 초기화입니다.

그리고에 대한 허용 대답에 따라 이 질문에 ,

  1. POD 멤버가 생성자에서 또는 C ++ 11 클래스 내 초기화를 통해 초기화되지 않은 경우 기본 초기화됩니다.

  2. 답은 스택이나 힙에 관계없이 동일합니다.

  3. C ++ 98 (그리고 나중에는 아님)에서 새로운 int ()가 0 초기화를 수행하도록 지정되었습니다.

기본 생성자기본 초기화 주위에 ( 작지만 ) 머리 를 감싸려고했지만 설명을 얻을 수 없었습니다.


3
MAIN.CPP : 18 : 흥미롭게도 I B에 대한 경고를 34 : 경고 'b.bar::b를'기능이 초기화되지 않은 사용된다 -Wuninitialized] coliru.stacked-crooked.com/a/d1b08a4d6fb4ca7e
tkausl

8
bar의 생성자는 사용자가 제공하지만 foo의 생성자는 기본값입니다.
Jarod42

2
@PeteBecker, 이해합니다. 어떻게하면 RAM이 약간 흔들려서 0이 없으면 다른 것이되어야합니다. ;) ps 프로그램을 수십 번 실행했습니다. 큰 프로그램이 아닙니다. 시스템에서이를 실행하고 테스트 할 수 있습니다. a0입니다. b아니다. 것 같습니다 a초기화됩니다.
Duck Dodgers

2
@JoeyMallone "사용자가 제공하는 방법"에 대한 정의 :의 정의 bar::bar()가 가시적 이라고 보장 main()할 수 없습니다. 별도의 컴파일 단위로 정의 될 수 있으며 main()선언 만 보이는 동안 매우 사소한 것을 수행 할 수 있습니다. 나는 당신이 bar::bar()(전체 상황이 직관적이지 않더라도) 별도의 컴파일 단위에 정의 를 배치하는지 여부에 따라이 동작이 바뀌지 않아야한다는 것에 동의 할 것이라고 생각합니다 .
Max Langhof

2
@balki 또는 int a = 0;당신은 정말 명시 적으로되고 싶습니다.
NathanOliver 2016 년

답변:


109

여기서 문제는 매우 미묘합니다. 당신은 그렇게 생각할 것입니다

bar::bar() = default;

컴파일러에서 생성 한 기본 생성자를 제공하지만 실제로는 사용자가 제공 한 것으로 간주됩니다. [dcl.fct.def.default] / 5 상태 :

명시 적 기본 함수와 암시 적으로 선언 된 함수를 통칭하여 기본 함수라고하며 구현시 이들에 대한 암시 적 정의를 제공해야합니다 ([class.ctor] [class.dtor], [class.copy.ctor], [class.copy.assign ]), 삭제 된 것으로 정의 할 수 있습니다. 함수는 사용자가 선언하고 첫 번째 선언에서 명시 적으로 기본값을 지정하거나 삭제하지 않으면 사용자가 제공합니다.사용자가 제공 한 명시 적 기본값 기능 (즉, 첫 번째 선언 후 명시 적으로 기본값이 지정됨)은 명시 적으로 기본값이 지정된 지점에서 정의됩니다. 이러한 기능이 암시 적으로 삭제 된 것으로 정의되면 프로그램이 잘못 구성됩니다. [참고 : 첫 번째 선언 후 함수를 기본값으로 선언하면 효율적인 코드 실행과 간결한 정의를 제공하면서 진화하는 코드 기반에 안정적인 이진 인터페이스를 사용할 수 있습니다. — 끝 참고]

강조 광산

따라서 bar()처음 선언 할 때 기본값을 지정하지 않았으므로 이제 사용자 제공으로 간주됩니다. 그 때문에 [dcl.init] /8.2

T가 사용자 제공 또는 삭제 된 기본 생성자가없는 클래스 유형일 가능성이있는 경우, 오브젝트는 0으로 초기화되고 기본 초기화에 대한 의미 적 제한 조건이 점검되며 T가 중요하지 않은 기본 생성자를 갖는 경우 , 객체는 기본적으로 초기화됩니다.

더 이상 적용되지 않으며 값 초기화가 b아니라 [dcl.init] /8.1에 따라 기본 초기화됩니다.

T가 기본 생성자 ([class.default.ctor])가 없거나 사용자가 제공하거나 삭제 한 기본 생성자가없는 (cv-qualified) 클래스 유형 ([class]) 인 경우 객체는 기본적으로 초기화됩니다. ;


52
내 말은 (*_*).... 언어의 기본 구성을 사용하려면 언어 초안을 잘 읽어야합니다. 그러면 할렐루야! 그러나 아마도 당신이 말하는 것 같습니다.
Duck Dodgers

12
@balki 예, bar::bar() = default라인 을 벗어난 것은 인라인을하는 것과 같습니다 bar::bar(){}.
NathanOliver

15
@JoeyMallone 네, C ++은 꽤 복잡 할 수 있습니다. 이유가 무엇인지 잘 모르겠습니다.
NathanOliver 2016 년

3
이전 선언이있는 경우 기본 키워드를 사용한 후속 정의는 멤버를 0으로 초기화하지 않습니다. 권리? 맞습니다. 여기서 일어나는 일입니다.
NathanOliver 2016 년

6
그 이유는 당신의 인용문에 있습니다. 아웃 라인의 기본값은 "진행중인 코드베이스에 안정적인 바이너리 인터페이스를 사용하면서 효율적인 실행 및 간결한 정의를 제공하는 것"입니다. 필요한 경우 나중에 ABI를 중단하지 않고 사용자 작성 본문. 라인 외부 정의는 내재적으로 인라인이 아니므로 기본적으로 하나의 TU에만 표시 될 수 있습니다. 클래스 정의 만 보는 다른 TU는 명시 적으로 기본값으로 정의되어 있는지 알 방법이 없습니다.
TC

25

거동의 차이에 따라, 사실에서 비롯 [dcl.fct.def.default]/5, bar::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-qualified) 클래스 유형 인 경우 객체는 기본적으로 초기화됩니다.

  • T가 사용자 제공 또는 삭제 된 기본 생성자가없는 클래스 유형일 가능성 이있는 경우 , 오브젝트는 0으로 초기화 되고 기본 초기화에 대한 의미 적 제한 조건이 점검되며 T가 중요하지 않은 기본 생성자를 갖는 경우 , 객체는 기본적으로 초기화됩니다.

  • ...

에서 [dcl.init.list] :

객체의 목록 초기화 또는 T 유형의 참조는 다음과 같이 정의됩니다.

  • ...

  • 그렇지 않으면 이니셜 라이저 목록에 요소가없고 T가 기본 생성자를 가진 클래스 유형 인 경우 개체는 값으로 초기화됩니다.

에서 비토리오 로미오의 대답


10

에서 cppreference :

집계 초기화는 집계를 초기화합니다. 목록 초기화 형식입니다.

집계는 다음 유형 중 하나입니다.

[한조각]

  • 클래스 유형 [snip]

    • [싹둑] (다른 표준 버전에 대한 변형이 있습니다)

    • 사용자 제공, 상속 또는 명시 적 생성자 없음 (명시 적으로 기본 또는 삭제 된 생성자가 허용됨)

    • [snip] (두 클래스에 적용되는 규칙이 더 있습니다)

이 정의 foo가 주어지면 집계 bar는 아니지만 (사용자 제공, 기본이 아닌 생성자가 있음).

따라서를위한 foo, T object {arg1, arg2, ...};집계 초기화를위한 구문입니다.

집계 초기화의 효과는 다음과 같습니다.

  • [snip] (이 경우와 관련이없는 일부 세부 정보)

  • 이니셜 라이저 절 수가 멤버 수보다 적거나 이니셜 라이저 목록이 완전히 비어있는 경우 나머지 멤버는 값으로 초기화 됩니다.

따라서 a.a값이 초기화되며 이는 초기화가 int0임을 의미합니다.

를 들어 bar, T object {};다른 한편으로는 값 초기화입니다 (클래스 인스턴스가 아닌 멤버의 값 초기화의!). 기본 생성자가있는 클래스 유형이므로 기본 생성자가 호출됩니다. 기본값을 정의한 기본 생성자는 멤버 초기화를 갖지 않기 때문에 멤버를 초기화합니다 int(정적이 아닌 스토리지의 경우), 결정 되지 b.b않은 값이 남습니다 .

포드 유형의 경우 기본 초기화는 0 초기화입니다.

아니요. 잘못되었습니다.


추신 실험과 결론에 대한 단어 : 출력이 0이라는 것이 변수가 0으로 초기화되었음을 의미하지는 않습니다. 가비지 값에는 0이 완벽하게 가능합니다.

이를 위해 게시하기 전에 프로그램을 5-6 번 정도 실행했으며 지금은 약 10 번, a는 항상 0입니다. b 조금씩 바뀝니다.

값이 여러 번 동일하다는 사실이 반드시 초기화되었다는 것을 의미하지는 않습니다.

또한 set (CMAKE_CXX_STANDARD 14)로 시도했습니다. 결과는 같습니다.

여러 컴파일러 옵션에서 결과가 동일하다는 것이 변수가 초기화되었다는 의미는 아닙니다. (경우에 따라 표준 버전을 변경하면 초기화 여부를 변경할 수 있습니다).

어떻게하면 RAM이 약간 흔들려서 거기에 0이 있으면 다른 무언가가되어야합니다.

초기화되지 않은 값을 0이 아닌 것으로 만드는 C ++의 보장 방법은 없습니다.

변수가 초기화되었음을 아는 유일한 방법은 프로그램을 언어 규칙과 비교하고 규칙이 초기화되었다고 말하는지 확인하는 것입니다. 이 경우 a.a실제로 초기화됩니다.


"기본으로 정의한 기본 생성자는 멤버 이니셜 라이저가 없기 때문에 멤버를 초기화합니다. int의 경우에는 불확실한 값으로 남겨집니다." -> 어! "포드 유형의 경우 기본 초기화는 0 초기화입니다." 아니면 내가 틀렸어?
Duck Dodgers

2
@JoeyMallone POD 유형의 기본 초기화는 초기화되지 않습니다.
NathanOliver

@NathanOliver, 그러면 더 혼란 스럽습니다. 그러면 어떻게 와서 a초기화됩니다. 나는 a기본적으로 초기화되고 멤버 POD의 기본 초기화는 0으로 초기화 되었다고 생각했습니다 . 그렇다면 a이 프로그램을 몇 번이나 실행하더라도 운 좋게도 항상 제로가됩니다.
Duck Dodgers

@JoeyMallone Then how come a is initialized.값이 초기화 되었기 때문입니다. I was thinking a is default initialized그렇지 않습니다.
eerorika

3
@JoeyMallone 걱정하지 마십시오. C ++로 책을 초기화하지 않아도됩니다. 유튜브는 대부분의 (그것이 얼마나 나쁜 지적처럼) 실망과 초기화에 몇 가지 동영상을 가지고 당신은있는 기회의 CppCon을받을 경우 youtube.com/watch?v=7DTlWPgX6zs을
NathanOliver

0

Meh, test.cppgcc & 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;
}

멍청한 놈,

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
}

컴파일러는 최적화되지 않은 단계에서 이것을 no-op / inline 호출로 추론하기에 충분하지 않습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.