Sao Chép Đối Tượng Trong C++: Copy Constructor Toàn Tập

Copy Constructor Trong C++

Copy constructor là một trong những khái niệm cốt lõi trong lập trình C++ hướng đối tượng. Bài viết này sẽ giúp bạn hiểu rõ cơ chế hoạt động cũng như các tình huống mà copy constructor được gọi.

Khi Nào Copy Constructor Được Kích Hoạt?

Có ba trường hợp chính khiến copy constructor được gọi tự động:

  • Khởi tạo đối tượng từ một đối tượng khác: Khi bạn tạo một biến mới và khởi giá trị cho nó bằng một biến đã tồn tại
  • Truyền đối số cho hàm theo giá trị: Khi một đối tượng được truyền vào hàm mà không phải qua tham chiếu hoặc con trỏ
  • Hàm trả về giá trị đối tượng: Khi hàm trả về một đối tượng theo giá trị
class Student
{...};

Student s1;                    // Gọi constructor mặc định
Student s2(s1);                // Gọi copy constructor
Student s3 = s1;               // Tương đương với dòng trên
Student s4;                    // Constructor mặc định
s4 = s1;                       // Chỉ gán, không gọi copy constructor

Điểm quan trọng cần lưu ý là toán tử "=" trong quá trình khởi tạo (dòng Student s3 = s1) thực chất vẫn gọi copy constructor, chứ không phải toán tử gán thông thường.

Cấu Trúc Của Copy Constructor

Dạng chuẩn của copy constructor có dạng:

ClassName::ClassName(const ClassName& other)

Nếu lớp không định nghĩa copy constructor, trình biên dịch C++ sẽ tự động sinh ra một bản mặc định. Bản mặc định này thực hiện sao chép từng thành viên (memberwise copy). Tuy nhiên, trong nhiều trường hợp, đặc biệt khi lớp quản lý tài nguyên động, bạn cần tự viết copy constructor để tránh lỗi double-free.

Ví Dụ Thực Tế Với Biến Theo Dõi

Để hiểu rõ hơn về quá trình gọi copy constructor, chúng ta sẽ sử dụng một biến static để đếm số đối tượng. Cách tiếp cận này giúp quan sát trực quan khi nào constructor, copy constructor và destructor được kích hoạt.

#include <iostream>
#include <string>
using namespace std;

static int objectCounter = 0;

class Tracker
{
public:
    Tracker() { 
        objectCounter++; 
        display("Default constructor"); 
    }
    
    Tracker(const Tracker& t) { 
        objectCounter++; 
        display("Copy constructor"); 
    }
    
    void showStatus(const string& info = "")
    {
        if (!info.empty()) cout << info << ": ";
        cout << "Total objects: " << objectCounter << endl;
    }
    
    ~Tracker() {
        objectCounter--;
        display("Destructor");
    }

private:
    void display(const string& action) {
        cout << action << " - Objects active: " << objectCounter << endl;
    }
};

Tracker process(Tracker param) {
    cout << "=== Entering process() ===" << endl;
    param.showStatus("Parameter inside function");
    cout << "=== Leaving process() ===" << endl;
    return param;
}

int main()
{
    Tracker original;
    original.showStatus("After creating original");
    
    Tracker result = process(original);
    original.showStatus("After function call");
    
    return 0;
}

Phân Tích Luồng Hoạt Động

Khi hàm process() được gọi, một bản sao của đối tượng original được tạo thông qua copy constructor để truyền vào hàm. Khi hàm kết thúc và trả về giá trị, một bản sao khác lại được tạo để gán cho result. Cơ chế này đảm bảo tính toàn vẹn dữ liệu nhưng cũng tiêu tốn tài nguyên.

Để tối ưu hiệu suất, bạn nên truyền đối số theo tham chiếu hằng (const reference) thay vì theo giá trị. Cách làm này tránh việc tạo ra các bản sao không cần thiết trong suốt quá trình thực thi.

void optimizedFunction(const Tracker& obj);  // Không gọi copy constructor

Kết Luận

Hiểu rõ cơ chế hoạt động của copy constructor giúp bạn viết mã C++ hiệu quả và tránh được các lỗi liên quan đến quản lý bộ nhớ. Hãy luôn cân nhắc việc tự định nghĩa copy constructor khi lớp của bạn quản lý tài nguyên động hoặc có semantics đặc biệt.

Thẻ: C++ copy-constructor copy-initialization object-lifecycle constructors

Đăng vào ngày 29 tháng 8 lúc 04:02