Quy tắc của ba là gì?


2148
  • Những gì hiện sao chép một đối tượng trung bình?
  • Các constructor sao chéptoán tử gán sao chép là gì?
  • Khi nào tôi cần phải tự khai báo chúng?
  • Làm thế nào tôi có thể ngăn chặn các đối tượng của tôi bị sao chép?

52
Hãy đọc toàn bộ chủ đề nàycác c++-faqthẻ wiki trước khi bạn bỏ phiếu đóng cửa .
sbi

13
@Binary: Ít nhất hãy dành thời gian để đọc các cuộc thảo luận bình luận trước khi bạn bỏ phiếu. Văn bản được sử dụng đơn giản hơn nhiều, nhưng Fred được yêu cầu mở rộng về nó. Ngoài ra, trong khi đó là bốn câu hỏi về mặt ngữ pháp , nó thực sự chỉ là một câu hỏi với nhiều khía cạnh. (Nếu bạn không đồng ý với điều đó, thì hãy chứng minh POV của bạn bằng cách tự trả lời từng câu hỏi đó và để chúng tôi bỏ phiếu cho kết quả.)
sbi

1
Fred, đây là một bổ sung thú vị cho câu trả lời của bạn về C ++ 1x: stackoverflow.com/questions/4782757/iêu . Chúng ta xử lý việc này thế nào đây?
sbi


4
Hãy nhớ rằng, kể từ C ++ 11, tôi nghĩ rằng điều này đã được nâng cấp lên quy tắc năm hoặc một cái gì đó tương tự.
paxdiablo

Câu trả lời:


1795

Giới thiệu

C ++ xử lý các biến của các loại do người dùng định nghĩa với ngữ nghĩa giá trị . Điều này có nghĩa là các đối tượng được sao chép hoàn toàn trong các bối cảnh khác nhau và chúng ta nên hiểu "sao chép một đối tượng" thực sự có nghĩa là gì.

Chúng ta hãy xem xét một ví dụ đơn giản:

class person
{
    std::string name;
    int age;

public:

    person(const std::string& name, int age) : name(name), age(age)
    {
    }
};

int main()
{
    person a("Bjarne Stroustrup", 60);
    person b(a);   // What happens here?
    b = a;         // And here?
}

(Nếu bạn bối rối bởi name(name), age(age)phần này, đây được gọi là danh sách khởi tạo thành viên .)

Chức năng thành viên đặc biệt

Sao chép một personđối tượng có nghĩa là gì? Các mainchức năng cho thấy hai kịch bản sao chép khác nhau. Việc khởi tạo person b(a);được thực hiện bởi hàm tạo sao chép . Công việc của nó là xây dựng một đối tượng mới dựa trên trạng thái của một đối tượng hiện có. Việc chuyển nhượng b = ađược thực hiện bởi toán tử gán sao chép . Công việc của nó nói chung phức tạp hơn một chút, vì đối tượng mục tiêu đã ở trong một số trạng thái hợp lệ cần được xử lý.

Vì chúng tôi tự khai báo cả hàm tạo sao chép cũng như toán tử gán (cũng không phải hàm hủy), chúng được định nghĩa ngầm cho chúng tôi. Trích dẫn từ tiêu chuẩn:

[...] sao chép hàm tạo và toán tử gán sao chép, [...] và hàm hủy là các hàm thành viên đặc biệt. [ Lưu ý : Việc triển khai sẽ ngầm khai báo các hàm thành viên này cho một số loại lớp khi chương trình không khai báo rõ ràng chúng. Việc thực hiện sẽ ngầm định nghĩa chúng nếu chúng được sử dụng. [...] ghi chú cuối ] [n3126.pdf phần 12 §1]

Theo mặc định, sao chép một đối tượng có nghĩa là sao chép các thành viên của nó:

Hàm tạo sao chép được định nghĩa ngầm định cho lớp không liên kết X thực hiện một bản sao thành viên của các tiểu dự án của nó. [n3126.pdf phần 12.8 §16]

Toán tử gán sao chép được định nghĩa ngầm định cho lớp không liên kết X thực hiện gán sao chép thành viên của các tiểu dự án của nó. [n3126.pdf phần 12.8 §30]

Định nghĩa ngầm

Các hàm thành viên đặc biệt được định nghĩa ngầm định persontrông như thế này:

// 1. copy constructor
person(const person& that) : name(that.name), age(that.age)
{
}

// 2. copy assignment operator
person& operator=(const person& that)
{
    name = that.name;
    age = that.age;
    return *this;
}

// 3. destructor
~person()
{
}

Sao chép thành viên là chính xác những gì chúng ta muốn trong trường hợp này: nameageđược sao chép, vì vậy chúng ta có được một personđối tượng độc lập, độc lập . Hàm hủy được định nghĩa ngầm luôn trống. Điều này cũng tốt trong trường hợp này vì chúng tôi không có được bất kỳ tài nguyên nào trong hàm tạo. Các hàm hủy của các thành viên được gọi ngầm sau khi hàm personhủy kết thúc:

Sau khi thực thi phần thân của hàm hủy và phá hủy bất kỳ đối tượng tự động nào được phân bổ trong phần thân, một hàm hủy cho lớp X gọi các hàm hủy cho các thành viên [...] trực tiếp của X [n3126.pdf 12.4 §6]

Quản lý tài nguyên

Vậy khi nào chúng ta nên khai báo các hàm thành viên đặc biệt một cách rõ ràng? Khi lớp của chúng ta quản lý một tài nguyên , nghĩa là khi một đối tượng của lớp chịu trách nhiệm về tài nguyên đó. Điều đó thường có nghĩa là tài nguyên được thu nhận trong hàm tạo (hoặc được truyền vào hàm tạo) và được giải phóng trong hàm hủy.

Chúng ta hãy quay ngược thời gian để chuẩn C ++. Không có điều gì như vậy std::string, và các lập trình viên đã yêu con trỏ. Các personlớp có thể nhìn như thế này:

class person
{
    char* name;
    int age;

public:

    // the constructor acquires a resource:
    // in this case, dynamic memory obtained via new[]
    person(const char* the_name, int the_age)
    {
        name = new char[strlen(the_name) + 1];
        strcpy(name, the_name);
        age = the_age;
    }

    // the destructor must release this resource via delete[]
    ~person()
    {
        delete[] name;
    }
};

Ngay cả ngày nay, mọi người vẫn viết các lớp theo phong cách này và gặp rắc rối: " Tôi đã đẩy một người vào một vectơ và bây giờ tôi gặp lỗi bộ nhớ điên rồ! " Hãy nhớ rằng theo mặc định, sao chép một đối tượng có nghĩa là sao chép các thành viên của nó, nhưng sao chépname thành viên sao chép một con trỏ, không phải mảng ký tự mà nó trỏ tới! Điều này có một số tác dụng khó chịu:

  1. Thay đổi thông qua a có thể được quan sát thông qua b.
  2. Một lần b bị phá hủy, a.namelà một con trỏ lơ lửng.
  3. Nếu a bị phá hủy, xóa con trỏ lơ lửng sẽ mang lại hành vi không xác định .
  4. Vì bài tập không tính đến name chỉ ra trước khi chuyển nhượng, sớm hay muộn bạn sẽ bị rò rỉ bộ nhớ ở mọi nơi.

Định nghĩa rõ ràng

Vì sao chép thành viên không có hiệu ứng mong muốn, chúng ta phải xác định rõ ràng hàm tạo sao chép và toán tử gán gán sao chép để tạo các bản sao sâu của mảng ký tự:

// 1. copy constructor
person(const person& that)
{
    name = new char[strlen(that.name) + 1];
    strcpy(name, that.name);
    age = that.age;
}

// 2. copy assignment operator
person& operator=(const person& that)
{
    if (this != &that)
    {
        delete[] name;
        // This is a dangerous point in the flow of execution!
        // We have temporarily invalidated the class invariants,
        // and the next statement might throw an exception,
        // leaving the object in an invalid state :(
        name = new char[strlen(that.name) + 1];
        strcpy(name, that.name);
        age = that.age;
    }
    return *this;
}

Lưu ý sự khác biệt giữa khởi tạo và gán: chúng ta phải phá bỏ trạng thái cũ trước khi gán nameđể tránh rò rỉ bộ nhớ. Ngoài ra, chúng tôi phải bảo vệ chống lại việc tự gán mẫu x = x. Nếu không có kiểm tra đó, delete[] namesẽ xóa mảng chứa chuỗi nguồn , bởi vì khi bạn viết x = x, cả hai this->namethat.namechứa cùng một con trỏ.

An toàn ngoại lệ

Thật không may, giải pháp này sẽ thất bại nếu new char[...]ném ngoại lệ do cạn kiệt bộ nhớ. Một giải pháp có thể là giới thiệu một biến cục bộ và sắp xếp lại các câu lệnh:

// 2. copy assignment operator
person& operator=(const person& that)
{
    char* local_name = new char[strlen(that.name) + 1];
    // If the above statement throws,
    // the object is still in the same state as before.
    // None of the following statements will throw an exception :)
    strcpy(local_name, that.name);
    delete[] name;
    name = local_name;
    age = that.age;
    return *this;
}

Điều này cũng quan tâm đến việc tự giao mà không cần kiểm tra rõ ràng. Một giải pháp mạnh mẽ hơn nữa cho vấn đề này là thành ngữ sao chép và trao đổi , nhưng tôi sẽ không đi sâu vào chi tiết về an toàn ngoại lệ ở đây. Tôi chỉ đề cập đến các ngoại lệ để đưa ra quan điểm sau: Viết các lớp quản lý tài nguyên là khó.

Tài nguyên không thể quét

Một số tài nguyên không thể hoặc không được sao chép, chẳng hạn như xử lý tệp hoặc mutexes. Trong trường hợp đó, chỉ cần khai báo hàm tạo sao chép và toán tử gán gán sao chép privatemà không đưa ra định nghĩa:

private:

    person(const person& that);
    person& operator=(const person& that);

Ngoài ra, bạn có thể kế thừa từ boost::noncopyablehoặc khai báo chúng là đã xóa (trong C ++ 11 trở lên):

person(const person& that) = delete;
person& operator=(const person& that) = delete;

Quy tắc của ba

Đôi khi bạn cần triển khai một lớp quản lý tài nguyên. (Không bao giờ quản lý nhiều tài nguyên trong một lớp, điều này sẽ chỉ dẫn đến đau.) Trong trường hợp đó, hãy nhớ quy tắc ba :

Nếu bạn cần phải khai báo rõ ràng hàm hủy, sao chép hàm tạo hoặc toán tử gán gán sao chép, có lẽ bạn cần khai báo rõ ràng cả ba hàm này.

(Thật không may, "quy tắc" này không được thi hành theo tiêu chuẩn C ++ hoặc bất kỳ trình biên dịch nào tôi biết.)

Quy tắc của năm

Từ C ++ 11 trở đi, một đối tượng có thêm 2 hàm thành viên đặc biệt: hàm tạo di chuyển và phép gán di chuyển. Quy tắc của năm tiểu bang để thực hiện các chức năng là tốt.

Một ví dụ với chữ ký:

class person
{
    std::string name;
    int age;

public:
    person(const std::string& name, int age);        // Ctor
    person(const person &) = default;                // Copy Ctor
    person(person &&) noexcept = default;            // Move Ctor
    person& operator=(const person &) = default;     // Copy Assignment
    person& operator=(person &&) noexcept = default; // Move Assignment
    ~person() noexcept = default;                    // Dtor
};

Quy tắc không

Quy tắc 3/5 cũng được gọi là quy tắc 0/3/5. Phần không của quy tắc nói rằng bạn được phép không viết bất kỳ hàm thành viên đặc biệt nào khi tạo lớp của bạn.

Khuyên bảo

Hầu hết thời gian, bạn không cần phải tự mình quản lý tài nguyên, bởi vì một lớp hiện có như std::stringđã làm điều đó cho bạn. Chỉ cần so sánh mã đơn giản bằng cách sử dụng một std::stringthành viên với giải pháp thay thế dễ bị lỗi và dễ bị lỗi bằng cách sử dụng a char*và bạn sẽ bị thuyết phục. Miễn là bạn tránh xa các thành viên con trỏ thô, quy tắc ba không có khả năng liên quan đến mã của riêng bạn.


4
Fred, tôi sẽ cảm thấy tốt hơn về việc bỏ phiếu của tôi nếu (A) bạn sẽ không đánh vần việc chuyển nhượng được thực thi kém trong mã có thể sao chép và thêm một ghi chú nói rằng nó sai và tìm ở nơi khác trong bản in; hoặc sử dụng c & s trong mã hoặc chỉ cần bỏ qua việc thực hiện tất cả các thành viên (B) này, bạn sẽ rút ngắn nửa đầu, điều này ít liên quan đến RoT; (C) bạn sẽ thảo luận về việc giới thiệu ngữ nghĩa di chuyển và ý nghĩa của RoT.
sbi

7
Nhưng sau đó, bài viết nên được thực hiện C / W, tôi nghĩ vậy. Tôi thích rằng bạn giữ các điều khoản chủ yếu chính xác (nghĩa là bạn nói " toán tử gán bản sao " và rằng bạn không chạm vào cái bẫy chung mà bài tập không thể ám chỉ một bản sao).
Julian Schaub - litb

4
@Prasoon: Tôi không nghĩ việc cắt bỏ một nửa câu trả lời sẽ được coi là "chỉnh sửa công bằng" cho câu trả lời không CW.
sbi

69
Sẽ thật tuyệt nếu bạn cập nhật bài đăng của mình cho C ++ 11 (tức là chuyển nhà xây dựng / chuyển nhượng)
Alexander Malakhov

5
@solalito Bất cứ điều gì bạn phải phát hành sau khi sử dụng: khóa đồng thời, xử lý tệp, kết nối cơ sở dữ liệu, ổ cắm mạng, bộ nhớ heap ...
fredoverflow

509

Các Rule của Ba là một quy luật của cho C ++, về cơ bản nói

Nếu lớp của bạn cần bất kỳ

  • một nhà xây dựng sao chép ,
  • một toán tử gán ,
  • hoặc một kẻ hủy diệt ,

được định nghĩa rõ ràng, sau đó có khả năng cần cả ba người trong số họ .

Lý do cho điều này là cả ba trong số chúng thường được sử dụng để quản lý tài nguyên và nếu lớp của bạn quản lý tài nguyên, nó thường cần quản lý sao chép cũng như giải phóng.

Nếu không có ngữ nghĩa tốt để sao chép tài nguyên mà lớp của bạn quản lý, thì hãy xem xét cấm sao chép bằng cách khai báo (không xác định ) hàm tạo sao chép và toán tử gán như private.

(Lưu ý rằng phiên bản mới sắp tới của tiêu chuẩn C ++ (là C ++ 11) thêm ngữ nghĩa di chuyển vào C ++, có khả năng sẽ thay đổi Quy tắc ba. Tuy nhiên, tôi biết quá ít về điều này để viết phần C ++ 11 về quy tắc của ba.)


3
Một giải pháp khác để ngăn chặn việc sao chép là kế thừa (riêng tư) từ một lớp không thể sao chép (như boost::noncopyable). Nó cũng có thể rõ ràng hơn nhiều. Tôi nghĩ rằng C ++ 0x và khả năng "xóa" các chức năng có thể giúp ích ở đây, nhưng đã quên cú pháp: /
Matthieu M.

2
@Matthieu: Yep, nó cũng hoạt động. Nhưng trừ khi noncopyablelà một phần của lib std, tôi không coi đó là một cải tiến. (Ồ, và nếu bạn quên cú pháp xóa, bạn đã quên mor ethan tôi từng biết. :))
sbi

3
@Daan: Xem câu trả lời này . Tuy nhiên, tôi khuyên bạn nên để dính vào Martinho 's Rule của Zero . Đối với tôi, đây là một trong những quy tắc quan trọng nhất đối với C ++ được đặt ra trong thập kỷ qua.
sbi

3
Quy tắc không của Martinho bây giờ tốt hơn (không có sự tiếp quản phần mềm quảng cáo rõ ràng) nằm trên archive.org
Nathan Kidd

161

Luật của ba lớn là như quy định ở trên.

Một ví dụ dễ hiểu, bằng tiếng Anh đơn giản, về loại vấn đề mà nó giải quyết:

Hàm hủy không mặc định

Bạn đã phân bổ bộ nhớ trong hàm tạo của bạn và do đó bạn cần phải viết một hàm hủy để xóa nó. Nếu không, bạn sẽ gây rò rỉ bộ nhớ.

Bạn có thể nghĩ rằng đây là công việc được thực hiện.

Vấn đề sẽ là, nếu một bản sao được tạo từ đối tượng của bạn, thì bản sao đó sẽ trỏ đến cùng bộ nhớ với đối tượng ban đầu.

Một khi, một trong số này xóa bộ nhớ trong hàm hủy của nó, cái còn lại sẽ có một con trỏ tới bộ nhớ không hợp lệ (cái này được gọi là con trỏ lơ lửng) khi nó cố gắng sử dụng nó, mọi thứ sẽ có lông.

Do đó, bạn viết một hàm tạo sao chép để nó phân bổ các đối tượng mới các phần bộ nhớ của riêng chúng để hủy.

Toán tử gán và xây dựng bản sao

Bạn đã phân bổ bộ nhớ trong hàm tạo của bạn cho một con trỏ thành viên của lớp. Khi bạn sao chép một đối tượng của lớp này, toán tử gán mặc định và hàm tạo sao chép sẽ sao chép giá trị của con trỏ thành viên này sang đối tượng mới.

Điều này có nghĩa là đối tượng mới và đối tượng cũ sẽ chỉ vào cùng một phần bộ nhớ, vì vậy khi bạn thay đổi nó trong một đối tượng, nó cũng sẽ bị thay đổi đối với các objerct khác. Nếu một đối tượng xóa bộ nhớ này, đối tượng khác sẽ tiếp tục cố gắng sử dụng nó - eek.

Để giải quyết vấn đề này, bạn viết phiên bản riêng của hàm tạo sao chép và toán tử gán. Các phiên bản của bạn phân bổ bộ nhớ riêng cho các đối tượng mới và sao chép qua các giá trị mà con trỏ đầu tiên đang trỏ đến thay vì địa chỉ của nó.


4
Vì vậy, nếu chúng ta sử dụng một hàm tạo sao chép thì bản sao được tạo nhưng ở một vị trí bộ nhớ khác hoàn toàn và nếu chúng ta không sử dụng hàm tạo sao chép thì sao chép được tạo nhưng nó trỏ đến cùng một vị trí bộ nhớ. đó là những gì bạn đang cố gắng nói? Vì vậy, một bản sao không có hàm tạo sao chép có nghĩa là một con trỏ mới sẽ ở đó nhưng chỉ đến cùng một vị trí bộ nhớ, tuy nhiên nếu chúng ta có trình tạo sao chép được xác định rõ ràng bởi người dùng thì chúng ta sẽ có một con trỏ riêng trỏ đến một vị trí bộ nhớ khác nhưng có dữ liệu.
Không thể phá vỡ

4
Xin lỗi, tôi đã trả lời từ rất lâu rồi nhưng câu trả lời của tôi dường như vẫn không có ở đây :-( Về cơ bản, vâng - bạn hiểu rồi :-)
Stefan

1
Làm thế nào để nguyên tắc etend cho toán tử gán sao chép? Câu trả lời này sẽ hữu ích hơn nếu thứ 3 trong Quy tắc ba sẽ được đề cập.
DBedrenko

1
@DBedrenko, "bạn viết một hàm tạo sao chép để nó cấp phát các đối tượng mới cho bộ nhớ riêng của chúng ..." đây là nguyên tắc tương tự mở rộng cho toán tử gán sao chép. Bạn không nghĩ rằng tôi đã làm rõ điều đó?
Stefan

2
@DBedrenko, tôi đã thêm một số thông tin. Điều đó làm cho nó rõ ràng hơn?
Stefan

44

Về cơ bản nếu bạn có một hàm hủy (không phải là hàm hủy mặc định) thì có nghĩa là lớp mà bạn đã xác định có một số cấp phát bộ nhớ. Giả sử rằng lớp được sử dụng bên ngoài bởi một số mã máy khách hoặc bởi bạn.

    MyClass x(a, b);
    MyClass y(c, d);
    x = y; // This is a shallow copy if assignment operator is not provided

Nếu MyClass chỉ có một số thành viên được nhập nguyên thủy, toán tử gán mặc định sẽ hoạt động nhưng nếu nó có một số thành viên con trỏ và các đối tượng không có toán tử gán thì kết quả sẽ không thể đoán trước. Do đó, chúng ta có thể nói rằng nếu có một cái gì đó cần xóa trong hàm hủy của một lớp, chúng ta có thể cần một toán tử sao chép sâu, điều đó có nghĩa là chúng ta nên cung cấp một hàm tạo sao chép và toán tử gán.


36

Sao chép một đối tượng có nghĩa là gì? Có một số cách bạn có thể sao chép các đối tượng - hãy nói về 2 loại mà bạn có thể đề cập nhất - bản sao sâu và bản sao nông.

Vì chúng tôi đang sử dụng ngôn ngữ hướng đối tượng (hoặc ít nhất là giả định như vậy), giả sử bạn có một phần bộ nhớ được phân bổ. Vì đó là ngôn ngữ OO, chúng ta có thể dễ dàng tham khảo các khối bộ nhớ mà chúng ta phân bổ vì chúng thường là các biến nguyên thủy (ints, chars, byte) hoặc các lớp mà chúng ta đã xác định được tạo từ các kiểu và nguyên thủy của chính chúng ta. Vì vậy, hãy nói rằng chúng tôi có một lớp Xe như sau:

class Car //A very simple class just to demonstrate what these definitions mean.
//It's pseudocode C++/Javaish, I assume strings do not need to be allocated.
{
private String sPrintColor;
private String sModel;
private String sMake;

public changePaint(String newColor)
{
   this.sPrintColor = newColor;
}

public Car(String model, String make, String color) //Constructor
{
   this.sPrintColor = color;
   this.sModel = model;
   this.sMake = make;
}

public ~Car() //Destructor
{
//Because we did not create any custom types, we aren't adding more code.
//Anytime your object goes out of scope / program collects garbage / etc. this guy gets called + all other related destructors.
//Since we did not use anything but strings, we have nothing additional to handle.
//The assumption is being made that the 3 strings will be handled by string's destructor and that it is being called automatically--if this were not the case you would need to do it here.
}

public Car(const Car &other) // Copy Constructor
{
   this.sPrintColor = other.sPrintColor;
   this.sModel = other.sModel;
   this.sMake = other.sMake;
}
public Car &operator =(const Car &other) // Assignment Operator
{
   if(this != &other)
   {
      this.sPrintColor = other.sPrintColor;
      this.sModel = other.sModel;
      this.sMake = other.sMake;
   }
   return *this;
}

}

Một bản sao sâu là nếu chúng ta khai báo một đối tượng và sau đó tạo một bản sao hoàn toàn riêng biệt của đối tượng ... chúng ta kết thúc với 2 đối tượng trong 2 bộ nhớ hoàn toàn.

Car car1 = new Car("mustang", "ford", "red");
Car car2 = car1; //Call the copy constructor
car2.changePaint("green");
//car2 is now green but car1 is still red.

Bây giờ hãy làm điều gì đó kỳ lạ. Giả sử car2 được lập trình sai hoặc cố ý chia sẻ bộ nhớ thực mà car1 được tạo ra. (Thường thì đó là một sai lầm khi làm điều này và trong các lớp học thường là cái chăn được thảo luận.) Giả vờ rằng bất cứ khi nào bạn hỏi về car2, bạn thực sự đang giải quyết một con trỏ đến không gian bộ nhớ của car1 ... đó là một bản sao nông Là.

//Shallow copy example
//Assume we're in C++ because it's standard behavior is to shallow copy objects if you do not have a constructor written for an operation.
//Now let's assume I do not have any code for the assignment or copy operations like I do above...with those now gone, C++ will use the default.

 Car car1 = new Car("ford", "mustang", "red"); 
 Car car2 = car1; 
 car2.changePaint("green");//car1 is also now green 
 delete car2;/*I get rid of my car which is also really your car...I told C++ to resolve 
 the address of where car2 exists and delete the memory...which is also
 the memory associated with your car.*/
 car1.changePaint("red");/*program will likely crash because this area is
 no longer allocated to the program.*/

Vì vậy, bất kể bạn đang viết ngôn ngữ nào, hãy cẩn thận về ý nghĩa của bạn khi sao chép các đối tượng vì hầu hết thời gian bạn muốn có một bản sao sâu.

Các constructor sao chép và toán tử gán sao chép là gì? Tôi đã sử dụng chúng ở trên. Hàm tạo sao chép được gọi khi bạn nhập mã như Car car2 = car1; Về cơ bản nếu bạn khai báo một biến và gán nó trong một dòng, đó là khi hàm tạo sao chép được gọi. Toán tử gán là những gì xảy ra khi bạn sử dụng dấu bằng-- car2 = car1;. Thông báo car2không được tuyên bố trong cùng một tuyên bố. Hai đoạn mã bạn viết cho các hoạt động này có thể rất giống nhau. Trong thực tế, mẫu thiết kế điển hình có một chức năng khác mà bạn gọi để đặt mọi thứ một khi bạn hài lòng, bản sao / bài tập ban đầu là hợp pháp - nếu bạn nhìn vào mã dài tay tôi đã viết, các hàm này gần giống nhau.

Khi nào tôi cần phải tự khai báo chúng? Nếu bạn không viết mã được chia sẻ hoặc sản xuất theo cách nào đó, bạn thực sự chỉ cần khai báo chúng khi bạn cần chúng. Bạn cần phải biết ngôn ngữ chương trình của bạn làm gì nếu bạn chọn sử dụng nó 'một cách tình cờ' và không tạo ra một ngôn ngữ - tức là bạn có được trình biên dịch mặc định. Tôi hiếm khi sử dụng các hàm tạo sao chép, nhưng ghi đè toán tử gán là rất phổ biến. Bạn có biết bạn có thể ghi đè cả phép cộng, phép trừ, v.v.

Làm thế nào tôi có thể ngăn chặn các đối tượng của tôi bị sao chép? Ghi đè tất cả các cách bạn được phép phân bổ bộ nhớ cho đối tượng của mình bằng chức năng riêng là một sự khởi đầu hợp lý. Nếu bạn thực sự không muốn mọi người sao chép chúng, bạn có thể công khai và cảnh báo cho lập trình viên bằng cách ném một ngoại lệ và cũng không sao chép đối tượng.


5
Câu hỏi đã được gắn thẻ C ++. Giải trình mã giả này không làm rõ chút gì về "Quy tắc ba" được xác định rõ nhất, và chỉ lan truyền sự nhầm lẫn ở mức tồi tệ nhất.
sehe

26

Khi nào tôi cần phải tự khai báo chúng?

Quy tắc của ba quy định rằng nếu bạn tuyên bố bất kỳ

  1. xây dựng bản sao
  2. sao chép toán tử gán
  3. kẻ hủy diệt

sau đó bạn nên khai báo cả ba. Nó phát triển từ quan sát rằng sự cần thiết phải tiếp nhận ý nghĩa của hoạt động sao chép hầu như luôn xuất phát từ lớp thực hiện một số loại quản lý tài nguyên và hầu như luôn ngụ ý rằng

  • bất cứ việc quản lý tài nguyên nào đã được thực hiện trong một thao tác sao chép có thể cần phải được thực hiện trong hoạt động sao chép khác và

  • hàm hủy lớp cũng sẽ tham gia quản lý tài nguyên (thường phát hành nó). Tài nguyên cổ điển được quản lý là bộ nhớ và đây là lý do tại sao tất cả các lớp Thư viện tiêu chuẩn quản lý bộ nhớ (ví dụ: các bộ chứa STL thực hiện quản lý bộ nhớ động) đều khai báo ba bộ ba lớn: cả hoạt động sao chép và bộ hủy.

Hậu quả của Quy tắc ba là sự hiện diện của hàm hủy do người dùng khai báo cho thấy rằng bản sao thông minh thành viên đơn giản khó có thể phù hợp với các hoạt động sao chép trong lớp. Điều đó, đến lượt nó, gợi ý rằng nếu một lớp tuyên bố một hàm hủy, các hoạt động sao chép có lẽ không nên được tạo tự động, bởi vì chúng sẽ không làm đúng. Vào thời điểm C ++ 98 được thông qua, tầm quan trọng của dòng lý luận này chưa được đánh giá đầy đủ, vì vậy, trong C ++ 98, sự tồn tại của một người sử dụng tuyên bố hủy diệt không ảnh hưởng đến sự sẵn sàng của trình biên dịch để tạo ra các hoạt động sao chép. Điều đó tiếp tục là trường hợp trong C ++ 11, nhưng chỉ vì việc hạn chế các điều kiện mà các hoạt động sao chép được tạo ra sẽ phá vỡ quá nhiều mã kế thừa.

Làm thế nào tôi có thể ngăn chặn các đối tượng của tôi bị sao chép?

Khai báo hàm tạo sao chép & toán tử gán sao chép dưới dạng chỉ định truy cập riêng.

class MemoryBlock
{
public:

//code here

private:
MemoryBlock(const MemoryBlock& other)
{
   cout<<"copy constructor"<<endl;
}

// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other)
{
 return *this;
}
};

int main()
{
   MemoryBlock a;
   MemoryBlock b(a);
}

Trong C ++ 11 trở đi, bạn cũng có thể khai báo toán tử sao chép & toán tử gán

class MemoryBlock
{
public:
MemoryBlock(const MemoryBlock& other) = delete

// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other) =delete
};


int main()
{
   MemoryBlock a;
   MemoryBlock b(a);
}

16

Nhiều câu trả lời hiện có đã chạm vào hàm tạo sao chép, toán tử gán và hàm hủy. Tuy nhiên, trong bài C ++ 11, việc giới thiệu di chuyển ngữ nghĩa có thể mở rộng điều này vượt quá 3.

Gần đây Michael Claisse đã có một bài nói chuyện chạm vào chủ đề này: http://channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class


10

Quy tắc ba trong C ++ là một nguyên tắc cơ bản của thiết kế và phát triển ba yêu cầu mà nếu có định nghĩa rõ ràng trong một trong các hàm thành viên sau, thì lập trình viên nên xác định hai hàm thành viên còn lại với nhau. Cụ thể ba chức năng thành viên sau đây là không thể thiếu: hàm hủy, hàm tạo sao chép, toán tử gán sao chép.

Copy constructor trong C ++ là một constructor đặc biệt. Nó được sử dụng để xây dựng một đối tượng mới, đó là đối tượng mới tương đương với một bản sao của một đối tượng hiện có.

Toán tử gán gán là một toán tử gán đặc biệt thường được sử dụng để chỉ định một đối tượng hiện có cho các đối tượng khác có cùng loại đối tượng.

Có những ví dụ nhanh:

// default constructor
My_Class a;

// copy constructor
My_Class b(a);

// copy constructor
My_Class c = a;

// copy assignment operator
b = a;

7
Xin chào, câu trả lời của bạn không thêm bất cứ điều gì mới. Những cái khác bao quát chủ đề ở độ sâu hơn nhiều, và chính xác hơn - câu trả lời của bạn là gần đúng và thực tế là sai ở một số nơi (cụ thể là không có "phải" ở đây; "rất có thể nên"). Nó thực sự không có giá trị của bạn trong khi đăng loại câu trả lời này cho các câu hỏi đã được trả lời kỹ lưỡng. Trừ khi bạn có những thứ mới để thêm.
Mat

1
Ngoài ra, có bốn ví dụ nhanh, bằng cách nào đó có liên quan đến hai trong số ba mà Quy tắc ba đang nói đến. Quá nhiều nhầm lẫn.
anatolyg
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.