Khi nào chúng ta phải sử dụng các hàm tạo bản sao?


87

Tôi biết rằng trình biên dịch C ++ tạo một phương thức khởi tạo sao chép cho một lớp. Trong trường hợp nào chúng ta phải viết một hàm tạo bản sao do người dùng định nghĩa? Bạn có thể cho một số ví dụ?



1
Một trong những trường hợp phải viết copy-ctor của riêng bạn: Khi bạn phải làm deep copy. Cũng lưu ý rằng ngay khi bạn tạo ctor, không có ctor mặc định nào được tạo cho bạn (trừ khi bạn sử dụng từ khóa mặc định).
harshvchawla

Câu trả lời:


75

Hàm tạo bản sao được tạo bởi trình biên dịch thực hiện việc sao chép khôn ngoan thành viên. Đôi khi điều đó là không đủ. Ví dụ:

class Class {
public:
    Class( const char* str );
    ~Class();
private:
    char* stored;
};

Class::Class( const char* str )
{
    stored = new char[srtlen( str ) + 1 ];
    strcpy( stored, str );
}

Class::~Class()
{
    delete[] stored;
}

trong trường hợp này, việc sao chép thành viên khôn ngoan của storedthành viên sẽ không sao chép bộ đệm (chỉ con trỏ mới được sao chép), vì vậy bản sao đầu tiên bị hủy chia sẻ bộ đệm sẽ gọi delete[]thành công và bản thứ hai sẽ chạy vào hành vi không xác định. Bạn cần hàm tạo sao chép sâu (và cả toán tử gán).

Class::Class( const Class& another )
{
    stored = new char[strlen(another.stored) + 1];
    strcpy( stored, another.stored );
}

void Class::operator = ( const Class& another )
{
    char* temp = new char[strlen(another.stored) + 1];
    strcpy( temp, another.stored);
    delete[] stored;
    stored = temp;
}

10
Nó không thực hiện bit-khôn ngoan, nhưng sao chép thành viên, đặc biệt gọi copy-ctor cho các thành viên kiểu lớp.
Georg Fritzsche,

7
Đừng viết toán tử assingment như vậy. Nó không phải là ngoại lệ an toàn. (nếu cái mới ném ra một ngoại lệ, đối tượng được để ở trạng thái không xác định với bộ nhớ trỏ tới một phần được phân bổ theo thỏa thuận của bộ nhớ (CHỈ phân bổ bộ nhớ sau khi tất cả các thao tác có thể ném đã hoàn thành thành công)). Một giải pháp đơn giản là sử dụng idium hoán đổi bản sao.
Martin York

@sharptooth dòng thứ 3 từ dưới bạn có delete stored[];và tôi tin rằng nó phải đượcdelete [] stored;
Peter Ajtai

4
Tôi biết đó chỉ là một ví dụ, nhưng bạn nên chỉ ra giải pháp tốt hơn là sử dụng std::string. Ý tưởng chung là chỉ các lớp tiện ích quản lý tài nguyên mới cần quá tải Big Three, và tất cả các lớp khác chỉ nên sử dụng các lớp tiện ích đó, loại bỏ nhu cầu xác định bất kỳ lớp nào trong Big Three.
GManNickG

2
@Martin: Tôi muốn chắc chắn rằng nó được chạm khắc bằng đá. : P
GManNickG

46

Tôi hơi bực mình rằng quy tắc của Rule of Fivekhông được trích dẫn.

Quy tắc này rất đơn giản:

Quy tắc năm :
Bất cứ khi nào bạn đang viết một trong các hàm hủy, mã lệnh sao chép, toán tử gán bản sao, mã lệnh di chuyển hoặc toán tử gán di chuyển, bạn có thể cần viết bốn toán tử còn lại.

Nhưng có một hướng dẫn chung hơn mà bạn nên làm theo, xuất phát từ nhu cầu viết mã an toàn ngoại lệ:

Mỗi tài nguyên nên được quản lý bởi một đối tượng chuyên dụng

Dưới đây @sharptoothlà mã vẫn còn (chủ yếu) tốt, tuy nhiên nếu anh ta đã thêm một thuộc tính thứ hai đến lớp mình nó sẽ không có. Hãy xem xét lớp sau:

class Erroneous
{
public:
  Erroneous();
  // ... others
private:
  Foo* mFoo;
  Bar* mBar;
};

Erroneous::Erroneous(): mFoo(new Foo()), mBar(new Bar()) {}

Điều gì xảy ra nếu new Barném? Làm cách nào để bạn xóa đối tượng được trỏ tới mFoo? Có những giải pháp (cấp chức năng thử / bắt ...), chúng chỉ không mở rộng quy mô.

Cách thích hợp để đối phó với tình huống này là sử dụng các lớp thích hợp thay vì các con trỏ thô.

class Righteous
{
public:
private:
  std::unique_ptr<Foo> mFoo;
  std::unique_ptr<Bar> mBar;
};

Với việc triển khai cùng một hàm tạo (hoặc thực sự, đang sử dụng make_unique), giờ đây tôi có quyền an toàn ngoại lệ miễn phí !!! Thật thú vị phải không? Và trên hết, tôi không còn cần phải lo lắng về một trình hủy phù hợp! Tôi cần phải viết của riêng mình Copy ConstructorAssignment Operatormặc dù, vì unique_ptrkhông xác định các hoạt động này ... nhưng nó không quan trọng ở đây;)

Và do đó, sharptoothlớp của được xem lại:

class Class
{
public:
  Class(char const* str): mData(str) {}
private:
  std::string mData;
};

Tôi không biết về bạn, nhưng tôi thấy tôi dễ dàng hơn;)


Đối với C ++ 11 - quy tắc năm bổ sung vào quy tắc ba toán tử Move Constructer và Move Assignment.
Robert Andrzejuk

1
@Robb: Lưu ý rằng trên thực tế, như đã trình bày trong ví dụ trước, bạn thường nên hướng tới Quy tắc số không . Chỉ các lớp kỹ thuật chuyên biệt (chung chung) mới nên quan tâm đến việc xử lý một tài nguyên, tất cả các lớp khác nên sử dụng các con trỏ / vùng chứa thông minh đó và đừng lo lắng về nó.
Matthieu M.

@MatthieuM. Đồng ý :-) Tôi đã đề cập đến Quy tắc năm, vì câu trả lời này có trước C ++ 11 và bắt đầu bằng "Big Three", nhưng cần đề cập rằng bây giờ "Big Five" có liên quan. Tôi không muốn bỏ phiếu cho câu trả lời này vì nó đúng trong ngữ cảnh được hỏi.
Robert Andrzejuk

@Robb: Điểm tốt, tôi đã cập nhật câu trả lời để đề cập đến Quy tắc năm thay vì Ba lớn. Hy vọng rằng hầu hết mọi người đã chuyển sang trình biên dịch có khả năng C ++ 11 ngay bây giờ (và tôi tiếc cho những người vẫn chưa).
Matthieu M.

32

Tôi có thể nhớ lại từ thực tế của mình và nghĩ đến các trường hợp sau khi người ta phải xử lý việc khai báo / xác định rõ ràng hàm tạo bản sao. Tôi đã nhóm các trường hợp thành hai loại

  • Tính đúng đắn / Ngữ nghĩa - nếu bạn không cung cấp hàm tạo bản sao do người dùng xác định, các chương trình sử dụng kiểu đó có thể không biên dịch được hoặc có thể hoạt động không chính xác.
  • Tối ưu hóa - cung cấp một giải pháp thay thế tốt cho hàm tạo sao chép do trình biên dịch tạo ra cho phép làm cho chương trình nhanh hơn.


Độ đúng / Ngữ nghĩa

Tôi đặt ra trong phần này các trường hợp mà việc khai báo / xác định hàm tạo bản sao là cần thiết cho hoạt động chính xác của các chương trình sử dụng kiểu đó.

Sau khi đọc qua phần này, bạn sẽ tìm hiểu về một số cạm bẫy khi cho phép trình biên dịch tự tạo ra phương thức khởi tạo sao chép. Do đó, như seand đã lưu ý trong câu trả lời của mình , luôn an toàn khi tắt khả năng sao chép cho một lớp mới và cố ý bật nó sau khi thực sự cần thiết.

Cách tạo một lớp không thể sao chép trong C ++ 03

Khai báo một copy-constructor riêng tư và không cung cấp triển khai cho nó (để xây dựng không thành công ở giai đoạn liên kết ngay cả khi các đối tượng của kiểu đó được sao chép trong phạm vi riêng của lớp hoặc bởi bạn bè của nó).

Cách tạo một lớp không thể sao chép trong C ++ 11 hoặc mới hơn

Khai báo copy-constructor với =deleteat end.


Bản sao nông và sâu

Đây là trường hợp được hiểu rõ nhất và thực sự là trường hợp duy nhất được đề cập trong các câu trả lời khác. shaprtooth đã che phủ nó khá tốt. Tôi chỉ muốn nói thêm rằng tài nguyên sao chép sâu mà đối tượng độc quyền sở hữu có thể áp dụng cho bất kỳ loại tài nguyên nào, trong đó bộ nhớ được cấp phát động chỉ là một loại. Nếu cần, việc sao chép sâu một đối tượng cũng có thể yêu cầu

  • sao chép các tệp tạm thời trên đĩa
  • mở một kết nối mạng riêng
  • tạo một chuỗi công nhân riêng biệt
  • phân bổ bộ đệm khung OpenGL riêng biệt
  • Vân vân

Đối tượng tự đăng ký

Hãy xem xét một lớp mà tất cả các đối tượng - bất kể chúng được xây dựng như thế nào - PHẢI được đăng ký bằng cách nào đó. Vài ví dụ:

  • Ví dụ đơn giản nhất: duy trì tổng số các đối tượng hiện có. Đăng ký đối tượng chỉ là tăng bộ đếm tĩnh.

  • Một ví dụ phức tạp hơn là có một sổ đăng ký singleton, nơi các tham chiếu đến tất cả các đối tượng hiện có của loại đó được lưu trữ (để các thông báo có thể được gửi đến tất cả chúng).

  • Con trỏ thông minh được tính tham chiếu có thể được coi chỉ là một trường hợp đặc biệt trong danh mục này: con trỏ mới "tự đăng ký" với tài nguyên được chia sẻ thay vì trong sổ đăng ký toàn cầu.

Hoạt động tự đăng ký như vậy phải được thực hiện bởi BẤT KỲ phương thức khởi tạo nào của loại và phương thức tạo bản sao cũng không ngoại lệ.


Đối tượng có tham chiếu chéo nội bộ

Một số đối tượng có thể có cấu trúc bên trong không tầm thường với các tham chiếu chéo trực tiếp giữa các đối tượng con khác nhau của chúng (trên thực tế, chỉ cần một tham chiếu chéo nội bộ như vậy là đủ để kích hoạt trường hợp này). Phương thức khởi tạo sao chép do trình biên dịch cung cấp sẽ phá vỡ các liên kết nội bộ đối tượng , chuyển đổi chúng thành liên kết giữa các đối tượng .

Một ví dụ:

struct MarriedMan;
struct MarriedWoman;

struct MarriedMan {
    // ...
    MarriedWoman* wife;   // association
};

struct MarriedWoman {
    // ...
    MarriedMan* husband;  // association
};

struct MarriedCouple {
    MarriedWoman wife;    // aggregation
    MarriedMan   husband; // aggregation

    MarriedCouple() {
        wife.husband = &husband;
        husband.wife = &wife;
    }
};

MarriedCouple couple1; // couple1.wife and couple1.husband are spouses

MarriedCouple couple2(couple1);
// Are couple2.wife and couple2.husband indeed spouses?
// Why does couple2.wife say that she is married to couple1.husband?
// Why does couple2.husband say that he is married to couple1.wife?

Chỉ các đối tượng đáp ứng các tiêu chí nhất định mới được phép sao chép

Có thể có các lớp mà các đối tượng an toàn để sao chép khi ở một số trạng thái (ví dụ: trạng thái được xây dựng mặc định) và không an toàn khi sao chép theo cách khác. Nếu chúng ta muốn cho phép sao chép các đối tượng an toàn để sao chép, thì - nếu lập trình bảo vệ - chúng ta cần kiểm tra thời gian chạy trong hàm tạo bản sao do người dùng xác định.


Các đối tượng phụ không thể sao chép

Đôi khi, một lớp có thể sao chép sẽ tổng hợp các đối tượng con không thể sao chép. Thông thường, điều này xảy ra đối với các đối tượng có trạng thái không thể quan sát (trường hợp đó được thảo luận chi tiết hơn trong phần "Tối ưu hóa" bên dưới). Trình biên dịch chỉ giúp nhận ra trường hợp đó.


Đối tượng con gần như có thể sao chép

Một lớp, có thể sao chép được, có thể tổng hợp một đối tượng con của một kiểu gần như có thể sao chép. Kiểu gần như có thể sao chép không cung cấp một hàm tạo sao chép theo nghĩa chặt chẽ, nhưng có một hàm tạo khác cho phép tạo một bản sao khái niệm của đối tượng. Lý do để làm cho một kiểu gần như có thể sao chép là khi không có thỏa thuận đầy đủ về ngữ nghĩa sao chép của kiểu đó.

Ví dụ, xem lại trường hợp tự đăng ký đối tượng, chúng ta có thể tranh luận rằng có thể có các tình huống mà một đối tượng phải được đăng ký với trình quản lý đối tượng toàn cục chỉ khi nó là một đối tượng độc lập hoàn chỉnh. Nếu nó là một đối tượng con của một đối tượng khác, thì trách nhiệm quản lý nó là với đối tượng chứa nó.

Hoặc, cả sao chép nông và sâu phải được hỗ trợ (không có cách nào trong số đó là mặc định).

Sau đó, quyết định cuối cùng được để cho những người sử dụng kiểu đó - khi sao chép các đối tượng, họ phải chỉ định rõ ràng (thông qua các đối số bổ sung) phương pháp sao chép dự định.

Trong trường hợp lập trình một cách tiếp cận không phòng thủ, thì cũng có thể có cả một hàm tạo bản sao thông thường và một phương thức khởi tạo gần như sao chép. Điều này có thể được chứng minh khi trong phần lớn các trường hợp, một phương pháp sao chép đơn lẻ nên được áp dụng, trong khi trong những trường hợp hiếm hoi nhưng được hiểu rõ thì nên sử dụng các phương pháp sao chép thay thế. Sau đó, trình biên dịch sẽ không phàn nàn rằng nó không thể xác định rõ ràng hàm tạo bản sao; người dùng sẽ có trách nhiệm duy nhất là ghi nhớ và kiểm tra xem một đối tượng con của loại đó có nên được sao chép thông qua một hàm tạo gần như sao chép hay không.


Không sao chép trạng thái có liên quan chặt chẽ với danh tính của đối tượng

Trong một số ít trường hợp, một tập hợp con của trạng thái có thể quan sát được của đối tượng có thể tạo thành (hoặc được coi là) một phần không thể tách rời của danh tính đối tượng và không thể chuyển giao cho các đối tượng khác (mặc dù điều này có thể gây tranh cãi đôi chút).

Ví dụ:

  • UID của đối tượng (nhưng cái này cũng thuộc trường hợp "tự đăng ký" từ trên xuống, vì id phải được lấy trong hành động tự đăng ký).

  • Lịch sử của đối tượng (ví dụ: ngăn xếp Hoàn tác / Làm lại) trong trường hợp đối tượng mới không được kế thừa lịch sử của đối tượng nguồn, mà thay vào đó bắt đầu bằng một mục lịch sử duy nhất "Được sao chép lúc <TIME> từ <OTHER_OBJECT_ID> ".

Trong những trường hợp như vậy, hàm tạo sao chép phải bỏ qua việc sao chép các đối tượng con tương ứng.


Thực thi chữ ký chính xác của phương thức tạo bản sao

Chữ ký của hàm tạo bản sao do trình biên dịch cung cấp phụ thuộc vào những hàm tạo bản sao nào có sẵn cho các đối tượng con. Nếu ít nhất một đối tượng con không có hàm tạo bản sao thực (lấy đối tượng nguồn bằng tham chiếu không đổi) mà thay vào đó có hàm tạo sao chép đột biến (lấy đối tượng nguồn bằng tham chiếu không hằng số) thì trình biên dịch sẽ không có lựa chọn nào khác nhưng để khai báo một cách ngầm định và sau đó xác định một phương thức tạo bản sao thay đổi.

Bây giờ, điều gì sẽ xảy ra nếu phương thức tạo bản sao "đột biến" của kiểu đối tượng con không thực sự thay đổi đối tượng nguồn (và được viết đơn giản bởi một lập trình viên không biết về consttừ khóa)? Nếu chúng ta không thể sửa mã đó bằng cách thêm phần bị thiếu const, thì tùy chọn khác là khai báo hàm tạo bản sao do người dùng xác định của riêng chúng ta với một chữ ký chính xác và phạm tội chuyển thành a const_cast.


Copy-on-write (COW)

Một vùng chứa COW đã cho đi các tham chiếu trực tiếp đến dữ liệu bên trong của nó PHẢI được sao chép sâu tại thời điểm xây dựng, nếu không nó có thể hoạt động như một chốt đếm tham chiếu.

Mặc dù COW là một kỹ thuật tối ưu hóa, logic này trong phương thức khởi tạo sao chép là rất quan trọng để triển khai chính xác nó. Đó là lý do tại sao tôi đặt trường hợp này ở đây thay vì trong phần "Tối ưu hóa", nơi chúng ta sẽ đi tiếp.



Tối ưu hóa

Trong các trường hợp sau, bạn có thể muốn / cần phải xác định hàm tạo bản sao của riêng mình do các mối quan tâm về tối ưu hóa:


Tối ưu hóa cấu trúc trong quá trình sao chép

Hãy xem xét một vùng chứa hỗ trợ hoạt động loại bỏ phần tử, nhưng có thể làm như vậy bằng cách chỉ cần đánh dấu phần tử đã xóa là đã xóa và tái chế vị trí của nó sau đó. Khi một bản sao của vùng chứa như vậy được tạo ra, có thể hợp lý khi thu gọn dữ liệu còn sót lại thay vì giữ nguyên các vị trí "đã xóa".


Bỏ qua sao chép trạng thái không thể quan sát

Một đối tượng có thể chứa dữ liệu không phải là một phần của trạng thái có thể quan sát được của nó. Thông thường, đây là dữ liệu được lưu trong bộ nhớ đệm / ghi nhớ được tích lũy trong suốt thời gian tồn tại của đối tượng để tăng tốc các hoạt động truy vấn chậm nhất định được thực hiện bởi đối tượng. Có thể an toàn khi bỏ qua việc sao chép dữ liệu đó vì nó sẽ được tính toán lại khi (và nếu!) Các hoạt động liên quan được thực hiện. Việc sao chép dữ liệu này có thể là không hợp lý, vì nó có thể nhanh chóng bị vô hiệu nếu trạng thái có thể quan sát được của đối tượng (từ đó dữ liệu được lưu trong bộ nhớ cache được dẫn xuất) được sửa đổi bằng các thao tác thay đổi (và nếu chúng tôi không sửa đổi đối tượng, tại sao chúng tôi lại tạo sâu sao chép sau đó?)

Việc tối ưu hóa này chỉ được chứng minh nếu dữ liệu phụ trợ lớn so với dữ liệu đại diện cho trạng thái có thể quan sát được.


Tắt tính năng sao chép ngầm

C ++ cho phép vô hiệu hóa việc sao chép ngầm bằng cách khai báo hàm tạo bản sao explicit. Khi đó các đối tượng của lớp đó không thể được chuyển vào các hàm và / hoặc trả về từ các hàm theo giá trị. Thủ thuật này có thể được sử dụng cho một loại có vẻ nhẹ nhưng thực sự rất đắt để sao chép (mặc dù vậy, làm cho nó gần như có thể sao chép có thể là lựa chọn tốt hơn).

Trong C ++ 03, khai báo một hàm tạo bản sao cũng cần xác định nó (tất nhiên, nếu bạn định sử dụng nó). Do đó, việc sử dụng một phương thức tạo bản sao như vậy chỉ đơn thuần là ngoài mối quan tâm đang được thảo luận có nghĩa là bạn phải viết cùng một mã mà trình biên dịch sẽ tự động tạo cho bạn.

C ++ 11 và các tiêu chuẩn mới hơn cho phép khai báo các hàm thành viên đặc biệt (hàm tạo mặc định và sao chép, toán tử gán sao chép và hàm hủy) với một yêu cầu rõ ràng để sử dụng triển khai mặc định (chỉ cần kết thúc khai báo bằng =default).



VIỆC CẦN LÀM

Câu trả lời này có thể được cải thiện như sau:

  • Thêm mã mẫu khác
  • Minh họa trường hợp "Đối tượng có tham chiếu chéo nội bộ"
  • Thêm một số liên kết

6

Nếu bạn có một lớp có nội dung được cấp phát động. Ví dụ: bạn lưu tên sách dưới dạng ký tự * và đặt tên sách mới, bản sao sẽ không hoạt động.

Bạn sẽ phải viết một phương thức khởi tạo sao chép title = new char[length+1]và sau đó strcpy(title, titleIn). Hàm tạo bản sao sẽ chỉ thực hiện một bản sao "nông".


2

Copy Constructor được gọi khi một đối tượng được truyền bởi giá trị, được trả về bởi giá trị hoặc được sao chép rõ ràng. Nếu không có hàm tạo bản sao, c ++ sẽ tạo một hàm tạo sao chép mặc định để tạo một bản sao cạn. Nếu đối tượng không có con trỏ đến bộ nhớ được cấp phát động thì bản sao nông sẽ thực hiện.


0

Bạn thường nên tắt copy ctor và operator = trừ khi lớp đặc biệt cần nó. Điều này có thể ngăn chặn sự thiếu hiệu quả chẳng hạn như chuyển một đối số theo giá trị khi có ý định tham chiếu. Ngoài ra, các phương thức do trình biên dịch tạo ra có thể không hợp lệ.


-1

Hãy xem xét đoạn mã dưới đây:

class base{
    int a, *p;
public:
    base(){
        p = new int;
    }
    void SetData(int, int);
    void ShowData();
    base(const base& old_ref){
        //No coding present.
    }
};
void base :: ShowData(){
    cout<<this->a<<" "<<*(this->p)<<endl;
}
void base :: SetData(int a, int b){
    this->a = a;
    *(this->p) = b;
}
int main(void)
{
    base b1;
    b1.SetData(2, 3);
    b1.ShowData();
    base b2 = b1; //!! Copy constructor called.
    b2.ShowData();
    return 0;
}

Output: 
2 3 //b1.ShowData();
1996774332 1205913761 //b2.ShowData();

b2.ShowData();cung cấp đầu ra rác vì có một hàm tạo sao chép do người dùng xác định được tạo mà không có mã nào được viết để sao chép dữ liệu một cách rõ ràng. Vì vậy, trình biên dịch không tạo ra giống nhau.

Chỉ nghĩ là chia sẻ kiến ​​thức này với tất cả các bạn, mặc dù hầu hết các bạn đều biết rồi.

Chúc mừng ... Chúc bạn viết mã vui vẻ !!!

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.