C++虚函数在g++中的实现分析

本文是我在追查一个诡异core问题的过程中收获的一点心得,把公司项目相关的背景和特定条件去掉后,仅取其中通用的C++虚函数实现部分知识记录于此。

在开始之前,原谅我先借用一张图黑一下C++:

C++虚函数在g++中的实现分析

“无敌”的C++

如果你也在写C++,请一定小心…至少,你要先有所了解: 当你在写虚函数的时候,g++在写什么?

先写个例子

为了探索C++虚函数的实现,我们首先编写几个用来测试的类,代码如下:

C++

#include <iostream> using namespace std; class Base1 { public: virtual void f() { cout << "Base1::f()" << endl; } }; class Base2 { public: virtual void g() { cout << "Base2::g()" << endl; } }; class Derived : public Base1, public Base2 { public: virtual void f() { cout << "Derived::f()" << endl; } virtual void g() { cout << "Derived::g()" << endl; } virtual void h() { cout << "Derived::h()" << endl; } }; int main(int argc, char *argv[]) { Derived ins; Base1 &b1 = ins; Base2 &b2 = ins; Derived &d = ins; b1.f(); b2.g(); d.f(); d.g(); d.h(); }

C++虚函数在g++中的实现分析

代码采用了多继承,是为了更多的分析出g++的实现本质,用UML简单的画一下继承关系:

C++虚函数在g++中的实现分析

示例代码UML图

代码的输出结果和预期的一致,C++实现了虚函数覆盖功能,代码输出如下:

Derived::f() Derived::g() Derived::f() Derived::g() Derived::h()

开始分析!

我写这篇文章的重点是尝试解释g++编译在底层是如何实现虚函数覆盖和动态绑定的,因此我假定你已经明白基本的虚函数概念以及虚函数表(vtbl)和虚函数表指针(vptr)的概念和在继承实现中所承担的作用,如果你还不清楚这些概念,建议你在继续阅读下面的分析前先补习一下相关知识,https://www.linuxidc.com/Linux/2015-07/119662.htm 。

通过本文,我将尝试解答下面这三个问题:

g++如何实现虚函数的动态绑定?

vtbl在何时被创建?vptr又是在何时被初始化?

Linux中运行的C++程序虚拟存储器中,vptr、vtbl存放在虚拟存储的什么位置?

首先是第一个问题:

g++如何实现虚函数的动态绑定?

这个问题乍看简单,大家都知道是通过vptr和vtbl实现的,那就让我们刨根问底的看一看,g++是如何利用vptr和vtbl实现的。

第一步,使用 -fdump-class-hierarchy 参数导出g++生成的类内存结构:

Vtable for Base1 Base1::_ZTV5Base1: 3u entries 0 (int (*)(...))0 4 (int (*)(...))(& _ZTI5Base1) 8 Base1::f Class Base1 size=4 align=4 base size=4 base align=4 Base1 (0xb6acb438) 0 nearly-empty vptr=((& Base1::_ZTV5Base1) + 8u) Vtable for Base2 Base2::_ZTV5Base2: 3u entries 0 (int (*)(...))0 4 (int (*)(...))(& _ZTI5Base2) 8 Base2::g Class Base2 size=4 align=4 base size=4 base align=4 Base2 (0xb6acb474) 0 nearly-empty vptr=((& Base2::_ZTV5Base2) + 8u) Vtable for Derived Derived::_ZTV7Derived: 8u entries 0 (int (*)(...))0 4 (int (*)(...))(& _ZTI7Derived) 8 Derived::f 12 Derived::g 16 Derived::h 20 (int (*)(...))-0x000000004 24 (int (*)(...))(& _ZTI7Derived) 28 Derived::_ZThn4_N7Derived1gEv Class Derived size=8 align=4 base size=8 base align=4 Derived (0xb6b12780) 0 vptr=((& Derived::_ZTV7Derived) + 8u) Base1 (0xb6acb4b0) 0 nearly-empty primary-for Derived (0xb6b12780) Base2 (0xb6acb4ec) 4 nearly-empty vptr=((& Derived::_ZTV7Derived) + 28u)

如果看不明白这些乱七八糟的输出,没关系(当然能看懂更好),把上面的输出转换成图的形式就清楚了:

C++虚函数在g++中的实现分析

vptr和vtbl

其中有几点尤其值得注意:

我用来测试的机器是32位机,所有vptr占4个字节,每个vtbl中的函数指针也是4个字节

每个类的主要(primal)vptr放在类内存空间的起始位置(由于我没有声明任何成员变量,可能看不清楚)

在多继承中,对应各个基类的vptr按继承顺序依次放置在类内存空间中,且子类与第一个基类共用同一个vptr

子类中声明的虚函数除了覆盖各个基类对应函数的指针外,还额外添加一份到第一个基类的vptr中(体现了共用的意义)

有了内存布局后,接下来观察g++是如何在这样的内存布局上进行动态绑定的。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/00c65645f930aa0f449a6e6343cfba94.html