C++虚函数,虚函数表,虚继承,虚继承表

一、虚函数

类中用virtual关键字修饰的函数。

作用:主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。

class Vfptr_classA{
public:
    virtual void function1(){
        cout<<"function1"<<endl;
    }
    virtual void function2(){
        cout<<"function2"<<endl;
    }
};

二、虚函数表

1、虚函数表格大小

先看空类的大小

//空类
class Vfptr_classA{
};

int main(){
    
    Vfptr_classA Vfptr_A;

    cout << "sizeof Vfptr_A :" << sizeof(Vfptr_A) <<endl;
    return 0;
}


##打印
sizeof Vfptr_A :1

2、有一个虚函数的类

//有一个虚函数的类
class Vfptr_classA{
public:
    virtual void function1(){
        cout<<"function1"<<endl;
    }
};

int main(){
    
    Vfptr_classA Vfptr_A;

    cout << "sizeof Vfptr_A :" << sizeof(Vfptr_A) <<endl;
    return 0;
}


##打印
sizeof Vfptr_A :4

3、有2个虚函数的类

//虚函数表 Vfptr
class Vfptr_classA{
public:
    virtual void function1(){
        cout<<"function1"<<endl;
    }
    virtual void function2(){
        cout<<"function2"<<endl;
    }
};

int main(){
    
    Vfptr_classA Vfptr_A;

    cout << "sizeof Vfptr_A :" << sizeof(Vfptr_A) <<endl;
    return 0;
}


##打印
sizeof Vfptr_A :4

由此可以正式,类在内存中记录虚函数是以一个指针记录的,并且该指针指向一个数组,数组中装着的是虚函数的地址。同时,经过实验,发现32bit的编译器下,虚函数表的指针大小是4字节。64bit的编译器下,虚函数表的指针大小是8字节

2、虚函数表在类内存中的位置

首先我们通过代码分析一下:

//虚函数表 Vfptr
class Vfptr_classA{
public:
    int a;
    virtual void function1(){
        cout<<"function1"<<endl;
    }
    virtual void function2(){
        cout<<"function2"<<endl;
    }

    void show(){
        cout<<"show"<<endl;
    }

};

int main(){
//虚函数表
    Vfptr_classA Vfptr_A;
    cout << "address Vfptr_A :" << &Vfptr_A <<endl;
    cout << "address Vfptr_A.a :" << &Vfptr_A.a <<endl;
return 0;
}

##打印
address Vfptr_A :00EFF9F8
address Vfptr_A.a :00EFF9FC

可以看到,成员变量到类的首地址有4个字节(虚函数表指针),成员变量的地址在虚函数表后,由此可以通过取类变量地址的方式获取虚函数表地址。

会有同学问,如果有成员函数呢??成员函数不占用对象的内存。这是因为所有的函数都是存放代码区的,不管是全局函数,还是成员函数。

 

3、如何利用虚函数表调用虚函数

直接上代码

//虚函数表 Vfptr
class Vfptr_classA{
public:
    virtual void function1(){
        cout<<"function1"<<endl;
    }
    virtual void function2(){
        cout<<"function2"<<endl;
    }
};

typedef void (*fun)();  //定义函数指针



int main(){

//虚函数表
    Vfptr_classA Vfptr_A;

    int * Vfptr_As = (int *)(*((int *)&Vfptr_A)); //获取虚函数表
    //实际上是一个数组
    for(int i = 0 ; Vfptr_As[i] != 0 ; ++i){
        cout<< "fptr "<< i <<"address :"<<Vfptr_As[i] <<endl;
        fun f = (fun)Vfptr_As[i];
        f();
    }

    return 0;
}

##打印
fptr 0address :11080325
function1
fptr 1address :11080335
function2

注意:以上代码为32位,虚函数表指针为4字节。而若用64为编译器运行,则会获取地址失败,原因是64位编译器时,虚函数表指针为8字节,需要用long long类型指针获取。

    Vfptr_classA Vfptr_A;
    long long * Vfptr_As = (long long *)(*((long long *)&Vfptr_A));
    cout<< "Vfptr :"<<Vfptr_As<<endl;
    for(int i = 0 ; Vfptr_As[i] != 0 ; ++i){
        cout<< "fptr "<< i <<"address :"<<Vfptr_As[i] <<endl;
        fun f = (fun)Vfptr_As[i];
        f();
    }

##打印
fptr 0address :140695608234229
function1
fptr 1address :140695608233989
function2

4、继承类多态(虚函数重写)

1、继承

class Vfptr_classA{
public:
    virtual void function1(){
        cout<<"function1"<<endl;
    }
    virtual void function2(){
        cout<<"function2"<<endl;
    }
};

class Vfptr_classB : public Vfptr_classA{
public:
    virtual void function3(){
        cout<<"function3"<<endl;
    }

    int  b;
};

##主函数执行
    Vfptr_classB Vfptr_B;
    cout << "sizeof Vfptr_B :" << sizeof(Vfptr_B) <<endl;
    cout << "address Vfptr_B :" << &Vfptr_B <<endl;
    cout << "address Vfptr_B.b :" << &Vfptr_B.b <<endl;


##打印
sizeof Vfptr_B :8
address Vfptr_B :009DFB74
address Vfptr_B.b :009DFB78

由上可知,派生类B在头部也有一张虚函数表,大小为4字节(32bit),成员变量在后。而通过调试看

发现,派生类头部共用基类的虚函数列表,而派生类新增加的虚函数会保存在基类的虚函数后面。

2、重写

class Vfptr_classB : public Vfptr_classA{
public:
    virtual void function3(){
        cout<<"function3"<<endl;
    }
     void function1(){
        cout<<"ClassB function1"<<endl;
    }

    int  b;
};



##主函数执行
int main(){
    Vfptr_classB Vfptr_B;

    int * Vfptr_Bs = (int *)(*((int *)&Vfptr_B));
    cout<< "Vfptr :"<<Vfptr_Bs<<endl;
    for(int i = 0 ; Vfptr_Bs[i] != 0 ; ++i){
        cout<< "fptr "<< i <<"address :"<<Vfptr_Bs[i] <<endl;
        fun f = (fun)Vfptr_Bs[i];
        f();
    }
    return 0;
}
##打印
fptr 0address :8589982
ClassB function1
fptr 1address :8589957
function2
fptr 2address :8589637
function3

发现变化了,由打印顺序看,由于派生类B重写了基类A的function1,重写后的函数会被替换到虚函数列表中基类A的function1的位置。看一下调试

证实了我们的猜测。

3、多重继承

1、重写一个基类的虚函数

class Vfptr_classA{
public:
    virtual void function1(){
        cout<<"function1"<<endl;
    }
    virtual void function2(){
        cout<<"function2"<<endl;
    }
};

class Vfptr_classC{
public:
    virtual void function4(){
        cout<<"function4"<<endl;
    }
};
class Vfptr_classB : public Vfptr_classA , public Vfptr_classC{
public:
    virtual void function3(){
        cout<<"function3"<<endl;
    }
     void function1(){
        cout<<"ClassB function1"<<endl;
    }

    int  b;
};



int main(){

    Vfptr_classB Vfptr_B;
    cout << "sizeof Vfptr_B :" << sizeof(Vfptr_B) <<endl;
    cout << "address Vfptr_B :" << &Vfptr_B <<endl;
    cout << "address Vfptr_B.b :" << &Vfptr_B.b <<endl;
    return 0;
}


##打印
sizeof Vfptr_B :12
address Vfptr_B :00B3F74C
address Vfptr_B.b :00B3F754

 

打开调试内存

可以看到,此时在B的内存中,头部是基类A的虚函数表,跟着的是基类B的虚函数表,它们各占用4个字节。同时,重写基类A的function1函数,会覆盖原来基类A的function1函数在虚函数表的位置。

猜想:在B中重写基类C的function4,是否也会覆盖基类C中的funciton4在虚函数表的位置??好,我们继续做实验。

2、重写两个基类中的虚函数

class Vfptr_classA{
public:
    virtual void function1(){
        cout<<"function1"<<endl;
    }
    virtual void function2(){
        cout<<"function2"<<endl;
    }
};

class Vfptr_classC{
public:
    virtual void function4(){
        cout<<"function4"<<endl;
    }
};
class Vfptr_classB : public Vfptr_classA , public Vfptr_classC{
public:
    virtual void function3(){
        cout<<"function3"<<endl;
    }
     void function1(){
        cout<<"ClassB function1"<<endl;
    }
     void function4(){
        cout<<"ClassB function4"<<endl;
    }

    int  b;
};



int main(){

    Vfptr_classB Vfptr_B;
    cout << "sizeof Vfptr_B :" << sizeof(Vfptr_B) <<endl;
    cout << "address Vfptr_B :" << &Vfptr_B <<endl;
    cout << "address Vfptr_B.b :" << &Vfptr_B.b <<endl;
    return 0;
}


##打印
sizeof Vfptr_B :12
address Vfptr_B :00B3F74C
address Vfptr_B.b :00B3F754

猜想没错。

总结:

虚函数是动态多态(程序运行时多态,重载为静态多态)。实现方式为:

1、当类自身有虚函数,则会创建一张虚函数表,放置于类的开头,占用4个字节内存(64位为8bit)。

2、当类的父类中有虚函数,则派生类会继承父类的虚函数表,同时若派生类有新的虚函数,则会在父类的虚函数表后面追加。

3、当派生类为多重继承,并且多个父类都有虚函数,则会全部继承父类的虚函数表,顺序以继承顺序,并且派生类中新增的虚函数只会添加在第一个父类虚函数表中。

4、当派生类重写父类的虚函数,则重写后的函数会取缔原来虚函数在虚函数表中的位置。

 

 

4、虚函数运用时的理解

接着上,3.2(重写两个基类中的虚函数)中的例子

主程序中执行

##主程序中执行
    Vfptr_classA * ptrA = new Vfptr_classB();
    ptrA ->function1();

##打印
ClassB function1

猜想:

声明一个基类A的指针ptrA ,实例化出派生类B的对象。

而ptrA 指针类型是基类A,则只能索引到派生类B中的基类A的部分:

即此时,ptrA 应该指向的是派生类B中函数列表

 

但是

当我们调试的时候发现

ptrA指针指向B的整个内存空间,只是不能通过

这种方式调用其他派生类的函数。虽然我们依然可以通过地址偏移,找到B空间的第二张表(基类C的虚函数表),但不建议这样用,代码可读性变差。

    Vfptr_classA * ptrA = new Vfptr_classB();

    int * Vfptr_Bs = (int *)(*((int *)ptrA + 1));        //通过把(int *)ptrA + 1来获取下一张表
    for(int i = 0 ; Vfptr_Bs[i] != 0 ; ++i){
        cout<< "fptr "<< i <<"address :"<<Vfptr_Bs[i] <<endl;
        fun f = (fun)Vfptr_Bs[i];
        f();
    }

 

三、虚继承

虚继承和虚函数不相关!!!

虚继承是喂了解决菱形继承问题的一种手段。

1、菱形继承问题

class Vbptr_classA{
public:
    int a;
};

class Vbptr_classB : public Vbptr_classA{
public:
    int b;
};

class Vbptr_classC : public Vbptr_classA{
public:
    int c;
};

class Vbptr_classD : public Vbptr_classB , Vbptr_classC{
public:
    int d;
};

int main()
{
    Vbptr_classD D;
    D.a = 1;
    return 0;
}

当D调用基类A中的a成员,则会报错

原因是:D实例化时会先实例化B,C 。B,C实例化又会先实例化基类A,则最终,D中会存在两个int a;

2、虚继承

class Vbptr_classA{
public:
    int a;
};

class Vbptr_classB : virtual public Vbptr_classA{
public:
    int b;
};

class Vbptr_classC : virtual public Vbptr_classA{
public:
    int c;
};

class Vbptr_classD : public Vbptr_classB , Vbptr_classC{
public:
    int d;
};


int main()
{
   Vbptr_classD D;
    return 0;
}

观察内存

一看,同样是B里面有A,C里面也有A,后面多了一个基类A。但是细心的就可以发现,B中的A的地址与C中A的地址是与最后面的基类A的地址是一致的。所以我们可以得出结论,以这种方式的虚继承时,B和C中会创建一张虚基表,记录的是基类A的位置,记录方式是以地址+偏移量的方式记录,而真正A的实例化部分会放在最后。则由此就可以解决空间拷贝的问题。

3、虚基表(虚表)的理解

未写完。。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值