Menu
CoddyTech

É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ề nullptr

Quan 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 Dog

Mặ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.

challenge icon

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 licensePlate có 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
  • Vehicles.h: Định nghĩa ba loại phương tiện dẫn xuất:

    Car:

    • Một thành viên int seatCount có 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 cargoCapacity có 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 hasSidecar có 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 ra Inspecting helmet storage in <licensePlate> nếu xe có thùng phụ, hoặc No helmet storage in <licensePlate> nếu không có
  • main.cpp: Đọc ba đầu vào, mỗi đầu vào trên một dòng riêng:
    1. Biển số xe của ô tô
    2. Biển số xe của xe tải
    3. Biển số xe của xe máy

    Tạo một Car có 4 chỗ ngồi, một Truck có sức chở 10.5 tấn và một Motorcycle có 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:

    1. In mô tả bằng getDescription()
    2. Dùng dynamic_cast để thử ép kiểu sang từng loại 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()

    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-789

Hã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;
}
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