[C++ OOP Thực Chiến] Bài 38: Cài đặt kế thừa (Phần 2) - Định luật dọn rác và Quyền phản nghịch!
Chào anh em! Ở [Bài 37], chúng ta đã theo dõi vòng đời của một Object đi từ lúc cấp phát bộ nhớ (Constructor). Chúng ta biết rằng Lớp Cha luôn được ưu tiên xây dựng trước Lớp Con.
Nhưng trong các hệ thống Backend, việc đóng kết nối và giải phóng bộ nhớ (Destructor) cũng quan trọng không kém việc khởi tạo. Nếu giải phóng sai thứ tự, hệ thống của bạn sẽ văng lỗi Segmentation Fault (Tràn bộ nhớ) ngay lập tức.
Hôm nay, chúng ta sẽ mổ xẻ "Định luật dọn rác" và một tính năng cực mạnh của OOP: Ghi đè phương thức.
1. Định luật dọn rác (Destructor): Phá mái trước, đào móng sau!
Nguyên lý hoạt động của Hàm hủy (Destructor) trong C++ hoàn toàn NGƯỢC LẠI với Hàm khởi tạo (Constructor). Nó tuân theo cơ chế LIFO (Last In, First Out - Vào sau ra trước).
Quy tắc: Lớp Con sẽ bị hủy TRƯỚC, sau khi dọn dẹp xong Lớp Con, hệ thống mới tiến hành hủy Lớp Cha.
Tại sao C++ lại thiết kế như vậy?
Đây là một sự tính toán cực kỳ logic của những nhà sáng lập ngôn ngữ. Hãy nhớ lại, Lớp Con đang xài ké tài sản của Lớp Cha (ví dụ biến host, port). Trong quá trình "hấp hối" (chạy hàm Destructor), Lớp Con có thể vẫn cần gọi đến các biến này để ghi file log lần cuối.
Nếu C++ giết Lớp Cha trước, các biến host, port bốc hơi mất. Lúc Lớp Con gọi hàm log, nó sẽ truy cập vào một vùng nhớ chết Server crash! Do đó, Con phải dọn dẹp xong xuôi thì Cha mới được phép "nhắm mắt".
2. Quyền phản nghịch: Ghi đè phương thức (Method Overriding / Hiding)
Giả sử Lớp Cha BaseDatabase cung cấp một hàm connect() để kết nối cơ bản. Lớp Con MySQLDatabase được kế thừa hàm đó.
Nhưng ngặt nỗi, kết nối MySQL phức tạp hơn, cần truyền thêm tham số bảo mật. Thằng Con chê hàm connect() của Cha viết quá cùi, nó muốn tự viết một hàm connect() của riêng nó!
C++ hoàn toàn cho phép Lớp Con định nghĩa lại một hàm có TÊN Y HỆT hàm của Lớp Cha.
Khi bạn gọi myDb.connect(), C++ sẽ ưu tiên chạy hàm của Lớp Con, và ngầm che giấu (hide) hàm của Lớp Cha đi. Đây chính là quyền "phản nghịch"!
3. Code Demo: Vòng đời tử thần & Sự nổi loạn
Hãy cùng xem đoạn code chứng minh 2 lý thuyết trên. Chúng ta tiếp tục phát triển hệ thống Database của Bài 37:
#include <iostream>
#include <string>
using namespace std;
// --- LỚP CHA ---
class BaseDatabase {
protected:
string host;
public:
BaseDatabase(string h) : host(h) {
cout << "[+] BaseDb Constructor: Xay mong Host " << host << "\n";
}
~BaseDatabase() {
cout << "[-] BaseDb Destructor: Dao mong, xoa Host " << host << "\n";
}
// Hàm kết nối gốc của Cha
void connect() {
cout << "[Base] Dang ket noi den he thong co ban...\n";
}
};
// --- LỚP CON ---
class MySQLDatabase : public BaseDatabase {
private:
string username;
public:
MySQLDatabase(string h, string user) : BaseDatabase(h) {
username = user;
cout << "[+] MySQL Constructor: Lop mai Username " << username << "\n";
}
~MySQLDatabase() {
cout << "[-] MySQL Destructor: Pha mai, luu log truoc khi xoa Username " << username << "\n";
// Nếu Cha bị hủy trước thì không thể in ra host được nữa!
}
// GHI ĐÈ (LÀM PHẢN): Định nghĩa lại hàm connect()
void connect() {
cout << "[MySQL] Dang ket noi bao mat cao bang user '" << username << "'...\n";
}
// MẸO: Hàm của con VẪN CÓ THỂ gọi hàm của cha nếu muốn
void connectCaHai() {
this->connect(); // Gọi connect() của chính nó (MySQL)
BaseDatabase::connect(); // Cú pháp ép buộc gọi hàm connect() của Cha!
}
};
int main() {
cout << "--- HE THONG LOG DESTRUCTOR & OVERRIDING ---\n\n";
{ // Tạo một Scope (phạm vi) cục bộ để test hàm hủy
cout << ">>> KHOI TAO OBJECT:\n";
MySQLDatabase myDb("127.0.0.1", "admin_hieu");
cout << "\n>>> GOI HAM KET NOI:\n";
// Ưu tiên chạy hàm của Lớp Con (Hàm Cha đã bị che khuất)
myDb.connect();
cout << "\n>>> KET THUC SCOPE (CHUAN BI HUY OBJECT)...\n";
} // Vừa ra khỏi ngoặc nhọn này, Destructor sẽ tự động chạy
return 0;
}
Kết quả khi chạy:
>>> KHOI TAO OBJECT:
[+] BaseDb Constructor: Xay mong Host 127.0.0.1
[+] MySQL Constructor: Lop mai Username admin_hieu
>>> GOI HAM KET NOI:
[MySQL] Dang ket noi bao mat cao bang user 'admin_hieu'...
>>> KET THUC SCOPE (CHUAN BI HUY OBJECT)...
[-] MySQL Destructor: Pha mai, luu log truoc khi xoa Username admin_hieu
[-] BaseDb Destructor: Dao mong, xoa Host 127.0.0.1
Nhận xét:
- Về Hàm hủy: Đúng như sách giáo khoa! Constructor là
Cha -> Con, còn Destructor làCon -> Cha. - Về Hàm
connect: Mặc dùmyDbcó kế thừaconnectcủa Cha, nhưng vì nó tự viết lại nên C++ đã ưu tiên chạy bản local của nó. Tuy nhiên, Lớp Cha không hề mất đi hàmconnect, nếu muốn, Lớp Con vẫn có thể đào nó lên xài bằng toán tử phân giảiBaseDatabase::connect().
Tạm kết & Gợi mở
Tuyệt vời! Anh em đã làm chủ được toàn bộ vòng đời sinh tử của một Object trong Kế thừa, cũng như biết cách "độ" lại các hàm của Lớp Cha cho phù hợp với Lớp Con.
Tuy Kế thừa là một công cụ mạnh mẽ giúp tái sử dụng mã nguồn và thiết kế hệ thống cực tốt, nhưng nó lại là con dao hai lưỡi. Rất nhiều dự án thực tế thất bại ê chề, mã nguồn trở thành một mớ "Spaghetti Code" (rối rắm như mì Ý) chỉ vì các Lập trình viên lạm dụng Kế thừa vô tội vạ. Hoặc đau đầu hơn là đối mặt với những "lỗ hổng" về Kế thừa vòng, Kế thừa kim cương...
Để không rơi vào bẫy của những thiết kế tồi, anh em tuyệt đối không được bỏ qua bài học tiếp theo. Hẹn gặp lại ở Bài 39: Lưu ý cài đặt kế thừa - Những cái bẫy chết người của OOP!
All Rights Reserved