Menu
CoddyTech

动态类型转换与 RTTI

本课属于 Coddy C++ 学习旅程的“面向对象编程”单元。第 62 课,共 104 课。

有时使用多态时,你需要在运行时确定对象的实际类型,或安全地将基类指针转换为派生类指针。C++ 提供了 RTTI(运行时类型信息)和 dynamic_cast 来处理这些情况。

dynamic_cast 可以在继承层次结构中安全地转换指针或引用。与 static_cast 不同,它会在运行时进行检查,如果转换无效,则返回 nullptr:

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);  // 转换成功:返回有效指针

if (dog) {
    dog->bark();  // 可以安全地调用 Dog 特有的方法
}

Cat* cat = dynamic_cast<Cat*>(animal);  // 转换失败:返回 nullptr

重要:dynamic_cast 只适用于多态类型(至少有一个虚函数的类)。typeid 运算符可以让你查询对象的实际类型:

#include <typeinfo>

Animal* pet = new Dog();
std::cout << typeid(*pet).name() << std::endl;  // 输出 Dog 的类型信息

虽然 dynamic_cast 很有用,但频繁使用通常表明设计存在问题。尽可能优先使用虚函数,这样对象就能处理特定于类型的行为,而无需显式进行类型检查。

challenge icon

挑战

简单

让我们构建一个车辆检查系统,使用 dynamic_cast 安全地识别不同类型的车辆并与之交互。你将创建一个车辆继承层次结构,其中检查员需要执行特定类型的检查,而只有某些车辆支持这些检查。

你需要将代码分布在 3 个文件中:

  • Vehicle.h:定义一个基类 Vehicle,表示系统中的任意车辆:
    • 一个受保护的 std::string licensePlate 成员
    • 一个用于初始化车牌号的构造函数
    • 一个虚方法 getDescription(),返回 "Vehicle: <licensePlate>"
    • 一个虚析构函数
  • Vehicles.h:定义 3 种派生车辆类型:

    Car:

    • 一个私有的 int seatCount 成员
    • 一个接收车牌号和座位数的构造函数
    • 重写 getDescription(),使其返回 "Car: <licensePlate>"
    • 一个方法 inspectSeatbelts(),用于打印:Inspecting <seatCount> seatbelts in <licensePlate>

    Truck:

    • 一个私有的 double cargoCapacity 成员,单位为吨
    • 一个接收车牌号和货物容量的构造函数
    • 重写 getDescription(),使其返回 "Truck: <licensePlate>"
    • 一个方法 inspectCargo(),用于打印:Inspecting cargo area (<cargoCapacity> tons) in <licensePlate>

    Motorcycle:

    • 一个私有的 bool hasSidecar 成员
    • 一个接收车牌号和侧车状态的构造函数
    • 重写 getDescription(),使其返回 "Motorcycle: <licensePlate>"
    • 一个方法 inspectHelmetStorage():如果车辆有侧车,则打印 Inspecting helmet storage in <licensePlate>;如果没有,则打印 No helmet storage in <licensePlate>
  • main.cpp:读取 3 个输入,每个输入占一行:
    1. 汽车的车牌号
    2. 卡车的车牌号
    3. 摩托车的车牌号

    创建一辆有 4 个座位的 Car、一辆载重量为 10.5 吨的 Truck,以及一辆带侧车的 Motorcycle。将这 3 辆车存入一个由 Vehicle* 指针组成的数组。

    遍历数组,并对每辆车执行以下操作:

    1. 使用 getDescription() 打印车辆描述
    2. 使用 dynamic_cast 尝试将车辆转换为每种派生类型
    3. 如果成功转换为 Car*,则调用 inspectSeatbelts()
    4. 如果成功转换为 Truck*,则调用 inspectCargo()
    5. 如果成功转换为 Motorcycle*,则调用 inspectHelmetStorage()

    在每辆车的检查结果之间打印一个空行。完成后,清理动态分配的对象。

例如,输入 ABC-123、TRK-456 和 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

注意,只有当实际对象类型与目标类型匹配时,dynamic_cast 才会返回有效指针。对于每辆车,3 次类型转换中只有 1 次会成功,因此你可以安全地调用对应类型的检查方法。这就是 RTTI 的作用:在运行时确定实际类型,并据此执行操作。

亲手试试

#include <iostream>
#include <string>
#include "Vehicle.h"
#include "Vehicles.h"

using namespace std;

int main() {
    // 读取输入
    string carPlate, truckPlate, motorcyclePlate;
    cin >> carPlate;
    cin >> truckPlate;
    cin >> motorcyclePlate;

    // TODO: 创建一辆有 4 个座位的 Car
    // TODO: 创建一辆载重 10.5 吨的 Truck
    // TODO: 创建一辆带边车的 Motorcycle(true)

    // TODO: 将这 3 个对象存入 Vehicle* 指针数组

    // TODO: 遍历数组,对每辆车执行以下操作:
    //   1. 使用 getDescription() 输出车辆描述
    //   2. 使用 dynamic_cast 尝试转换为每种派生类型
    //   3. 如果转换为 Car* 成功,调用 inspectSeatbelts()
    //   4. 如果转换为 Truck* 成功,调用 inspectCargo()
    //   5. 如果转换为 Motorcycle* 成功,调用 inspectHelmetStorage()
    //   6. 在每辆车的检查结果之间输出一个空行

    // TODO:清理动态分配的对象

    return 0;
}
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: C++ 在线编译器