一、什么是虚函数表
编译器之所以能通过指针指向的对象找到虚函数,是因为在创建对象时额外地增加了虚函数表。如果一个类包含了虚函数,那么在创建该类的对象时就会额外地增加一个数组,数组中的每一个元素都是虚函数的入口地址。不过数组和对象是分开存储的,为了将对象和数组关联起来,编译器还要在对象中安插一个指针,指向数组的起始位置。这个数组就是虚函数表。
假设有三个类,它们之间有继承关系,写一下:
class A {
public:
virtual void display();
virtual void func_A();
protected:
int m_a;
};
class B :public A{
public:
virtual void display();
virtual void func_B();
protected:
int m_b;
};
class C :public A, public B{
public:
virtual void display();
virtual void func_C();
protected:
int m_c;
};
那么,每个类的对象模型如下所示:
二、RTTI(Runtime Type Identification)——运行时类型识别
一般情况下,在编译期间就能确定一个表达式的类型,但是当存在多态时,有些表达式的类型在编译期间就无法确定了,必须等到程序运行后根据实际的环境来确定。
根据前面讲过的知识,C++ 的对象内存模型主要包含了以下几个方面的内容:
- 如果没有虚函数也没有虚继承,那么对象内存模型中只有成员变量。
- 如果类包含了虚函数,那么会额外添加一个虚函数表,并在对象内存中插入一个指针,指向这个虚函数表
- 如果类包含了虚继承,那么会额外添加一个虚基类表,并在对象内存中插入一个指针,指向这个虚基类表。
现在我们再补充一下,在虚函数表的前面,其实还有一个指向type_info对象的指针,以帮助程序在运行时获取对象的类型信息。那么什么是type_info对象呢?
class type_info {
public:
virtual ~type_info();
int operator==(const type_info& rhs) const;
int operator!=(const type_info& rhs) const;
int before(const type_info& rhs) const;
const char* name() const;
const char* raw_name() const;
private:
void *_m_data;
char _m_d_name[1];
type_info(const type_info& rhs);
type_info& operator=(const type_info& rhs);
};
- const char* name() const:返回一个能表示类型名称的字符串。但是C++标准并没有规定这个字符串是什么形式的,例如对于上面的objInfo.name()语句,VC/VS 下返回“class Base”,但 GCC 下返回“4Base”。
- bool before (const type_info& rhs) const:判断一个类型是否位于另一个类型的前面,rhs 参数是一个 type_info 对象的引用。但是C++标准并没有规定类型的排列顺序,不同的编译器有不同的排列规则,程序员也可以自定义。要特别注意的是,这个排列顺序和继承顺序没有关系,基类并不一定位于派生类的前面。
- bool operator== (const type_info& rhs) const:重载运算符“==”,判断两个类型是否相同,rhs 参数是一个 type_info 对象的引用。
- bool operator!= (const type_info& rhs) const:重载运算符“!=”,判断两个类型是否不同,rhs 参数是一个 type_info 对象的引用。
为了深刻理解RTTI和type_info机制,我们来写一个例子:
#include <iostream>
using namespace std;
class Base{
public:
virtual void func();
protected:
int m_a;
int m_b;
};
void Base::func(){ cout<<"Base"<<endl; }
class Derived: public Base{
public:
void func();
private:
int m_c;
};
void Derived::func(){ cout<<"Derived"<<endl; }
int main(){
Base *p;
int n;
cin>>n;
if(n <= 100){
p = new Base();
}else{
p = new Derived();
}
cout<<typeid(*p).name()<<endl;
return 0;
}
这个例子里面的虚函数表应该是这样的:
编译器会在虚函数表 vftable 的开头插入一个指针,指向当前类对应的 type_info 对象。当程序在运行阶段获取类型信息时,可以通过对象指针 p 找到虚函数表指针 vfptr,再通过 vfptr 找到 type_info 对象的指针,进而取得类型信息。虽然这么做会消耗资源,但也是不得已而为之。
这种在程序运行后确定对象的类型信息的机制称为运行时类型识别(Run-Time Type Identification,RTTI)。在 C++ 中,只有类中包含了虚函数时才会启用 RTTI 机制,其他所有情况都可以在编译阶段确定类型信息。
三、静态绑定、动态绑定与多态
我们知道,函数调用实际上是执行函数体中的代码。函数体是内存中的一个代码段,函数名就表示该代码段的首地址,函数执行时就从这里开始。说得简单一点,就是必须要知道函数的入口地址,才能成功调用函数。
找到函数名对应的地址,然后将函数调用处用该地址替换,这称为函数绑定。 在编译期间(包括链接期间)就能找到函数名对应的地址,完成函数的绑定,程序运行后直接使用这个地址即可。这称为静态绑定(Static binding)。但是有时候在编译期间想尽所有办法都不能确定使用哪个函数,必须要等到程序运行后根据具体的环境或者用户操作才能决定。这称为动态绑定(dynamic binding)。
在多态的情况下,特别是虚函数,编译器常常不能在编译时期即时知道它指向的类型,所以干脆采用动态绑定。
四、继承链
在 C++ 中,除了 typeid 运算符,dynamic_cast 运算符和异常处理也依赖于 RTTI 机制,并且要能够通过派生类获取基类的信息,或者说要能够判断一个类是否是另一个类的基类,这样上节讲到的内存模型就不够用了,我们必须要在基类和派生类之间再增加一条
绳索
,把它们
连接
起来,形成一条通路,让程序在各个对象之间
游走
。在面向对象的编程语言中,我们称此为继承链(Inheritance Chain)。
下面这个例子看起来简单,但是它的继承模型很复杂:
class A{
protected:
int a1;
public:
virtual int A_virt1();
virtual int A_virt2();
static void A_static1();
void A_simple1();
};
class B{
protected:
int b1;
int b2;
public:
virtual int B_virt1();
virtual int B_virt2();
};
class C: public A, public B{
protected:
int c1;
public:
virtual int A_virt2();
virtual int B_virt2();
};
关于多态的部分到这里就完结了,撒花~
(如有转载请注明作者与出处,欢迎建议和讨论,thanks)