0

[C++ OOP Thực Chiến] Bài 37: Cài đặt kế thừa (Phần 1) - Định luật "Xây móng trước khi lợp mái"

Chào anh em! Ở [Bài 36], chúng ta đã biết cách dùng protected để chia sẻ tài sản từ Cha xuống Con. Đồng thời, mình có để lại một câu hỏi lớn ở hàm Constructor: Tại sao lớp DevBackend lại có cú pháp : NhanVien(ten, luong) kỳ lạ như vậy?

Hôm nay, chúng ta sẽ mổ xẻ quá trình "chào đời" của một Object trong Kế thừa để hiểu rõ cơ chế cấp phát bộ nhớ của C++.

1. Định luật Khởi tạo: Ai sinh ra trước?

Khi bạn gõ lệnh DevBackend dev("Hieu", 2500); để đúc ra một Object lớp Con, C++ sẽ làm gì trong RAM?

Có một quy tắc bất di bất dịch trong C++: Lớp Cha luôn luôn được khởi tạo TRƯỚC Lớp Con.

Tại sao lại như vậy? Hãy tưởng tượng Lớp Cha là "cái móng nhà", Lớp Con là "cái mái nhà". Bạn không thể lợp mái khi chưa có móng. Hơn nữa, Lớp Con kế thừa và sử dụng các biến của Lớp Cha (như hoTen, luongCoBan). Nếu Lớp Cha chưa được cấp phát bộ nhớ và gán giá trị, Lớp Con lấy cái gì ra mà dùng?

2. Cú pháp truyền tham số cho Cha (Member Initializer List)

Vì Lớp Cha phải ra đời trước, nên khi Lớp Con được khởi tạo, việc ĐẦU TIÊN nó phải làm là cung cấp nguyên liệu (tham số) để xây dựng Lớp Cha.

Nếu Lớp Cha có Constructor mặc định (không tham số), C++ sẽ tự động gọi nó một cách âm thầm. Nhưng nếu Lớp Cha có Constructor cần tham số (ví dụ NhanVien bắt buộc phải có tenluong), Lớp Con BẮT BUỘC phải truyền tham số ngược lên cho Cha thông qua danh sách khởi tạo (Initializer List).

Cú pháp chuẩn:

LopCon(Kiểu thamSo1, Kiểu thamSo2) : LopCha(thamSo1) {
    // Thân hàm khởi tạo của Lớp Con
}

Dấu hai chấm : chính là cánh cổng nối từ Con ngược lên Cha.

3. Code Demo: Vòng đời khởi tạo Core System

Để chứng minh định luật "Cha trước, Con sau", chúng ta hãy cùng đặt các dòng log cout vào trong Constructor của một hệ thống Database:

#include <iostream>
#include <string>

using namespace std;

// --- LỚP CHA: Cốt lõi của mọi Database ---
class BaseDatabase {
protected:
    string host;
    int port;

public:
    // Constructor của Cha
    BaseDatabase(string h, int p) : host(h), port(p) {
        cout << "[1] BaseDatabase Constructor duoc goi! (Xay mong)\n";
        cout << "    -> Thiet lap Host: " << host << ", Port: " << port << "\n";
    }
};

// --- LỚP CON: Database MySQL cụ thể ---
class MySQLDatabase : public BaseDatabase {
private:
    string username;

public:
    // Constructor của Con
    // NHẬN 3 tham số, nhưng lập tức ĐẨY 2 tham số (h, p) lên cho Cha xây dựng trước
    MySQLDatabase(string h, int p, string user) : BaseDatabase(h, p) {
        username = user; // Sau khi Cha xây xong, Con mới tự xây phần của mình
        cout << "[2] MySQLDatabase Constructor duoc goi! (Lop mai)\n";
        cout << "    -> Thiet lap Username: " << username << "\n";
    }

    void connect() {
        cout << "\n[Success] Ket noi MySQL den " << host << ":" << port 
             << " bang user '" << username << "' thanh cong!\n";
    }
};

int main() {
    cout << "--- HE THONG LOG CONSTRUCTOR ---\n\n";

    // Khởi tạo Object lớp Con
    cout << "Bat dau tao Object MySQLDatabase...\n";
    MySQLDatabase myDb("127.0.0.1", 3306, "admin_hieu");

    myDb.connect();

    return 0;
}

Kết quả khi chạy:

Bat dau tao Object MySQLDatabase...
[1] BaseDatabase Constructor duoc goi! (Xay mong)
    -> Thiet lap Host: 127.0.0.1, Port: 3306
[2] MySQLDatabase Constructor duoc goi! (Lop mai)
    -> Thiet lap Username: admin_hieu

[Success] Ket noi MySQL den 127.0.0.1:3306 bang user 'admin_hieu' thanh cong!

Nhận xét:

Nhìn vào Terminal, anh em thấy rõ ràng hàm main gọi đúc MySQLDatabase, nhưng dòng chữ [1] BaseDatabase Constructor lại được in ra trước! Lớp con đã phải tạm dừng, "chạy nộp mạng" dữ liệu lên cho Lớp cha khởi tạo xong xuôi, rồi nó mới tiếp tục thực thi phần {} của mình.

Tạm kết & Gợi mở

Tuyệt vời! Anh em đã nắm rõ quy luật "Cha sinh trước, Con sinh sau" trong quá trình khởi tạo (Constructor).

Tuy nhiên, có sinh thì phải có diệt. Khi Object myDb trong hàm main kết thúc vòng đời và bị hệ thống dọn dẹp bằng Destructor (Hàm hủy), theo anh em, phần bộ nhớ của Cha hay của Con sẽ bị phá hủy trước?

Và một tình huống cực kỳ phổ biến nữa: Giả sử Lớp Cha có một hàm tên là connect(), nhưng Lớp Con lại thấy cái hàm đó code cùi quá, nó muốn TỰ VIẾT LẠI một hàm connect() của riêng nó cho xịn hơn thì phải làm sao? Lớp Con có quyền "phản nghịch" đè nén logic của Cha không?

Tất cả những bí mật về hàm hủy và kỹ thuật Ghi đè (Overriding) nền tảng sẽ được bật mí trong 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! Nhớ Upvote để mình có động lực gõ phím tiếp nhé!


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.