在面向对象开发中,我们经常使用基类指针或基类引用去指向派生类对象。如果这时需要调用某个派生类独有的成员函数,应该怎么在程序运行时确认 “它实际指向的到底是不是这个派生类” 呢?
当基类与派生类构成多态时,C++ 提供的 dynamic_cast(动态类型转换)就是专门用来解决这个问题的。它不能用于 int、double 这类基本数值,而是专门在程序运行时核对对象的真实类型,判断能否安全地把基类指针或引用转换为目标派生类。
向下转换与基本语法
我们可以把类的继承关系看作一棵家族树:基类 Animal 在上方,派生类 Cat 和 Dog 在下方。从派生类转为基类叫做 “向上转换”,因为一只猫肯定属于动物,直接写 Animal* animal = &cat; 就能自动完成,不需要使用 dynamic_cast。
反过来,从基类转回派生类叫做 “向下转换”。由于一只动物不一定是猫,也可能是狗,因此我们需要使用 dynamic_cast 在运行时做安全检查,如下图所示。

语法:
dynamic_cast<目标类型>(表达式)说明:
其中,尖括号 <> 里的目标类型必须是类的指针或引用,比如 Cat* 或 Cat&。圆括号 () 里的表达式则是对应的基类指针或基类引用。
需要注意的是,使用 dynamic_cast 向下转换有一个硬性前提:基类中必须至少包含一个虚函数。只有当基类带有虚函数时,C++ 才会在对象内部记录运行时类型信息。你可以把它看成一张记录真实类型的 “身份卡”,供 dynamic_cast 在运行时核对身份。
在实际开发中,多态基类通常都会定义一个虚析构函数,比如写成 virtual ~Animal() = default;。这样既能保证派生类对象被安全销毁,也自然满足了 dynamic_cast 的使用前提。
示例 1:基类指针向下转换
#include <iostream>
class Animal {
public:
virtual ~Animal() = default;
};
class Cat : public Animal {
public:
void catchMouse() const {
std::cout << "小猫正在抓老鼠" << '\n';
}
};
int main() {
Cat cat;
Animal* animal = &cat;
// 将基类指针安全转为 Cat*
Cat* catPtr = dynamic_cast<Cat*>(animal);
if (catPtr != nullptr) {
catPtr->catchMouse();
}
return 0;
}运行结果如下。
小猫正在抓老鼠分析:
在这个例子中,虽然指针 animal 的声明类型是 Animal*,但它实际指向的是一个 Cat 对象。由于 catchMouse() 是 Cat 类独有的成员函数,直接写 animal->catchMouse() 无法通过编译。
当我们使用 dynamic_cast<Cat*>(animal) 转换时,程序会在运行时核对 animal 指向的对象确实是 Cat,转换成功后返回有效的 Cat* 指针,随后就能正常调用 catPtr->catchMouse() 了。
这里小伙伴们要留意,如果基类 Animal 中没有任何虚函数,对象内部就没有运行时类型信息,下面这种写法在编译阶段就会直接报错。
class Animal {
};
class Cat : public Animal {
};
Cat cat;
Animal* animal = &cat;
Cat* catPtr = dynamic_cast<Cat*>(animal); // 报错:基类不含虚函数转换失败时的处理方式
当基类指针或引用实际指向的对象并不是目标派生类时,dynamic_cast 会在运行时识别出来并给出失败反馈。根据目标类型是 “指针” 还是 “引用”,转换失败时的表现分为以下 2 种情况。
1. 指针转换失败(返回 nullptr)
对于指针形式来说,如果基类指针实际指向的对象不是目标派生类,dynamic_cast 不会强行返回一个错误的地址,而是直接返回空指针 nullptr。
示例 2:指针向下转换失败
#include <iostream>
class Animal {
public:
virtual ~Animal() = default;
};
class Cat : public Animal {
public:
void catchMouse() const {
std::cout << "小猫正在抓老鼠" << '\n';
}
};
class Dog : public Animal {
};
int main() {
Dog dog;
Animal* animal = &dog;
// 尝试将指向 Dog 的指针转为 Cat*
Cat* catPtr = dynamic_cast<Cat*>(animal);
if (catPtr == nullptr) {
std::cout << "转换失败,该对象不是 Cat" << '\n';
} else {
catPtr->catchMouse();
}
return 0;
}运行结果如下。
转换失败,该对象不是 Cat分析:
从运行结果可以看出,由于 animal 实际指向的是 Dog 对象而非 Cat 对象,dynamic_cast<Cat*>(animal) 在运行时核对不通过,最终返回了 nullptr。如果传入的源指针本身就是 nullptr,转换结果同样也是 nullptr。
因此小伙伴们在写代码时一定要养成习惯:使用指针形式的 dynamic_cast 后,必须先通过 if 判断返回值是否为空指针。如果不做检查就直接执行 catPtr->catchMouse(),一旦转换失败就会对空指针解引用,导致程序直接崩溃。
2. 引用转换失败(抛出 std::bad_cast)
引用和指针有一个明显区别:C++ 中有空指针 nullptr,但不存在 “空引用”。也就是说,引用必须绑定到一个真实存在的对象上,没办法通过返回空值来表示转换失败。
因此,当我们将 dynamic_cast 用于引用时,如果运行时发现实际对象不是目标派生类,程序会直接抛出 std::bad_cast 异常。此时我们需要通过 #include <typeinfo> 引入头文件,并配合 try...catch 捕获该异常。
示例 3:引用向下转换失败
#include <iostream>
#include <typeinfo> // 提供 std::bad_cast
class Animal {
public:
virtual ~Animal() = default;
};
class Cat : public Animal {
public:
void catchMouse() const {
std::cout << "小猫正在抓老鼠" << '\n';
}
};
class Dog : public Animal {
};
int main() {
Dog dog;
Animal& animal = dog;
try {
// 尝试将 Dog 对象转为 Cat& 引用
Cat& cat = dynamic_cast<Cat&>(animal);
cat.catchMouse();
} catch (const std::bad_cast&) {
std::cout << "引用转换失败" << '\n';
}
return 0;
}运行结果如下。
引用转换失败分析:
在这个例子中,由于 animal 实际绑定的是 Dog 对象,无法转换为 Cat&。程序执行到 dynamic_cast<Cat&>(animal) 时会立即抛出 std::bad_cast 异常并跳转到 catch 分支,不会继续执行后面的 cat.catchMouse()。
因为这里在 catch 分支内不需要读取异常的具体信息,所以我们直接省略了异常变量名。
那么在实际开发中,我们应该选择指针形式还是引用形式呢?如果 “对象可能不是目标类型” 属于正常的业务分支,优先使用指针形式,配合 if 检查 nullptr 最直观。只有当逻辑上要求它必须是目标类型、一旦不符就属于程序错误时,才选用引用形式。
dynamic_cast 与 static_cast 的区别
把基类指针向下转为派生类指针时,dynamic_cast 和前面学过的 static_cast 在语法上都能写出来。那它们之间有什么区别呢?
两者的关键区别,就在于程序运行时会不会核对对象的真实类型。static_cast 只在编译阶段看两个类有没有继承关系,只要存在继承关系就直接放行,不会在运行时检查实际对象到底是不是目标派生类。
Dog dog;
Animal* animal = &dog;
// 正确方式
Cat* safeCat = dynamic_cast<Cat*>(animal);
// 错误方式
Cat* wrongCat = static_cast<Cat*>(animal);在上面代码中,animal 实际指向的是 Dog 对象。如果我们误用 static_cast<Cat*>(animal) 强行转换,wrongCat 并不会变成 nullptr,而是得到了一个指向 Dog 内存的错误 Cat* 指针。
一旦后续通过 wrongCat 访问 Cat 独有的成员,就会触发未定义行为,导致读出错误数据甚至程序崩溃。而使用 dynamic_cast<Cat*>(animal) 则会在运行时识别出类型不匹配,安全返回 nullptr。
多态开发中的类型设计建议
初学多态时,不少小伙伴拿到基类指针后,习惯写一连串 if (dynamic_cast<Cat*>(p))、else if (dynamic_cast<Dog*>(p)) 去逐个判断具体类型,然后再调用各自的函数。在实际开发中,更推荐大家遵循以下 2 条原则:
共同行为优先使用虚函数:像发出叫声 speak() 这类所有派生类都具备的共同行为,我们应该在基类 Animal 中定义 virtual 虚函数,直接通过 animal->speak() 多态调用,不需要动用 dynamic_cast。
独有行为再使用 dynamic_cast:只有当某个功能确实是单个派生类独有的,比如只有 Cat 会抓老鼠 catchMouse()、不适合放进基类接口时,我们才使用 dynamic_cast 安全向下转换后再调用。
