深入理解C++虚函数

面向对象三大特性:封装、继承、多态

这篇博客主要讲的就是C++是如何实现多态的

三个字 虚函数

在C++中,多态表示“以一个public基类的指针(或引用),寻址出一个派生类的对象”

关于指针和引用的区别,可以看我的这篇博客C++指针和引用的区别

这篇博客将涵盖C++虚函数的方方面面,带你领略C++虚函数背后的机制和原理

1、为什么虚函数一定要通过指针或者引用来调用才能实现多态?

To get polymorphic behavior in C++, the member functions called must be virtual and objects must be manipulated through pointers or references.

一个对象所能表现的类型,只有它本身的类型。但一个指针或引用所表现的类型,既可以是基类,也可以是派生类

通过一个对象调用虚函数,编译器在编译期就根据该对象的类型找到了函数的地址,而通过指针或引用调用虚函数,编译器不知道该指针和引用所指的真正类型,它必须等到执行期的时候,通过指针调用虚函数表指针,调用存放于虚函数表内相应的虚函数,从而实现真正的多态

1)为什么通过一个对象调用虚函数,编译器不通过像以指针或引用调用虚函数那样等待执行期再调用虚函数呢?

嗯,这是一个好问题。想解答这个问题,我们需要去看看背后的C++对象模型

举个例子,假如我们有两个类A和B,A中有一个虚函数a(),B是A的派生类,B重写了自己版本的a()

class A
{
public:
    void virtual a()
    {
        cout<<"A"<<endl;
    }
private:
    int pa;
};

class B
{
public:
    void virtual a()
    {
        cout<<"A turn to B"<<endl;
    }
    void virtual b()
    {
        cout<<"B"<<endl;
    }
private:
    int pb;
};

那么如果有以下语句

    A a;
    B b;
    a=b;

我们想想,在执行完a=b;之后,a对象到底发生了什么事


显然,b对象遭遇到了切割,也就是说,a对象只保留了b对象的A类部分,而B类部分,全部被切割

注意到a对象的A类部分有个虚函数表指针,那么你可能会说,如果b对象把虚函数表指针复制过去了,那么a对象不就可以通过虚函数表指针找到相应的虚函数表,并调用相应的虚函数了吗?

第一、编译器在初始化及赋值操作之间做出了仲裁。编译器必须确保如果某个对象含有一个或一个以上的虚函数表指针,那些虚函数表指针的内容不会被基类对象初始化或改变

也就是说,无论是A a=b;还是A a(b);还是a=b;,a对象的虚函数表指针都不会被直接设为b对象的虚函数表指针,编译器会根据a对象的实际类型适当的设置虚函数表指针

第二、即使通过一些技巧,把a对象的虚函数表指针设为b对象的虚函数表指针,但如果你还是通过a对象调用虚函数,而不是通过a对象的指针或引用调用虚函数,你调用的虚函数依旧是编译器在编译期就决定了的特定为A类的虚函数,这没有为什么,所有的C++编译器都是这么干的

那么如果我们把派生类的指针复制给基类的指针,又会发生什么事情呢?

很简单,编译器只需要简单的替换一下指针就好了

一个指针或一个引用之所以支持多态,是因为它们并不引发内存中任何与类型有关的内存操作,会受到改变的,只有它们所指向的内存的“大小和内容解释方式”而已

也就是说,把b对象的指针赋值给a对象的指针的时候,a对象的指针就指向b对象,不引起任何内存内容的改变,但a对象的指针由于是A类型的,它能指向的内存的大小只能是一个A类的大小,那就意味着虽然a对象的指针指向b对象的起始地址,但它并不能解释整个b对象,它能解释的,只有b对象的A类部分,你不能用a对象的指针调用类b对象另外定义B类的函数和数据

“指向不同类型的指针”间的差异,既不在其指针表示法不同,也不在其内容(代表一个地址)不同,而是在其所寻址出来的对象类型不同。也就是说,“指针类型”会教导编译器如何解释某个特定地址中的内存内容及其大小

现在你应该对虚函数的实现的机制有个大概的认识了,但具体的实现是怎样的呢

1)单一继承的时候,无论继承深度有多深,都只会有一个虚函数表指针


这也就是说,派生类对于基类的虚函数,有以下三种处理的情况

1、直接继承基类的虚函数,不改写
2、按自己的需要改写基类的虚函数
3、自己额外增加虚函数

前面两种情况很好理解,第三种情况,当派生类自己增加额外的虚函数的时候,是自己增加一个额外的虚函数表指针,指向新的虚函数表,里面存放自己额外的虚函数,还是在原来的虚函数表中开辟新的位置存放呢?答案是后者

也就是说,假如我有个A类、B类和C类,B类继承A类,C类继承B类,但是A类没有定义虚函数,B类也没有定义虚函数,而C类定义了自己的虚函数,那么这个虚函数表指针会放在哪里呢?答案是放在基类。准确的说,是第一个基类,也就是A类。如果把虚函数表指针放在B类专属区域中,那么就无法通过A类指针调用C类的虚函数,而把虚函数表指针放在A类中,既可以通过A类指针调用C类的虚函数,也可以通过B类指针调用C类的虚函数,因为B类继承A类,自然有A类的虚函数表指针


上面所述的是单一继承的情况,如果是多重继承的时候,又会有一点不一样了

单一继承提供了一种很自然的多态的形式,是关于继承体系中的基类和派生类之间的转换。你会看到基类和派生类的对象都是从相同的地址开始,其间差异只在于派生类比较大,用以容纳它自己的非静态的成员数据

2)多重继承的问题主要发生于派生类对象和其第二或后继的基类对象之间的转换

class A
{
public:
    void virtual a()
    {
        cout<<"A"<<endl;
    }
private:
    int pa;
};

class B
{
public:
    void virtual a()
    {
        cout<<"A turn to B"<<endl;
    }
    void virtual b()
    {
        cout<<"B"<<endl;
    }
private:
    int pb;
};

class C:public A,public B
{
public:
    void virtual a()
    {
        cout<<"A turn to C"<<endl;
    }
    void virtual b()
    {
        cout<<"B turn to C"<<endl;
    }
    void virtual c()
    {
        cout<<"C"<<endl;
    }
};

什么意思呢?简单的说,假设我有A类、B类和C类,C类继承A类和B类,那么如果我要将C类指针赋值给A类指针,就只需简单的赋值就好,因为C类指针的起始地址和A类指针的起始地址是一样的。但我想将C类指针赋值给B类指针,就不能简单替换指针,因为C类指针的起始地址和B类指针的起始地址不一样,需要将C类指针加上适当的偏移才能得到B类指针

    A* a=new A();
    B* b=new B();
    C* c=new C();
    a=c;
    b=c;
    cout<<a<<endl;//0x68d900
    cout<<b<<endl;//0x68d908
    cout<<c<<endl;//0x68d900

可以看到,虽然我将c指针直接赋值给b指针,但两者的地址是不一样的,你观察会发现,两者地址相差了8个字节,这正好是A类的大小,一个虚函数表指针加上一个int类型的A类成员

也就是说,当我们写下b=c;的时候,编译器暗地里帮我们改成b=c?c+sizeof(A):0;,这个注意不能直接赋值,因为b可能为0,当b为0时,把sizeof(A)赋值给a肯定是不对的


你可能会说,上面都是些长篇大论,要是有代码佐证就好了

#include<iostream>

using namespace std;

class A
{
public:
    void virtual a()
    {
        cout<<"A"<<endl;
    }
private:
    int pa;
};

class B
{
public:
    void virtual a()
    {
        cout<<"A turn to B"<<endl;
    }
    void virtual b()
    {
        cout<<"B"<<endl;
    }
private:
    int pb;
};

class C:public A,public B
{
public:
    void virtual a()
    {
        cout<<"A turn to C"<<endl;
    }
    void virtual b()
    {
        cout<<"B turn to C"<<endl;
    }
    void virtual c()
    {
        cout<<"C"<<endl;
    }
private:
    int pc;
};
int main()
{
    void(*fun)();//定义一个函数指针,该函数指针的返回值和参数都和我们的虚函数一样
    C* c=new C();
    fun=(void(*)())*((int*)*(int*)(c));
    fun();//A turn to C,输出第一个虚函数表的第一个函数
    fun=(void(*)())*((int*)*(int*)(c)+1);
    fun();//B turn to C,输出第一个虚函数表的第二个函数
    fun=(void(*)())*((int*)*(int*)(c)+2);
    fun();//C,输出第一个虚函数表的第三个函数
    cout<<*((int*)*(int*)(c)+3)<<endl;//-8

    fun=(void(*)())*((int*)*((int*)(c)+sizeof(A)/4));//由第一个虚函数表指针加上一个偏移获得
    fun();//B turn to C,输出第二个虚函数表的第一个函数
    fun=(void(*)())*((int*)*((int*)(c)+sizeof(A)/4)+1);
    fun();//C,输出第二个虚函数表的第二个函数
    cout<<*((int*)*((int*)(c)+sizeof(A)/4)+2)<<endl;//0,表示这是最后一个虚函数表
}

猜你喜欢

转载自blog.csdn.net/fordreamyue/article/details/79502064