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.
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ở chungEmployeevới:- Thuộc tính
std::string namevàint idcó 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
- Thuộc tính
Developer.h: Định nghĩa lớpDeveloperdùng kế thừa public ảo từEmployee:- Thuộc tính thành viên
std::string languagecó 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
- Thuộc tính thành viên
Manager.h: Định nghĩa lớpManagerdùng kế thừa public ảo từEmployee:- Thuộc tính thành viên
int teamSizecó 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
- Thuộc tính thành viên
main.cpp: Đọc bốn giá trị đầu vào, mỗi giá trị trên một dòng riêng:- Tên (chuỗi)
- ID nhân viên (số nguyên)
- Ngôn ngữ lập trình (chuỗi)
- Quy mô nhóm (số nguyên)
Định nghĩa lớp
TeamLeadkế thừa public từ cảDevelopervà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 đó đếnDevelopervà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ượtgetInfo(),code()vàmanage() - Một hàm hủy in ra:
TeamLead [<name>] promoted out
Tạo một đối tượng
TeamLeadbên trong một khối phạm vi, gọishowRole(), 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;
}
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
1Nền tảng về OOP
Tệp bên ngoàiBiên dịch và tạo bản dựng C++Tệp header và tệp nguồnNamespace và phạm viGiới thiệu về OOP trong C++Lớp và đối tượngCon trỏ thisPhương thức (hàm thành viên)Thuộc tính (dữ liệu thành viên)Cơ bản về hàm khởi tạo và hàm hủyÔn tập: máy tính đơn giản4Thuộc tính của lớp
Thành viên đối tượng và thành viên tĩnhHàm truy xuất và hàm thiết lậpHàm thành viên hằngTừ khóa mutablePhương thức và biến tĩnhHàm và lớp bạnÔn tập: Trình quản lý tài khoản ngân hàng7Kế thừa
Kế thừa cơ bảnMức truy cập trong kế thừaThứ tự gọi hàm khởi tạo và hàm hủyGhi đè phương thứcHàm ảo và VTableĐa kế thừaKế thừa ảoÔn tập: hệ phân cấp nhân viên2Quản lý bộ nhớ
Bộ nhớ ngăn xếp và heapCon trỏ và tham chiếuBộ nhớ động (new/delete)Con trỏ thông minh trong C++RAII trong C++Ôn tập: Trình quản lý mảng động5Tính đóng gói
Phạm vi truy cập trong C++Tìm hiểu sâu về phạm vi truy cậpẨn thông tinStruct và lớpLớp lồng nhau và lớp bên trongÔn tập: hệ thống hồ sơ sinh viên8Đa hình
Đa hình lúc biên dịch và khi chạyNạp chồng hàmÔn lại hàm ảoHàm ảo thuần túyLớp trừu tượngThiết kế interface trong C++Ép kiểu động và RTTIÔn tập: Máy tính hình học3Hàm khởi tạo và hàm hủy
Hàm khởi tạo mặc địnhHàm khởi tạo có tham sốHàm khởi tạo sao chépHàm khởi tạo di chuyểnDanh sách khởi tạoHàm khởi tạo ủy quyềnTìm hiểu sâu về hàm hủyQuy tắc 3 / 5 / 0Ôn tập: lớp chuỗi6Nạp chồng toán tử
Giới thiệu về nạp chồng toán tửNạp chồng toán tử số họcNạp chồng toán tử so sánhToán tử luồngNạp chồng toán tử gánNạp chồng toán tử [] và ()Toán tử chuyển đổi kiểuÔn tập: lớp ma trận9Mẫu
Mẫu hàmMẫu lớpChuyên biệt hóa mẫuMẫu biến tham sốCơ bản về SFINAE và đặc điểm kiểuÔn tập: vùng chứa tổng quátTự luyện tập: Trình biên dịch C++ online