大伙都知道,如果要实现C++的多态,那么,基类中相应的函数必须被声明为虚函数(或纯虚函数)。举个例子:
class Point {
public:
Point(float x = 0.0, float y = 0.0) : _x(x), _y(y) {
}
virtual float z(); //virtual function
protected:
float _x, _y;
};
我推测编译器会这样处理这个类:处理Point类时,它会安插this指针
、vptr指针
和增加一张虚表vtbl
。
比如,构造函数可能被编译器改造成如下模样(很惊讶吧!构造函数也有返回值的哟~):
Point* Point(Point* this, float x = 0.0, float y = 0.0) : _x(x), _y(y) {
this->__vptr_Point = __vtbl_Point; //pointer to virtual table
//以下为用户自定义部分
this->_x = x;
this->_y = y;
return this;
}
从上述伪代码中可以看到:其一,类成员函数被编译器在第一个位置强制安插了一个this
指针。其二,编译器给类增加了一个虚表指针__vptr_Point
,它指向了Point类的虚表__vtbl_Point
(注意,一个是vptr另一个是vtbl)。
也正是这个vptr与vtbl的动态关联…奠定了C++多态的基础。
目前,编译器维护的虚表可能是这个样子:
[0] type_info for Point
[1] Point::z()
vtbl[0]
处存放的是Point类的信息(我下面会介绍),vtbl[1]
处就是Point::z()
函数了。那么,可以这么调用它。
//Point* ptr = new Point();
ptr->__vptr_Point[1](ptr);
为什么是这个样子呢?其一,__vptr_Point[1]
可以找到这个函数的入口。但是,调用时Point::z()
不是没有参数吗?不要忘记了编译器会为我们传入第一个参数this
——这里就是ptr
。
好了,那vtbl[0] type_info for Point
又是什么?——它可能被解释为Point类在内存中的基准(或者位置)。不同的基准,就代表vptr
关联着不同的vtbl
,导致的最直接结果肯定是会调用不同的函数,这样产生的现象就是多态。
我们考虑一下多重继承,举个例子(假设有以下继承关系且有虚函数出没):
class Base1 {
};
class Base2 {
};
class Derived : public Base1, public Base2 {
};
在内存中,根据C++的多继承规则——Derived
应该是这样的(低地址)[Base1, Base2, Derived](高地址),可以图示一下:
[ [Base 1 //Derived begin
__vptr_base1 ]
[Base 2
__vptr_base2 ] //此处一定要指向Base2的虚表,否则Base2* ptr2 = new Derived;会出问题
[Derived] ] //Derived end
很显然,Base1的首地址和Derived的首地址是一致的。那么,对于Base2来说,以下这个过程中…会发生什么?
Base2* ptr2 = new Derived;
如果盲目的将Derived type_info一股脑给Base2,是不是ptr2指向的地址就不对了?所以编译器会有一个这样的操作:
//将Base2* ptr2 = new Derived;拆分为以下两句
Derived* temp = new Derived;
Base2* ptr2 = temp ? temp + sizeof(Base1) : 0;
new Derived
返回的地址会是Base1
的首地址(我再次强调,Derived和Base1首地址是一致的)。而Base2在内存中紧接着Base1,所以必须做个偏移,使ptr2指向Derived中的Base2首地址,并将__vptr_base2指派给它,也就是关联好Base2的虚表vtbl。
这里,做了个判断是考虑到有temp == nullptr
(也就是Base2* ptr2 = nullptr;)这种情况的存在。
看到这里应该明白了 vtbl[0] type_info for Point 它可能是virtual base class offsets ——也就是说,它是对应类的基址(或内存中的地址)。而编译器在vtbl[1-n]
中存放相对于vtbl[0]
的offset
,这是C++之父赞赏的一种做法。
综上,调用某个虚函数时,ptr->__vptr_Point[1](ptr);
== ptr->__vptr_Point[0]+1(ptr);
此时offset=1。
以上都是基础知识,那么复杂的虚继承对象是如何构造起来的呢?考虑以下代码,Derived是如何避免多次构造Base的?
class Base {
public:
Base() {
}
};
class Mid1 : virtual public Base {
public:
Mid1() : Base() {
}
};
class Mid2 : virtual public Base {
public:
Mid2() : Base() {
}
};
class Derived : public Mid1, public Mid2 {
public:
Derived() : Mid1(), Mid2() {
}
};
有一种的做法是编译器对构造函数进行扩充,添加了一个bool __most_derived
的开关,正是由于这个开关的添加,使得底层子类Derived可以压制它的直接基类Mid1、Mid2对顶部虚基类Base的构造。为了突出主次,没有添加类似于vptr设置的相关语句(编译器扩充后的伪代码如下):
class Base {
public:
Base() {
}
};
class Mid1 : virtual public Base {
public:
Mid1* Mid1(Mid1* this, bool __most_derived) {
if (__most_derived != false) {
this->Base::Base();
}
}
};
class Mid2 : virtual public Base {
public:
Mid2* Mid2(Mid2* this, bool __most_derived) {
if (__most_derived != false) {
this->Base::Base();
}
}
};
class Derived : public Mid1, public Mid2 {
public:
Derived* Derived(Derived* this, bool __most_derived) {
if (__most_derived != false) {
this->Base::Base();
}
this->Mid1(false); //压制Mid1调用Base构造函数
this->Mid2(false);
}
};
读到这里,我想给大伙推荐一篇文章《C++中定义一个不能被继承的类(友元类+类模板)》该问题正是利用了虚继承的相关性质。
2018-12-24 北京 海淀
Reference: 《深度探索C++对象模型》 侯捷 译