c++面试之从虚函数表到Rtti一次性讲清楚

一、虚函数表

虚函数表(vtable)一次性讲透,C++面试高频,结合对象内存

核心结论先记牢:

不是每个对象拥有一张虚函数表;是每个拥有虚函数的类,共享一张虚函数表。对象里面只存一个虚函数指针(vptr),指向这个类的虚函数表。

1、内存布局(最关键)

class Base{

public:

virtual void func(){}

};

class Derived : public Base{

public:

void func() override {}

};

🔹类层面:Base 类、Derived类各自各有一张虚函数表vtable(属于类,全局静态区,只有一份)

• Base::vtable:存放 Base::func 的函数地址

• Derived::vtable:存放 Derived::func 的函数地址(重写覆盖)

🔹对象层面:

Base b;

Derived d;

对象内存结构:

// 对象 b

vptr 指针 → 指向 Base类的虚函数表 Base::vtable

// 对象 d

vptr 指针 → 指向 Derived类的虚函数表 Derived::vtable

✅关键点:

  1. vtable 是类的,不是对象的,全局只存一份,所有同类对象共用同一张表

  2. vptr(虚函数指针)是对象的成员,存在对象内存里面,每个对象都有

  3. 只要类里面有virtual函数,这个类就会生成vtable;实例化对象就会多出一个vptr指针。

没有虚函数的类:对象里没有vptr,没有虚函数表。

2、多态是怎么工作的?

Base* p = new Derived();

p->func();

  1. p 指针指向 Derived 对象;

  2. 对象内部的vptr指向Derived::vtable;

  3. 运行时通过vptr找到虚函数表,取出Derived::func地址调用;

👉运行时动态决定调用哪个函数,这就是多态。

如果没有virtual,就是静态编译期确定调用,不经过虚函数表。

3、继承场景:多重继承、虚继承

①单继承(上面例子)

子类重写虚函数,修改自己虚函数表里对应条目,父类的虚函数表不受影响。

②多重继承

class A{ virtual void f(); };

class B{ virtual void g(); };

class C : public A, public B {};

• C对象内存:有两个vptr指针

◦ vptr_A → A类的vtable

◦ vptr_B → B类的vtable

每一个带虚函数的父类,子类对象就会多一个vptr。

③虚继承(virtual继承)

虚继承的虚函数表会有特殊处理,解决菱形继承的二义性,共享一份虚基类vtable。

4、面试高频坑点,一定要背

Q1:一个对象有虚函数表吗?

❌错误:对象有虚函数表

✅正确:对象只有虚函数指针vptr,虚函数表属于类,全局唯一,同类所有对象共享。

Q2:什么时候生成vtable?

• 类中只要出现 virtual 函数,编译器就为该类生成vtable。

• 即使是纯虚函数,也会生成虚函数表,只是表中对应位置是nullptr。

class Abstract{

public:

virtual void foo() = 0; //纯虚函数,依然存在vtable

};

抽象类不能实例化,但类依旧有vtable。

Q3:虚函数表存在内存哪个区域?

• 只读静态段(.rodata),属于程序全局数据,不是堆,不是栈。

• 每个程序运行,只生成一份。

Q4:构造函数可以virtual吗?析构函数呢?

• 构造函数不能virtual:构造的时候对象还没完全构造完,vptr还没初始化好,无法走虚表。

• 析构函数建议virtual:

Base* p = new Derived();

delete p;

如果析构不是virtual:只会调用Base析构,Derived部分不会析构,内存泄漏。

父类析构virtual,子类析构自动virtual。

Q5:虚函数带来的开销?

  1. 对象多一个vptr指针,增加内存;

  2. 调用虚函数:运行时间接寻址,通过vptr查表,比普通函数调用慢一点点。

现代编译器优化(如devirtualization)可以消除部分开销。

5、极简记忆口诀

类有虚表vtable,对象有虚指针vptr;

同类对象共用一张虚表,每个对象自带vptr;

多态靠vptr去查表,运行时找到函数地址。

补充:和你之前的PIML模式关联

PIML(Impl)模式,把实现放到cpp内部,如果Impl类有虚函数,同样会产生vptr/vtable。

二、RTTI 运行时类型信息

RTTI、dynamic_cast、typeid 一次性讲透(C++工程实战版)

RTTI:Run‑Time Type Information,运行时类型信息。

作用:程序运行的时候,获取对象真实的类型。

核心两个工具:typeid、dynamic_cast。

⚠️前提:类必须有虚函数(有vtable),RTTI才生效;没有虚函数,只能拿到静态编译期类型。

1、typeid 运算符

两种用法

  1. typeid(变量/对象)

  2. typeid(类型)

class Base { virtual void f(){} };

class Derived : public Base {};

Base* p = new Derived();

cout << typeid(*p).name() << endl; // 运行时:Derived

cout << typeid(p).name() << endl; // 静态:Base*

• 当表达式是带虚函数的多态对象:typeid读取虚函数表里面的类型信息,返回运行时真实类型。

• 如果没有虚函数:typeid返回编译期静态类型,不做运行时判断。

注意:.name()返回的字符串是编译器实现相关,不同编译器输出不一样,不要拿字符串做逻辑判断!

❌错误写法:if(typeid(obj).name() == "Derived"),跨编译器会失效。

✅正确:用typeid(a) == typeid(b)比较类型对象。

if(typeid(*p) == typeid(Derived))

{

// 真实对象是Derived

}

2、dynamic_cast 动态转换(最重要)

语法

dynamic_cast<目标类型*>(源指针);

dynamic_cast<目标类型&>(源引用);

规则

  1. 只能用于多态类(含有virtual函数),否则编译报错。

  2. 指针转换:转换失败返回nullptr。

  3. 引用转换:转换失败抛std::bad_cast异常,不会返回null。

示例

class Base{ virtual void f(){} };

class Derived : public Base {};

Base* p = new Derived();

Derived* d = dynamic_cast<Derived*>(p);

if(d != nullptr)

{

// 转换成功,真实对象是Derived,可以安全调用子类方法

}

Base* p2 = new Base();

Derived* d2 = dynamic_cast<Derived*>(p2);

// d2 == nullptr,转换失败

对比其他转换:

  1. static_cast:编译期转换,不做运行时检查。父类指针强转子类,哪怕对象不是子类,也不会报错,直接内存越界崩溃。

  2. dynamic_cast:运行时检查,依靠vtable里的RTTI信息,安全的向下转型(down‑cast)。

工程场景:插件框架、回调拿到基类指针,需要判断是不是某个具体实现类,就用dynamic_cast。

为什么引用转换失败抛异常?

引用不能为nullptr,没有空引用,所以失败只能抛异常。

Base b;

Derived& ref = dynamic_cast<Derived&>(b); // 抛出 bad_cast

3、底层原理(和虚函数表vtable挂钩)

RTTI信息存放在虚函数表的头部。

每个带虚函数的类,vtable前面会附加一个指向type_info结构体的指针,type_info保存该类的类型信息。

  1. dynamic_cast:拿到对象vptr → 访问vtable → 读取type_info,判断继承关系,做安全类型转换。

  2. typeid:同样通过vptr拿到type_info,返回真实类型。

关键点:没有虚函数,就没有vtable,就没有RTTI信息,dynamic_cast直接编译报错。

4、工程里的坑与取舍(工控Qt开发重点)

坑1:不要滥用dynamic_cast

dynamic_cast是运行时查表,有开销。

设计良好的面向对象,尽量用虚函数多态,而不是判断类型再强转。

反模式:

// 不好的写法,大量if‑else判断类型

if(auto d = dynamic_cast<Derived1*>(obj)){

d->do1();

}else if(auto d = dynamic_cast<Derived2*>(obj)){

d->do2();

}

更好:把do1/do2做成虚函数,直接调用obj->doWork(),交给多态处理。

什么时候必须用dynamic_cast?

外部回调、插件接口、拿到基类指针,无法通过虚函数实现业务逻辑,必须区分具体子类。

坑2:dynamic_cast 只支持公有继承

私有/保护继承,dynamic_cast会转换失败返回nullptr。

只有public继承体系下,RTTI才能正常工作。

坑3:typeid不要用.name()做业务判断

.name()编译器自定义,MSVC、GCC输出完全不一样,不能用来做字符串比较。

正确:直接比较typeid(a) == typeid(b)。

坑4:RTTI可以关闭

编译器选项:GCC -fno‑rtti,关闭RTTI。

关闭后:dynamic_cast、typeid全部失效,编译报错。

嵌入式/工控部分追求极致体积的项目会关闭RTTI,代价就是不能用动态类型识别。Qt项目默认开启RTTI。

5、面试高频问答

  1. Q:dynamic_cast和static_cast区别?

A:static_cast编译期转换,不做运行时检查,向下转型不安全;

dynamic_cast运行时依靠RTTI(vtable)做检查,向下转型安全,失败返回nullptr;要求类必须有虚函数。

  1. Q:为什么没有虚函数不能用dynamic_cast?

A:dynamic_cast依赖虚函数表里面的RTTI类型信息,没有vtable就拿不到运行时类型。

  1. Q:typeid对非多态对象返回什么?

A:返回编译期静态类型,不会识别真实子类。

  1. Q:dynamic_cast转换失败,指针和引用分别是什么行为?

A:指针返回nullptr;引用抛出std::bad_cast异常。

总结速记

  1. RTTI运行时类型信息,依赖虚函数表vtable;没有虚函数就没有RTTI。

  2. typeid:获取类型,多态对象得到运行时真实类型。

  3. dynamic_cast:安全向下转型,指针失败返回nullptr,引用抛异常。

  4. 设计优先用虚函数多态;只有不得已才用dynamic_cast做类型判断。

整套C++面向对象回顾(我们已经讲完)

虚函数表vtable/vptr → 构造析构调用虚函数坑 → 虚析构 → 多重继承 → RTTI(typeid / dynamic_cast)。

相关推荐
傲世仙尊2 小时前
System V 进程间通信详解:共享内存、消息队列与信号量(CSDN博客)
linux·开发语言·c++
cvby2 小时前
C++11
开发语言·c++
6Hzlia2 小时前
【Classic 150 刷题计划】 LeetCode 26. 删除有序数组中的重复项 | C++ 快慢双指针经典模板
c++·算法·leetcode
夜不会漫长3 小时前
C++:类和对象(2)
java·javascript·c++
无小道4 小时前
C++ 中 `explicit` 关键字详解:彻底理解隐式类型转换
c++·explicit
蒸蒸yyyyzwd4 小时前
cpp 选手秋招学习笔记 day34
c++·八股
Tairitsu_H4 小时前
[C++] C++11类和STL的新特性
开发语言·c++·stl·c++11
Lucis__4 小时前
基于责任链模式的消息队列—异步处理流水线的最佳实践
linux·c++·消息队列·责任链模式·ipc
Jackson_GJH5 小时前
C++虚函数、虚继承、虚基类
c++·c++基础