Làm thế nào để các thành viên lớp C ++ được khởi tạo nếu tôi không làm điều đó một cách rõ ràng?


158

Giả sử tôi có một lớp học với memebers tin ptr, name, pname, rname, crnameage. Điều gì xảy ra nếu tôi không tự khởi tạo chúng? Đây là một ví dụ:

class Example {
    private:
        int *ptr;
        string name;
        string *pname;
        string &rname;
        const string &crname;
        int age;

    public:
        Example() {}
};

Và sau đó tôi làm:

int main() {
    Example ex;
}

Làm thế nào là các thành viên khởi tạo trong ex? Điều gì xảy ra với con trỏ? Làm stringintnhận 0-intialized với các hàm tạo mặc định string()int()? Còn thành viên tham khảo thì sao? Ngoài ra những gì về tài liệu tham khảo const?

Những gì khác tôi nên biết về?

Có ai biết một hướng dẫn bao gồm những trường hợp này? Có lẽ trong một số cuốn sách? Tôi có quyền truy cập trong thư viện của trường đại học với rất nhiều sách C ++.

Tôi muốn tìm hiểu nó để tôi có thể viết các chương trình tốt hơn (không có lỗi). Bất kỳ thông tin phản hồi sẽ giúp!


3
Để biết các đề xuất về sách, hãy xem stackoverflow.com/questions/388242/ Ấn
Mike Seymour

Mike, ow, ý tôi là chương từ một số cuốn sách giải thích nó. Không phải toàn bộ cuốn sách! :)
Bodacydo

Có lẽ sẽ là một ý tưởng tốt để đọc toàn bộ cuốn sách về ngôn ngữ mà bạn dự định lập trình. Và nếu bạn đã đọc một cuốn sách và nó không giải thích điều này, thì đó không phải là một cuốn sách hay.
Tyler McHenry

2
Scott Meyers (một chuyên gia tư vấn nổi tiếng về C ++ phổ biến) tuyên bố trong C ++ hiệu quả , "các quy tắc rất phức tạp - quá phức tạp để đáng ghi nhớ, theo ý kiến ​​của tôi .... đảm bảo rằng tất cả các nhà xây dựng khởi tạo mọi thứ trong đối tượng." Vì vậy, theo ý kiến ​​của ông, cách dễ nhất để (cố gắng) viết mã "không có lỗi" không phải là cố gắng ghi nhớ các quy tắc (và trên thực tế, ông không trình bày chúng trong sách), mà là khởi tạo một cách rõ ràng mọi thứ. Tuy nhiên, lưu ý rằng ngay cả khi bạn thực hiện phương pháp này trong mã của riêng mình, bạn có thể làm việc trên các dự án được viết bởi những người không, vì vậy các quy tắc vẫn có thể có giá trị.
Kyle Strand

2
@TylerMcHenry Những cuốn sách nào về C ++ mà bạn cho là "tốt"? Tôi đã đọc một vài cuốn sách về C ++, nhưng không ai trong số họ giải thích điều này hoàn toàn. Như đã lưu ý trong nhận xét trước đây của tôi, Scott Meyers từ chối rõ ràng để cung cấp các quy tắc hoàn chỉnh trong C ++ hiệu quả . Tôi cũng đã đọc C ++ hiện đại hiệu quả của Meyers , Kiến thức chung C ++ của Dewhurst và Chuyến tham quan C ++ của Stroustrup . Theo trí nhớ của tôi, không ai trong số họ giải thích các quy tắc hoàn chỉnh. Rõ ràng là tôi có thể đã đọc được tiêu chuẩn, nhưng tôi hầu như không coi đó là một "cuốn sách hay"! : D Và tôi mong Stroustrup có thể giải thích điều đó trong Ngôn ngữ lập trình C ++ .
Kyle sợi

Câu trả lời:


206

Thay vì khởi tạo rõ ràng, việc khởi tạo các thành viên trong các lớp hoạt động giống hệt với khởi tạo các biến cục bộ trong các hàm.

Đối với các đối tượng , hàm tạo mặc định của chúng được gọi. Ví dụ, đối với std::string, hàm tạo mặc định đặt nó thành một chuỗi rỗng. Nếu lớp của đối tượng không có hàm tạo mặc định, nó sẽ là một lỗi biên dịch nếu bạn không khởi tạo nó một cách rõ ràng.

Đối với các kiểu nguyên thủy (con trỏ, ints, v.v.), chúng không được khởi tạo - chúng chứa bất kỳ thứ gì tùy ý xảy ra ở vị trí bộ nhớ trước đó.

Đối với tài liệu tham khảo (ví dụ std::string&), việc không khởi tạo chúng là bất hợp pháp và trình biên dịch của bạn sẽ khiếu nại và từ chối biên dịch mã đó. Tài liệu tham khảo phải luôn được khởi tạo.

Vì vậy, trong trường hợp cụ thể của bạn, nếu chúng không được khởi tạo rõ ràng:

    int *ptr;  // Contains junk
    string name;  // Empty string
    string *pname;  // Contains junk
    string &rname;  // Compile error
    const string &crname;  // Compile error
    int age;  // Contains junk

4
+1. Điều đáng chú ý là, theo định nghĩa tiêu chuẩn nghiêm ngặt, các thể hiện của các kiểu nguyên thủy cùng với nhiều thứ khác (bất kỳ vùng lưu trữ nào) đều được coi là đối tượng .
stinky472

7
"Nếu lớp của đối tượng không có hàm tạo mặc định, đó sẽ là lỗi biên dịch nếu bạn không khởi tạo nó một cách rõ ràng" điều đó sai ! Nếu một lớp không có hàm tạo mặc định, nó được cung cấp một hàm tạo mặc định mặc định là trống.

19
@wiz Tôi nghĩ rằng anh ấy có nghĩa đen là 'nếu đối tượng không có hàm tạo mặc định' như trong trường hợp không được tạo, thì đó sẽ là trường hợp nếu lớp định nghĩa rõ ràng bất kỳ hàm tạo nào khác ngoài mặc định (sẽ không tạo ctor mặc định). Nếu chúng ta quá giáo dục, có lẽ chúng ta sẽ nhầm lẫn nhiều hơn là giúp đỡ và Tyler đã nói rõ về điều này khi trả lời tôi trước đây.
stinky472

8
@ wiz-loz Tôi sẽ nói rằng foocó một nhà xây dựng, nó chỉ là ẩn. Nhưng đó thực sự là một cuộc tranh luận về ngữ nghĩa.
Tyler McHenry

4
Tôi hiểu "constructor mặc định" là một constructor có thể được gọi mà không có đối số. Đây sẽ là một trong những bạn tự xác định hoặc được tạo bởi trình biên dịch. Vì vậy, thiếu nó, có nghĩa là không được xác định bởi chính bạn cũng không được tạo ra. Hoặc đó là cách tôi nhìn thấy nó.
5 giờ

28

Đầu tiên, hãy để tôi giải thích danh sách mem-khởi tạo là gì. Một danh sách mem-initializer- là một danh sách bằng dấu phẩy của mem-initializer s, trong đó mỗi mem-initializer là một tên thành viên tiếp theo (, tiếp theo là một biểu hiện danh sách , theo sau là một ). Các biểu hiện-list là cách thành viên được xây dựng. Ví dụ: trong

static const char s_str[] = "bodacydo";
class Example
{
private:
    int *ptr;
    string name;
    string *pname;
    string &rname;
    const string &crname;
    int age;

public:
    Example()
        : name(s_str, s_str + 8), rname(name), crname(name), age(-4)
    {
    }
};

các mem-initializer-list của các nhà xây dựng người dùng cung cấp, không có đối số là name(s_str, s_str + 8), rname(name), crname(name), age(-4). Đây mem-initializer-list phương tiện mà các namethành viên được khởi tạo bởi các std::stringnhà xây dựng mà phải mất hai vòng lặp đầu vào , các rnamethành viên được khởi tạo với một tham chiếu đến name, các crnamethành viên được khởi tạo với một const-tham chiếu đến name, và các agethành viên được khởi tạo với giá trị -4.

Mỗi constructor có danh sách mem-khởi tạo riêng và các thành viên chỉ có thể được khởi tạo theo thứ tự quy định (về cơ bản là thứ tự các thành viên được khai báo trong lớp). Do đó, các thành viên của Examplechỉ có thể được khởi tạo theo thứ tự: ptr, name, pname, rname, crname, và age.

Khi bạn không chỉ định trình khởi tạo mem của thành viên, tiêu chuẩn C ++ sẽ nói:

Nếu thực thể là thành viên dữ liệu phi tập trung ... thuộc loại lớp ..., thì thực thể được khởi tạo mặc định (8.5). ... Mặt khác, thực thể không được khởi tạo.

Ở đây, vì namelà thành viên dữ liệu phi tập trung của loại lớp, nên nó được khởi tạo mặc định nếu không có trình khởi tạo nào nameđược chỉ định trong danh sách mem-khởi tạo . Tất cả các thành viên khác Examplekhông có loại lớp, vì vậy chúng không được khởi tạo.

Khi tiêu chuẩn nói rằng chúng không được khởi tạo, điều này có nghĩa là chúng có thể có bất kỳ giá trị nào . Do đó, vì đoạn mã trên không khởi tạo pname, nó có thể là bất cứ thứ gì.

Lưu ý rằng bạn vẫn phải tuân theo các quy tắc khác, chẳng hạn như quy tắc rằng các tham chiếu phải luôn được khởi tạo. Đó là một lỗi biên dịch để không khởi tạo tài liệu tham khảo.


Đây là cách tốt nhất để khởi tạo thành viên khi bạn muốn tách biệt khai báo (in .h) và định nghĩa (in .cpp) mà không hiển thị quá nhiều nội bộ.
Matthieu

11

Bạn cũng có thể khởi tạo thành viên dữ liệu tại điểm bạn khai báo chúng:

class another_example{
public:
    another_example();
    ~another_example();
private:
    int m_iInteger=10;
    double m_dDouble=10.765;
};

Tôi sử dụng hình thức này khá nhiều độc quyền, mặc dù tôi đã đọc một số người coi đó là "hình thức xấu", có lẽ vì nó chỉ mới được giới thiệu gần đây - tôi nghĩ trong C ++ 11. Đối với tôi nó hợp lý hơn.

Một khía cạnh hữu ích khác cho các quy tắc mới là làm thế nào để khởi tạo dữ liệu - các thành viên là chính các lớp. Ví dụ, giả sử đó CDynamicStringlà một lớp đóng gói xử lý chuỗi. Nó có một hàm tạo cho phép bạn chỉ định giá trị ban đầu của nó CDynamicString(wchat_t* pstrInitialString). Bạn rất có thể sử dụng lớp này như một thành viên dữ liệu trong một lớp khác - giả sử một lớp đóng gói một giá trị sổ đăng ký trong trường hợp này lưu trữ một địa chỉ bưu chính. Để 'mã cứng' tên khóa đăng ký mà bạn viết này sử dụng dấu ngoặc nhọn:

class Registry_Entry{
public:
    Registry_Entry();
    ~Registry_Entry();
    Commit();//Writes data to registry.
    Retrieve();//Reads data from registry;
private:
    CDynamicString m_cKeyName{L"Postal Address"};
    CDynamicString m_cAddress;
};

Lưu ý lớp chuỗi thứ hai chứa địa chỉ bưu chính thực tế không có bộ khởi tạo nên hàm tạo mặc định của nó sẽ được gọi khi tạo - có thể tự động đặt nó thành một chuỗi trống.


9

Nếu lớp ví dụ của bạn được khởi tạo trên ngăn xếp, nội dung của các thành viên vô hướng chưa được khởi tạo là ngẫu nhiên và không xác định.

Đối với một thể hiện toàn cầu, các thành viên vô hướng chưa được khởi tạo sẽ bằng không.

Đối với các thành viên là bản thân của các lớp, các hàm tạo mặc định của chúng sẽ được gọi, vì vậy đối tượng chuỗi của bạn sẽ được khởi tạo.

  • int *ptr; // con trỏ chưa được khởi tạo (hoặc zeroed nếu toàn cục)
  • string name; // constructor được gọi, khởi tạo với chuỗi rỗng
  • string *pname; // con trỏ chưa được khởi tạo (hoặc zeroed nếu toàn cục)
  • string &rname; // lỗi biên dịch nếu bạn không khởi tạo được
  • const string &crname; // lỗi biên dịch nếu bạn không khởi tạo được
  • int age; // giá trị vô hướng, chưa được khởi tạo và ngẫu nhiên (hoặc bằng 0 nếu toàn cục)

Tôi đã thử nghiệm và dường như string nametrống rỗng sau khi khởi tạo lớp trên ngăn xếp. Bạn có chắc chắn về câu trả lời của bạn?
Bodacydo

1
chuỗi sẽ có một hàm tạo theo mặc định cung cấp một chuỗi rỗng - Tôi sẽ làm rõ câu trả lời của tôi
Paul Dixon

@bodacydo: Paul là chính xác, nhưng nếu bạn quan tâm đến hành vi này, sẽ không bao giờ đau lòng để được rõ ràng. Ném nó trong danh sách khởi tạo.
Stephen

Cảm ơn đã làm rõ và giải thích!
Bodacydo

2
Nó không phải là ngẫu nhiên! Ngẫu nhiên là từ quá lớn cho điều đó! Nếu các thành viên vô hướng là ngẫu nhiên, chúng ta sẽ không cần bất kỳ trình tạo số ngẫu nhiên nào khác. Hãy tưởng tượng một chương trình phân tích dữ liệu "còn lại" - giống như các tệp chưa xóa trong bộ nhớ - dữ liệu không phải là ngẫu nhiên. Nó thậm chí không được xác định! Nó thường rất khó định nghĩa, vì thông thường chúng ta không biết máy của mình làm gì. Nếu "dữ liệu ngẫu nhiên" mà bạn vừa xóa là hình ảnh duy nhất của cha bạn, mẹ bạn thậm chí có thể thấy khó chịu nếu bạn nói rằng đó là ngẫu nhiên ...
slyy2048

5

Các thành viên không tĩnh chưa được khởi tạo sẽ chứa dữ liệu ngẫu nhiên. Trên thực tế, chúng sẽ chỉ có giá trị của vị trí bộ nhớ mà chúng được gán.

Tất nhiên đối với các tham số đối tượng (như string) hàm tạo của đối tượng có thể thực hiện khởi tạo mặc định.

Trong ví dụ của bạn:

int *ptr; // will point to a random memory location
string name; // empty string (due to string's default costructor)
string *pname; // will point to a random memory location
string &rname; // it would't compile
const string &crname; // it would't compile
int age; // random value

2

Các thành viên có hàm tạo sẽ có hàm tạo mặc định của chúng được gọi để khởi tạo.

Bạn không thể phụ thuộc vào nội dung của các loại khác.


0

Nếu nó nằm trong ngăn xếp, nội dung của các thành viên chưa được khởi tạo không có hàm tạo riêng sẽ là ngẫu nhiên và không xác định. Ngay cả khi nó là toàn cầu, sẽ là một ý tưởng tồi nếu dựa vào việc họ bị loại trừ. Cho dù nó có trong ngăn xếp hay không, nếu một thành viên có hàm tạo riêng, nó sẽ được gọi để khởi tạo nó.

Vì vậy, nếu bạn có chuỗi * pname, con trỏ sẽ chứa rác ngẫu nhiên. nhưng đối với tên chuỗi, hàm tạo mặc định cho chuỗi sẽ được gọi, cung cấp cho bạn một chuỗi rỗng. Đối với các biến loại tham chiếu của bạn, tôi không chắc chắn, nhưng có lẽ nó sẽ là một tham chiếu đến một đoạn bộ nhớ ngẫu nhiên.


0

Nó phụ thuộc vào cách lớp được xây dựng

Trả lời câu hỏi này là hiểu được một tuyên bố trường hợp chuyển đổi lớn trong tiêu chuẩn ngôn ngữ C ++, và một câu hỏi khó mà chỉ những người phàm nhân mới có được trực giác.

Như một ví dụ đơn giản về điều khó khăn như thế nào:

main.cpp

#include <cassert>

int main() {
    struct C { int i; };

    // This syntax is called "default initialization"
    C a;
    // i undefined

    // This syntax is called "value initialization"
    C b{};
    assert(b.i == 0);
}

Trong khởi tạo mặc định, bạn sẽ bắt đầu từ: https://en.cppreference.com/w/cpp/lingu/default_initialization chúng ta đi đến phần "Hiệu ứng của khởi tạo mặc định là" và bắt đầu câu lệnh tình huống:

  • "nếu T không phải là POD ": không (định nghĩa của POD tự nó là một tuyên bố chuyển đổi lớn)
  • "nếu T là kiểu mảng": không
  • "nếu không, không có gì được thực hiện": do đó, nó được để lại với một giá trị không xác định

Sau đó, nếu ai đó quyết định khởi tạo giá trị, chúng tôi sẽ truy cập https://en.cppreference.com/w/cpp/lingu/value_initialization "Tác động của khởi tạo giá trị là" và bắt đầu tuyên bố trường hợp:

  • "nếu T là một loại lớp không có hàm tạo mặc định hoặc với hàm tạo mặc định do người dùng cung cấp hoặc đã xóa": không phải như vậy. Bây giờ bạn sẽ dành 20 phút để làm cho các điều khoản đó:
    • chúng tôi có một hàm tạo mặc định được xác định ngầm định (đặc biệt vì không có hàm tạo nào khác được xác định)
    • nó không do người dùng cung cấp (được định nghĩa ngầm)
    • nó không bị xóa ( = delete)
  • "nếu T là loại lớp có hàm tạo mặc định không do người dùng cung cấp cũng không bị xóa": có
    • "Đối tượng được khởi tạo bằng 0 và sau đó nó được khởi tạo mặc định nếu nó có hàm tạo mặc định không tầm thường": không có hàm tạo không tầm thường, chỉ khởi tạo bằng không. Định nghĩa "không khởi tạo" ít nhất là đơn giản và thực hiện những gì bạn mong đợi: https://en.cppreference.com/w/cpp/lingu/zero_initialization

Đây là lý do tại sao tôi thực sự khuyên bạn không bao giờ nên dựa vào khởi tạo "ẩn". Trừ khi có các lý do hiệu suất mạnh, khởi tạo rõ ràng mọi thứ, hoặc trên hàm tạo nếu bạn đã xác định hoặc sử dụng khởi tạo tổng hợp. Nếu không, bạn làm cho mọi thứ rất rất rủi ro cho các nhà phát triển trong tương lai.

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.