C++ 虚函数及多态原理详解

概述

虚函数机制作为C++面向对象的重要支撑,但是我们对它知之甚少,因为它由编译器实现。今天就对虚函数进行整理,这样出去吹牛会更有底气。

1、虚函数的支持原理

每一个拥有虚函数的类会产生出一堆指向virtual functions的指针,放在表格中,这个表格被称为virtual table(虚函数表)

定义类的对象时编译器会给这个对象插入一个合成指针,指向相关的virtual table。这个指针通常被称为vptr(虚指针)。vptr的设定和重置都由每个类的构造、析构和赋值运算符自动完成。

需要注意的是:每个类所关联的type_info(类型信息)也经由virtual table被指出,通常放在第一个表格。如下:

class Point
{
public:
	Point( float xval);
	virtual ~Point();
	float x() const;
	
protected:
	virtual ostream& print( ostream& os) const;
	
	float _x;
};

C++ 虚函数及多态原理详解
这样做优点在于它的空间和存取时间的效率。缺点是增加或删除虚函数需要进行重新编译。

2、单一继承下的虚函数

虚函数的继承主要用于实现多态。多态表示一个public base clas 的指针或引用,寻址出一个派生类的对象。表现为,调用同一个函数,展现出不同的动作。

多态的底层实现需要知道两个细节:1、指针或引用所指向的真实类型,这可使我们可以选择正确的函数。2、正确的函数实际的位置,以便调用。

通过上一节的讲解,你可能已经知道了,类型信息存放在virtual table的第一个表格。

一个单一继承类中只会有一个virtual table,每一个table内含所有虚函数的的地址,其中虚函数包括:

  1. 当前类所定义的虚函数,它会改写(overriding)一个可能存在的基类虚函数
  2. 继承自基类的虚函数。这是在派生类决定不改写虚函数时才会出现
  3. 纯虚函数也会包含

每个虚函数都会被指派一个固定的索引值,这个索引在整个继承体系中保持与特定的虚函数关联。比如:索引 0 为类类型信息,索引 1 为虚析构,索引 2 为纯虚函数…等。如下举例:

class Point
{
public:
	virtual ~Point();
	virtual Point& mult( float ) = 0;
	float x() const { return _x; };
	virtual float y() const { return 0; };
	virtual float z() const { return 0; };
	//...
	
protect:
	Point( float x = 0.0 );
	float _x;
};

class Point2d : public Point
{
public:
	Point2d( float x = 0.0, float y = 0.0 ) : Point(x),_y(y){}
	~Point2d();
	
	//改写基类虚函数
	Point2d& mult( float );
	float y() const { return _y; };
	//....
protected:
	float _y;
};

class Point3d : public Point2d
{
public:
	Point3d( float x = 0.0, float y = 0.0, float z = 0.0 ) : Point2d(x,y),_z(z){}
	~Point3d();
	
	//改写基类虚函数
	Point3d& mult( float );
	float z() const { return _z;	};
	//......
protected:
	float _z;
};

类的继承关系如上,各个类的virtual table布局则如下所示:
C++ 虚函数及多态原理详解
通过上面也可以看出:
1、派生类继承基类的虚函数,会把基类的虚函数地址拷贝到相应的表格中。如:基类的z()总是放在索引为4的表格中。
2、派生类自己的虚函数实例,必须放到对应的表格中。如:类型都要放到索引为0的表格,虚析构要放到索引为1的表格。
3、新加入一个虚函数,virtual table的尺寸会增大一个表格,用来存放新的虚函数

以上的举例有一个特殊的情况:对于表达式(不管ptr指向的是什么类型):

ptr->z();

因为每个virtual table表中都含有虚函数z,而每个对象中都会拥有vptr,而vptr又指向唯一的virtual table,编译器知道调用函数对应函数的索引值,所以编译器会转化为:

(*ptr->vptr[ 4 ]) (ptr);

这种就不需要在执行期确定类型了。

3、多重继承下的虚函数

多重继承中支持虚函数,主要是第二个及后续的基类身上。简单来说就是会有一个偏移值(offset)。下面来看个例子:

class Base1
{
public:
	Base1();
	virtual ~Base1();
	virtual void speakClearly();
	virtual Base1* clone() const;
protected:
	float	data_Base1;
};

class Base2
{
public:
	Base2();
	virtual ~Base2();
	virtual void mumble();
	virtual Base2* clone() const;
protected:
	float	data_Base2;
};

class Derived : public Base1 , public Base2
{
public:
	Derived();
	virtual ~Derived();
	virtual Derived* clone() const;
protected:
	float	data_Derived;
};

每个类中virtual table 的布局如下:
C++ 虚函数及多态原理详解
在多重继承之下,一个派生类内含n-1个额外的virtual tables,n表示其基类的个数(单一继承不会有额外的virtual table)。

虽然有多个虚函数表,但是当你创建对象时,会给对象中的vptr赋值不一样。比如:

Base1* ptr1 = new Derived();		//vptr指向上面的虚函数表
Base2* ptr2 = new Derived();		//vptr指向下面的虚函数表

为了提高执行期链接器的效率,有的编译器会对多个虚函数表进行连接,然后通过表格的首地址加上一个offset获得实际的虚函数表。

4、虚拟继承下的虚函数

对于虚继承我们之前整理过类成员分布虚继承,一般而言会在虚函数表中有指向虚基类的指针,然后根据这个指针在操作。

还有的编译器是在对象中拥有多个vptr,分别指向不同的虚函数表,这样使用时就不会出错。

因为虚继承比较复杂,而且是间接寻址,效率比较低,所以尽量少用。

感谢大家,我是假装很努力的YoungYangD(小羊)。

参考资料:
《深度探索 C++对象模型》