C++虚函数调用的真正过程
如标题所言,在没有看到我这篇文章分析之前,你可能一直以为C++中的动态多态实现的机制是在运行时去动态的查找虚函数表的元素。"运行时动态的查找虚函数表的元素",这句话是否正确,那么久看你对这里的动态查找的定义了,如果你的意思是根据某个函数符号去动态到虚函数表中查找对应的实现(类似于遍历数组的形式),那么这个说法就是错误的,若动态只是指的去虚函数表(数组)中取值那么这就是正确的。
虚函数表的生成
废话不多说,我们直接上源代码和汇编一一对应: 查看汇编的网站
- 源代码
class Base {
public:
virtual void test() {}
virtual void test1() {}
virtual void test2() {}
};
class Son : public Base {
public:
void test() override {}
void test1() override {}
void test2() override{}
};
int main(){
Base base;
Son son;
}
- 汇编
介于很多人可能不太懂汇编,我就简单描述下上述汇编对应到C++函数中的行为,前面的main以及它之前的所有汇编都是具体的函数实现对应二进制的代码段,而从 vtable for Son
开始,后面的都类似于C++的全局变量,对应到二进制的data段中。
虚函数的调用
我们发现汇编中生成的虚函数表数据都是固定的而且就是一个数组结构,那么具体会如何调用呢?是会动态扫描整个数组的数据吗?我们通过基类指针来调用虚函数表的函数来试着生成汇编看看:
- 源码
class Base {
public:
virtual void test() {}
virtual void test1() {}
virtual void test2() {}
};
class Son : public Base {
public:
void test() override {}
void test1() override {}
void test2() override{}
};
int main(){
Base base;
Son son;
Base* b = &son;
b->test();
}
- 汇编
上述汇编关于虚函数表的查找我都已经标红并逐行对应到编程语言的语言概念中了。 我们发现具体的虚函数调用并不会在运行时去扫描虚函数表,而是已经提前知道了你要调用的函数在虚函数表中的第几个元素,然后直接取地址解引用调用即可。
不信你可以把 b->test()
的代码换成调用 b->test1()
你会发现整个调用过程就多了一行算偏移值的指令,如下:
总结
经过上述汇编的验证,所谓虚函数的动态调用,我们不如说是编译器根据C++源代码的动态生成。 这个动态生成有两个方面:
- 虚函数表:根据不同类型单独生成对应的虚函数表,具体的值符合符合多态的行为(子类重写则把具体的值换掉)。
- 虚函数调用:根据C++源代码中调用函数的行为,识别到需要调用虚函数表中的第几个元素,然后生成对应的汇编去调用。
如果需要讨论虚函数的引入是否会有较多的性能损失,那么我想说的是,只是多了三四条汇编指令的区别。。。