Cách Sử Dụng Copy Constructor

Trong bài viết này, chúng ta sẽ khám phá “copy constructor” – một hàm tạo đặc biệt trong lập trình hướng đối tượng, có chức năng tạo ra một đối tượng mới từ một đối tượng đã tồn tại. Bài viết cung cấp 20 ví dụ sử dụng chính xác về cú pháp và có nghĩa, cùng hướng dẫn chi tiết về ý nghĩa, cách dùng, cú pháp, và các lưu ý quan trọng.

Phần 1: Hướng dẫn sử dụng “copy constructor” và các lưu ý

1. Ý nghĩa cơ bản của “copy constructor”

“Copy constructor” là một hàm tạo (constructor) đặc biệt trong C++ và các ngôn ngữ lập trình hướng đối tượng khác:

  • Tạo bản sao: Tạo một đối tượng mới là bản sao của một đối tượng đã có.

Dạng liên quan: “constructor” (hàm tạo), “deep copy” (sao chép sâu), “shallow copy” (sao chép nông).

Ví dụ:

  • Copy Constructor: `MyClass(const MyClass &other)` (Hàm tạo sao chép của lớp MyClass.)
  • Constructor: `MyClass()` (Hàm tạo mặc định.)
  • Deep Copy: Tạo bản sao hoàn toàn độc lập của dữ liệu.

2. Cách sử dụng “copy constructor”

a. Cú pháp cơ bản

  1. ClassName(const ClassName &obj)
    Ví dụ: `MyClass(const MyClass &other) { … }`

b. Khi nào copy constructor được gọi?

  1. Khởi tạo đối tượng mới bằng một đối tượng đã tồn tại
    Ví dụ: `MyClass obj2 = obj1;`
  2. Truyền đối tượng vào hàm theo giá trị
    Ví dụ: `void myFunction(MyClass obj) { … }`
  3. Trả về đối tượng từ hàm theo giá trị
    Ví dụ: `MyClass myFunction() { … return obj; }`

c. Phân biệt deep copy và shallow copy

Loại sao chép Mô tả Ví dụ
Shallow Copy Sao chép giá trị của các thuộc tính, bao gồm cả con trỏ. Các con trỏ trỏ đến cùng một vùng nhớ. Nếu `obj1.ptr` và `obj2.ptr` cùng trỏ đến một vùng nhớ, thay đổi dữ liệu qua `obj1.ptr` sẽ ảnh hưởng đến `obj2.ptr`.
Deep Copy Sao chép giá trị của các thuộc tính và tạo bản sao mới của vùng nhớ được trỏ bởi các con trỏ. `obj1.ptr` và `obj2.ptr` trỏ đến các vùng nhớ khác nhau, do đó thay đổi dữ liệu qua một con trỏ không ảnh hưởng đến con trỏ còn lại.

3. Một số trường hợp sử dụng copy constructor

  • Quản lý bộ nhớ: Đảm bảo cấp phát và giải phóng bộ nhớ đúng cách khi sao chép đối tượng chứa con trỏ.
    Ví dụ: Khi đối tượng chứa một con trỏ đến một mảng, copy constructor cần cấp phát một mảng mới và sao chép dữ liệu sang mảng mới này.
  • Tạo bản sao độc lập: Tạo một bản sao hoàn toàn độc lập với đối tượng gốc.
    Ví dụ: Khi cần tạo một bản sao của một đối tượng mà không muốn thay đổi trên bản sao ảnh hưởng đến đối tượng gốc.
  • Tránh lỗi dangling pointer: Ngăn chặn các con trỏ “treo” (dangling pointer) khi đối tượng gốc bị hủy.
    Ví dụ: Nếu copy constructor không được định nghĩa, có thể xảy ra trường hợp con trỏ trong bản sao trỏ đến vùng nhớ đã được giải phóng.

4. Lưu ý khi sử dụng “copy constructor”

a. Ngữ cảnh phù hợp

  • Khi đối tượng chứa con trỏ: Bắt buộc phải định nghĩa copy constructor để thực hiện deep copy nếu muốn tạo bản sao độc lập.
    Ví dụ: Đối tượng chứa con trỏ đến một chuỗi ký tự.
  • Khi cần kiểm soát quá trình sao chép: Copy constructor cho phép tùy chỉnh cách đối tượng được sao chép.
    Ví dụ: Loại trừ một số thuộc tính khỏi quá trình sao chép.

b. Phân biệt với assignment operator

  • “Copy Constructor” vs “Assignment Operator”:
    “Copy Constructor”: Tạo một đối tượng *mới* từ một đối tượng đã tồn tại.
    “Assignment Operator”: Gán giá trị của một đối tượng đã tồn tại cho một đối tượng *đã được khởi tạo*.
    Ví dụ: `MyClass obj2 = obj1;` (Copy Constructor) / `obj2 = obj1;` (Assignment Operator)

c. Nếu không định nghĩa copy constructor

  • Compiler sẽ tạo một copy constructor mặc định: Copy constructor mặc định thực hiện shallow copy.
    Ví dụ: Nếu đối tượng chứa con trỏ, bản sao sẽ chia sẻ cùng vùng nhớ với đối tượng gốc.

5. Những lỗi cần tránh

  1. Quên định nghĩa copy constructor khi cần deep copy:
    – Hậu quả: Lỗi dangling pointer, thay đổi trên bản sao ảnh hưởng đến đối tượng gốc.
  2. Không xử lý đúng các trường hợp đặc biệt trong copy constructor:
    – Ví dụ: Không kiểm tra xem con trỏ có null hay không trước khi sao chép.
  3. Gọi copy constructor một cách không cần thiết:
    – Ví dụ: Truyền đối tượng vào hàm theo giá trị khi có thể truyền theo tham chiếu.

6. Mẹo để ghi nhớ và sử dụng hiệu quả

  • Luôn xem xét liệu đối tượng có chứa con trỏ hay không: Nếu có, hãy nghĩ đến việc định nghĩa copy constructor.
  • Hiểu rõ sự khác biệt giữa deep copy và shallow copy: Chọn phương pháp phù hợp với yêu cầu của bài toán.
  • Sử dụng RAII (Resource Acquisition Is Initialization): Kết hợp với copy constructor để quản lý tài nguyên một cách an toàn.

Phần 2: Ví dụ sử dụng “copy constructor” và các dạng liên quan

Ví dụ minh họa

  1. MyClass obj1(10); MyClass obj2 = obj1; (Khởi tạo obj2 bằng copy constructor.)
  2. MyClass obj3 = MyClass(20); (Khởi tạo tạm thời và sao chép.)
  3. void printMyClass(MyClass obj) { cout << obj.value; } printMyClass(obj1); (Truyền theo giá trị, gọi copy constructor.)
  4. MyClass createMyClass() { MyClass obj(30); return obj; } MyClass obj4 = createMyClass(); (Trả về theo giá trị, gọi copy constructor.)
  5. MyClass* ptr1 = new MyClass(40); MyClass obj5 = *ptr1; (Sao chép đối tượng được trỏ bởi con trỏ.)
  6. vector vec; vec.push_back(obj1); (Sao chép obj1 vào vector.)
  7. MyClass obj6; obj6 = obj1; (Sử dụng assignment operator, không phải copy constructor sau khi obj6 được khởi tạo).
  8. MyClass obj7(obj1); (Tương đương với MyClass obj7 = obj1; )
  9. MyClass* ptr2 = new MyClass(*ptr1); (Tạo đối tượng mới trên heap bằng copy constructor.)
  10. MyClass obj8 = static_cast(50); (Ép kiểu và sao chép.)
  11. class Derived : public MyClass { public: Derived(const Derived& other) : MyClass(other) {} }; (Copy constructor trong lớp kế thừa.)
  12. template class MyTemplate { public: MyTemplate(const MyTemplate& other) {} }; (Copy constructor trong template.)
  13. MyClass obj9(std::move(obj1)); (Move constructor, thường nhanh hơn copy constructor.)
  14. MyClass obj10 = {}; (Khởi tạo mặc định, không gọi copy constructor.)
  15. struct MyStruct { MyClass obj; }; MyStruct s1; s1.obj = obj1; (Assignment operator cho thành viên struct).
  16. union MyUnion { MyClass obj; int value; }; MyUnion u1; u1.obj = obj1; (Assignment operator cho thành viên union).
  17. std::optional opt = obj1; (Sao chép obj1 vào optional.)
  18. std::shared_ptr sharedPtr1 = std::make_shared(obj1); (Copy constructor của MyClass được gọi khi tạo bản sao của shared_ptr.)
  19. std::unique_ptr uniquePtr1 = std::make_unique(MyClass(60)); (Không thể sao chép unique_ptr, cần move.)
  20. MyClass obj11 = (MyClass)70; (Ép kiểu và sao chép.)