zz C++ 虚函数表解析
前言
C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员 函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。
关于虚函数的使用方法,我在这里不做过多的阐述。大家可以看看相关的C++的书籍。在这篇文章中,我只想从虚函数的实现机制上面为大家 一个清晰的剖析。
当然,相同的文章在网上也出现过一些了,但我总感觉这些文章不是很容易阅读,大段大段的代码,没有图片,没有详细的说明,没有比较,没有举一反三。不利于学习和阅读,所以这是我想写下这篇文章的原因。也希望大家多给我提意见。
言归正传,让我们一起进入虚函数的世界。
读《C++对象模型》- 执行期语意学
一、对象的构造和解构
一般而言,constructor和destructor的安插都如下:
//C++伪码
{
Point point;
// point.Point::Point() //一般会被安插在这里
...
// point.Point::~Point() //一般会被安插在这里
}
如果一个作用域或函数中有一个以上的离开点,情况会稍微混乱一些。Destructor必须被放在每一个离开点之前,例如:
{
Point point;
// constructor 在这里
switch( int (point.x()) ) {
case -1:
//...
// destructor在这里
return;
case 0:
//...
//destructor在这里
return;
case 1:
//...
//destructor在这里
return;
default:
//...
//destructor在这里
return;
}
//destructor在这里
}
读《C++对象模型》- 构造、解构、拷贝语意学
constructor可能内带大量的隐藏码,因为编译器会扩充每一个constructor,扩充程度视class的继承体系而定。一般而言编译器所做的扩充操作大约如下:
- 记录在member initialization list中的data members初始化操作会被放进constructor的函数本身,并以members的声明顺序为顺序。
- 如果有一个member并没有出现在member initialization之中,但它有一个default constructor,那么该default constructor必须被调用。
- 在2之前,如果class object有virtual table pointer(s),它(们)被设定初值,指向适当的virtual table(s)。
- 在3之前,所有上一层的base class constructors必须被调用,以base class的声明顺序为顺序:
- 如果base class被列于member initialization list中,那么任何明确指定的参数都应该传递过去。
- 如果base class没有被列于member initialization list中,而它有default constructor,那么就调用之。
- 如果base class是多重继承下的第二或后继的base class,那么this指针必须有所调整。
- 在4之前,所有virtual base class constructors必须被调用,从左到右,从最深到最浅:
- 如果class被列于member initialization list中,那么如果有任何明确指定的参数,都应该传递过去。若没有列于list之中,而class中一个default constructor,也应该调用之。
- 此外,class中的每一个virtual base class subobject的偏移量(offset)必须在执行期可被存取。
- 如果class object是最底层的(most-derived)的class,其constructors可能被调用;某些用以支持这个行为的机制必须被放进来。
读《C++对象模型》- Function语意学
一、成员函数的调用方式
1.Nonstatic member functions
C++的设计准则之一就是:nonstatic member function 至少必须和一般的nonmember function有相同的效率。而实际上,编译器内部会将“member函数实体”转换为对等的“nonmember函数实体”。其转化步骤大致如下:
- 改写函数的signature(函数名称+参数数目+参数类型)以安插一个额外的参数到member function中,用以提供一个存取管道,使class object得以调用该函数。该额外参数被称为this指针。
- 将每一个“对nonstatic data member”的存取操作“改为经由this指针来存取”。
- 对member function重新写成一个外部函数。对函数名称进行“mangling”处理,使它在程序中成为独一无二的语汇(比如将参数 类型和返回值类型也编码进来)。
读《C++对象模型》- Data语意学
一、Data Member的存取
static data members,会被编译器提出于class之外,并被视为一个global变量(但只在class生命范围之内可见)。每一个member的存取许可(private, protected, public),以及与class的关联,并不会导致任何空间上或执行时间上的额外负担——不论是在个别的class objects或是在static data member本身。每一个static data member只有一个实体,存放在程序的data segment之中。
nonstatic data members直接存放在每一个class object之中。每一个nonstatic data member的偏移量(offset)在编译时期即可获知,甚至如果member属于一个base class subobject(派生自单一或多重继承串链)也是一样。因此,存取一个nonstatic data member,其效率和存取一个C struct member或一个nonderived class的member是一样的。
但是使用继承和指针还是会带一些差异的,如下代码:
// Point3d是一个derived class 其定义就忽略哈 Point3d origin, *pt = &origin; origin.x = 0.0; pt->x = 0.0
从上面代码中,“从origin存取”和“从pt存取”有什么重大的差异?答案是“当Point3d是一个derived class,而在其继承结构中有一个virtual base class,并且被存取的member(如上例中的x)是一个从该virtual base class继承而来的member时,就会有重大的差异“。这时候不能够说pt必然指向哪一种class type(因此也就不知道编译时期这个member真正的offset位置),所以这个存取操作必然延迟至执行期,经由一个额外的间接导引,才能够解决。但如果使用origin,就不会有这个问题,其类型无疑是Point3d Class,而即使它继承自virtual base class,members的offset位置也在编译时期就固定了。