机器码层面:虚函数与条件执行的差异解析
虚函数与条件执行在机器码层面的差异
我最近在琢磨虚函数的概念,翻维基百科看到这么个说法:
简而言之,虚函数定义了待执行的目标函数,但该目标函数在编译阶段可能无法确定。
我现在特别好奇,虚函数在机器码层面,和下面这种用switch实现的条件执行方式到底有啥不一样?先贴一下我写的示例代码:
void conditional_func(int func_to_run) { switch(func_to_run) { case 1: func1(); break; case 2: func2(); break; // ... 更多case分支 } } int main() { int func_to_run; // 读取输入确定要执行的函数标记 scanf("%d", &func_to_run); conditional_func(func_to_run); }
就像上面代码里展示的,最终执行的目标函数都是在运行时根据输入决定的,那它和虚函数的底层实现差异在哪呢?
1. 核心寻址逻辑完全不同
- switch条件执行:属于静态分支跳转。编译器会把case标签转换成两种形式——如果case值连续,就生成一张跳转表(jump table),运行时直接用
func_to_run作为索引查表得到函数地址跳转;如果case值离散,就生成一连串的cmp(比较)+jmp(跳转)指令。不管哪种方式,所有要跳转的函数地址在编译时就已经固定死在代码段里了。 - 虚函数调用:属于动态间接寻址。每个带虚函数的类都会生成一张虚函数表(vtable),里面按顺序存着该类所有虚函数的地址;每个类的对象实例里会有一个隐藏的虚表指针(vptr),指向所属类的vtable。运行时调用虚函数的流程是:先通过对象的vptr找到对应的vtable,再根据函数在表中的偏移量取出目标函数地址,最后执行调用——这个函数地址是运行时才确定的,完全取决于对象的实际类型。
2. 性能表现的差异点
- switch分支:如果是跳转表形式,跳转速度很快;但如果是离散case的比较分支,CPU的分支预测一旦猜错,就会触发流水线清空,带来不小的性能开销。而且分支的数量是编译时固定的,无法动态扩展。
- 虚函数调用:本质是几次内存读取(取vptr→取vtable里的函数地址)+ 一次间接调用,没有分支比较的过程,所以不存在分支预测失败的问题。虽然多了几次内存访问,但现代CPU的L1/L2缓存会把常用的vtable缓存起来,实际性能开销非常小,大部分场景下和switch不相上下,甚至性能表现更稳定。
3. 扩展性的底层支撑
- switch方式:要是想新增一个可执行的函数,必须修改
conditional_func里的switch分支,重新编译整个函数。编译后的机器码会新增对应的比较逻辑或跳转表项,灵活性极差,完全不符合开闭原则。 - 虚函数:新增子类只需要在子类里重写虚函数,编译器会自动为这个子类生成新的vtable,不需要修改任何原有代码。运行时只要对象是这个子类的实例,vptr会自动指向新的vtable,底层实现天然支持多态扩展,维护成本低很多。
4. 代码体积的区别
- switch:所有分支的跳转逻辑都集中在
conditional_func里,要是分支很多,这个函数的机器码体积会明显膨胀。 - 虚函数:虚函数表是每个类一份,存在只读数据段里,每个虚函数调用点的机器码都非常简洁(只是取指针、间接调用的几行指令)。就算新增很多子类,也只是新增对应的vtable,不会让单个函数的代码体积变大。
内容的提问来源于stack exchange,提问作者pooya13
相关产品推荐
相关产品推荐

