OOP Bài 10: Copy Constructor - Nỗi ám ảnh vùng nhớ và Sao chép trạng thái
1. Copy Constructor là gì?
Copy Constructor là một hàm tạo đặc biệt. Thay vì nhận các tham số rời rạc (như string, int), tham số đầu vào duy nhất của nó chính là một tham chiếu đến một Object khác cùng Class.
- Mục đích: Tạo ra một Object hoàn toàn mới (cấp phát RAM mới), nhưng lấy dữ liệu (trạng thái) sao chép y hệt từ Object gốc.
2. Tại sao C++ coi Copy Constructor là sống còn?
Trong C++, khác với các ngôn ngữ có Garbage Collector (GC), bạn có toàn quyền quyết định cấp phát bộ nhớ động (bằng new) và sử dụng con trỏ (Pointer).
Khi bạn truyền một Object vào một hàm theo dạng tham trị (Pass by Value), hoặc gán Object này cho Object kia (Obj A = Obj B), trình biên dịch C++ sẽ âm thầm tự động gọi Copy Constructor mặc định.
Nếu bạn không tự viết Copy Constructor, C++ sẽ làm một việc gọi là Shallow Copy (Sao chép nông): Nó copy nguyên si giá trị từng thuộc tính, bao gồm cả địa chỉ con trỏ.
🔥 Thảm họa Double Free (Giải phóng kép) trong C++
Giả sử hệ thống AFC có một class TurnstileLog quản lý log quẹt thẻ:
- Object A giữ một con trỏ trỏ tới vùng RAM (Heap) chứa 100 dòng log.
- Object B được copy từ Object A (Shallow Copy). Lúc này, con trỏ của B cũng trỏ tới cùng một vùng RAM của A.
- Khi Object A bị hủy, Destructor của A sẽ xóa vùng RAM đó (
delete[] logBuffer). - Một lúc sau, Object B bị hủy. Destructor của B lại cố gắng xóa vùng RAM đó một lần nữa. Chương trình ngay lập tức bị hệ điều hành "bắn bỏ" (Crash - Segfault - Double Free).
✅ Giải pháp: Viết Custom Copy Constructor để thực hiện Deep Copy (Sao chép sâu)
Chúng ta bắt buộc phải viết lại Copy Constructor, yêu cầu cấp phát một vùng RAM hoàn toàn mới, sau đó chép từng byte dữ liệu sang.
class TurnstileLog {
private:
int* logs; // Con trỏ trỏ ra Heap
int size;
public:
// 1. Parameterized Constructor
TurnstileLog(int s) {
size = s;
logs = new int[size]; // Cấp phát vùng RAM 1
}
// 2. COPY CONSTRUCTOR (Quyết định sống còn)
// Tham chiếu hằng: const TurnstileLog&
TurnstileLog(const TurnstileLog& source) {
size = source.size;
logs = new int[size]; // CẤP PHÁT VÙNG RAM 2 HOÀN TOÀN MỚI
// Copy từng phần tử dữ liệu (Deep Copy)
for(int i = 0; i < size; i++) {
logs[i] = source.logs[i];
}
}
// 3. Destructor
~TurnstileLog() {
delete[] logs; // Hủy an toàn, không sợ dẫm chân lên nhau
}
};
// ==========================
// Thực thi
TurnstileLog gate1_logs(100);
TurnstileLog gate2_logs = gate1_logs; // C++ tự động gọi Copy Constructor ở đây
3. Tại sao ở PHP, Java, hay Go ít nhắc đến Copy Constructor?
- Với PHP và Java: Khi bạn gán
$gate2 = $gate1;, ngôn ngữ KHÔNG tạo ra Object mới. Nó chỉ tạo thêm một con trỏ tham chiếu (Alias) trỏ về cùng một Object trên Heap. Do đó, chẳng có copy nào diễn ra cả. Nếu muốn thực sự clone (nhân bản) Object ở PHP, ta không dùng Copy Constructor mà dùng từ khóaclone, lúc này PHP sẽ kích hoạt magic method__clone()để ta xử lý Deep Copy. - Với Go (Golang): Go là ngôn ngữ hướng dữ liệu nhiều hơn. Nó không có khái niệm Constructor chuẩn OOP (không có từ khóa
class). Dữ liệu được quản lý quastruct. Khi gángate2 := gate1, Go mặc định làm Shallow Copy tất cả các field. Vì Go có Garbage Collector tự dọn dẹp vùng nhớ, thảm họa "Double Free" hiếm khi xảy ra, trừ khi bạn cố tình thao tác với pointer và manual concurrency.
Tóm lại: Hiểu Copy Constructor là hiểu về quyền kiểm soát vùng nhớ cấp thấp. Trong các hệ thống Backend chạy C++ đòi hỏi tối ưu tài nguyên gắt gao (như engine của máy bán vé, xử lý tín hiệu phần cứng), bỏ quên Copy Constructor đồng nghĩa với việc tạo ra những quả bom nổ chậm trên RAM.
All Rights Reserved