分类:
2010-07-31 15:59:33
http://saturnman.blog.163.com/blog/static/557611201062910383394/
摘要
本文就C++中虚函数的实现细节做一个说明,并就一些使用中常见的错误和注意事项做一个解析,我不会只告诉你不要怎样怎样做,而不给出原因,我尽可能从实现细节来说明为何这样做是不行的,或是不合适的。并尽可能给出直观的代码来说明问题。
一、虚函数是如何实现的,
C++中虚函数可以使用基类指针来调用子类的相关函数,实现所谓的多态这给程序编写带来了极大的方便,但是C++语言与C语言的目标是一致的,尽可能的提高程序的运行效率,从不浪费无谓的计算资源。这就给语言设计者提出了更高的要求,也给语言的使用者提出了挑战,如果使用不当,将有十分奇怪异bug出现,这些bug难以追踪且行为难以理解。因此知道一些底层细节将为开发带来十分大的好处。
先来说一下一般的函数是如何实现的,函数在编译后只剩来了2进 制的地址,原来函数名之类的信息将全部丢失,编译器做的只是在生在二进制代码时按照函数数的参数要求把参数放到指定的位置(寄存器或是堆栈)中,之后把程 序的指令指针指向函数入口地址就可以了,既然参数是编译器按归定的方式放入的,函数只要按格式取走参数运行就可以了,运行后把指定的结果返回。但是试想对 于一个其于对象指针的函数调用,编译时编译器只知道指针是什么类型,并不知指针指向是的是何类型,如下:
Base* b = new Derived();
指针b的类型是Base,b所指的类型编译器并不知是什么,在这里你可能会说编译器知道所指内存的类型是什么,但是不要忘了b所指的类型是可以在运行时改变的,单由简单的b自身的信息是不知如下代码应该调用哪个函数。
b->Func();
对于这种情况的调用编译器是如何做的呢?先来看看Func()是普通函数时的情景,代码如下。
#include
using namespace std;
class Base
{
public:
void Func()
{
cout<<"Func in Base::"<<endl;
}
};
class Derived : public Base
{
public:
void Func()
{
cout<<"Func in Derived::"<<endl;
}
};
int main()
{
Derived* d = new Derived();
Base* b = d;
d->Func(); //call Func with Derived*
b->Func(); //call Func with Base*
delete d; //clear up
return 0;
}
结果如下:
从结果可以看出,尽管指针指向的同一地址,但是最终的函数调用是依据指针本身的类型来调用相应的函数的,而非指针指向的内存的类型。
如果要实现使用指针所指向的内存对象的类型来调用该如何实现呢,我把可以把需要有这种特性的函数归为一类(毕竟需要有这样功能的函数的数目是不多的),我们把这些函数放入一个表中,所有的对象都有这样一个表,就叫做表V吧,这样如果编译器发表类似d->Func()这样的函数调用时,就先查看我们的函数是不是在表V中,如果是在表V中 的话,就不把函数的代码联接到这时,而是把表中的函数项的偏移地址放在这里,通过一个间接的办法来调用相应的函数,这个地方再仔细说一下,既然同一个类所 有对像的函数表是一样的,我们可以静态的生成这张表,把这表存在最终编译好的二进制代码中,而在对象中只存一个指向这个全局表的一个指针,如果我们把这个 指针都放在对像内存的首地址上,那么大家都是这样做,那么编译器可以少做很多工作,它只要把相应的函数调用上放上一个对象的首地址加下相应的偏移动量就可 以了。如下图。
Base::V table | |
Function name |
address |
Func1(); |
Base::Func1() |
Func2(); |
Base::Func2() |
Func3(); |
Base::Func3() |
Derived::V table | |
Function name |
address |
Func1(); |
Derived::Func1(); |
Func2(); |
Derived::Func2(); |
Func3(); |
Derived::Func3(); |
Object of class Base:: | |
Pointer |
Ptr=address(Base::V table) |
Var var1 |
int |
Var var2 |
double |
Var var3 |
int |
Object of class Derived::public Base | |
Pointer |
Ptr=address(Derived::V table) |
Var var1(derived from Base) |
int |
Var var2(derived from Base) |
double |
Var var3(derived from Base) |
Int |
Var var4(derived from Base) |
double |
可以认为虚表是类一个全局静态变量,所有类对象同享此同一变量。
由于在编译时并不知道对象在运行时会存在什么内存地址上,因此在编译期不能判断是调用的哪个函数,因此要使用偏移量来在运行时计算地址,比如这样一种情况:
Base* b = new Derived();
b->Func1();
编译时不能判断是哪个Func1应该被调用,但是可以产生如下的类似代码。
Pointer_type address = this->Ptr+2;
在运行时通过相对于Ptr指针的偏移来计算函数入口,这样就可以在运行时调用应该调用的函数了。这就是C++虚函数的工作原理通过3级指针来在运行时根据内存中存放的类型来调用正解的函数,类中只有应明为virtual语义的函数才会到使用虚表机制来存储。
二、 探密虚函数
如下代码:
#include
#include
#include
#include
using namespace std;
//Fonction to show binary info of an object
typedef unsigned char* pointer_type;
void show_byte(pointer_type pointer,int length);
void show_byte(pointer_type pointer,int length)
{
cout<<"address of object:"<<hex<<(int)pointer<<endl;
cout<<"var size in byte:"<<dec<<length<<endl;
for(int i=0;i<length;++i)
{
cout<<"|address: 0x"<<hex<<(int)(pointer+i)<<" | value:0x"<<(int)(*(pointer+i))<<" |"<<endl;
}
cout<<endl;
}
class Base
{
public:
Base()
{
cout<<"In Base::constructor."<<endl;
cout<<"Constructing "<<*m_pObj_Name<<endl;
cout<<"binary info:"<<endl;
show_byte((pointer_type)this,sizeof(*this));
}
Base(const string& name):
flag_var(0x12345678),
m_pObj_Name(new string(name))
{
cout<<"In Base::constructor."<<endl;
cout<<"Constructing "<<*m_pObj_Name<<endl;
cout<<"binary info:"<<endl;
show_byte((pointer_type)this,sizeof(*this));
}
void Util()
{
virtual_func1();
}
virtual ~Base() {delete m_pObj_Name;}
const string& GetName()
{
return *(this->m_pObj_Name);
}
private:
int flag_var;
string* m_pObj_Name;
virtual void pure_virtual_1() = 0;//pure virtual function
virtual void pure_virtual_2() = 0;//pure virtual function
virtual void virtual_func1() //normal virtual function
{
cout<<"virutal function in Base."<<endl;
}
};
class Derived: public Base
{
public:
Derived(string name):
Base(name)
{
cout<<"In Derived::constructor."<<endl;
cout<<"binary info:"<<endl;
show_byte((pointer_type)this,sizeof(*this));
}
void Util()
{
virtual_func1();
}
virtual ~Derived() {}
private:
virtual void pure_virtual_1() { printf("pure_virtual_1() in drived\n"); }
virtual void pure_virtual_2() { printf("pure_virtual_2() in drived\n"); }
virtual void virtual_func1()
{
cout<<"virutal function in Derived."<<endl;
}
};
int main()
{
Derived* d1 = new Derived("Derived_1");
Base* b1 = d1;
cout<<"After construct:"<<d1->GetName()<<endl;
show_byte((pointer_type)d1,sizeof(d1));
d1->Util();
b1->Util();
Derived* d2 = new Derived("Derived_2");
Base* b2 = d2;
cout<<"After construct:"<<d2->GetName()<<endl;
show_byte((pointer_type)d2,sizeof(d2));
d2->Util();
b2->Util();
delete d1;
delete d2;
return 0;
}
如下是运行的结果:
In Base::constructor.
Constructing Derived_1
binary info:
address of object:5a3ef0
var size in byte:12
|address: 0x5a3ef0 | value:0x94 |
|address: 0x5a3ef1 | value:0xa2 |
|address: 0x5a3ef2 | value:0x16 |
|address: 0x5a3ef3 | value:0x1 |
|address: 0x5a3ef4 | value:0x78 |
|address: 0x5a3ef5 | value:0x56 |
|address: 0x5a3ef6 | value:0x34 |
|address: 0x5a3ef7 | value:0x12 |
|address: 0x5a3ef8 | value:0x8 |
|address: 0x5a3ef9 | value:0x3f |
|address: 0x5a3efa | value:0x5a |
|address: 0x5a3efb | value:0x0 |
In Derived::constructor.
binary info:
address of object:5a3ef0
var size in byte:12
|address: 0x5a3ef0 | value:0x58 |
|address: 0x5a3ef1 | value:0xa2 |
|address: 0x5a3ef2 | value:0x16 |
|address: 0x5a3ef3 | value:0x1 |
|address: 0x5a3ef4 | value:0x78 |
|address: 0x5a3ef5 | value:0x56 |
|address: 0x5a3ef6 | value:0x34 |
|address: 0x5a3ef7 | value:0x12 |
|address: 0x5a3ef8 | value:0x8 |
|address: 0x5a3ef9 | value:0x3f |
|address: 0x5a3efa | value:0x5a |
|address: 0x5a3efb | value:0x0 |
After construct:Derived_1
address of object:5a3ef0
var size in byte:4
|address: 0x5a3ef0 | value:0x58 |
|address: 0x5a3ef1 | value:0xa2 |
|address: 0x5a3ef2 | value:0x16 |
|address: 0x5a3ef3 | value:0x1 |
virutal function in Derived.
virutal function in Derived.
In Base::constructor.
Constructing Derived_2
binary info:
address of object:5a3ba0
var size in byte:12
|address: 0x5a3ba0 | value:0x94 |
|address: 0x5a3ba1 | value:0xa2 |
|address: 0x5a3ba2 | value:0x16 |
|address: 0x5a3ba3 | value:0x1 |
|address: 0x5a3ba4 | value:0x78 |
|address: 0x5a3ba5 | value:0x56 |
|address: 0x5a3ba6 | value:0x34 |
|address: 0x5a3ba7 | value:0x12 |
|address: 0x5a3ba8 | value:0xb8 |
|address: 0x5a3ba9 | value:0x3b |
|address: 0x5a3baa | value:0x5a |
|address: 0x5a3bab | value:0x0 |
In Derived::constructor.
binary info:
address of object:5a3ba0
var size in byte:12
|address: 0x5a3ba0 | value:0x58 |
|address: 0x5a3ba1 | value:0xa2 |
|address: 0x5a3ba2 | value:0x16 |
|address: 0x5a3ba3 | value:0x1 |
|address: 0x5a3ba4 | value:0x78 |
|address: 0x5a3ba5 | value:0x56 |
|address: 0x5a3ba6 | value:0x34 |
|address: 0x5a3ba7 | value:0x12 |
|address: 0x5a3ba8 | value:0xb8 |
|address: 0x5a3ba9 | value:0x3b |
|address: 0x5a3baa | value:0x5a |
|address: 0x5a3bab | value:0x0 |
After construct:Derived_2
address of object:5a3ba0
var size in byte:4
|address: 0x5a3ba0 | value:0x58 |
|address: 0x5a3ba1 | value:0xa2 |
|address: 0x5a3ba2 | value:0x16 |
|address: 0x5a3ba3 | value:0x1 |
virutal function in Derived.
virutal function in Derived.
下面来分析一下结果,show_byte是一个辅助函数,它可以由指针和大小来显示内存块的内容,用它来查看各个阶段时类对象的内存内容,每个类有一个flag_var的标志整数变量,这个变量的值初始化一个十分特别的值,并且在基类的初始化列表中初始化,是希望我们能在查看内存时看到它并在类构造函数被调用之前其已初始化完毕,我们在内存中看到了它,如下:
|address: 0x5a3ba4 | value:0x78 |
|address: 0x5a3ba5 | value:0x56 |
|address: 0x5a3ba6 | value:0x34 |
|address: 0x5a3ba7 | value:0x12 |
如下我们看结果:
In Base::constructor.
Constructing Derived_1
binary info:
address of object:5a3ef0
var size in byte:12
|address: 0x5a3ef0 | value:0x94 | //pointer to Vtable
|address: 0x5a3ef1 | value:0xa2 |
|address: 0x5a3ef2 | value:0x16 |
|address: 0x5a3ef3 | value:0x1 |
|address: 0x5a3ef4 | value:0x78 | //flag_var
|address: 0x5a3ef5 | value:0x56 |
|address: 0x5a3ef6 | value:0x34 |
|address: 0x5a3ef7 | value:0x12 |
|address: 0x5a3ef8 | value:0x8 | //string*
|address: 0x5a3ef9 | value:0x3f |
|address: 0x5a3efa | value:0x5a |
|address: 0x5a3efb | value:0x0 |
类Derived的构造函数运行之前,类Base的构造函数前运行,上面是在Base运行最后时的类对象的内存情况,下面是Base构造函数运行完之后Derived 的构造函数运行最后的对象内存情况:
In Derived::constructor.
binary info:
address of object:5a3ef0
var size in byte:12
|address: 0x5a3ef0 | value:0x58 | //pointer to Vtable
|address: 0x5a3ef1 | value:0xa2 |
|address: 0x5a3ef2 | value:0x16 |
|address: 0x5a3ef3 | value:0x1 |
|address: 0x5a3ef4 | value:0x78 | //flag_var
|address: 0x5a3ef5 | value:0x56 |
|address: 0x5a3ef6 | value:0x34 |
|address: 0x5a3ef7 | value:0x12 |
|address: 0x5a3ef8 | value:0x8 | //string*
|address: 0x5a3ef9 | value:0x3f |
|address: 0x5a3efa | value:0x5a |
|address: 0x5a3efb | value:0x0 |
绿色的还是flag_var,请注意红色的部分,我们没有在构造函数中有改变类内存的代码,但是类的内存内容改变了,可以想象是编译器为我们在构造函数之前加入了加入了改变Vtable 指针的代码,在类Base的构造函数中时指向的类Base的全局虚函数表,在Derived的构造函数中指向类Derived的全局虚函数表。一天始我也百思不得其解为什么会有这样的情况,因为这样的话就会允许这样的代码编译通过,但运行时会出错退出。
#include
using namespace std;
class Base
{
public:
Base() { Init(); }
virtual ~Base() {}
void Init() { pure_virtual_func(); }
private:
virtual void pure_virtual_func() = 0;
};
class Derived: public Base
{
public:
Derived() { cout<<"Drived constructor."<<endl; }
virtual ~Derived() {}
private:
virtual void pure_virtual_func() { cout<<"pure_virtual_func() in derived."<<endl; }
};
int main()
{
Derived d;
return 0;
}
上面的代码实际上犯了C++的一大编码错误,不要在构造函数中调用类的虚函数,
下面来分析一下错误来源:
以下内容按时间顺序进行:
1-> Base 的vtable指针进行构建,纯虚函数指针指向一全局处理函数,此函数的功能是被调用时打出错误信息后把程序终止。
2-> Base 的构造函数运行,所有的类虚函数调用都在上面构造的指针指向的Vtable中查找
3-> Derived 的Vtable指针构建(这个Vtable指针就是上面的Vtable指针,一个对象只有一个Vtable指针),虚函数调用的地址从Vtable查找(Derived的Vtable与Base的Vtable不是同一个)
4-> Derived 的构造函数代码运行,所有的类虚函数调用都在上面构造的指针指向的Vtablke中查找。
好了,问题出在第二步,Base 的构造函数居然间接调用纯虚函数,这个就出错了。编译器为什么不提示错误呢,C++为了运行时效率和编译效率,Vtable指针动态逐层构造,从上到下。鬼才知你的函数实现没有,这种间接的事情编译器是无从查起的。你可以在C#中试相似的代码,它不是逐层构造的。在类的构造函数中调用虚函数是不合理的,因为如果调用的是话就是使用的基类的虚表进行调用,但是使用的指针并是子类的this指针,这与虚函数的调用一致性相违背,并且如果基类的虚函数为纯虚函数,这种调用不存在的函数的行为是求定义的。但是如果编译期直接把子类的虚表指针定死为子类虚表指针,那么在基类中构造函数中调用虚函数将调用到子类的虚函数,但是此时的环境是在父类的构造函数中,子类还没有构造完成,这时如果子类的虚函数使用未初始化的成员变量,将使行为更加危险,因些尽管C++本身没有规定不可以在构造函数中调用虚函数,但是这种行为是绝对不推荐的,因为它会造成程序难以预料的结果,尤其在多线程程序中,这种bug十分难以调试.
三、使用多态时要注意些什么
1. 对于使用多态的程序,甚至是使用继承的程序,要把类的析构函数声名为virtual,因为delete 本身也是根据指针类型来调用相应的构构函数的,如果在析构为virtual,则正确的析构函数将被调用(如果你写了正解的析构函数的话),因为这样会根据内存中对象的类型来析构对象。
2. 不要在类的构造函数和析构函数中直接使用类的虚函数,也不要间接使用,所谓间接就是调用普通成员函数,但是普通成员函数又调用类虚函数,因为不能确保正解的函数被调用并且也不能保证类的成员变量在这两种函数中处在可用状态。
Ref:
[1]Never Call Virtual Functions during Construction or Destruction
[2] C++,The draft international standard, N3092
[3]多态(Polymorphism)的实现机制(上)--C++篇
http://hi.baidu.com/daping_zhang/blog/item/e87163d06c42818fa0ec9cfc.html