虚函数与构造函数析构函数

来源:互联网 发布:淘宝企业店铺升级流程 编辑:程序博客网 时间:2024/05/29 02:49

问题提出:

1、为什么构造函数不能为虚函数?

2、为什么构造函数、析构函数调用虚函数没有多态效果?

3、什么情况下需要把析构函数声明为虚函数?

本章结论:

1、构造函数不能为虚函数,且构造函数、析构函数中直接调用的虚函数为静态联编,即没有多态效果;间接调用的虚函数需要查找虚函数表

2、通过基类指针删除派生类对象,如果派生类对象析构函数为实函数,则只会调用基类的构造函数

首先,我们先对C++的多态机制做一个介绍。

假设有A,B两个类,代码如下:

class A{public:int data_A;A(){ // code }void rf1(){ // code }void rf2(){ // code }virtual void vf1(){ // code }virtual void vf2(){ // code }}class B : public A{
public:int data_B;B(){ // code }void rf1(){ // code }virtual void vf1(){ // code }virtual void vf3(){ // code }}

每个含有虚函数的类都会有一个虚函数表(多继承可能会有多个,这里不做讨论),A与B的虚函数表如 图1 ,B类对象在内存中的存储如 图2(其中vtptr是一个指向虚表的指针,data_A区域用来存放A的数据成员,data_B区域用来存放B的数据成员)。当通过指针或者对象句柄调用虚函数时,首先查看指针所指向的对象中虚表指针的值,然后通过虚表指针找到相应的虚函数表,最后在虚函数表中找到相应的虚函数的地址,再对虚函数进行调用。这就是C++多态的机制,所以,如果一个基类指针指向派生类对象,通过指针调用虚函数时,调用的是派生类的虚函数,因为B类对象的虚表指针指向的是B的虚函数表。

那么为什么基类构造函数中调用虚函数调用的却还是基类的呢?


接下来我们来讨论下一个B类对象,到底是如何被创建的

(这也是纠结了个人很久,经过不断地进行实验探索,总结出来的,不一定正确啊,但至少可以解释目前我遇到的所有疑惑,欢迎评论)

个人认为,创建一个B类对象,可以分为以下几个过程:

1、给对象分配内存(内存分配情况如 图2 所示)

2、由系统自动调用B类的构造函数(仅调用,但不会立即执行函数体中的代码)

3、B类的构造函数调用A类的构造函数(这部分是隐藏行为,由编译器自动加进去的。我们经常所说的先调用基类的构造函数再调用派生类的构造函数,个人认为是不正确的)

4、A的构造函数对虚表指针 vtptr 进行初始化,让其指向A的虚表,然后执行A的函数体,退出

5、B的构造函数初始化虚表指针,让其指向B的虚表,然后执行B的函数体(从第2步开始调用B的构造函数到这里,其实中间发生了很多事),退出

6、对象创建完成

上面的理论有一些是可以通过实验进行验证的。第4步,在A的构造函数的函数体中,通过虚表指针vtptr,找到其指向的虚表,取出虚表中的第一个函数指针,通过函数指针进行函数调用,结果证明调用的A::vf1(),也就是说,此时的虚表指针是指向虚表A的。用同样的方法也验证了第5步,当B的构造函数的函数体执行时,虚表指针是指向虚表B的。

验证代码如下:

class A{public:int da;A(){ cout << "A() Begin" << endl;int* object_ptr = (int*)this; // 对象的起始地址,实际指向虚表指针int* vtable_ptr = (int*)*object_ptr;// 虚表指针,指向虚函数表cout << "object_ptr = " << object_ptr << " vtable_ptr = " << vtable_ptr << endl;void (*func_ptr)(void);// 定义一个 void fname(void) 类型的函数指针func_ptr = (void (*)(void))vtable_ptr[0];// 赋以虚函数表中第0个函数指针(*func_ptr)();// 通过函数指针调用函数func_ptr = (void (*)(void))vtable_ptr[1];// 赋以虚函数表中第1个函数指针(*func_ptr)();cout << endl << "A() End" << endl << endl;}virtual void vf1(){ cout << " vf1():A "; }virtual void vf2(){ cout << " vf2():A "; }void rf1() { cout << " rf1():A ";}void rf2() { cout << " rf2():A ";}};class B : public A{public:int db;B(){cout << "B() Begin" << endl;int* object_ptr = (int*)this; int* vtable_ptr = (int*)*object_ptr;cout << "object_ptr = " << object_ptr << " vtable_ptr = " << vtable_ptr << endl;void (*func_ptr)(void);func_ptr = (void (*)(void))vtable_ptr[0];(*func_ptr)();func_ptr = (void (*)(void))vtable_ptr[1];(*func_ptr)();func_ptr = (void (*)(void))vtable_ptr[2];(*func_ptr)();cout << endl << "B() End" << endl;}virtual void vf1(){ cout << " vf1():B " ;}virtual void vf3(){ cout << " vf3():B " ;}void rf1(){ cout << "rf1():B" << endl;}};int main(){A* pa = new B;return 0;}
输出结果如下:



上面的理论能够很好地说明为什么构造函数不能为虚函数,因为虚表指针的初始化是由构造函数完成的,如果构造函数可以为虚函数,那么调用构造函数时,通过虚表指针查找虚函数表,但此时虚表指针还不知道指向什么地方,就会发生意想不到的结果。

也许你以为上面的理论能够解释为什么构造函数中调用虚函数没有多态效果,的确,在执行A的构造函数体时,虚表指针指向的是A的虚表,所以调用虚函数也是在虚表A中查找,当然调用的是A的。没错,上面的理论能够解释这一问题。但是,接下来的实验却又产生了新的问题。

且看下面的代码

int vtp;class A{public:int da;A(){ vtp = *(int*)this;// 保存虚表指针(此时是A的虚表地址)到vtpvf1();}virtual void vf1(){ cout << " vf1():A " << endl;}virtual void vf2(){ cout << " vf2():A" << endl;}void rf()  { cout << "rf():A" << endl;}};class B : public A{public:int db;B(){*(int*)this = vtp;// 把保存到vtp的A的虚表地址赋给虚表指针vf1();}virtual void vf1(){ cout << " vf1():B " << endl;}virtual void vf2(){ cout << " vf2():B" << endl;}void rf(){ cout << "rf():B" << endl;}};int main(){A* pa = new B;        pa -> vf1();        delete pa;}

输出结果为:


是不是很奇怪,从第2行的输出可以看出B的构造函数中调用的虚函数居然不是A类的,可是此时虚表指针是指向虚表A的啊?第3行的结果在意料之中,的确调用的是A的虚函数

为什么会出现这种情况呢?

个人认为,构造函数直接调用的虚函数是静态联编的,不需要通过虚表指针查找虚表(能力有限,不懂汇编,所以无法验证)

但是,不是被构造函数直接调用的虚函数,比如A()调用vf1(),vf1()中又调用vf2(),此时,vf2()是要通过虚表查找的,属于动态联编

验证代码与实验结果如下:

int vtp;class A{public:int da;A(){ vtp = *(int*)this;// 保存虚表指针(此时是A的虚表地址)到vtpvf1();}virtual void vf1(){ cout << " vf1():A " ; vf2();}virtual void vf2(){ cout << " vf2():A" << endl;}void rf()  { cout << "rf():A" << endl;}};class B : public A{public:int db;B(){*(int*)this = vtp;// 把保存到vtp的A的虚表地址赋给虚表指针vf1();}virtual void vf1(){ cout << " vf1():B " ; vf2(); }virtual void vf2(){ cout << " vf2():B" << endl;}void rf(){ cout << "rf():B" << endl;}};int main(){A* pa = new B;        pa -> vf1();        delete pa;}
输出结果如下:



虚析构函数

先看下面两段代码并比较输出结果的差异

class A{public:~A(){ cout << "~A()" << endl;}};class B : public A{public:~B(){ cout << "~B()" << endl;}};int main(){A* pa = new B;delete pa;return 0;}
输出结果:


class A{public:virtual ~A(){ cout << "~A()" << endl;}};class B : public A{public:~B(){ cout << "~B()" << endl;}};int main(){A* pa = new B;delete pa;return 0;}
输出结果:


当通过基类指针删除一个派生类对象时,如果基类的析构函数没有声明为virtual,则只调用了基类的析构函数。假想如果派生类动态申请了内存空间,在析构函数中释放这些空间,但由于派生类的析构函数都没有被调用,就会千万内存泄露。如果基类的析构函数声明为virtual,由上面的结果可以知道,派生类的析构函数就会被调用。

刚开始在这里我有一个疑问,当析构函数被声明为virtual时,调用析构函数的确会查找虚表,但是找到的虚函数是~B(),为什么~A()也被执行了呢?

个人认为,与构造函数类似,派生类的析构函数也会隐式地调用基类的析构函数,派生类的析构函数由系统自动调用,然后执行派生类析构函数的函数体,析构函数的函数体执行完毕后,再调用基类的析函数。
另外还有一个需要注意的地方,在析构函数中,虚表指针的值也会发生改变的,当B的析构函数体执行时,虚表指针指向的是虚表B(这个已经被验证);当A的析构函数的函数体执行时,虚表指针指向虚表A。由此,我们可以大胆地推测,析构函数执行的流程为:

修改虚表指针 -> 执行函数体 -> 调用基类的析构函数 -> 退出

析构函数调用虚函数

析构函数中调用虚数的情况与构造函数类似,被直接调用的虚函数也是没有多态效果的,可以认为是静态联编,被间接调用的虚函数有多态效果。

总结:

要了解C++多态的机制,就必须清晰地知道创建一个派生类对象的过程是什么样的,由于构造函数,析构函数有很多编译器添加进去的隐藏行为,这使得我们在学习的时候变得十分困难,教材中一般也不会介绍这些。如果能够学会一些反汇编方面的知识,对C/C++的底层实现是很有帮助的,可惜啊,博主不会。

以上只是推测出来的,非摘自权威教材,所以阅后欢迎讨论,共同完善进步

另:附几个可能有用的链接

http://www.cnblogs.com/ccdev/archive/2012/12/25/2832268.html

http://codepeak.iteye.com/blog/766529

http://blog.csdn.net/haoel/article/details/1948051?reload#comments

http://blog.csdn.net/apemancsdn/article/details/82606


0 0