一、虚函数表
虚函数表(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
✅关键点:
-
vtable 是类的,不是对象的,全局只存一份,所有同类对象共用同一张表
-
vptr(虚函数指针)是对象的成员,存在对象内存里面,每个对象都有
-
只要类里面有virtual函数,这个类就会生成vtable;实例化对象就会多出一个vptr指针。
没有虚函数的类:对象里没有vptr,没有虚函数表。
2、多态是怎么工作的?
Base* p = new Derived();
p->func();
-
p 指针指向 Derived 对象;
-
对象内部的vptr指向Derived::vtable;
-
运行时通过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:虚函数带来的开销?
-
对象多一个vptr指针,增加内存;
-
调用虚函数:运行时间接寻址,通过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 运算符
两种用法
-
typeid(变量/对象)
-
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<目标类型&>(源引用);
规则
-
只能用于多态类(含有virtual函数),否则编译报错。
-
指针转换:转换失败返回nullptr。
-
引用转换:转换失败抛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,转换失败
对比其他转换:
-
static_cast:编译期转换,不做运行时检查。父类指针强转子类,哪怕对象不是子类,也不会报错,直接内存越界崩溃。
-
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保存该类的类型信息。
-
dynamic_cast:拿到对象vptr → 访问vtable → 读取type_info,判断继承关系,做安全类型转换。
-
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、面试高频问答
- Q:dynamic_cast和static_cast区别?
A:static_cast编译期转换,不做运行时检查,向下转型不安全;
dynamic_cast运行时依靠RTTI(vtable)做检查,向下转型安全,失败返回nullptr;要求类必须有虚函数。
- Q:为什么没有虚函数不能用dynamic_cast?
A:dynamic_cast依赖虚函数表里面的RTTI类型信息,没有vtable就拿不到运行时类型。
- Q:typeid对非多态对象返回什么?
A:返回编译期静态类型,不会识别真实子类。
- Q:dynamic_cast转换失败,指针和引用分别是什么行为?
A:指针返回nullptr;引用抛出std::bad_cast异常。
总结速记
-
RTTI运行时类型信息,依赖虚函数表vtable;没有虚函数就没有RTTI。
-
typeid:获取类型,多态对象得到运行时真实类型。
-
dynamic_cast:安全向下转型,指针失败返回nullptr,引用抛异常。
-
设计优先用虚函数多态;只有不得已才用dynamic_cast做类型判断。
整套C++面向对象回顾(我们已经讲完)
虚函数表vtable/vptr → 构造析构调用虚函数坑 → 虚析构 → 多重继承 → RTTI(typeid / dynamic_cast)。