这篇博客来讲一下g++实现的C++对象模型中的虚函数的实现,包括:单一继承体系下的虚函数,多继承下的虚函数和虚继承下的虚函数。其中虚继承下的虚函数在《深度探索C++对象模型》中只是说很复杂,受限于技术力和查到的资料,这里我只是对于g++的部分实现进行观察。
1. 单一继承体系下的虚函数
在前面的博客中我们已经通过对虚表的探索讲了虚函数的一般实现,大体上来说就是编译器会在适当的时候(在单一继承体系中就是当类中第一次出现虚函数的时候)添加一个虚表指针,指向属于该类的虚函数表,而所有虚函数的地址会出现在虚表指针的固定表项,也就是说在继承体系下的一个虚函数会被赋予固定的虚表下标。当派生类覆写(override)了基类的虚函数时,新的虚函数的地址会出现在基类虚函数在虚表中的位置,在多态调用虚函数时从虚表中取出虚函数地址来调用,从而实现多态。
一般而言,在单一继承体系下每一个类都只有一个虚表,在这个虚表中存有所有active virtual functions(中文版《深度探索C++对象模型》没有翻译,我这里也直接使用了,在我的理解里就是派生类所有有效的、能用的虚函数)的地址。这些active virtual functions包括:
- 该类所定义的所有虚函数,包括其覆写(override)的基类的虚函数;
- 继承自基类的虚函数,如果派生类不覆写这些虚函数的话;
- 一个pure_vairtual_called()函数实体,她既可以扮演pure virtual function的空间保卫者角色,也可以当作异常处理函数(有时候会用到)【《深度探索C++对象模型》原话】
// test23.cpp
class Base {
public:
Base(int i)
: m_i(i)
{}
virtual
~Base() {
m_i = 0;
}
virtual
int getInt() {
return m_i;
}
virtual
void increaseInt() {
m_i++;
}
virtual
long getLong() = 0;
private:
int m_i;
};
class Derived: public Base {
public:
Derived(int i, long l)
: Base(i),
m_l(l)
{}
virtual
~Derived() {
m_l = 0;
}
virtual
int getInt() override { // overrid Base::getInt()
return Base::getInt() + 1;
}
virtual
long getLong() override { // overrid Base::getLong(),在Base中是一个纯虚函数
return m_l;
}
virtual
void increaseLong() { // new virtual function
++m_l;
}
private:
long m_l;
};
int main() {
Derived* pd = new Derived(1, 2L);
int i = pd->getInt();
pd->increaseInt();
long l = pd->getLong();
pd->increaseLong();
pd->~Derived();
delete pd;
}
另外,在这里我们可以注意到一个问题,虚表指针指向的空间,前两个表项都显示是Derived::~Derived(),也就是都是析构函数,而且地址不一样,这是怎么回事?我们看一下这两处地方的汇编代码:
可以看到,第一个析构函数就是普通的析构函数它先调用了我们自己定义的析构函数,再调用了基类的析构函数Base::~Base;而第二个虚构函数则是先调用了第一个析构函数,再调用了::operator delete
(_ZdlPvm使用c++filt工具查看可知其就是operator delete(void*, unsigned long)
)。
那是不是就是当我们自己调用Derived::~Derived时调用第一个,使用delete操作符时调用的就是第二个呢?我们看到反汇编:
可以看到确实是这样的。同时,我们还有一个小发现,就是当delete操作符操作的指针是nullptr时,是不会调用析构函数的,编译器真是相当费心了(在我的测试下好像是只有delete一个指向有虚析构函数的对象的指针时才会检查,否则就直接不检查调用::operator delete
)。
关于最后一个,因为我们无法实例化抽象基类,所以使用-fdump-class-hierarchy
选项查看类信息:
Vtable for Base
Base::_ZTV4Base: 7 entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI4Base)
16 0
24 0
32 (int (*)(...))Base::getInt
40 (int (*)(...))Base::increaseInt
48 (int (*)(...))__cxa_pure_virtual
Class Base
size=16 align=8
base size=12 base align=8
Base (0x0x7f24b28e7960) 0
vptr=((& Base::_ZTV4Base) + 16)
Vtable for Derived
Derived::_ZTV7Derived: 8 entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI7Derived)
16 (int (*)(...))Derived::~Derived
24 (int (*)(...))Derived::~Derived
32 (int (*)(...))Derived::getInt
40 (int (*)(...))Base::increaseInt
48 (int (*)(...))Derived::getLong
56 (int (*)(...))Derived::increaseLong
Class Derived
size=24 align=8
base size=24 base align=8
Derived (0x0x7f24b277d1a0) 0
vptr=((& Derived::_ZTV7Derived) + 16)
Base (0x0x7f24b28e7de0) 0
primary-for Derived (0x0x7f24b277d1a0)
我们可以看到在Base
类的48偏移处确实有一个__cxa_pure_virtual表项,应该就是所谓的pure_vairtual_called,在结合Derived
类的虚表,在对应位置是Derived::getLong,说明正是使用该函数占位了Base::getLong这个虚函数。
2. 多重继承下的虚函数
在单一继承体系下一切都显得那么美好,完全不涉及到指针的调整,因为所有的指针转化都不需要做底层的调整,始终指向类的开头。你可能现在还不能理解,在看完这一部分后再来看上面这一句话就会感慨:啊,单一继承是这么简单的事!
但在多重继承下事情开始变得复杂,看下面的例子:
// test24.cpp
class Base1 {
pu