动态类型转换与 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 很有用,但频繁使用通常表明设计存在问题。尽可能优先使用虚函数,这样对象就能处理特定于类型的行为,而无需显式进行类型检查。
挑战
简单让我们构建一个车辆检查系统,使用 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 个输入,每个输入占一行:- 汽车的车牌号
- 卡车的车牌号
- 摩托车的车牌号
创建一辆有 4 个座位的
Car、一辆载重量为 10.5 吨的Truck,以及一辆带侧车的Motorcycle。将这 3 辆车存入一个由Vehicle*指针组成的数组。遍历数组,并对每辆车执行以下操作:
- 使用
getDescription()打印车辆描述 - 使用
dynamic_cast尝试将车辆转换为每种派生类型 - 如果成功转换为
Car*,则调用inspectSeatbelts() - 如果成功转换为
Truck*,则调用inspectCargo() - 如果成功转换为
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;
}
这节课包含一个小测验。开始学习即可作答,并记录你的进度。
面向对象编程中的所有课
自己练习: C++ 在线编译器