Menu
CoddyTech

Kế thừa ảo

Thuộc phần Lập trình hướng đối tượng trong hành trình C++ của Coddy. Bài học 54 trên 104.

vấn đề kim cương xảy ra khi một lớp (class) kế thừa từ hai lớp có chung một lớp cơ sở. Nếu không xử lý đặc biệt, lớp dẫn xuất sẽ có hai bản sao của lớp cơ sở chung, gây ra sự mơ hồ và lãng phí bộ nhớ.

class Animal {
public:
    int age;
};

class Mammal : public Animal {};
class Bird : public Animal {};

class Bat : public Mammal, public Bird {};

Bat b;
b.age = 5;  // Lỗi: không rõ là 'age' nào?

Lớp Bat chứa hai đối tượng con Animal riêng biệt: một thông qua Mammal và một thông qua Bird. Điều này tạo ra sơ đồ kế thừa hình kim cương, đặt tên cho vấn đề này.

Kế thừa ảo giải quyết vấn đề này bằng cách đảm bảo chỉ tồn tại một bản sao của lớp cơ sở chung. Thêm từ khóa virtual khi kế thừa từ lớp cơ sở dùng chung:

class Animal {
public:
    int age;
    Animal(int a = 0) : age(a) {}
};

class Mammal : virtual public Animal {
public:
    Mammal(int a = 0) : Animal(a) {}
};

class Bird : virtual public Animal {
public:
    Bird(int a = 0) : Animal(a) {}
};

class Bat : public Mammal, public Bird {
public:
    Bat(int a) : Animal(a), Mammal(a), Bird(a) {}
};

Bat b(5);
b.age = 10;  // Hoạt động rồi! Chỉ có một 'age'

Lưu ý rằng Bat phải trực tiếp khởi tạo Animal trong hàm khởi tạo. Với kế thừa ảo, lớp dẫn xuất sâu nhất chịu trách nhiệm khởi tạo lớp cơ sở ảo, bất kể các lớp trung gian.

challenge icon

Thử thách

Dễ

Hãy xây dựng một hệ thống quản lý nhân viên để minh họa cách kế thừa ảo giải quyết vấn đề hình thoi. Bạn sẽ tạo một hệ thống phân cấp trong đó TeamLead kế thừa cả Developer lẫn Manager, mà cả hai đều dùng chung lớp cơ sở Employee.

Bạn sẽ sắp xếp code trong bốn tệp:

  • Employee.h: Định nghĩa lớp cơ sở chung Employee với:
    • Thuộc tính std::string name và int id có phạm vi protected
    • Một hàm khởi tạo nhận cả hai giá trị và in ra: Employee [<name>] hired with ID <id>
    • Một phương thức getInfo() có phạm vi public và in ra: Employee: <name> (ID: <id>)
    • Một hàm hủy ảo in ra: Employee [<name>] record closed
  • Developer.h: Định nghĩa lớp Developer dùng kế thừa public ảo từ Employee:
    • Thuộc tính thành viên std::string language có phạm vi protected
    • Một hàm khởi tạo nhận name, id và language: truyền name và id cho Employee, lưu language, rồi in ra: Developer [<name>] specializes in <language>
    • Một phương thức code() có phạm vi public và in ra: <name> is coding in <language>
    • Một hàm hủy in ra: Developer [<name>] signed off
  • Manager.h: Định nghĩa lớp Manager dùng kế thừa public ảo từ Employee:
    • Thuộc tính thành viên int teamSize có phạm vi protected
    • Một hàm khởi tạo nhận name, id và team size: truyền name và id cho Employee, lưu team size, rồi in ra: Manager [<name>] leads a team of <teamSize>
    • Một phương thức manage() có phạm vi public và in ra: <name> is managing <teamSize> people
    • Một hàm hủy in ra: Manager [<name>] stepped down
  • main.cpp: Đọc bốn giá trị đầu vào, mỗi giá trị trên một dòng riêng:
    1. Tên (chuỗi)
    2. ID nhân viên (số nguyên)
    3. Ngôn ngữ lập trình (chuỗi)
    4. Quy mô nhóm (số nguyên)

    Định nghĩa lớp TeamLead kế thừa public từ cả Developer và Manager:

    • Một hàm khởi tạo nhận cả bốn tham số và phải trực tiếp khởi tạo Employee (lớp cơ sở ảo), sau đó đến Developer và Manager
    • Hàm khởi tạo in ra: TeamLead [<name>] ready to lead and code!
    • Một phương thức showRole() gọi lần lượt getInfo(), code() và manage()
    • Một hàm hủy in ra: TeamLead [<name>] promoted out

    Tạo một đối tượng TeamLead bên trong một khối phạm vi, gọi showRole(), rồi để đối tượng ra khỏi phạm vi. Sau khối lệnh, in ra: Organization restructured!

Ví dụ, với các giá trị đầu vào Alice, 101, C++ và 5:

Employee [Alice] hired with ID 101
Developer [Alice] specializes in C++
Manager [Alice] leads a team of 5
TeamLead [Alice] ready to lead and code!
Employee: Alice (ID: 101)
Alice is coding in C++
Alice is managing 5 people
TeamLead [Alice] promoted out
Manager [Alice] stepped down
Developer [Alice] signed off
Employee [Alice] record closed
Organization restructured!

Hãy chú ý rằng chỉ có một lần gọi hàm khởi tạo Employee và một lần gọi hàm hủy Employee: kế thừa ảo bảo đảm chỉ tồn tại một bản sao của lớp cơ sở dùng chung. TeamLead phải trực tiếp khởi tạo Employee vì với kế thừa ảo, lớp dẫn xuất sâu nhất chịu trách nhiệm khởi tạo lớp cơ sở ảo.

Tự làm thử

#include <iostream>
#include <string>
#include "Developer.h"
#include "Manager.h"

using namespace std;

// TODO: Định nghĩa lớp TeamLead kế thừa công khai từ cả Developer và Manager
// Lưu ý: Khi dùng kế thừa ảo, TeamLead phải trực tiếp khởi tạo Employee (lớp cơ sở ảo)
class TeamLead : public Developer, public Manager {
public:
    // TODO: Triển khai hàm khởi tạo nhận name, id, language và teamSize
    // Phải khởi tạo: Employee trước (lớp cơ sở ảo), rồi đến Developer và Manager
    // Cần in ra: TeamLead [<name>] ready to lead and code!
    TeamLead(const std::string& name, int id, const std::string& language, int teamSize)
        : Employee(name, id),
          Developer(name, id, language),
          Manager(name, id, teamSize) {
        // TODO: In ra thông báo của hàm khởi tạo
    }

    // TODO: Triển khai phương thức showRole()
    // Cần gọi getInfo(), code() và manage() theo đúng thứ tự đó
    void showRole() {
        // TODO: Gọi ba phương thức
    }

    // TODO: Triển khai hàm hủy
    // Cần in ra: TeamLead [<name>] promoted out
    ~TeamLead() {
        // TODO: In ra thông báo của hàm hủy
    }
};

int main() {
    // Đọc dữ liệu đầu vào
    string name;
    int id;
    string language;
    int teamSize;

    getline(cin, name);
    cin >> id;
    cin.ignore();
    getline(cin, language);
    cin >> teamSize;

    // TODO: Tạo đối tượng TeamLead trong phạm vi (scope) của một khối lệnh (block)
    // Gọi showRole(), rồi để đối tượng ra khỏi phạm vi
    {
        // TODO: Tạo TeamLead và gọi showRole()
    }

    // In ra thông báo cuối cùng sau khối lệnh
    cout << "Organization restructured!" << endl;

    return 0;
}
quiz iconTự kiểm tra

Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.

Tất cả bài học trong Lập trình hướng đối tượng

Tự luyện tập: Trình biên dịch C++ online