浅谈C++多态实现原理(虚继承的奥秘)

我自己搭建了博客,以后可能不太在CSDN上发博文了,https://www.qingdujun.com/。


在C++中,多态表示“以一个public base class的指针(或reference),寻址出一个derived class object”的意思。——Stanley B. Lippman

大伙都知道,如果要实现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;
}

1 从编译器看多态

从上述伪代码中可以看到:其一,类成员函数被编译器在第一个位置强制安插了一个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[1-n]中存放相对于vtbl[0]offset(这里说的是虚函数位置,而vtbl[0]说的是类位置),这是C++之父赞赏的一种做法。

vtbl[0] type_info for Point 它除了有virtual base class offsets,另外还有其他信息

这些其他信息,需要保证dynamic_cast正常的工作——在执行器从type_info中取得指针所指向的类的具体信息。

pfct pf = dynamic_cast<pfcp>(pt);
((type_info*)(pt->vptr[0]))->_type_descriptor;

可以在编译器目录中,找到具体信息,也可以参考 https://en.cppreference.com/w/cpp/header/typeinfo

2 虚继承的奥秘

综上,调用某个虚函数时,可以先找到类基址,然后找到虚函数的偏移量,最终两者合成虚函数地址,也就是这样: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++中定义一个不能被继承的类(友元类+类模板)》该问题正是利用了虚继承的相关性质。



©qingdujun
2018-12-24 北京 海淀


Reference: 《深度探索C++对象模型》 侯捷 译

©️2020 CSDN 皮肤主题: 精致技术 设计师:CSDN官方博客 返回首页