C++ 虚函数详解

文章目录

    • 虚函数的作用
    • 虚函数的原理:虚函数表指针和虚函数表
    • 虚函数表是如何存储虚函数的
      • 单重继承
      • 多重继承
    • 析构函数和虚函数
    • 纯虚函数

虚函数的作用

虚函数是为了实现动态多态。多态是指为不同的数据类型提供统一的接口,分为静态多态和动态多态。静态多态包括函数重载和模板函数,动态多态是:指针或引用类型可以根据运行中实际指向的派生类型的不同,来执行不同派生类的方法。

举个例子,有一个基类 Character 表示职业统称,玩家会选择职业战士 Warrior 或者魔法师 Magician,然后所有的逻辑例如攻击,防守都会根据玩家选择的不同职业有不同的效果。

class Character{
public:
    virtual void attack(){}	//虚函数实现多态
};

class Warrior : public Character{
public:
    void attack(){
        cout << "Slash the Monster"<< endl;	//斩击怪物
    }
};

class Magician : public Character{
    void attack(){
        cout << "Fire the Ball"<< endl;	//释放火球
    }
};

int main(){

    Character* cter = new Character();
    //玩家选择战士
    cter = new Warrior();
	//所有的业务逻辑都可以用cter->method()的方式执行,相当于统一接口
    cter->attack();
    cter->defense();
}

如果没有使用动态多态的话,根据玩家的选择,要重新声明对象,并且写相应的逻辑代码,很麻烦

int main(){

    //选择战士
    Warrior* wior = new Warrior();
    wior->attack();
    wior->defense();
    ...
    //选择魔法师又要重写另一套代码
}

注意:虚函数的继承是永久性的,也就是说父类声明某个函数为虚函数,在孙子类,孙子孙子类中也还是虚函数,仍然可以实现动态多态:根据实际指向的类型来选择执行的方法。不过为了可读性和意义明确,派生类也建议加上virtual关键字。

虚函数的原理:虚函数表指针和虚函数表

如果不存在虚函数的话,对象执行方法的过程:根据对象声明的类型以及方法的名称,去代码区找到对应的方法,然后调用。

Character* cter = new Warrior();
cter->attack();	//没有虚函数的话,调用的还是Character类的attack()方法

为此,引入了虚函数表指针vptr以及虚函数表。每个对象有一个虚函数表指针,每个类有一个虚函数表。虚函数表指针大小为4字节,存储在对象的起始地址。虚函数表中存储的是对应类的所有虚函数。

有了虚函数之后的对象执行方法过程:
1、先根据对象本身的虚函数表指针找到虚函数表
2、在虚函数表中查找有没有该方法,有的话就执行,没有找到的话跳转到3
3、根据声明的类型和方法名称去代码区找该方法(原本的过程)。

虚函数表是如何存储虚函数的

以下列举了单重继承和多重继承两种方式下虚函数表是怎么存储虚函数的。

单重继承

1、子类继承了父类,那么虚函数表一开始是和父类一样的。
2、如果子类重写了父类的某个虚函数,那么在虚函数表中,子类重写的虚函数地址会覆盖父类相应的虚函数地址。
3、如果子类新增加了某个虚函数,这个虚函数的地址会添加到虚函数表的尾部。

用代码举例说明以及验证:

class Base{
    public:
        virtual void f(){
            cout << "Base: f()" << endl;
        }
        virtual void g(){
            cout << "Base: g()" << endl;
        }
        virtual void h(){
            cout << "Base: h()" << endl;
        }

};
//重写了Base的f()方法,新增了i()方法
class Drive: public Base{
    public:
        virtual void f(){
            cout << "Drive: f()" << endl;
        }
        virtual void i(){
            cout << "Drive: f1()" << endl;
        }

};
//重写了Drive继承的Base的g()方法,新增了j()方法
class End: public Drive{
    public:
        virtual void g(){
            cout << "End: g()" << endl;
        }
        virtual void j(){
            cout << "End: g1()" << endl;
        }

};

验证代码,有点长,可跳过直接看结果

int main(){

    Base b;
    Drive d;
    End e;

    typedef void (*func)(void);
    func f;
    
    int* pBase = (int*)*(int*)(&b);	//此时pBase 存储的是虚函数表的起始地址

    cout << "顺序执行Base的虚函数表中的函数:" << endl;

    f = (func)*(pBase + 0);		//*pBase 得到虚函数表中第一个函数地址
    f();
    f = (func)*(pBase + 1);
    f();
    f = (func)*(pBase + 2);
    f();

    cout << endl << "顺序执行Drive的虚函数表中的函数:" << endl;

    int* pDrive = (int*)*(int*)(&d);
    f = (func)*(pDrive + 0);
    f();
    f = (func)*(pDrive + 1);
    f();
    f = (func)*(pDrive + 2);
    f();
    f = (func)*(pDrive + 3);
    f();

    cout << endl << "顺序执行End的虚函数表中的函数:" << endl;

    int* pEnd = (int*)*(int*)(&e);
    f = (func)*(pEnd + 0);
    f();
    f = (func)*(pEnd + 1);
    f();
    f = (func)*(pEnd + 2);
    f();
    f = (func)*(pEnd + 3);
    f();
    f = (func)*(pEnd + 4);
    f();

    return 0;
}

输出结果

顺序执行Base的虚函数表中的函数:
Base: f()
Base: g()
Base: h()

顺序执行Drive的虚函数表中的函数:
Drive: f()
Base: g()
Base: h()
Drive: i()

顺序执行End的虚函数表中的函数:
Drive: f()
End: g()
Base: h()
Drive: i()
End: j()

关于验证过程中的一些代码,如int* pDrive = (int*)*(int*)(&d); f = (func)*(pDrive + 0);
不太清楚的可以看另一篇文章 逐步解析(func)*((int*)*(int*)(&d))

Drive重写了父类Base的Base:f()方法,所以Drive:f()覆盖了旧函数;Drive新增的i()方法添加至Drive虚函数表末尾。
End重写了父类Drive的Base:g()方法,所以End:g()覆盖了旧函数;End新增的j()方法添加至End虚函数表末尾。
C++ 虚函数详解_第1张图片

多重继承

1、子类继承了多少个父类,子类就拥有多少个对应的虚函数表。
2、子类对象实例的内存空间是这样的:基类1的部分、基类2的部分…自己的部分,顺序存储,其中基类1包括他的虚函数指针以及成员变量,基类2也包含他的虚函数指针以及成员变量…每个虚函数指针指向对应虚函数表。虚函数指针大小为四个字节。
注意!:多重继承下的子类对象的多个虚函数指针在各个基类部分开始处,而不是连续存放(特殊情况)。
3、对于第一个虚函数表,如果子类中有第一个虚函数表中不存在的虚函数(即使是重写的其他虚函数表的函数),将会被视为新函数添加到第一个虚函数表的末尾。例如我重写了第二个虚函数表中的某个函数,而这个函数在第一个虚函数表中不存在,那么这个函数不止会覆盖第二个虚函数表中的旧函数,还会被视为新增的函数添加到第一个虚函数表的末尾。
4、如果子类重写了某个父类的虚函数,将会在对应的虚函数表中进行覆盖。

看代码比较好懂:

class Base1{
    public:
        virtual void f(){
            cout << "Base1: f()" << endl;
        }
        virtual void g(){
            cout << "Base1: g()" << endl;
        }
        virtual void h(){
            cout << "Base1: h()" << endl;
        }

};
class Base2{
    public:
        virtual void i(){
            cout << "Base2: i()" << endl;
        }
        virtual void j(){
            cout << "Base2: j()" << endl;
        }
        virtual void k(){
            cout << "Base2: k()" << endl;
        }

};
//分别重写了Base1的f()和Base2的i()
//新增加了d()
class Drive: public Base1, public Base2{
    public:
        virtual void f(){
            cout << "Drive: f()" << endl;
        }
        virtual void i(){
            cout << "Drive: i()" << endl;
        }
        virtual void d(){
            cout << "Drive: d()" << endl;
        }
};

验证代码,可直接看输出结果:

int main(){

    Drive* d = new Drive();
    typedef void (*func)(void);
    func f;

    cout << "顺序执行派生类Drive每个虚函数表中的函数:" << endl << endl << "第一个虚函数表:" << endl;

    int* pDrive = (int*)*(int*)d;	//此时pDrive存储着第一个虚函数表的地址
    f = (func)*(pDrive + 0);
    f();
    f = (func)*(pDrive + 1);
    f();
    f = (func)*(pDrive + 2);
    f();
    f = (func)*(pDrive + 3);
    f();
    f = (func)*(pDrive + 4);
    f();
    cout << endl << "第二个虚函数表:" << endl;

    pDrive = (int*)*((int*)d + 1);		//此时pDrive存储着第二个虚函数表的地址

    f = (func)*(pDrive + 0);
    f();
    f = (func)*(pDrive + 1);
    f();
    f = (func)*(pDrive + 2);
    f();
    
    return 0;
}

//输出
顺序执行派生类Drive每个虚函数表中的函数:

第一个虚函数表:
Drive: f()
Base1: g()
Base1: h()
Drive: i()
Drive: d()

第二个虚函数表:
Drive: i()
Base2: j()
Base2: k()

C++ 虚函数详解_第2张图片

1、新增:对于第一个虚函数表而言,类Drive的Drive:i()和Drive:d()方法都是新增的,因为第一个虚函数表没有这两个函数,所以Drive:i()和Drive:d()会添加到第一个虚函数表的末尾。
2、覆盖:Drive重写了Base1:f()和Base2:i(),所以在虚函数表1中Drive:f()覆盖了Base:f(),在虚函数表2中Drive:i()覆盖了Base2:i()。

析构函数和虚函数

考虑这样一种情况:

class Base{
public:
    Base(){};
    ~Base(){	//正确做法是加上virtual
        cout << "Base decnst" << endl;
    }
};

class Drive: public Base{
public:
    Drive(){};
    ~Drive(){
        cout << "Drive decnst" << endl;
    }
};

int main(){

    Base* b = new Base();
    b = new Drive();
    delete b;
}
//输出
Base decnst

因为Base的析构函数不是虚函数,所以当父类指针指向子类,调用方法时,执行的还是父类的方法。只析构了父类对象,没有析构子类对象。
所以在这种情况下,需要把父类的析构函数声明为virtual,这样才会调用子类的析构函数,正确地析构对象,上述代码改正后输出:

Drive decnst
Base decnst

先析构子类对象,再析构父类对象。

纯虚函数

纯虚函数的意义是提供一个公共接口,供派生类实现。格式为在虚函数后面加上"=0"。例如:
virtual void attack() = 0;
1、带有纯虚函数的类是抽象类,抽象类不能实例化,但是可以声明为指针类型或者引用。
2、如果派生类没有实现纯虚函数的话,那么该派生类也是抽象类。

参考:C++ 虚函数表解析 陈皓
在此博客的基础上撰写了这篇文章,并对其中一些错误的地方进行了修改。如有些地方可能没注意概念说错了,以及多重继承中的虚函数指针不一定是连续存放,而是存放在子类对象实例的各个基类部分开始处(正确)。

你可能感兴趣的:(C++,多态,指针,c++)