• 探究多态的原理与实现:虚函数表、动态绑定与抽象类


    一、多态概念

    多态性(polymorphism)是面向对象编程的一个重要概念,它允许基类的指针或引用在运行时可以指向不同的派生类对象,并根据实际对象的类型调用相应的成员函数。

    简单来说,多态不同继承关系的类对象,调用同一函数,产生了不同的行为

    同时,必须满足以下构成条件才称为多态:

    1. 存在继承关系

    2. 使用基类的指针或引用调用函数

    通过将派生类对象的地址赋值给基类指针或引用,可以在运行时根据实际对象的类型来动态调用适当的函数。这样才能实现多态性,使得同名函数在不同的派生类对象中产生不同的行为。

    1. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

    图例:

    在这里插入图片描述


    二、多态实现(具体)

    2.1 虚函数

    定义:

    class Base {
    public:
        virtual void func() { // 使用 virtual 关键字声明虚函数
            // 函数实现
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    2.2 虚函数 重写

    虚函数的重写是指 派生类中重新实现(覆盖)基类中已经声明为虚拟的函数

    一般重写函数时可以加上override关键字

    代码举例:

    // 基类
    class Base
    {
    public:
    	virtual void func() 
    	{
    		std::cout << "Base::func()" << std::endl;
    	}
    };
    
    // 派生类
    class Derived : public Base 
    {
    public:
    	// void func() override{} 
    	void func()  // 省略了override关键字
    	{
    		std::cout << "Derived::func()" << std::endl;
    	}
    };
    
    int main() 
    {
    	// 使用基类指针指向派生类对象
    	Base* basePtr = new Derived();
    
    	// 动态调用虚函数,根据实际对象类型选择不同的实现
    	basePtr->func(); // 输出: Derived::func() 
    
    	delete basePtr;
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    其中,虚函数的重写有两种特殊情况:

    1. 协变: 指的是派生类重写基类虚函数时,与基类虚函数返回值类型不同

    但是 返回类型是 基类虚函数返回类型的子类型

    代码举例:

    class A{}; // 基类
    class B : public A {}; // 派生类
    
    // 基类Person
    class Person {
    public:
    	virtual A* f() {return new A;}
    };
    
    // 派生类Student
    class Student : public Person {
    public:
    	virtual B* f() {return new B;}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    1. 析构函数重写

    如果 基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写

    (当编译器编译类的时候,它会自动为析构函数生成一个唯一的名称,以便在程序中正确地调用析构函数,所以重写条件成立)

    代码举例:

    class Base {
    public:
        virtual ~Base() {
            cout << "Base的析构函数" << endl;
        }
    };
    
    class Derived : public Base {
    public:
        ~Derived() {
            cout << "Derived的析构函数" << endl;
        }
    };
    
    int main() {
        Base* ptr = new Derived();
        delete ptr;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    输出结果:

    Derived的析构函数
    Base的析构函数
    
    • 1
    • 2

    2.3 override关键字 与 final关键字

    override 关键字

    介绍虚函数重写时我们提到:
    一般函数重写时我们加上override关键字,其作用在于:

    1. 提示编译器,override 关键字在函数声明中使用,可以让编译器知道这是一个重写的虚函数,并在编译期对函数进行检查,如果没有发现重写,编译器会报错
    2. 易读性和易维护性
    3. 预防错误:如果派生类中的函数签名与基类虚函数的签名不匹配,并且没有使用 override 关键字,那么编译器可能会将该函数视为新的虚函数,而不是重写。

    代码举例:

    class Base {
    public:
        virtual void foo() {
            std::cout << "Base 的 foo 函数" << std::endl;
        }
    };
    
    class Derived : public Base {
    public:
        void foo() override {
            std::cout << "Derived 的 foo 函数" << std::endl;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    final 关键字

    final关键字用于在C++中声明不可继承的类、禁止重写的虚函数或禁止Lambda函数进行捕获。这里我们介绍其在虚函数中的用法:

    在虚函数的声明中使用final关键字可以 阻止派生类对该虚函数进行重写

    代码举例:

    class Base {
    public:
        virtual void foo() final {
            // 函数定义
        }
    };
    
    class Derived : public Base {
    public:
        // 下面的代码会引发编译错误,因为Derived试图重写被标记为final的foo函数
        void foo() override {
            // 函数定义
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    2.4 重载、重写(覆盖)、重定义(隐藏)的区别

    在这里插入图片描述

    对于重写,派生类可以不必使用 virtual 关键字
    因为在派生类中重写一个虚函数时,它会自动成为虚函数,无需再次使用 virtual 关键字进行声明。


    三、抽象类

    3.1 概念

    在虚函数的后面写上 =0 ,这个函数称为 纯虚函数

    包含纯虚函数的类叫做抽象类(也叫接口类)

    • 抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。
    • 纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承
    // 抽象类
    class AbstractClass {
    public:
    	// 纯虚函数,没有实现
    	virtual void pureVirtualFunc() = 0;
    
    	// 普通成员函数,有实现
    	void commonFunc() {
    		std::cout << "这是一个普通的成员函数" << std::endl;
    	}
    };
    
    // 派生类
    class ConcreteClass : public AbstractClass {
    public:
    	void pureVirtualFunc() override {
    		std::cout << "派生类实现了纯虚函数" << std::endl;
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    3.2 实现继承 && 接口继承

    1. 实现继承 指的是从父类继承所有的实现代码和数据成员到子类中,使子类能够重用父类的代码和数据。子类可以覆盖或扩展从父类继承来的成员,进一步定制自己的行为。

    普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。

    1. 接口继承 则是与实现无关的继承方式,它只继承从父类继承来的接口规范(方法签名),而没有提供任何实现。子类必须实现所有从父类继承来的接口,并可以根据需要添加新的接口。

    虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。


    四、多态原理

    4.1 虚函数表

    虚函数表 是C++中实现多态性的一种机制。它是一张用于存储虚函数地址的表格,每个包含虚函数的类都会有一个对应的虚函数表。

    • 当一个类声明了虚函数时,编译器会在该类的对象中添加一个指向虚函数表的指针,通常被称为 虚函数指针(vptr)

    虚函数表是一个静态的数据结构,在程序运行时被创建,并且与类的对象无关。

    虚函数表中保存了虚函数的地址,它是一个由指针组成的数组,每个指针都指向对应虚函数的实际代码。子类继承了父类的虚函数表,并可以通过修改虚函数表中的指针来实现对虚函数的覆盖或扩展。这样,在通过基类指针或引用调用虚函数时,实际执行的是根据对象类型确定的子类中的虚函数。


    我们看下面的代码,试问sizeof(Base)是多少

    class Base
    {
    public:
    	virtual void Func1()
    	{
    		cout << "Func1()" << endl;
    	}
    private:
    	int _b = 1;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 在上面的例子中:Base 是一个基类,它包含一个虚函数 Func1() 和一个私有成员变量 _b。而虚函数和非虚函数对于类的大小没有影响。

    • C++中,sizeof(Base) 的结果是编译器在编译时计算出来的类型的大小(以字节为单位),故为4。

    为什么虚函数和非虚函数对于类的大小没有影响?
    因为 C++ 实现了一种叫做虚表(vtable)的机制,用来支持虚函数的动态派发。

    在这里插入图片描述

    /// 在之前代码的基础上,加上虚函数Func2(),非虚函数Func3()
    /// 只有虚函数Func1()被重写
    
    // 基类
    class Base
    {
    public:
    	virtual void Func1() // Func1(),被派生类重写
    	{
    		cout << "Base::Func1()" << endl;
    	}
    	virtual void Func2()
    	{
    		cout << "Base::Func2()" << endl;
    	}
    	void Func3()
    	{
    		cout << "Base::Func3()" << endl;
    	}
    private:
    	int _b = 1;
    };
    
    // 派生类
    class Derive : public Base
    {
    public:
    	virtual void Func1()
    	{
    		cout << "Derive::Func1()" << endl;
    	}
    private:
    	int _d = 2;
    };
    
    int main()
    {
    	Base b;
    	Derive d;
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    运行代码,观察下面的监视窗口:
    在这里插入图片描述
    得出以下结论:

    1. 基类b对象派生类d对象 虚表是不一样的,由于Func1完成了重写,所以d的虚表中存的是重写的 Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
    2. 由于Func2继承下来后是虚函数,则放进虚表,Func3由于不是虚函数,被继承下来但不会放进虚表。
    3. 虚函数表本质 是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。

    关于虚表生成:

    派生类的虚表生成:

    1. 先将基类中的虚表内容拷贝一份到派生类虚表中。
    2. 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数。
    3. 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

    虚函数,虚表的存放位置:

    • 我们知道:虚表存的是虚函数指针,不是虚函数虚函数和普通函数一样的,都是存在于代码段,只是虚函数的指针存到了虚表中
    • 而类对象中存的不是虚表,存的是虚表指针。那么虚表存在哪,根据上面监视窗口所示,在vs下是存在代码段的

    4.2 原理解释

    C++中,当一个类声明了虚函数时,编译器会在该类的对象中添加一个指向虚函数表的指针,通常被称为虚函数指针(vptr)。虚函数表是一个静态的数据结构,包含了所有虚函数的地址。子类继承了父类的虚函数表,并可以通过修改虚函数表中的指针来实现对虚函数的覆盖或扩展。这样,在通过基类指针或引用调用虚函数时,实际执行的是根据对象类型确定的子类中的虚函数。

    由于C++中的引用和指针都支持动态绑定,因此可以通过基类引用或指针来实现多态。当程序通过基类指针或引用调用虚函数时,实际执行的是根据动态类型确定的子类中的虚函数。这意味着同样的代码对不同类型的对象的行为也是不同的。

    4.3 动态绑定 && 静态绑定

    上文提到了 绑定的概念:C++中,绑定(Binding)指的是将函数调用与函数实现关联起来的过程

    静态绑定(Static Binding)和动态绑定(Dynamic Binding)是两种不同的绑定方式。

    静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载。


    五、单继承与多继承关系的虚函数表

    5.1 单继承中的虚函数表

    // 基类
    class Base {
    public:
    	virtual void func1() { cout << "Base::func1" << endl; } // 被派生类 重写
    	virtual void func2() { cout << "Base::func2" << endl; }
    private:
    	int a;
    };
    // 派生类
    class Derive :public Base {
    public:
    	virtual void func1() { cout << "Derive::func1" << endl; }
    	virtual void func3() { cout << "Derive::func3" << endl; }
    	virtual void func4() { cout << "Derive::func4" << endl; }
    private:
    	int b;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    对于上面的代码,当我们进行调试,会发现监视窗口中看不到,因为监视窗口隐藏了这两个函数,我们利用下面的代码打印出虚表函数来查看信息。

    在这里插入图片描述

    typedef void(*VFPTR) (); // 声明函数指针
    void PrintVTable(VFPTR vTable[])
    {
    	// 依次取虚表里的虚函数指针(vfptr)地址,并打印
    	// 通过调用可以看出存的函数
    	cout << "虚表地址-> " << vTable << endl;
    	for (int i = 0; vTable[i] != nullptr; ++i)
    	{
    		printf("第%d个虚函数地址: 0x%x,-> ", i, vTable[i]);
    		VFPTR fun = vTable[i];
    		fun(); // 执行虚函数
    	}
    	cout << endl;
    }
    
    int main()
    {
    	Base b;
    	Derive d;
    
    	// 使用指针类型转换和取地址运算符& 获取 b、d 对象的虚函数表指针
    	VFPTR* vTableb = (VFPTR*)(*(int*)&b);
    	PrintVTable(vTableb); // 打印虚函数表存储的虚函数指针地址
    
    	VFPTR* vTabled = (VFPTR*)(*(int*)&d);
    	PrintVTable(vTabled);
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    执行上面代码后,有以下结果:

    在这里插入图片描述

    5.2 多继承中的虚函数表

    下面的代码,演示了多继承的虚函数表结构:

    #include 
    using namespace std;
    
    // 基类A
    class A {
    public:
        virtual void func1() { cout << "A::func1" << endl; }
        virtual void func2() { cout << "A::func2" << endl; }
    };
    
    // 基类B
    class B {
    public:
        virtual void func3() { cout << "B::func3" << endl; }
        virtual void func4() { cout << "B::func4" << endl; }
    };
    
    // 派生类C,多继承自A和B
    class C : public A, public B {
    public:
        virtual void func5() { cout << "C::func5" << endl; }
        virtual void func6() { cout << "C::func6" << endl; }
    };
    
    typedef void(*VFPTR) ();
    
    void PrintVTable(VFPTR vTable[])
    {
        cout << "虚表地址-> " << vTable << endl;
        for (int i = 0; vTable[i] != nullptr; ++i)
        {
            cout << "第" << i << "个虚函数地址: " << vTable[i] << ",-> ";
            VFPTR fun = vTable[i];
            fun(); // 执行虚函数
        }
        cout << endl;
    }
    
    int main()
    {
        C c;
    
        // 使用指针类型转换和取地址运算符& 获取 c 对象的虚函数表指针
        VFPTR* vTableC = (VFPTR*)(*(int*)&c);
        PrintVTable(vTableC);
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    5.3 菱形继承 && 虚拟继承

    菱形继承与虚拟继承

  • 相关阅读:
    机器学习算法基础--聚类问题的评价指标
    Linuxd中常见命令
    APP 页面秒开优化方面总结~
    二叉树与二叉搜索树的公共祖先 力扣 (Python)
    python一招完美搞定Chromedriver的自动更新
    腾讯云入侵
    java+springboot+vue电子数码产品商城推荐系统9wwcp
    MongoDB的备份和恢复
    React面试题总结(二)
    家政按摩上门服务小程序搭建
  • 原文地址:https://blog.csdn.net/Dreaming_TI/article/details/133891545