Khởi tạo không C ++ - Tại sao `b` trong chương trình này chưa được khởi tạo, nhưng` a` được khởi tạo?


135

Theo câu trả lời được chấp nhận (và duy nhất) cho câu hỏi Stack Overflow này ,

Xác định hàm tạo với

MyTest() = default;

thay vào đó sẽ khởi tạo không đối tượng.

Vậy thì tại sao lại như sau,

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

sản xuất đầu ra này:

0 32766

Cả hai constructor được định nghĩa là mặc định? Đúng? Và đối với các loại POD, khởi tạo mặc định là không khởi tạo.

Và theo câu trả lời được chấp nhận cho câu hỏi này ,

  1. Nếu một thành viên POD không được khởi tạo trong hàm tạo cũng như thông qua khởi tạo trong lớp C ++ 11, thì nó được khởi tạo mặc định.

  2. Câu trả lời là như nhau bất kể stack hay heap.

  3. Trong C ++ 98 (và không phải sau đó), int () mới được chỉ định là thực hiện khởi tạo 0.

Mặc dù cố gắng quấn của tôi (mặc dù nhỏ ) đầu xung quanh nhà xây dựng mặc địnhkhởi tạo mặc định , tôi không thể đưa ra một lời giải thích.


3
Thật thú vị, tôi thậm chí còn nhận được một cảnh báo cho b: main.cpp: 18: 34: cảnh báo: 'b.bar::b' được sử dụng chưa được khởi tạo trong chức năng này [-Wuninitialized] coliru.stacked-crooking.com/a/d1b08a4d6fb4ca7e
tkausl

8
barPhương thức khởi tạo là người dùng được cung cấp trong khi phương foothức khởi tạo là mặc định.
Jarod42

2
@PeteBecker, tôi hiểu điều đó. Làm thế nào tôi có thể bằng cách nào đó lắc RAM của tôi một chút để nếu không có số 0 ở đó, bây giờ nó sẽ là một cái gì đó khác. ;) ps Tôi đã chạy chương trình một chục lần. Nó không phải là một chương trình lớn. Bạn có thể chạy nó và kiểm tra nó trên hệ thống của bạn. abằng không. bkhông phải. Có vẻ như ađược khởi tạo.
Vịt Dodgers

2
@JoeyMallone Về "nó được cung cấp bởi người dùng như thế nào": Không có gì đảm bảo rằng định nghĩa của nó có thể bar::bar()nhìn thấy được main()- nó có thể được định nghĩa trong một đơn vị biên dịch riêng biệt và làm một cái gì đó không tầm thường trong khi main()chỉ hiển thị tuyên bố. Tôi nghĩ bạn sẽ đồng ý rằng hành vi này không nên thay đổi tùy thuộc vào việc bạn đặt bar::bar()định nghĩa của bạn trong một đơn vị biên dịch riêng biệt hay không (ngay cả khi toàn bộ tình huống là không trực quan).
Max Langhof

2
@balki Hoặc int a = 0;là bạn muốn thực sự rõ ràng.
NathanOliver

Câu trả lời:


109

Vấn đề ở đây là khá tinh tế. Bạn sẽ nghĩ rằng

bar::bar() = default;

sẽ cung cấp cho bạn một trình biên dịch tạo trình tạo mặc định, và nó có, nhưng hiện tại nó được coi là người dùng cung cấp. [dcl.fct.def.default] / 5 tiểu bang:

Các hàm được mặc định rõ ràng và các hàm được khai báo ngầm được gọi chung là các hàm mặc định và việc triển khai sẽ cung cấp các định nghĩa ngầm cho chúng ([class.ctor] [class.dtor], [class.copy.ctor], [class.copy.assign ]), có nghĩa là xác định chúng là đã xóa. Một hàm được người dùng cung cấp nếu nó được khai báo bởi người dùng và không được mặc định hoặc xóa rõ ràng trong khai báo đầu tiên của nó.Hàm được mặc định rõ ràng do người dùng cung cấp (nghĩa là được mặc định rõ ràng sau khi khai báo đầu tiên) được xác định tại điểm mà nó được mặc định rõ ràng; nếu một chức năng như vậy được định nghĩa ngầm là bị xóa, chương trình sẽ không được định dạng. [Lưu ý: Khai báo một hàm như được mặc định sau khai báo đầu tiên của nó có thể cung cấp thực thi hiệu quả và định nghĩa ngắn gọn trong khi cho phép giao diện nhị phân ổn định cho cơ sở mã phát triển. - lưu ý cuối]

nhấn mạnh của tôi

Vì vậy, chúng ta có thể thấy rằng vì bạn không mặc định bar()khi bạn khai báo lần đầu tiên, nên giờ đây nó được coi là người dùng cung cấp. Do đó [dcl.init] /8.2

nếu T là loại lớp (có thể đủ điều kiện cv) mà không có hàm tạo mặc định do người dùng cung cấp hoặc bị xóa, thì đối tượng được khởi tạo bằng 0 và các ràng buộc ngữ nghĩa cho khởi tạo mặc định được chọn và nếu T có hàm tạo mặc định không tầm thường , đối tượng được khởi tạo mặc định;

không còn áp dụng và chúng tôi không khởi tạo giá trị bmà thay vào đó, khởi tạo mặc định theo [dcl.init] /8.1

nếu T là loại lớp (có thể đủ điều kiện cv) ([class]) không có hàm tạo mặc định ([class.default.ctor]) hoặc hàm tạo mặc định do người dùng cung cấp hoặc bị xóa, thì đối tượng được khởi tạo mặc định ;


52
Ý tôi là (*_*).... Nếu thậm chí sử dụng các cấu trúc cơ bản của ngôn ngữ, tôi cần đọc bản in đẹp của bản thảo ngôn ngữ, thì Hallelujah! Nhưng nó có vẻ là những gì bạn nói.
Vịt Dodgers

12
@balki Vâng, làm bar::bar() = defaultngoài luồng cũng giống như làm bar::bar(){}nội tuyến.
NathanOliver

15
@JoeyMallone Vâng, C ++ có thể khá phức tạp. Tôi không chắc lý do của việc này là gì.
NathanOliver

3
Nếu có một tuyên bố trước đó, thì một định nghĩa tiếp theo với từ khóa mặc định sẽ KHÔNG khởi tạo các thành viên. Đúng? Chính xác. Đó là những gì đang xảy ra ở đây.
NathanOliver

6
Lý do là có ngay trong trích dẫn của bạn: điểm mặc định ngoài luồng là "cung cấp thực thi hiệu quả và định nghĩa ngắn gọn trong khi cho phép giao diện nhị phân ổn định thành cơ sở mã đang phát triển", nói cách khác, cho phép bạn chuyển sang một cơ thể người dùng viết sau này nếu cần thiết mà không phá vỡ ABI. Lưu ý rằng định nghĩa ngoài dòng không hoàn toàn theo dòng và do đó chỉ có thể xuất hiện trong một TU theo mặc định; một TU khác nhìn thấy định nghĩa lớp một mình không có cách nào để biết nếu nó được định nghĩa rõ ràng là mặc định.
TC

25

Sự khác biệt trong hành vi xuất phát từ thực tế là, theo [dcl.fct.def.default]/5, bar::barlà do người dùng cung cấp , nơi foo::fookhông phải là 1 . Kết quả là, foo::foosẽ khởi tạo giá trị các thành viên của nó (có nghĩa là: không khởi tạo foo::a ) nhưng bar::barsẽ không được khởi tạo 2 .


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

Một hàm được người dùng cung cấp nếu nó được khai báo bởi người dùng và không được mặc định hoặc xóa rõ ràng trong khai báo đầu tiên của nó.

2)

Từ [dcl.init # 6] :

Để khởi tạo giá trị một đối tượng thuộc loại T có nghĩa là:

  • nếu T là loại lớp (có thể đủ điều kiện cv) không có hàm tạo mặc định ([class.ctor]) hoặc hàm tạo mặc định do người dùng cung cấp hoặc bị xóa, thì đối tượng được khởi tạo mặc định;

  • nếu T là loại lớp (có thể đủ điều kiện cv) mà không có hàm tạo mặc định do người dùng cung cấp hoặc đã xóa, thì đối tượng được khởi tạo bằng 0 và các ràng buộc ngữ nghĩa cho khởi tạo mặc định được chọn và nếu T có hàm tạo mặc định không tầm thường , đối tượng được khởi tạo mặc định;

  • ...

Từ [dcl.init.list] :

Danh sách khởi tạo một đối tượng hoặc tham chiếu loại T được định nghĩa như sau:

  • ...

  • Mặt khác, nếu danh sách trình khởi tạo không có phần tử và T là loại lớp có hàm tạo mặc định, đối tượng được khởi tạo giá trị.

Từ câu trả lời của Vittorio Romeo


10

Từ cppreference :

Tổng hợp khởi tạo khởi tạo tổng hợp. Nó là một hình thức khởi tạo danh sách.

Một tổng hợp là một trong các loại sau:

[bắn tỉa]

  • loại lớp [snip], có

    • [snip] (có các biến thể cho các phiên bản tiêu chuẩn khác nhau)

    • không có các hàm tạo do người dùng cung cấp, kế thừa hoặc rõ ràng (các hàm tạo bị xóa mặc định hoặc bị xóa rõ ràng được cho phép)

    • [snip] (có nhiều quy tắc hơn, áp dụng cho cả hai lớp)

Đưa ra định nghĩa này, foolà một tổng hợp, trong khi barkhông (nó có hàm tạo không do người dùng cung cấp, không mặc định).

Do đó foo, T object {arg1, arg2, ...};là cú pháp để khởi tạo tổng hợp.

Tác dụng của khởi tạo tổng hợp là:

  • [snip] (một số chi tiết không liên quan đến trường hợp này)

  • Nếu số lượng mệnh đề khởi tạo ít hơn số lượng thành viên hoặc danh sách khởi tạo hoàn toàn trống, các thành viên còn lại được khởi tạo giá trị .

Do đó, a.agiá trị được khởi tạo, có intnghĩa là khởi tạo bằng không.

Cho bar, T object {};mặt khác là giá trị khởi tạo (các trường hợp lớp, không phải giá trị khởi tạo của các thành viên!). Vì nó là một kiểu lớp với hàm tạo mặc định, nên hàm tạo mặc định được gọi. Hàm tạo mặc định mà bạn đã xác định mặc định khởi tạo các thành viên (do không có bộ khởi tạo thành viên), trong trường hợp int(với bộ lưu trữ không tĩnh) để lại b.bgiá trị không xác định.

Và đối với các kiểu nhóm, khởi tạo mặc định là khởi tạo bằng không.

Không. Điều này sai.


PS Một từ về thí nghiệm của bạn và kết luận của bạn: Thấy đầu ra bằng 0 không nhất thiết có nghĩa là biến không được khởi tạo. Số không là số hoàn toàn có thể cho một giá trị rác.

cho rằng tôi đã chạy chương trình có thể 5 ~ 6 lần trước khi đăng và khoảng 10 lần bây giờ, a luôn là số không. b thay đổi xung quanh một chút.

Thực tế là giá trị giống nhau nhiều lần không có nghĩa là nó đã được khởi tạo.

Tôi cũng đã thử với bộ (CMAKE_CXX_STANDARD 14). Kết quả là giống nhau.

Thực tế là kết quả giống nhau với nhiều tùy chọn trình biên dịch không có nghĩa là biến được khởi tạo. (Mặc dù trong một số trường hợp, việc thay đổi phiên bản tiêu chuẩn có thể thay đổi cho dù nó được khởi tạo).

Làm thế nào tôi có thể bằng cách nào đó lắc RAM của tôi một chút để nếu không có số 0 ở đó, bây giờ nó sẽ là thứ khác

Không có cách nào được đảm bảo trong C ++ để làm cho giá trị giá trị chưa được xác định xuất hiện khác không.

Cách duy nhất để biết rằng một biến được khởi tạo là so sánh chương trình với các quy tắc của ngôn ngữ và xác minh rằng các quy tắc nói rằng nó được khởi tạo. Trong trường hợp a.anày là thực sự khởi tạo.


"Hàm tạo mặc định mà bạn đã xác định mặc định khởi tạo các thành viên (do không có trình khởi tạo thành viên), trong trường hợp int để lại nó với giá trị không xác định." -> hả! "đối với kiểu nhóm, khởi tạo mặc định là khởi tạo bằng không." hoặc là tôi sai?
Vịt Dodgers

2
@JoeyMallone Khởi tạo mặc định các loại POD không phải là khởi tạo.
NathanOliver

@NathanOliver, Rồi tôi lại càng bối rối. Sau đó, làm thế nào đến ađược khởi tạo. Tôi đã nghĩ alà khởi tạo mặc định và khởi tạo mặc định cho POD thành viên là, khởi tạo bằng không. Làa sau đó may mắn luôn luôn đến số không, bất kể tôi chạy chương trình này bao nhiêu lần.
Vịt Dodgers

@JoeyMallone Then how come a is initialized.Vì đó là giá trị khởi tạo.I was thinking a is default initializedKhông phải vậy.
eerorika

3
@JoeyMallone Đừng lo lắng về điều đó. Bạn có thể tạo một cuốn sách ra khỏi khởi tạo trong C ++. Nếu bạn có cơ hội, CppCon trên youtube có một vài video về khởi tạo với sự thất vọng nhất (như chỉ ra mức độ tệ hại của nó) là youtube.com/watch?v=7DTlWPgX6zs
NathanOliver

0

Meh, tôi đã thử chạy đoạn mã bạn cung cấp test.cpp, thông qua gcc & clang và nhiều cấp độ tối ưu hóa:

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 ]

Vì vậy, đó là nơi thú vị, nó cho thấy rõ ràng bản dựng O0 đang đọc số ngẫu nhiên, có lẽ là không gian ngăn xếp.

Tôi nhanh chóng bật IDA của mình để xem chuyện gì đang xảy ra:

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

Bây giờ, những gì bar::bar(bar *this)không?

void __fastcall bar::bar(bar *this)
{
  ;
}

Hừm, không có gì. Chúng tôi đã phải sử dụng lắp ráp:

.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

Vì vậy, yeah, nó chỉ là, không có gì, những gì các nhà xây dựng về cơ bản là this = this. Nhưng chúng tôi biết rằng nó thực sự đang tải các địa chỉ ngăn xếp chưa được khởi tạo ngẫu nhiên và in nó.

Điều gì xảy ra nếu chúng ta cung cấp giá trị rõ ràng cho hai cấu trúc?

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

Nhấn lên tiếng kêu, 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 ]

Số phận tương tự với g ++ là tốt:

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 ]

Vì vậy, điều này có nghĩa là nó thực sự là một khởi tạo trực tiếp bar b(0), không phải là khởi tạo tổng hợp.

Điều này có thể là bởi vì nếu bạn không cung cấp một triển khai hàm tạo rõ ràng thì đây có thể là một biểu tượng bên ngoài, ví dụ:

bar::bar() {
  this.b = 1337; // whoa
}

Trình biên dịch không đủ thông minh để suy ra đây là một cuộc gọi nội tuyến / không trong một giai đoạn không được tối ưu hóa.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.