Ép kiểu động và RTTI
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 62 trên 104.
Đôi khi khi làm việc với tính đa hình, bạn cần xác định kiểu thực tế của một đối tượng trong thời gian chạy hoặc chuyển đổi an toàn con trỏ lớp cơ sở thành con trỏ lớp dẫn xuất. C++ cung cấp RTTI (Thông tin kiểu trong thời gian chạy) và dynamic_cast cho những trường hợp này.
dynamic_cast chuyển đổi an toàn con trỏ hoặc tham chiếu trong một hệ thống phân cấp kế thừa. Khác với static_cast, nó kiểm tra trong thời gian chạy và trả về nullptr nếu phép chuyển đổi không hợp lệ:
class Animal {
public:
virtual ~Animal() = default;
};
class Dog : public Animal {
public:
void bark() { std::cout << "Woof!" << std::endl; }
};
class Cat : public Animal {};
Animal* animal = new Dog();
Dog* dog = dynamic_cast<Dog*>(animal); // Thành công: trả về con trỏ hợp lệ
if (dog) {
dog->bark(); // Có thể gọi phương thức (method) riêng của Dog một cách an toàn
}
Cat* cat = dynamic_cast<Cat*>(animal); // Thất bại: trả về nullptrQuan trọng: dynamic_cast chỉ hoạt động với kiểu đa hình (polymorphic types), tức là các lớp có ít nhất một hàm ảo (virtual function). Toán tử (operator) typeid cho phép bạn truy vấn kiểu thực tế của một đối tượng:
#include <typeinfo>
Animal* pet = new Dog();
std::cout << typeid(*pet).name() << std::endl; // In ra thông tin kiểu dữ liệu của DogMặc dù dynamic_cast hữu ích, việc dùng nó thường xuyên thường cho thấy thiết kế có vấn đề. Hãy ưu tiên các hàm ảo khi có thể, vì chúng cho phép đối tượng xử lý hành vi riêng theo kiểu mà không cần kiểm tra kiểu rõ ràng.
Thử thách
DễHãy xây dựng một hệ thống kiểm tra phương tiện sử dụng dynamic_cast để nhận diện an toàn và tương tác với các loại phương tiện khác nhau. Bạn sẽ tạo một hệ thống phân cấp phương tiện, trong đó người kiểm tra cần thực hiện những bước kiểm tra riêng theo từng loại mà chỉ một số phương tiện hỗ trợ.
Bạn sẽ sắp xếp code trong ba tệp:
Vehicle.h: Định nghĩa lớp cơ sởVehicle, đại diện cho mọi phương tiện trong hệ thống:- Một thành viên
std::string licensePlatecó phạm vi protected - Một hàm khởi tạo khởi tạo biển số xe
- Một phương thức ảo
getDescription()trả về"Vehicle: <licensePlate>" - Một hàm hủy ảo
- Một thành viên
Vehicles.h: Định nghĩa ba loại phương tiện dẫn xuất:Car:- Một thành viên
int seatCountcó phạm vi private - Một hàm khởi tạo nhận biển số xe và số ghế
- Ghi đè
getDescription()để trả về"Car: <licensePlate>" - Một phương thức
inspectSeatbelts()in ra:Inspecting <seatCount> seatbelts in <licensePlate>
Truck:- Một thành viên
double cargoCapacitycó phạm vi private, tính bằng tấn - Một hàm khởi tạo nhận biển số xe và sức chở hàng
- Ghi đè
getDescription()để trả về"Truck: <licensePlate>" - Một phương thức
inspectCargo()in ra:Inspecting cargo area (<cargoCapacity> tons) in <licensePlate>
Motorcycle:- Một thành viên
bool hasSidecarcó phạm vi private - Một hàm khởi tạo nhận biển số xe và trạng thái có thùng phụ hay không
- Ghi đè
getDescription()để trả về"Motorcycle: <licensePlate>" - Một phương thức
inspectHelmetStorage()in raInspecting helmet storage in <licensePlate>nếu xe có thùng phụ, hoặcNo helmet storage in <licensePlate>nếu không có
- Một thành viên
main.cpp: Đọc ba đầu vào, mỗi đầu vào trên một dòng riêng:- Biển số xe của ô tô
- Biển số xe của xe tải
- Biển số xe của xe máy
Tạo một
Carcó 4 chỗ ngồi, mộtTruckcó sức chở 10.5 tấn và mộtMotorcyclecó thùng phụ. Lưu cả ba vào một mảng con trỏVehicle*.Duyệt qua mảng và thực hiện các bước sau cho từng phương tiện:
- In mô tả bằng
getDescription() - Dùng
dynamic_castđể thử ép kiểu sang từng loại dẫn xuất - Nếu ép kiểu sang
Car*thành công, gọiinspectSeatbelts() - Nếu ép kiểu sang
Truck*thành công, gọiinspectCargo() - Nếu ép kiểu sang
Motorcycle*thành công, gọiinspectHelmetStorage()
In một dòng trống giữa phần kiểm tra từng phương tiện. Khi hoàn tất, giải phóng các đối tượng đã cấp phát động.
Ví dụ, với các đầu vào ABC-123, TRK-456 và MTR-789:
Car: ABC-123
Inspecting 4 seatbelts in ABC-123
Truck: TRK-456
Inspecting cargo area (10.5 tons) in TRK-456
Motorcycle: MTR-789
Inspecting helmet storage in MTR-789Hãy chú ý rằng dynamic_cast chỉ trả về con trỏ hợp lệ khi kiểu thực tế của đối tượng khớp với kiểu đích. Với mỗi phương tiện, chỉ một trong ba phép ép kiểu thành công, nhờ đó bạn có thể gọi an toàn phương thức kiểm tra dành riêng cho loại phương tiện đó. Đây là sức mạnh của RTTI: xác định kiểu thực tế tại thời điểm chạy và xử lý tương ứng.
Tự làm thử
#include <iostream>
#include <string>
#include "Vehicle.h"
#include "Vehicles.h"
using namespace std;
int main() {
// Đọc dữ liệu đầu vào
string carPlate, truckPlate, motorcyclePlate;
cin >> carPlate;
cin >> truckPlate;
cin >> motorcyclePlate;
// TODO: Tạo một Car có 4 chỗ ngồi
// TODO: Tạo một Truck có tải trọng 10.5 tấn
// TODO: Tạo một Motorcycle có thùng xe bên cạnh (true)
// TODO: Lưu cả ba vào một mảng con trỏ Vehicle*
// TODO: Duyệt mảng và thực hiện với từng xe:
// 1. In ra mô tả của xe bằng getDescription()
// 2. Dùng dynamic_cast để thử ép kiểu sang từng kiểu dẫn xuất
// 3. Nếu ép kiểu sang Car* thành công, gọi inspectSeatbelts()
// 4. Nếu ép kiểu sang Truck* thành công, gọi inspectCargo()
// 5. Nếu ép kiểu sang Motorcycle* thành công, gọi inspectHelmetStorage()
// 6. In một dòng trống giữa các lần kiểm tra xe
// TODO: Giải phóng các đối tượng được cấp phát động
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