C++ virtual函数 实现机制

38 篇文章 10 订阅

最近一直在用C,C++不熟了,本系列旨在复习那尘封已久的C++知识,顺便联想个、倒推一些相关知识点,希望能够系统地复习一下,融会贯通。串的点有点多,所以会稍微乱点。

WARNING:个人笔记,记录学习过程,参考资料和时间跨度也较大,前边的假设很多会被后边推翻,所以会有暂时性的错误。一定要看完,不然别怪我误导人哦偷笑



虚函数基本特性,派生类中可以覆盖或隐藏基类的实现,同参数列表的,先会覆盖,用基类指针可以选出对象所对应的类中虚函数的实现。

不同参数列表的,会发生类似重写的隐藏没触发虚函数的机制,基类只能用基类的虚函数实现,即便用基类指针指向派生类对象,也不能调用派生类的虚函数实现。


下面主要讨论虚函数的同参数列表的覆盖问题。


首先,想看看virtual这个属性是怎么向下(子孙)继承的,又能不能从某一代起向下取消掉

测试代码如下:

#include<iostream>
using namespace std;
class A
{
public:
        virtual void printSelf(){cout << "A" << endl;}
private:
        int test;
};
class B : public A
{
public:
        void printSelf(){cout << "B" << endl;}
        void printSelf2(){cout << "B2" << endl;}
private:
        int test;
};
class C : public B
{
public:
        void printSelf(){cout << "C" << endl;}
        void printSelf2(){cout << "C2" << endl;}
private:
        int test;
};
int main(){
        A a;
        B b;
        C c;
        A *Ptr;
        B *PtrB;

        //base class ptr
        Ptr = &a;
        Ptr->printSelf();
        Ptr = &b;
        Ptr->printSelf();
        Ptr = &c;
        Ptr->printSelf();

        PtrB = &b;
        PtrB->printSelf();
        PtrB->printSelf2();
        PtrB = &c;
        PtrB->printSelf();
        PtrB->printSelf2();


}

经测试,不管B的函数加不加“virtual",都是能 编译过,但是 编译都通过归都通过,两者是不是一个意思呢?

运行结果:

A
B
C
B
B2
C
B2


实测发现,因为A里边加过”virtual“,所以B写不写virtual,结果全都一样。

通过指针Ptr和打印结果可以发现,无论B类中写不写virtual,用基类的指针都能确定对象c的类C。

并且,通过PtrB可以看到,这个性质能延续下来,也就是我想在B类里加”virtual“的字面意思——B类指针也能准确区分B和C类的对象b和c。

PS:虚函数的多态,引用也可以达到和指针同样的效果。


通过对照函数可以看到printSelf2就没有多态的性质。那么问题来了:printSelf的多态继承下来就再没办法取消了吗?没法变成printSelf2那样的”不虚“的函数了吗?不能洗白了?

有人说C++的特性是这样的,继承嘛,不能取消。

顺便,找到以前C++ primer读书笔记:派生类不能改变virtual现状,基类函数没有virtual的时候派生类不能声明成有,基类有virtual的时候派生类声明不声明都一样。 但是要补充说明一下,基类没有virtual,其实是能声明称有的,只不过这个“有”,指的是从B往后是虚的(其实等于重新声明了,另立门户,在原名称上加个virtual和用新名字声明虚函数是一样的),而不是让A有,也就是用A类指针的时候,无论对象是什么类的,都只认A类的实现,没有多态特性。


但是感觉很不灵活啊,所谓重写就行?我在B里边不是给了printSelf()的新实现了吗?只能改实现内容,不能改多态属性?假如我实现一个类继承了一个封装好的类A,或者说C继承了B,看不到它上层的A类实现有virtual这么一句,那么我现在不想要多态这个属性,我也以为没有,结果不就出错了?

所以是从用法上杜绝错误?假设基类都是不可靠的,想用虚函数,都从新类开始声明虚函数?

其实这是应该的,父亲和儿子到底什么关系,和爷爷没关系,都是透明的。如果你只是想使用父子之间的特性,那你也没有爷爷辈的指针,所以也无所谓错不错了。

关键词:virtual实现多态的基本方法和特性&多态性的继承。

======================================================================================================================


那么话说回来,虚函数又是怎样实现的,怎么就能让一个基类指针区分出指向的对象到底是属于基类还是派生类?

下边学下virtual的实现机制:

首先看一下virtual函数的size

#include<iostream>
using namespace std;
class A
{
public:
//      virtual void printSelf(){cout << "A" << endl;}
//      virtual void printSelf();//not allowed
        void printSelf();
//      virtual void printSelf(){}
//      virtual void printSelf() = 0;
};
打印出A的 size

只有一个普通函数的类,大小是1.

有虚函数的类,无论是哪种形式,虚函数实现的、未实现的,大小都是4(32位)。也就是虚函数要占一个指针空间。

这个空间占下来是干什么的。

其实,virtual的成员函数,在编译阶段基类指针是不知道它是属于哪个类的,直到运行阶段(非全局的类对象的初始化也在运行阶段啊,这些都是运行到那句才算的,可以理解)才确定,所以需要一些标记。


查阅资料发现,C++有静多态和动多态,virtual实现的主要是动多态,所以上边的问题也就比较清晰了,都是运行时决定的。

这里用到了VTABLE虚函数表,当编译器发现一个类有虚函数时就会建立这样一个表(C++ primer有介绍?我书丢了不记得),一个类对应一个VTABLE,和对象无关。

基类和派生类都有自己的VTABLE,并且排列一样!是真的吗?这样做有什么用?如果都一样,派生类指针岂不是能指向基类从而识别基类的虚函数实现了?当然只是原则上这样。派生类指针不允许指向基类对象是从语法上就排斥掉的,所以无从验证!那么我如果用地址强指呢?有没有可能验证这个问题?

测试代码如下:

#include<iostream>
using namespace std;
class A
{
public:
        virtual void printSelf(){cout << "A" << endl;}
private:
        int test;
};
class B : public A
{
public:
        void printSelf(){cout << "B" << endl;}
private:
        int test;
};
class C : public B
{
public:
        void printSelf(){cout << "C" << endl;}
private:
        int test;
};

int main(){
        A a;
        B b;
        C c;
        A *Ptr;
        B *PtrB;
        Ptr = &a;
        PtrB = (B*)Ptr;
        PtrB->printSelf();
}
B类指针强指A类对象是语法上不允许,但是可以通过A类指针转换一下,但是这个例子似乎只能说明,B类指针指到了A类对象。那既然指到了A类对象,因为指针自身又不含信息,所以也许用的也是A类的VTABLE,所以识别出了A类对象,怎样让B类指针指向B类对象再识别出A类对象呢?这本来就是个悖论,正常用也没这样的,你指向的是个B类对象,怎么再把你当成A类对象呢?

这个疑问似乎是不正确的,如果用一个A类指针指向B、C类的对象,那岂不是都是B、C类自己的VTABLE了,那样一来就没有这个机制了。

不过如果“因为指针自身又不含信息,所以也许用的也是A类的VTABLE”这个假设成立,倒也解开了“为什么每个类都要有一个和基类相同的VTABLE”了,因为这样指针指到哪个对象都能通过这个对象对应的类的VTABLE找到想要的信息。(其实这个说法是不对的,不是相同的vtable!后边会补充!


继续参考别人的信息:生成一个对象会有一个vptr,vptr指向本类的VTABLE,所以用静态类型是基类的指针或引用在运行的时候,此时对象已经产生,就得到了vptr,也就能区分是用基类还是派生类的函数了。

这就都能说通了,反正指针本身不可能携带那么多信息来识别对象,那么从对象入手,对象带了vptr指针,vptr指向类的函数表VTABLE,这样就知道对象对应哪个类的实现,或者更准确地说,是哪个virtual函数的实现了。


最后,这个“相同的VTABLE”,是指的相同的copy,还是真身,从参考文字上看,像是一份copy。但我感觉就是真身,把这些基类和派生类的一个虚函数统一做一个VTABLE,里边有各个实现,然后每个类的对象用vptr指向该类的实现。这样用基类指针去调用一个对象的虚函数时,直接通过那个vptr来找到对应的函数实现。其实是找到表,然后对应表里的实现。

上一张网上找的图,比较权威应该:

class Base
{
public:
    FunctionPointer *__vptr;
    virtual void function1() {};
    virtual void function2() {};
};
 
class D1: public Base
{
public:
    virtual void function1() {};
};
 
class D2: public Base
{
public:
    virtual void function2() {};
};



这个图把函数实现也扔到类里表示了,和类成员有点混淆,其实可以单放一块,反正是那意思吧,就是说有的没覆盖的,还是用的基类的实现,还得用箭头指回去。覆盖的,动多态的,在派生类(的对象)是指向派生类的实现的。供指针选择。


这张图揭示了一个不变的真理,继承过程中,产生了多个虚表,但是反应到D2类本身,只有一个虚表!那两个虚函数表是Base和D1的!


小结:虚函数的机制——VirtualTable&vPtr

========================================================================================================================

关于内存,类本身对应的VTABLE估计就不占用类的大小(只限于sizeof说的那个大小,实际编译给类分配的空间不知道怎么算)了,对象中的virtual函数占4字节(32位)就是因为那是个vptr指针,指针要占用空间,而普通函数就不占用。类的VTABLE虽然不占用空间,但是sizeof(A)中virtual函数的声明还是要占用4字节的,也许是类指向VTABLE的指针占用了?也许为了和对象保持同步?

用基本类型成员变量来做个对比,静态的、不属于对象的成员变量,在sizeof()是看不出占用空间的,而非静态的、属于对象的成员变量就占用空间,所以sizeof()应该是测算的对象的大小,这也就能说通vptr占用空间,不管类里边用不用vptr,都和对象大小保持同步。

网上找到的参考“首先sizeof 不是函数 只是一个操作符.sizeof a因为a是数组名,当sizeof作用于一个class 、struct时,返回这些类型对象所占字节数。

=========================================================================================================================

最新补充:用了gdb后的结论:

来看一下内存分布吧,代码如下:

#include<iostream>
using namespace std;
class A
{
public:
        virtual void printSelf(){cout << "A" << endl;}
private:
        int i;
};
class B : public A
{
public:
        void printSelf(){cout << "B" << endl;}
        virtual void printSelf2(){cout << "B2" << endl;}
private:
        int i;
};
class C : public B
{
public:
        void printSelf(){cout << "C" << endl;}
        void printSelf2(){cout << "C2" << endl;}
private:
        int i;
};

int main(){
        A a;
        B b;
        C c;
        A *Ptr;
        B *PtrB;
        Ptr = &b;
        Ptr->printSelf();
        Ptr = &c;
        Ptr->printSelf();

        Ptr = &a;
        PtrB = (B*)Ptr;
        PtrB->printSelf();
        cout << "class A's size:" << sizeof(A) << endl;
        cout << "object a's size:" << sizeof(a) << endl;
        cout << "class B's size:" << sizeof(B) << endl;
        cout << "object b's size:" << sizeof(b) << endl;
        cout << "class C's size:" << sizeof(C) << endl;
        cout << "object c's size:" << sizeof(c) << endl;

}
a、b、c分别为三个类的对象

用gdb看

(gdb) info locals
a = {_vptr.A = 0x8048be0, i = 134514304}
c = warning: can't find linker symbol for virtual table for `C' value
{<B> = {<A> = {_vptr.A = 0x80490fc, i = -1073744392}, i = 134515369}, 
  i = 3097936}
PtrB = 0x55fff4
b = {<A> = {_vptr.A = 0x8048bd0, i = 0}, i = 5636084}
Ptr = 0x8048a9b
可以看到那个_vptr指针,每个都指向不同的地址,就是各类对象各自的VTABLE的地址。

另外:通过观察内存可以看出,关于sizeof得到的结果,前边是有误的。

因为派生类也加了一个同名的变量i,虽然隐藏基类的i,但是它们是两个变量,甚至3个,也就是说派生类因为这个i的定义而大了4个字节。而不是因为多声明虚函数。(基础薄弱~派生这块同名变量占用空间应该是累加关系,而不是原样覆盖,同样是“祖宗透明”原理

如果把int i的声明全都去掉,gdb结果如下

(gdb) info locals
a = {_vptr.A = 0x8048be0}
c = {<B> = {<A> = {_vptr.A = 0x55fff4}, <No data fields>}, <No data fields>}
PtrB = 0x55fff4
b = {<A> = {_vptr.A = 0x8048bd0}, <No data fields>}
Ptr = 0x8048a9b
运行输出如下:
class A's size:4
object a's size:4
class B's size:4
object b's size:4
class C's size:4
object c's size:4
可见,无论有几个虚函数(从B类开始增加了一个虚函数),总共只有一个虚表指针,也就是说靠这个指针来找到对象所属的类,而无关于具体是哪个虚函数,是printSelf()还是printSelf2(),函数自有函数的对应, 个人理解是这样。这和前边参考的信息是有冲突的,前边理解的是vptr直达具体实现函数,现在看不是了,因为无论多少个虚函数,一个类就一个vptr。

关于vptr怎么区分类中那么多的虚函数,也许是地址偏移什么的,但是每个类的这个虚函数表就不可能一样了,和别人的“虚函数表全都相同”这一说法也有一些冲突。那个说法肯定是错误的。

小结:无论多少个虚函数,只要有,类就有且只有一个vptr。

但是,那是不算多重派生的前提下!!!下边会提到多重。

=========================================================================================================================


PS:疏漏,这段说的代码没贴出来,是基于上边的例子改的,a2也是一个A类对象。

最后,通过a2的vptr可以看出,同类不同对象,它的vptr也是不一样的(占用空间不就是干这个用的么),这样就不是一个类一个VTABLE了,是一个对象一个,有必要吗?(所以才占了一个指针的空间~)

(gdb) info locals
a = {_vptr.A = 0x8048cd8}
c = {<B> = {<A> = {_vptr.A = 0x0}, <No data fields>}, <No data fields>}
PtrB = 0x55fff4
a2 = {_vptr.A = 0x8048b70}
b = {<A> = {_vptr.A = 0x55fff4}, <No data fields>}
Ptr = 0x8048b7b


另外一个值得注意的细节是,B类指针PtrB的内容和对象b的_vptr.a的内容是一样的,虽然有点巧合,但似乎也证明了vptr在类中是最靠前的?如果这个代表地址的话。(gdb列出来的到底是内容还是地址?我也不熟悉,待查证


下边的输出显示a和a2的_vptr.A是一样的,这个比较符合预期,一个类就一个VTABLE地址,所有对象都指向它。

(gdb) info locals
a = {_vptr.A = 0x8048d60}
c = {<B> = {<A> = {_vptr.A = 0x8048d30}, <No data fields>}, <No data fields>}
PtrB = 0x55fff4
a2 = {_vptr.A = 0x8048d60}
b = {<A> = {_vptr.A = 0x8048d48}, <No data fields>}
Ptr = 0x8048bdb
两例不一样,难道是巧合?不是!程序运行到不同阶段,会有不一样的效果,比如对象还没初始化,里边当然是乱的,或者零地址了。

感受一下过程:

(gdb) info locals
a = warning: can't find linker symbol for virtual table for `A' value
{_vptr.A = 0x80486b0}
b2 = {<A> = {_vptr.A = 0x0}, <No data fields>}
c = {<B> = {<A> = {_vptr.A = 0x8048480}, <No data fields>}, <No data fields>}
PtrB = 0x55fff4
a2 = {_vptr.A = 0x8048bd0}
b = {<A> = {_vptr.A = 0x55fff4}, <No data fields>}
Ptr = 0x8048bdb
(gdb) n
33		A a2;
(gdb) n
34		B b;
(gdb) n
35		B b2;
(gdb) n
36		C c;
(gdb) n
37		printf("address of b is %p\n",&b);
(gdb) n
address of b is 0xbffff5dc
40		Ptr = &b;
(gdb) n
41		printf("content  of Ptr is %p\n",Ptr);
(gdb) info locals
a = {_vptr.A = 0x8048d60}
b2 = {<A> = {_vptr.A = 0x8048d48}, <No data fields>}
c = {<B> = {<A> = {_vptr.A = 0x8048d30}, <No data fields>}, <No data fields>}
PtrB = 0x55fff4
a2 = {_vptr.A = 0x8048d60}
b = {<A> = {_vptr.A = 0x8048d48}, <No data fields>}
Ptr = 0xbffff5dc
(gdb) n
content  of Ptr is 0xbffff5dc
43		Ptr->printSelf();
(gdb) info locals
a = {_vptr.A = 0x8048d60}
b2 = {<A> = {_vptr.A = 0x8048d48}, <No data fields>}
c = {<B> = {<A> = {_vptr.A = 0x8048d30}, <No data fields>}, <No data fields>}
PtrB = 0x55fff4
a2 = {_vptr.A = 0x8048d60}
b = {<A> = {_vptr.A = 0x8048d48}, <No data fields>}
Ptr = 0xbffff5dc
最后,连PtrB和b的巧合也没了。

小结:同类对象的vptr指向是一样的,因为都是一个类实现。vptr也是需要跟随对象初始化的,不是天生就有的。动态嘛,动态!

思考:每个对象都要占用一个指针?能不能用静态来全类共享从而节约空间呢?还是什么条件制约了?不行的!有限的占用0空间(C++是1byte)的一个对象,不能确定他的类型,不能找到对应的vtable,必须有这个指针。

=========================================================================================================================

接下来,看看多重继承的情况内存分布:

class A{
public:
        virtual ~A(){cout <<"A destruction"<<endl;}
        int a;
        void fooA(){}
        virtual void func(){cout <<"A func."<<endl;}
        virtual void funcA(){cout << "funcA."<<endl;}
};
class B{
public:
        virtual ~B(){cout <<"B destruction"<<endl;}
        int b;
        void fooB(){}
        virtual void func(){cout<<"B func."<<endl;}
        virtual void funcB(){cout <<"funcB."<<endl;}
};

class C:public A,public B
{
public:
        virtual ~C(){cout <<"C destruction"<<endl;}
        int c;
        void fooC(){}
        virtual void func(){cout <<"C func."<<endl;}
        virtual void funcC(){cout<<"funcC."<<endl;}
};

gdb:

(gdb) info locals
a = {_vptr.A = 0x0, a = 5636084}
b = {_vptr.B = 0x2f4550, b = 134513798}
c = warning: can't find linker symbol for virtual table for `C' value
{<A> = {_vptr.A = 0x8048614, a = 4043440}, <B> = {_vptr.B = 0x804a328, 
    b = -1073744392}, c = 134515705}
C继承A与B,分别为A和B保留了一个vptr,相应的,C的size也会更大

size of a is 8
size of b is 8
size of c is 20

8+8==16,多出来的4刚好是一个int c

多重派生,派生类是基类的体积和,vptr也不例外的被累加。



小结:多重派生有多个vptr,一个vptr已经无法兼顾两条线的派生了,这个也和派生本身的一些特性相符合。

=========================================================================================================================



思考:多重派生后边再派生会发生什么呢?比如多重之后继续多重,或者多重之后单重,里边应该是什么样的

(用sizeof()看类大小的时候,千万把这些个int变量和新生命的虚函数funcA funcB funcC funcD都屏蔽掉,免得干扰判断)

#include<iostream>
using namespace std;
class A{
public:
        virtual ~A(){cout <<"A destruction"<<endl;}
        int a;
        void fooA(){}
        virtual void func(){cout <<"A func."<<endl;}
        virtual void funcA(){cout << "funcA."<<endl;}
};
class A2{
public:
        virtual ~A2(){cout <<"A2 destruction"<<endl;}
        int a2;
        void fooA2(){}
        virtual void func(){cout <<"A2 func."<<endl;}
        virtual void funcA2(){cout << "funcA2."<<endl;}
};
class B{
public:
        virtual ~B(){cout <<"B destruction"<<endl;}
        int b;
        void fooB(){}
        virtual void func(){cout<<"B func."<<endl;}
        virtual void funcB(){cout <<"funcB."<<endl;}
};

class C:public A,public B
{
public:
        virtual ~C(){cout <<"C destruction"<<endl;}
        int c;
        void fooC(){}
        virtual void func(){cout <<"C func."<<endl;}
        virtual void funcC(){cout<<"funcC."<<endl;}
};
class D:public C
{
public:
        virtual ~D(){cout<<"D destruction"<<endl;}
        int d;
        void fooD(){}
        virtual void func(){cout<<"D func."<<endl;}
        virtual void funcD(){cout<<"funcD."<<endl;}
};
class E:public A2,public D
{
public:
        virtual ~E(){cout <<"E destruction"<<endl;}
        int e;
        void fooE(){}
        virtual void func(){cout <<"E func."<<endl;}
        virtual void funcE(){cout<<"funcE."<<endl;}
};


关系描述:C继承自A与B,D继承自C,E继承自D和一个新的独立的A2。每个类两个对象。

(gdb) info locals
a = {_vptr.A = 0x80493d0, a = 5636084}
a2 = {_vptr.A = 0x80493d0, a = 134513859}
d = {<C> = {<A> = {_vptr.A = 0x8049328, a = 1}, <B> = {
      _vptr.B = 0x8049348, b = 134515341}, c = 1}, d = 65535}
d2 = {<C> = {<A> = {_vptr.A = 0x8049328, 
      a = 134515313}, <B> = {_vptr.B = 0x8049348, 
      b = 134524724}, c = 134517256}, d = 1}
b = {_vptr.B = 0x80493a0, b = 134517097}
b2 = {_vptr.B = 0x80493a0, b = 134524300}
e = {<A2> = {_vptr.A2 = 0x80492c8, 
    a2 = 0}, <D> = {<C> = {<A> = {_vptr.A = 0x80492e4, 
        a = 134517072}, <B> = {_vptr.B = 0x8049304, 
        b = -1073744536}, c = 4204256}, d = 5636900}, 
  e = 134524048}
e2 = {<A2> = {_vptr.A2 = 0x80492c8, 
    a2 = 1}, <D> = {<C> = {<A> = {_vptr.A = 0x80492e4, 
        a = 0}, <B> = {_vptr.B = 0x8049304, b = 134524312}, 
      c = 1}, d = 4203845}, e = 4043440}
c = {<A> = {_vptr.A = 0x8049368, a = -1073744304}, <B> = {
    _vptr.B = 0x8049384, b = -1073744440}, c = 134514268}
c2 = {<A> = {_vptr.A = 0x8049368, a = -1207962224}, <B> = {
    _vptr.B = 0x8049384, b = 1}, c = -1073744456}

再次确认一下,同样一个_vptr.A,在基类A和子类C是不一样的,他们用的是不同的虚函数表。而在同类的对象a与a2中是一样的,说明同一个类直接就共享同一虚函数表(一个类就一个虚函数表,过程中产生了很多虚函数表,但是属于具体某类的,只有一个虚函数表,A->B->C继承中,A有A的虚函数表,B有B的,C有C的)就行了,继承过的类,他的表结构都发生了变化,相应的虚表地址肯定也不一样了。
重点看多重继承的C和D,首先,和基类的_vptr.A与_vptr.B肯定是不同的,然后地址偏差不大,姑且认为是两个虚函数表之间的偏差?!此处可以再做一个实验,在C或者D再额外加虚函数,看看这两个距离有没有变化,或者直接给A类或者B类加虚函数,因为函数数量的增加,应该是有差距的,这个差距应该是其中一个的,想看更多,应该再多几重继承,例如class D:public A,public B,public A2

先看E类吧,虽然是从D继承来的A、B,再单独继承A2,其实已经三个vptr指针了。所以这个vptr的数量是一直累加的。并且新继承的A2的地址其实更小,并且从vptr.A2到_vptr.A之间的差值和C一样,而不像D是刚好0x20个地址,通过大括号之类的也能看出端倪,虽然不完全理解这些标点符号的含义。毕竟D已经是原封不动继承C了~打包了,而C和E都是多重继承,就是加vptr的过程。。

到这里边也许是虚表占用,也许包含一些变量,下次有机会,或者有更深的理解的时候再来探讨了。


试试又不花钱,干嘛不来一发?再继承一个出来

class F:public E
{
public:
        virtual ~F(){cout<<"F destruction"<<endl;}
        int f;
        void foof(){}
        virtual void func(){cout<<"F func."<<endl;}
        virtual void funcF(){cout<<"funcF."<<endl;}
};

.......

f = {<E> = {<A2> = {_vptr.A2 = 0x80494c8, 
      a2 = -1207960648}, <D> = {<C> = {<A> = {
          _vptr.A = 0x80494e8, a = -1}, <B> = {
          _vptr.B = 0x8049508, b = 134513846}, c = 1}, 
      d = -1073744560}, e = 2099205}, f = 2169520}
f2 = {<E> = {<A2> = {_vptr.A2 = 0x80494c8, 
      a2 = 3110740}, <D> = {<C> = {<A> = {
          _vptr.A = 0x80494e8, a = 3120764}, <B> = {
---Type <return> to continue, or q <return> to quit---
          _vptr.B = 0x8049508, b = 2124752}, c = 13}, 
      d = 3110740}, e = 3115492}, f = 3116448}
F和D的情况一样,当E发生多重继承之后F又单继承,前边的_vptr们就被 E打包了,
<pre name="code" class="cpp">_vptr.A2 = 0x80494c8

 
<pre name="code" class="cpp">_vptr.A = 0x80494e8
 
_vptr.B = 0x8049508
刚刚好都是间隔0x20。。。。。
至于为什么是0x20?这是一个类虚表的预留大小?等等,我还不确定多重继承这情况是有几个虚函数表呢,毕竟这时候虚函数表的指针也不是一个了!

仔细看层级关系,应该是一层下来刚好是0x20的差距。


PS:既然是成员,_vptr是能直接访问的,而且,在对象中的位置也可以看到,和变量a挨着,在对象的首地址。

gdb调试对比

(gdb) info locals
a = {_vptr.A = 0x8049810, a = 5636084}
a2 = {_vptr.A = 0x8049810, a = 134513913}

(gdb) c
Continuing.
the address of a is 0xbffff5d8
a._vptr is 0x8049810
the address of a._vptr is 0xbffff5d8
a.a is 5636084
the address of a.a is 0xbffff5dc


不过类A还是太简单了,F的_vptr就不知道指导哪去了,和里边的_vptr.A _vptr.B _vptr.A2都不一样,而且不知道怎么直接输出_vptr.A _vptr.B _vptr.A2

f = {<E> = {<A2> = {_vptr.A2 = 0x3db2b0, 
      a2 = -1207960648}, <D> = {<C> = {<A> = {_vptr.A = 0x2f89e4, 
          a = -1}, <B> = {_vptr.B = 0x210fc4, b = 134513900}, c = 1}, 
      d = -1073744560}, e = 2099205}, f = 2169520}
f2 = warning: can't find linker symbol for virtual table for `F' value
warning:   found `std::wclog' instead
{<E> = {<A2> = {_vptr.A2 = 0x2f8a50, 
      a2 = 3110740}, <D> = {<C> = {<A> = {_vptr.A = 0x2f88a4, 
          a = 3120764}, <B> = {_vptr.B = 0xbffff528, b = 2124752}, c = 13}, 
      d = 3110740}, e = 3115492}, f = 3116448}
(gdb) c
Continuing.
the address of f is 0xbffff4f8
f._vptr is 0x80496a8
the address of f._vptr is 0xbffff4f8
f.f is 2169520

发现自己二了,果然还是C++学得少啊,用作用域就行了

        cout << "f._vptr is "<<f._vptr<<endl;
        cout << "the address of f._vptr is "<<&f._vptr<<endl;
        cout << "f._vptr.A is "<<f.A::_vptr<<endl;
        cout << "the address of f._vptr.A is "<<&(f.A::_vptr)<<endl;
        cout << "f._vptr.B is "<<f.B::_vptr<<endl;
        cout << "the address of f._vptr.B is "<<&(f.B::_vptr)<<endl;
        cout << "f._vptr.A2 is "<<f.A2::_vptr<<endl;
        cout << "the address of f._vptr.A2 is "<<&(f.A2::_vptr)<<endl;

最后调试,可以正确找到三个虚指针了,而且,默认的_vptr是和最后继承进来的A2对应的,都是0x8049768

f = {<E> = {<A2> = {_vptr.A2 = 0x8049768}, <D> = {<C> = {<A> = {
          _vptr.A = 0x804977c}, <B> = {
          _vptr.B = 0x8049790}, <No data fields>}, <No data fields>}, <No data fields>}, <No data fields>}
f2 = {<E> = {<A2> = {_vptr.A2 = 0x8049768}, <D> = {<C> = {<A> = {
          _vptr.A = 0x804977c}, <B> = {
          _vptr.B = 0x8049790}, <No data fields>}, <No data fields>}, <No data fields>}, <No data fields>}
(gdb) c
Continuing.
f._vptr is 0x8049768
the address of f._vptr is 0xbffff58c
f._vptr.A is 0x804977c
the address of f._vptr.A is 0xbffff590
f._vptr.B is 0x8049790
the address of f._vptr.B is 0xbffff594
f._vptr.A2 is 0x8049768
the address of f._vptr.A2 is 0xbffff58c



回到前文的一个问题,问什么这种虚函数的机制要被世代继承,不允许子孙另立门户取消虚函数(可以不写virtual,但是virtual属性已绑定)?因为基类指针要靠这个*_vptr来找虚函数的实现呢!!!!如果子孙某一代突然就没有这个虚函数,没有这个*_vptr了,那这个指向该子孙类对象的基类指针找实现的时候不是抓瞎了?这个动多态也就无法实现了吧!!!这又得说到继承的机制了,派生类是怎么划分属于基类的部分和新的部分的,这些我也没印象了,得系统的复习一下。。但可以肯定的是,这个_vptr是属于基类部分的,所以基类指针永远都找得到它,永远都能确定对象需要的虚函数实现。(反映到地址上,也许是类的低地址是基类的?_vptr是和对象地址一样,存在地址最低的地方)

小结:多重派生类F的多个虚表指针_vptr.A、_vptr.B、_vptrA2分别存在不同的基类A、B、A2部分(总之基类指针得能找到它啊)&虚函数的性质必须无条件继承。

=========================================================================================================================

突然想到类的分部,F类应该通过E继承了A2和D,再通过D继承了C,再通过C继承了A与B,上边也看到了是这样一个情况。下边就打印一下,看下分布,是否也像我从gdb的信息理解的那样

在上边的程序再加一些基类指针和打印语句:

        A2* pA2 = &f;
        A* pA = &f;
        B* pB = &f;


f._vptr pointed to 0x8049888
the address of f._vptr is 0xbfdb6fa0
f._vptr.A pointed to 0x804989c
the address of f._vptr.A is 0xbfdb6fa4
f._vptr.B is 0x80498b0
the address of f._vptr.B is 0xbfdb6fa8
f._vptr.A2 pointed to 0x8049888
the address of f._vptr.A2 is 0xbfdb6fa0
the address of f is 0xbfdb6fa0
the address pA point to is 0xbfdb6fa4
the address pB point to is 0xbfdb6fa8
the address pA2 point to is 0xbfdb6fa0
gdb调试与输出信息如下:

(gdb) info locals
a = {_vptr.A = 0x80499a0}
b2 = {_vptr.B = 0x8049970}
e = {<A2> = {_vptr.A2 = 0x80498c8}, <D> = {<C> = {<A> = {
        _vptr.A = 0x80498dc}, <B> = {
        _vptr.B = 0x80498f0}, <No data fields>}, <No data fields>}, <No data fields>}
f2 = {<E> = {<A2> = {_vptr.A2 = 0x8049888}, <D> = {<C> = {<A> = {
          _vptr.A = 0x804989c}, <B> = {
          _vptr.B = 0x80498b0}, <No data fields>}, <No data fields>}, <No data fields>}, <No data fields>}
pA2 = 0xbffff580
a2 = {_vptr.A = 0x80499a0}
b = {_vptr.B = 0x8049970}
e2 = {<A2> = {_vptr.A2 = 0x80498c8}, <D> = {<C> = {<A> = {
        _vptr.A = 0x80498dc}, <B> = {
        _vptr.B = 0x80498f0}, <No data fields>}, <No data fields>}, <No data fields>}
f = {<E> = {<A2> = {_vptr.A2 = 0x8049888}, <D> = {<C> = {<A> = {
          _vptr.A = 0x804989c}, <B> = {
          _vptr.B = 0x80498b0}, <No data fields>}, <No data fields>}, <No data fields>}, <No data fields>}
pA = 0xbffff584
c = {<A> = {_vptr.A = 0x8049948}, <B> = {
    _vptr.B = 0x804995c}, <No data fields>}
d2 = {<C> = {<A> = {_vptr.A = 0x8049908}, <B> = {
      _vptr.B = 0x804991c}, <No data fields>}, <No data fields>}
pB = 0xbffff588
c2 = {<A> = {_vptr.A = 0x8049948}, <B> = {
    _vptr.B = 0x804995c}, <No data fields>}
d = {<C> = {<A> = {_vptr.A = 0x8049908}, <B> = {
      _vptr.B = 0x804991c}, <No data fields>}, <No data fields>}
(gdb) c
Continuing.
f._vptr pointed to 0x8049888
the address of f._vptr is 0xbffff580
f._vptr.A pointed to 0x804989c
the address of f._vptr.A is 0xbffff584
f._vptr.B is 0x80498b0
the address of f._vptr.B is 0xbffff588
f._vptr.A2 pointed to 0x8049888
the address of f._vptr.A2 is 0xbffff580
the address of f is 0xbffff580
the address pA point to is 0xbffff584   (pointer of class C and class D has the same address)
the address pB point to is 0xbffff588
the address pA2 point to is 0xbffff580

1.最后继承的A2地址最小,先继承的A和B
2.基类指针指向的“该基类部分的”基址与“该基类部分的”虚指针地址相对应。说明这些虚表指针都是在“该基类部分的”低地址存放的,并且是伴随着该类的部分打包下去的(请原谅我的继承部分基础的缺失,顺带重新复习一下。)

3.consequently,_vptr的含义就解释通了,因为_vptr都是在类的低地址,又因为最后继承的类A2是在类的低地址,所以_vptr就等同于_vptr.A2(访问途径f.A2::_vptr

4.各派生类都在内存中单独存有各自的VTABLE(看_vptr的指向),即便他们继承了相同的基类。


附上我潦草的激光防伪配图:


PS:换算成上边老外帖子那个示意图的话,右边“F独有”的大圈应该是F_VTABLE。至于里边的TABLE A/B/A2,不确定确切的称呼,但不是原封不动的A/B/A2_VTABLE,至少地址不是。


回到最初的设想流程:一个基类指针指到对象,通过对象的虚表指针找到虚函数表,再从虚表里边找到对应的函数地址?

那么问题来了,因为里边有多个虚表指针和多个虚表,然后怎么确定这个函数是从哪个类继承来的,基类和派生类又该用哪个实现。

因为刚好基类指针pA/pB/pA2指到F对象时是指向各自的那块、也有各自不同的地址,不同的虚表指针和不同的虚函数表,这样多重继承的虚函数就成了一个互不干扰的并行状态。可以看成是单继承状态,F继承了A2,F继承了A,F继承了B(其实中间的CDE没起什么作用,忽略)。当基类指针pA/pB/pA2指到派生类对象F时,各自的虚指针发挥了作用,从F类和基类A/B/A2之间选择一个函数实现。但是具体靠什么来选择的,其实虚指针找到,就可以直接参考老外那张虚函数跳转的图了,你要找哪个函数,给你跳到哪个实现!!!然后,基本也就是函数的名称和地址了,比如首先靠函数名来区分是虚函数func1还是虚函数func2,然后再例如,选出一个函数func1(),你只要给出它的地址就完了,并不需要纠结它是基类的实现还是派生类的实现因为一个对象从出生起,他的虚函数的具体实现已经定了(动态不是指的这个动态,动态是针对指针的,每个对象已经确定了自己的虚函数实现)。到现在为止,这个流程,貌似让我给说通了!!!!!


虚函数的多重继承是完美了,非虚函数的多重继承却并不是,还有图中没画出来的虚表右边的内容,见

相关探讨

http://blog.csdn.net/huqinwei987/article/details/50743539



总得有一些对我们透明的,被认为自动完成的机制存在的,不然人怎么会迷信呢?找出机制,才能明白问题。



=========================================================================================================================


另一个问题,同名的虚函数在多重派生时怎么继承,记得是有warning,可能不允许吧?!你得确定这个虚函数到底和哪个基类形成多态,也许能走得通,继承成功,但最起码可能不是人那么想要的结果?记错了,其实是允许的,反正不同的血统线有不同的虚指针和虚表,从源头起他们就分开了,各继承各的,互不干扰。

真正的错误发生在派生类不重写虚函数的问题上,如果重写虚函数,那么刚好和多个基类都能形成多态,通过各自的虚表解决问题;如果不重写虚函数,派生类从多个基类继承虚函数,都不确定要使用谁了,virtual机制只是帮忙找到虚函数的真正实现,可并不能替派生类决定继承谁。

virtual_multi.cpp:110: 错误:对成员‘func’的请求有歧义
virtual_multi.cpp:25: 错误:candidates are: virtual void B::func()
virtual_multi.cpp:9: 错误:                virtual void A::func()
virtual_multi.cpp:35: 错误:                virtual void C::func()
virtual_multi.cpp:44: 错误:                virtual void D::func()
virtual_multi.cpp:17: 错误:                virtual void A2::func()



再考虑一个问题,各基类指针用“Base* p=&f”赋值,为什么基类指针能准确的识别并指向对象的基类部分地址?如下:

the address of f is 0xbf848f04
the address pA point to  0xbf848f08
the address pB point to  0xbf848f0c
the address pA2 point to  0xbf848f04
the address pC point to  0xbf848f08
the address pD point to  0xbf848f08

根据内存分布,低地址直接是虚表指针和成员变量,没有额外的“表”,怎么自动完成这个功能的?是C++重载了操作符“=”?功能重载了,实现也得靠一个地方存储信息吧?取地址要不要太智能?可能是被编译器优化了?要取指定对象的地址,编译器是知道这个对象的类型和基类类型的,编译器可以做到!(和malloc/free new/delete一个道理,并不需要记录任何信息,有地址就行了,其他的操作系统完成了。甚至new/delete还能额外调用析构函数,所以肯定要有编译器支持的识别机制了)



PS:上边多重派生例子又引起了一个新问题,虚析构又是怎么回事呢,同样是虚,同样继承,B析构就不调用A的析构,而C析构时既调用B的析构又调用A的析构。

好像虚析构的设立目的是有虚函数时能够用基类指针去正确的删除对象。



=========================================================================================================================

汇编:

各类型指针指向对象的操作
   x76              A2* pA2 = &f;                                   x
B+>x77              A* pA = &f;                                     x
b+ x78              B* pB = &f;                                     x
b+ x79              C* pC = &f;                                     x
   x80              D* pD = &f;                                     x
   x81              E* pE = &f; 
共六个(有些继承本来就没做改变,所以地址偏移一样的)
A2是后继承的,在最后,所以是原封不动的esp偏移0x20的对象地址。
其他的分别从A和B继承来,偏移4和8。
把这些地址复制到pA2/pA/pB/pC/pD/pE指针所在位置,完成指向。

<pre name="code" class="cpp">   x0x804891e <main()+170>  lea    0x20(%esp),%eax 
  x0x8048922 <main()+174>  mov   %eax,0x78(%esp)            
   0x8048926 <main()+178>   lea    0x20(%esp),%eax                  x
   x0x804892a <main()+182>  add    $0x4,%eax                        x
   x0x804892d <main()+185>  mov    %eax,0x7c(%esp)                  x
b+ x0x8048931 <main()+189>  lea    0x20(%esp),%eax                  x
   x0x8048935 <main()+193>  add    $0x8,%eax                        x
   x0x8048938 <main()+196>  mov    %eax,0x80(%esp)                  x
b+ x0x804893f <main()+203>  lea    0x20(%esp),%eax                  x
   x0x8048943 <main()+207>  add    $0x4,%eax                        x
   x0x8048946 <main()+210>  mov    %eax,0x84(%esp)                  x
   x0x804894d <main()+217>  lea    0x20(%esp),%eax                  x
   x0x8048951 <main()+221>  add    $0x4,%eax                        x
   x0x8048954 <main()+224>  mov    %eax,0x88(%esp)                  x
   x0x804895b <main()+231>  lea    0x20(%esp),%eax                  x
   x0x804895f <main()+235>  mov    %eax,0x8c(%esp)                  

 
下面开始提取func地址,运行函数
B+>x83              pA->func();                                     x
   x84              pB->func();                                     x
   x85              pC->func();                                     x
   x86              pD->func();                                     x
   x87              pE->func();  

其实各个指针操作出来都是一样的,因为同一个对象,又是虚函数。
B+>x0x8048966 <main()+242>  mov    0x7c(%esp),%eax                  x
   x0x804896a <main()+246>  mov    (%eax),%eax                      x
   x0x804896c <main()+248>  add    $0x8,%eax                        x
   x0x804896f <main()+251>  mov    (%eax),%edx                      x
   x0x8048971 <main()+253>  mov    0x7c(%esp),%eax                  x
   x0x8048975 <main()+257>  mov    %eax,(%esp)                      x
   x0x8048978 <main()+260>  call   *%edx                            x
   x0x804897a <main()+262>  mov    0x80(%esp),%eax                  x
   x0x8048981 <main()+269>  mov    (%eax),%eax                      x
   x0x8048983 <main()+271>  add    $0x8,%eax                        x
   x0x8048986 <main()+274>  mov    (%eax),%edx                      x
   x0x8048988 <main()+276>  mov    0x80(%esp),%eax                  x
   x0x804898f <main()+283>  mov    %eax,(%esp)   
   x0x8048992 <main()+286>  call   *%edx                 
>x0x8048994 <main()+288>  mov    0x84(%esp),%eax                  x
   x0x804899b <main()+295>  mov    (%eax),%eax                      x
   x0x804899d <main()+297>  add    $0x8,%eax                        x
   x0x80489a0 <main()+300>  mov    (%eax),%edx                      x
   x0x80489a2 <main()+302>  mov    0x84(%esp),%eax                  x
   x0x80489a9 <main()+309>  mov    %eax,(%esp)                      x
   x0x80489ac <main()+312>  call   *%edx                            x
   x0x80489ae <main()+314>  mov    0x88(%esp),%eax                  x
   x0x80489b5 <main()+321>  mov    (%eax),%eax                      x
   x0x80489b7 <main()+323>  add    $0x8,%eax                        x
   x0x80489ba <main()+326>  mov    (%eax),%edx                      x
   x0x80489bc <main()+328>  mov    0x88(%esp),%eax                  x
   x0x80489c3 <main()+335>  mov    %eax,(%esp)                      x
   x0x80489c6 <main()+338>  call   *%edx                            x
等等等等。。。。就不全粘贴了。大概解释下:
1.从指针pA2/pA/pB/pC/pD/pE等提取出对象对应类(主要分A、B、A2三基类)部分地址到eax
2.1从该地址的内存中提取该地址vptr所指向的地址——vtable的基址
2.2将该vtable的地址存入eax)
3.vtable加上偏移0x8找到函数入口地址(因为是共同的实现)(对象f 中不同类部分的vptr都指向了同一个vtable?还是说func都在各vtable的偏移0x8处? (对的,确切的说,是F类对象f的A2::func()都在各vtable的偏移0x8处)没看到太详细过程,打印发现,各指针调用func时,edx是不一样的,就是说有基类A2、A、B三个部分每个部分有不同的vptr,不同的vtable,甚至连vtable指向的函数地址也是重定向过的了?不然应该都指向E::func()。但是仔细一想,这样是有破绽的,假如还没继承到E类的,又怎么能指向E::func()呢?但是我是假定它继承到这个类后又重写vtable了!以为能说通,实际上不是这个机制!!!!)(前边的设想是不对的,可以看下面的分析)
4.将函数入口地址存入edx
5.另起一个流程,将指针pA2/pA/pB/pC/pD/pE地址存入eax寄存器
6.将eax内容存入esp指向的内存中,入栈?不同的指针有-1073744536 -1073744540 -1073744544三种,转换成16进制就是地址了。 算是入栈吧!把VPTR入栈?
7.找到函数入口执行。

回顾,看指针内容:
(gdb) print *pA
$30 = {_vptr.A = 0x8049a9c}
(gdb) print *pB
$31 = {_vptr.B = 0x8049ab0}
(gdb) print *pA2
$32 = {_vptr.A2 = 0x8049a88}
(gdb) print *pD
$34 = {<C> = {<A> = {_vptr.A = 0x8049a9c}, <B> = {
      _vptr.B = 0x8049ab0}, <No	data fields>}, <No data	fields>}
(gdb) print *pE
$35 = {<A2> = {_vptr.A2 = 0x8049a88}, <D> = {<C> = {<A> = {
        _vptr.A = 0x8049a9c}, <B> = {
        _vptr.B = 0x8049ab0}, <No data fields>}, <No data fields>}, <
No data	fields>}

可知,并 像表面所想象的“所有指针都指到对象同一位置,都从同一个table找到具体的函数实现!”
原因如下:
基类指针并不指向对象的基地址,而是该基类相应部分,这是铁律,不能破。既然各基类 指针指向对象时并没有统一指向一个固定的位置,比如基地址,比如最后继承的处于基地址处的A2类部分——的基地址,即vptr所在位置(内存示意图见前文手绘)。那么就 不能单纯的通过F类对象f的基地址处找vptr,通过vptr找到vtable,再从vtable找到指定的func()的实现。

基类指针存储的只是对象的对应部分的基地址,也就是说即使F继承了A、B、C、D、E、A2类,对一个A类指针来说,它近似一个a对象,其实这也就是没虚函数时的默认状态——基类指针直接无视派生部分, 基类指针真是很傻很天真。
所以虚函数能让基类指针pA找到派生类对象f的F::func()实现的关键就是,要在f对象的A类部分有一个外部链接,让基类指针pA能够跳出A类的局限。肯定要靠虚指针了,但是基类部分的vtable是否可以指向派生类实现呢?原则上可以,因为毕竟是派生类对象,它自己“清楚的很”,它可以给它的基类部分初始化成那样。看起来基类很傻很天真,但是基类的vtable已经逆袭了,能够链接到派生类的实现,谁叫此时“基类”本就是派生类的一部分呢!
这种结构只限于派生类对象,基类对象肯定办不到的,这也体现了虚函数表的动多态性!( 其实真想了解这个对象生成的时候,怎么给各个vtable赋值的,应该也是都成为了类的一部分,编译时候搞定的?





结合汇编的7个指令和相应地址分析再来分析一下具体过程:

以pB-func();为例进行分析
具体各变量的打印就省去了,直接将关键地址翻译到下图
(再解释下吧,C继承自A\B,D继承自C,E继承自D、A2,F继承自E,这个示例包含了多层继承和多重继承。)
(图中B vtable 并不是B类的vtable,是 F类对象中,指引B继承线的Vtable,A、A2同理)
(图中最右侧,则是具体的类和函数实现了,根据每个类不同的覆盖效果,每个func对应的实现所在类也有所不同)



补充:这里边只是省略了一个过程,其实中间过渡的C类和D类,都有自己的虚函数表,每一层继承都有自己的虚函数表,只不过大的虚函数指针是一个,实际存在形式应该是_vptr.C、_vptr.D

距离本篇开博已经几天了,分析到这,再回去看当初从国外论坛粘贴过来的图,竟然和自己画的是一样的是一样的,真不是硬靠的,只能是这么个结果。第二个解释都说不通。


再次补充:具体的vtable入口地址到具体的F::func()实现的跳转过程如下图:


PS:根据前边虚函数映射图,每条线都包含多个虚表,也就是途中每个vtable,还能拆成一系列vtable,C\D\E\F类对象都有不同的vtable,篇幅所限,没画

总结:多重派生时,怎么让基类指针正确识别派生类对象的正确虚函数实现呢?其实就是派生类F给每一个虚函数表所存储的对应入口地址加了一个跳转,原生的F类指针肯定就直接找到F::func()的入口地址。

但是还有个问题,如果最后继承的A2的虚表和F的表是重合的,A2指针指向F类对象,应该怎么实现呢?又差点忘了,没什么好怀疑的,对于一个F类对象,假设所有func都虚过了,并且在F内部有了实现,那么这个实现自然而然的就覆盖所有基类操作了,A2和F的vtable一样不是更省地方么,没有冲突,最终实现当然都是F::func()。


=========================================================================================================================

下边的不管了,入栈出栈的放别处讨论,不影响讨论虚表的机制:


关于call前边的指针入栈,属于函数调用前的切换吧。

但是不是参数,不知道是个什么东西。预留返回地址之类的?

比如把0x8049a48存到main()函数栈帧的esp。call的时候esp升(减)4,push的时候esp升(减)4

其实就是指针指向的f对象的地址入栈,然后在func()的局部有一个读取eax

函数调用之前:

   x0x804896e <main()+250>  mov    0x74(%esp),%eax                  x
   x0x8048972 <main()+254>  mov    (%eax),%eax                      x
   x0x8048974 <main()+256>  add    $0x8,%eax                        x
   x0x8048977 <main()+259>  mov    (%eax),%edx                      x
   x0x8048979 <main()+261>  mov    0x74(%esp),%eax                  x
   x0x804897d <main()+265>  mov    %eax,(%esp)                      x
   x0x8048980 <main()+268>  call   *%edx   


函数内部:

  >x0x80495d2 <F::func()>           push   %ebp                     x
   x0x80495d3 <F::func()+1>         mov    %esp,%ebp                x
   x0x80495d5 <F::func()+3>         sub    $0x18,%esp               x
   x0x80495d8 <F::func()+6>         movl   $0x8049858,0x4(%esp)     x
   x0x80495e0 <F::func()+14>        movl   $0x804b620,(%esp)        x
   x0x80495e7 <F::func()+21>        call   0x804873c <_ZStlsISt11chax
   x0x80495ec <F::func()+26>        movl   $0x804878c,0x4(%esp)     x
   x0x80495f4 <F::func()+34>        mov    %eax,(%esp)              x
   x0x80495f7 <F::func()+37>        call   0x804877c <_ZNSolsEPFRSoSx
   x0x80495fc <F::func()+42>        leave                           x
   x0x80495fd <F::func()+43>        ret   



=========================================================================================================================

虚析构:

下面引入一个新例子:

//虚析构的动态绑定
//虚析构的动态绑定
//注意两个虚析构不同名
#include<iostream>
#include<stdio.h>
#include<malloc.h>
class A{
public:
        A(){}
        virtual ~A(){printf("A's destructor!\n");}
};
class B:public A
{
private:
        void * ptr;
public:
        B()
        {
                ptr = malloc(10);
                printf("the address of heap block is 0x%p\n",ptr);
        }
        virtual ~B()
        {
                printf("B's destructor!\n");
                printf("the heap block(0x%p) will be free\n",ptr);
        }
};
int main(){
//顺带测一下非虚析构的情况,只执行C的析构,强转成功
//不过这样的问题是什么?基类部分析构成功,派生类部分未析构
//比如派生类析构需要delete某些堆空间,这样就浪费了内存空间?
//可以测试出来,做个循环,看看堆空间地址,虽然堆空间是动态的,链表,>不准
        B *pB = new B();
        delete pB;

                A* pA = new B();
                delete pA;

        printf("before return!\n");
        return 0;
}



虚析构和普通析构函数不同,首先函数名不相同,其次,虚析构不是互斥(或者叫覆写overwrite),而是派生类析构和基类析构的析构都执行!或者说,是因为虚,所以基类指针能够找到派生类对象的析构,派生类对象的析构又自动调用基类的析构。关于调用顺序,也是派生类析构结束后才走的基类析构,具体在哪一步跳过去的,汇编懒得看了,这个估计也没人关心。粗略看,至少顺序是没错的,不会存在谁先执行,包含谁,而是先派生类析构,执行完跳转到基类析构。

这是下边C和D的例子的代码,为避免print干扰,随便在析构里加了个变量声明,删了多余语句:

   x0x8048845 <D::~D()+1>           mov    %esp,%ebp                x
   x0x8048847 <D::~D()+3>           sub    $0x28,%esp               x
B+ x0x804884a <D::~D()+6>           movl   $0x1,-0xc(%ebp)          x
B+ x0x8048851 <D::~D()+13>          mov    0x8(%ebp),%eax           x
  >x0x8048854 <D::~D()+16>          mov    %eax,(%esp)              x
   x0x8048857 <D::~D()+19>          call   0x80487d4 <C::~C()>      x
   x0x804885c <D::~D()+24>          leave                           x
   x0x804885d <D::~D()+25>          ret                             x
   x
可以看到,是D析构结束后,call的C析构,地址0x80487d4。
  >x0x80487d4 <C::~C()>     push   %ebp                             x
   x0x80487d5 <C::~C()+1>   mov    %esp,%ebp                        x
   x0x80487d7 <C::~C()+3>   sub    $0x10,%esp                       x
b+ x0x80487da <C::~C()+6>   movl   $0x2,-0x4(%ebp)                  x
b+ x0x80487e1 <C::~C()+13>  leave                                   x
   x0x80487e2 <C::~C()+14>  ret                                     x
   x0x80487e3               nop                                     x
   x




继承相关知识(待补)而普通析构是互斥的。


如果不用虚析构,是可能带来问题的,比如派生类虚函数(不一定是虚,也看调用形式吧,总之,是派生类独有的操作)有空间申请,派生类对应的析构本来有对应的销毁,但是基类指针指过来以后,因为非虚,只调用基类的析构函数,所以销毁失败。


重点不是派生类析构会不会自动调用基类析构,是如果非虚,基类指针绝对找不到派生类析构,思维跳跃,容易跑偏,给自己提个醒。


等等等等,其他例子。。。。

========================================================================================================================

第二个例子想反映出不使用虚析构可能带来的问题,比如内存的浪费。

(下边可以略过)问题是肯定存在的,不过测试的对比方法不太成功,无论回收空间与否,大批量的申请堆空间都会导致堆空间爆掉!!

//虚析构的动态绑定
//虚析构的动态绑定
//注意两个虚析构不同名
#include<iostream>
#include<stdio.h>
#include<malloc.h>
class C{
public:
        C(){}
        ~C(){printf("C's destructor!\n");}
};
class D:public C
{
private:
        void * ptr;
public:
        D()
        {
                ptr = malloc(10);
                printf("the address of heap block is 0x%p\n",ptr);
        }
        ~D()
        {
                printf("D's destructor!\n");
                printf("the heap block(0x%p) will be free\n",ptr);
        }
};
int main(){
//顺带测一下非虚析构的情况,只执行C的析构,强转成功
//不过这样的问题是什么?基类部分析构成功,派生类部分未析构
//比如派生类析构需要delete某些堆空间,这样就浪费了内存空间?
//可以测试出来,做个循环,看看堆空间地址,虽然堆空间是动态的,链表,>不准
        D *pD = new D();
        delete pD;
        for(int i = 0;i < 100;i++)
        {
                C* pC = new D();
                delete pC;

        }
        printf("before return!\n");
        return 0;
}

部分输出如下:第一个带freee的是delete pD的情况,后边都是delete pC


但是因为一些系统自带的机制,这样不容易直观的找出区别(相对于循环里边使用D类,带析构delete ptr)

所以我加大了系数:

上边代码修改为:

        D()
        {
                ptr = malloc(100000);
                printf("the address of heap block is 0x%p\n",ptr);
        }
进行循环
        for(int i = 0;i < 1000000;i++)
        {
                C* pC = new D();
                delete pC;
        }

运行结果如下:

开始时,能够不断的分配堆空间,最后,爆掉了,没有了。


不过就算用D析构,最后也爆掉了!!!!









  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值