⚡C++ 对象模型与多态

RTTI、dynamic_casttypeid

面试回答

常见问法

  • 什么是 RTTI?
  • dynamic_casttypeid 分别用来做什么?
  • dynamic_caststatic_cast 有什么区别?
  • 为什么说 RTTI 是“补充机制”,不是面向对象设计的主流程?
  • typeid(*p)typeid(p) 为什么不同?
  • dynamic_cast 为什么通常要求基类有虚函数?

回答

RTTI(Run-Time Type Information,运行时类型识别)是 C++ 在运行期识别对象真实类型的机制。它主要解决一个问题:

当我手里只有“基类指针 / 基类引用”时,如何在运行时判断它实际指向的是哪个派生类对象?

RTTI 最常见的两个接口是:

  • dynamic_cast:做安全的运行时转型,尤其是向下转型交叉转型
  • typeid:获取对象或类型的类型信息

RTTI 通常和多态类型关联使用,也就是类里至少有一个虚函数。因为只有多态对象,运行时才会保留足够的类型信息,支持“真实动态类型”的识别。

一个典型例子:

#include <iostream>

struct Base {
    virtual ~Base() = default;
};

struct Derived : Base {
    void only_in_derived() {
        std::cout << "Derived\n";
    }
};

void f(Base* p) {
    if (auto d = dynamic_cast<Derived*>(p)) {
        d->only_in_derived();
    }
}

这段代码里,dynamic_cast<Derived*>(p) 会在运行时检查:p 是否真的指向一个 Derived 对象。 如果是,转型成功;如果不是,返回 nullptr

面试里最好补一句取舍:

如果业务逻辑本身可以通过虚函数完成动态分派,那么优先用虚函数;dynamic_casttypeid 更适合做边界检查、框架层适配、日志调试、少量类型分流,而不是替代多态设计。

追问

1)为什么 dynamic_cast 有运行时开销?

因为它不是“盲转”,而是要在运行期检查对象真实类型、继承关系以及偏移调整,尤其在多重继承、虚继承下更复杂,所以比 static_cast 更重。

2)dynamic_cast 指针版和引用版失败时有什么区别?

  • 指针版失败:返回 nullptr
  • 引用版失败:抛出 std::bad_cast
#include <typeinfo>

Base* p = nullptr;
Derived* dp = dynamic_cast<Derived*>(p); // dp == nullptr
try {
    Base b;
    Base& rb = b;
    Derived& rd = dynamic_cast<Derived&>(rb); // 抛 std::bad_cast
} catch (const std::bad_cast& e) {
}

3)typeid(*p)typeid(p) 为什么不同?

  • typeid(p) 看的是变量 p 本身的静态类型,比如 Base*

  • typeid(*p) 看的是 *p 这个对象表达式的类型

    • 如果它是多态类型,得到的是运行时动态类型
    • 如果不是多态类型,得到的是静态类型
#include <iostream>
#include <typeinfo>

struct Base {
    virtual ~Base() = default;
};
struct Derived : Base {};

Base* p = new Derived;

std::cout << typeid(p).name() << '\n';   // Base*
std::cout << typeid(*p).name() << '\n';  // Derived(通常如此)

4)什么时候不该用 RTTI?

当你只是想“根据对象类型做不同逻辑”时,如果这些逻辑本来就应该属于类的行为,优先用虚函数 / 多态。 频繁 dynamic_cast 往往说明设计没有把行为放回对象自身。


原理展开

1. RTTI 到底是什么

RTTI 的本质是:让程序在运行期知道一个对象的真实类型信息

在 C++ 里,编译器通常会为多态类型维护额外的运行时类型信息。你可以把它理解成:

  • 虚函数机制负责“运行时决定调哪个函数”
  • RTTI 负责“运行时识别对象到底是什么类型”

两者都和多态对象相关,但用途不同:

  • 虚函数:行为分派
  • RTTI:类型识别

所以 RTTI 不是面向对象的主线,而是多态机制的辅助能力。


2. dynamic_cast:安全转型,而不是强制转型

2.1 它主要解决什么问题

当你拿到一个 Base*,想把它转成 Derived* 时,编译器只知道“它是个基类指针”,并不知道它运行时是不是真的指向 Derived。 这时:

  • static_cast直接按继承关系转,不做运行时检查
  • dynamic_cast先检查再转

因此 dynamic_cast 的核心价值是:

保证“向下转型”在运行时是安全的

2.2 典型使用场景

向下转型(downcast)
struct Base {
    virtual ~Base() = default;
};
struct Derived : Base {
    void foo() {}
};

Base* p = new Derived;

if (auto d = dynamic_cast<Derived*>(p)) {
    d->foo();
}
交叉转型(cross cast,多重继承中常见)
struct A { virtual ~A() = default; };
struct B { virtual ~B() = default; };
struct C : A, B {};

A* pa = new C;
B* pb = dynamic_cast<B*>(pa); // 合法,运行时检查后做偏移调整
转成 void*,获取最派生对象地址

这是个常被忽略的点:

struct Base {
    virtual ~Base() = default;
};
struct Derived : Base {};

Base* p = new Derived;
void* raw = dynamic_cast<void*>(p); // 指向最派生对象

2.3 失败时的行为

指针版本

失败返回 nullptr

Base b;
Base* pb = &b;
Derived* pd = dynamic_cast<Derived*>(pb); // nullptr
引用版本

失败抛 std::bad_cast

#include <typeinfo>

Base b;
Base& rb = b;

try {
    Derived& rd = dynamic_cast<Derived&>(rb);
} catch (const std::bad_cast&) {
}

2.4 为什么通常要求基类是多态类型

dynamic_cast 要做运行时检查,编译器必须能拿到对象的动态类型信息。 而在实践中,这通常依赖于基类有虚函数

最常见做法就是给基类一个虚析构:

struct Base {
    virtual ~Base() = default;
};

这不仅让 RTTI 正常工作,也保证通过基类指针删除派生对象时行为正确。

2.5 为什么它比 static_cast

因为它要做额外工作:

  • 检查对象真实动态类型
  • 验证目标类型是否在继承层次中可达
  • 在多重继承 / 虚继承时调整指针偏移

所以 dynamic_cast 的成本通常高于 static_cast。 但面试里要强调判断依据:

不是“绝对不能用”,而是不要把它放进高频核心路径,更不要把整个多态设计退化成一堆运行时类型判断。


3. typeid:拿到类型信息,但别把它当“字符串判断工具”

3.1 它是做什么的

typeid 用来获得一个类型对应的 std::type_info 对象,用于比较类型信息。

#include <typeinfo>

int x = 0;
const std::type_info& ti = typeid(x);

3.2 typeid(T)typeid(expr) 的区别

typeid(T)

直接问“类型 T 是什么”,纯编译期含义明显。

typeid(expr)

问“表达式 expr 的类型信息是什么”。

关键点在于:

  • 如果 expr非多态类型表达式,结果看静态类型
  • 如果 expr多态对象的解引用表达式,结果会看动态类型
struct Base {
    virtual ~Base() = default;
};
struct Derived : Base {};

Base* p = new Derived;

typeid(p);   // Base*
typeid(*p);  // Derived

3.3 typeid(*p) 的空指针边界

这是高频追问点。

p 是空指针,并且 *p 对应的是多态类型对象表达式时,typeid(*p) 会抛出 std::bad_typeid

#include <typeinfo>

Base* p = nullptr;

try {
    auto& ti = typeid(*p); // 抛 std::bad_typeid
} catch (const std::bad_typeid&) {
}

这点和 typeid(p) 完全不同,因为 typeid(p) 只是问“变量 p 的类型”,不需要访问对象。

3.4 typeid(...).name() 能不能直接做业务判断?

不建议。

因为 name() 的返回值是实现相关的:

  • 不同编译器格式不同
  • 可读性、稳定性都不一致
  • 不能作为跨平台可靠逻辑依据

正确做法是:

  • 比较 typeid(a) == typeid(b)
  • 或者只把 name() 用于调试 / 日志输出
if (typeid(*p) == typeid(Derived)) {
    // ...
}

3.5 typeid 适合什么场景

更适合:

  • 调试、日志、框架诊断
  • 少量类型分流
  • 写通用组件时做类型确认

不适合:

  • 大量业务逻辑分派
  • 代替虚函数做行为选择
  • 依赖 name() 写核心逻辑

4. 工程实践里到底怎么选

4.1 优先级原则

工程里通常按这个顺序考虑:

  1. 能用虚函数表达行为,就优先用虚函数
  2. 确实需要安全识别真实类型时,再考虑 RTTI
  3. 如果类型层级清楚且你能保证正确性,才考虑 static_cast
  4. 绝不因为图省事,用 reinterpret_cast 硬转对象层级

4.2 什么时候 dynamic_cast 是合理的

  • 框架回调里拿到统一基类接口,需识别少数特殊派生类
  • 插件系统、GUI 组件树、AST 节点处理
  • 老系统中接口历史包袱较重,暂时无法完全重构为虚函数
  • 做防御式编程,希望转型失败可安全返回

4.3 什么时候它暴露了设计问题

如果你发现代码里到处都是:

if (auto d = dynamic_cast<D1*>(p)) { ... }
else if (auto d = dynamic_cast<D2*>(p)) { ... }
else if (auto d = dynamic_cast<D3*>(p)) { ... }

这通常说明:

  • 行为没有下沉到对象自身
  • 基类接口抽象不够完整
  • 代码正在用“类型判断”替代“多态分派”

面试里这是很加分的一句:

少量 dynamic_cast 是工具,满地 dynamic_cast 往往是设计味道。


对比总结

1. dynamic_caststatic_cast、虚函数 的取舍

方案本质是否做运行时检查典型用途优点缺点什么时候选
dynamic_cast安全运行时转型向下转型、交叉转型安全,失败可感知有运行时开销,代码容易变成类型判断需要确认真实类型且无法直接靠虚函数表达时
static_cast编译期已知关系的转换向上转型、明确可保证正确的转换快,语义明确向下转型若判断错会出问题你能证明类型一定正确时
虚函数运行时行为分派不需要手动转型按对象行为调用逻辑面向对象最自然,可扩展性最好需要提前设计好抽象接口只要问题是“做什么”而不是“你是谁”,优先选它

2. dynamic_casttypeid 的区别

项目dynamic_casttypeid
主要目的安全转型获取类型信息
核心问题“我能不能把它转成某种类型?”“它到底是什么类型?”
失败行为指针返回 nullptr;引用抛 std::bad_cast对多态空指针解引用表达式抛 std::bad_typeid
是否常用于业务逻辑可以,但不宜滥用更偏诊断、判断、调试
是否依赖多态动态类型常见场景下是想拿到动态类型时是

3. typeid(p)typeid(*p) 的区别

表达式看的是谁结果通常是什么
typeid(p)指针变量本身的类型Base*Derived* 这类静态类型
typeid(*p)指针所指对象表达式若基类多态,则可能得到动态类型,如 Derived

易错点

  • 把 RTTI 当成“面向对象主流程”,到处做类型判断,而不是优先设计虚函数接口。
  • 误以为 dynamic_cast 只是“更安全的 static_cast”,却不知道它是运行时检查
  • 不清楚 dynamic_cast 指针失败返回 nullptr,引用失败抛 std::bad_cast
  • 混淆 typeid(p)typeid(*p)
  • 误以为 typeid(...).name() 是稳定、可移植、可用于业务逻辑的字符串。
  • 忽略 typeid(*p)p == nullptr 且对象类型为多态时可能抛 std::bad_typeid
  • 基类没有虚函数,却还想依赖 RTTI 获取动态类型信息。
  • 本该通过虚函数建模行为,却写成一长串 if + dynamic_cast
  • 只记住“有开销”,却说不清开销来自哪里:本质是运行时类型检查和指针调整
  • 忽略一个工程细节:多态基类通常应该有虚析构函数

记忆技巧

  • RTTI 记两件事:

    1. 问身份typeid
    2. 安全转型dynamic_cast
  • 一句话记忆:

    虚函数解决“该做什么”,RTTI 解决“你到底是谁”。

  • 选择口诀:

    能靠虚函数,不靠 RTTI; 能静态保证,不上 dynamic_cast; 必须运行时确认,再用 RTTI。

  • 区分 typeid(p) / typeid(*p)

    一个看“指针自己”,一个看“指针指向的对象”。


面试速答版

RTTI 是 C++ 的运行时类型识别机制,用来在运行期识别多态对象的真实类型。最常用的两个接口是 dynamic_casttypeiddynamic_cast 用于安全地做向下转型或交叉转型,失败时指针返回 nullptr、引用抛 std::bad_casttypeid 用于获取类型信息,typeid(p) 看的是指针本身类型,typeid(*p) 在多态场景下看的是对象动态类型。工程上 RTTI 是补充机制,不应替代正常的虚函数多态;如果本来可以靠虚函数分派行为,就不要到处写 dynamic_cast 做类型判断。


面试加分版

RTTI,也就是运行时类型识别,本质上是让程序在运行期知道“这个基类指针或引用背后到底是哪种派生类对象”。在 C++ 里,它主要通过 dynamic_casttypeid 体现,通常依赖多态类型,也就是基类至少有一个虚函数。

dynamic_cast 的核心价值是“安全转型”。比如我手里只有一个 Base*,但我怀疑它实际指向 Derived,这时候如果直接 static_cast,编译器不会帮我做运行时检查,转错了就有风险;而 dynamic_cast 会先检查真实类型,再决定能不能转,所以特别适合向下转型和多重继承下的交叉转型。它的代价就是有运行时检查开销,尤其涉及复杂继承关系时更明显。指针版失败返回 nullptr,引用版失败抛 std::bad_cast,这是很常见的追问点。

typeid 则偏“类型识别”而不是“转型”。比如 typeid(p) 得到的是指针变量本身的类型,比如 Base*;而 typeid(*p) 如果 Base 是多态类型,得到的是对象的动态类型,比如 Derived。还有个边界是:如果 p 是空指针,那么 typeid(*p) 在多态场景下会抛 std::bad_typeid。另外,typeid(...).name() 只适合调试,因为返回值是实现相关的,不适合写业务判断。

工程实践里,我一般会这样选:如果问题本质是“对象应该做什么”,优先设计成虚函数多态;如果确实是在框架边界、适配层、调试诊断或少量特例处理中,需要知道对象真实类型,那 RTTI 很有价值。少量使用 dynamic_cast 是正常工具,但如果代码里大量出现 if-else + dynamic_cast,通常说明设计没有把行为下沉到对象本身,这时候更该考虑重构抽象,而不是继续堆 RTTI。