• 【C++笔记】多态的原理、单继承和多继承关系的虚函数表、 override 和 final、抽象类、重载、覆盖(重写)、隐藏(重定义)的对比


    1.final关键字

    引出:设计一个不能被继承的类。有如下方法:

    class A
    {
    private:
    	A(int a=0)
    		:_a(a)
    	{}
    public:
    	static A CreateOBj(int a=0)
    	{
    		return A(a);
    	}
    protected:
    	int _a;
    }
    //简介限制,子类构成函数无法调用父类构造函数初始化
    //子类的构造函数一定去调用父类的构造函数去初始化那一部分
    //而父类的构造函数继承下来后为不可见
    class B:public A
    {}
    int main()
    {
    	A aa=;//构造函数被私有化,报错
    	//可以通过如下方法进行A类对象的创建
    	A aa=A::CreateOBj(10);
    }
    
    • 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

    以上的方法虽然可以实现我们的需求,但是会对类进行强制性的封装,所有我们在C++11中还有更好的方法,通过final关键字来实现。

    1. C++11中final声明表示为最终类,表明该类不能被继承。
    2. 修饰函数,限制函数不能被重写。
    //修饰类
    class A final
    {
    protected:
    	int _a;
    };
    //直接限制报错,A不可被继承
    class B:public A
    {};
    //修饰函数
    class C
    {
    public:
    	virtual void f() final
    	{
    		cout<<"C::f()"<<endl;
    	}
    }
    class D:public C
    {
    public:
    	//直接报错
    	virtual void f(){}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    2. override

    override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

    class Car{
    public:
        virtual void Drive(){}
     };
     
    class Benz :public Car {
     public:
        virtual void Drive() override {cout << "Benz-舒适" << endl;}
     };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    3. 重载、覆盖(重写)、隐藏(重定义)的对比

    • 重载:
      1、两个函数在同一作用域
      2、函数名/参数相同
    • 重写(覆盖):
      1、两个函数分别在基类和派生类的作用域
      2、函数名/参数/返回值都必须相同(协变例外)
      3、两个函数必须是虚函数
    • 重定义(隐藏):
      1、两个函数分别在基类和派生类的作用域
      2、函数名相同
      3、两个基类和派生类的同名函数不构成重写就是重定义

    4.纯虚函数

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

    class Student
    {
    public:
    	virtual void show()=0;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5

    5.抽象类

    5.1概念

    包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承:本质上强制子类去完成虚函数的重写。

     class Car
     {
     public:
        virtual void Drive() = 0;
     };
     
    class Benz :public Car
     {
     public:
        virtual void Drive()
        {
            cout << "Benz-舒适" << endl;
        }
     };
     
    class BMW :public Car
     {
     public:
        virtual void Drive()
        {
            cout << "BMW-操控" << endl;
        }
     };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    5.2接口继承和实现继承

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

    6.多态的原理

    6.1认识虚函数表

    先来看一道经典的问题,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
    • 11

    很多一部分的答案是4Byte,但实际正确答案是8Byte,这是怎么一回事呢?看Visual Studion下的监视窗口。如下图。
    图1
    结论:
    1.除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。
    2.一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
    继续深入:如果一个类的继承了该类,那么派生类中这个表放了些什么呢?

    // 针对上面的代码我们做出以下改造
    // 1.我们增加一个派生类Derive去继承Base
    // 2.Derive中重写Func1
    // 3.Base再增加一个虚函数Func2和一个普通函数Func3
     class Base
     {
     public:
        virtual void 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
    • 43
    • 44

    看Visual Studion下的监视窗口。如下图。
    在这里插入图片描述
    通过对监视窗口的分析可以得出如下结论:

    1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在的部分,另一部分是自己的成员
    2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
    3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。
    4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
    5. 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
    6. 这里还有一个很容易混淆的问题:虚函数存在哪的?虚表存在哪的? 答:虚函数存在虚表,虚表存在对象中。注意上面的回答的错的。 但是很多同学都是这样深以为然的。注意:虚函数表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚函数表中。另外对象中存的不是虚函数表,存的是虚函数表指针。那么虚表存在哪的呢?代码段! 感兴趣的同学可以用如下代码去打印一下虚表的地址,来验证一下。
    typedef void(*VF_PTR)();
    
    //void PrintVFTable(VF_PTR table[])
    // 打印虚函数表中内容
    void PrintVFTable(VF_PTR* table)
    {
    	for (int i = 0; table[i] != nullptr; ++i)
    	{
    		printf("vft[%d]:%p->", i, table[i]);
    		VF_PTR f = table[i];
    		f();
    	}
    	cout << endl << endl;
    }
    int main()
    {
    	Base b;
    	PrintVFTable((VF_PTR*)(*(void**)&b));
    
    	Derive d;
    	PrintVFTable((VF_PTR*)(*(void**)&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

    7.在函数调用过程中,对象先找到虚函数表指针,通过该指针找到虚函数表,再在虚函数表中找到对应的函数指针,通过函数指针找到所需要的哪个函数。如下图所示。
    在这里插入图片描述

    6.2多态的原理

     class Person {
     public:
        virtual void BuyTicket() { cout << "买票-全价" << endl; }
     };
     class Student : public Person {
     public:
        virtual void BuyTicket() { cout << "买票-半价" << endl; }
     };
     
    void Func(Person& p)
     {
        p.BuyTicket();
     }
     
    int main()
     {
        Person Mike;
        Func(&Mike);
        
        Student Johnson;
        Func(&Johnson);
     
        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

    在这里插入图片描述

    1. 观察上图的红色箭头我们看到,p是指向mike对象时,p->BuyTicket在mike的虚表中找到虚函数是Person::BuyTicket。
    2. 观察上图的蓝色箭头我们看到,p是指向johnson对象时,p->BuyTicket在johson的虚表中找到虚函数是Student::BuyTicket。
    3. 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。
    4. 反过来思考我们要达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调用虚函数。反思一下为什么?单纯的对象的话所有的对象的虚函数表都是相同的,利用指针或者引用可以看作指向对象当中父类那一部分的成员或者指针。覆盖使其不同的虚表中,指向的虚函数不同,从而完成多种状态
    5. 看出满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的。
      !!!一图搞懂多态的原理
      在这里插入图片描述

    7.多继承中的虚函数表

     class Base1 {
     public:
        virtual void func1() {cout << "Base1::func1" << endl;}
        virtual void func2() {cout << "Base1::func2" << endl;}
     private:
        int b1;
     };
     
    class Base2 {
     public:
        virtual void func1() {cout << "Base2::func1" << endl;}
        virtual void func2() {cout << "Base2::func2" << endl;}
     private:
        int b2;
     };
     
    class Derive : public Base1, public Base2 {
    public:
     	virtual void func1() {cout << "Derive::func1" << endl;}
     	virtual void func3() {cout << "Derive::func3" << endl;}
    private:
     	int d1;
     	};
     	typedef void(*VFPTR) ();
     	void PrintVTable(VFPTR vTable[])
     	{
     		cout << " 虚表地址>" << vTable << endl;
     		for (int i = 0; vTable[i] != nullptr; ++i)
     		{
     			printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
     			VFPTR f = vTable[i];
     			f();
     		}
     		cout << endl;
     }
     int main()
     {
    	Derive d;
     	VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
     	PrintVTable(vTableb1);
     	VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d+sizeof(Base1)));
     	PrintVTable(vTableb2);
    	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

    在这里插入图片描述
    分析上述图片可以得出如下结论:
    1、多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中
    2、多继承时,子类重写了Base1和Base2虚函数func1,但是虚函数表中重写的func1的地址却不一样,但是没关系,他们最终还是调到同一个函数。这是因为存在偏移量的问题。

  • 相关阅读:
    DDR CTRL介绍
    c语言范例实例
    Android拖放startDragAndDrop拖拽Glide灵活加载堆叠圆角图,Kotlin(6)
    代码随想录训练营day50, 买卖股票的最佳时间III, IV
    Vue2&3全面知识总结六
    Excel常用函数
    【 SQL引擎 - analyze.cpp分析】
    Alins - 化繁为简、极致优雅的WebUI框架
    C语言——个位数为 6 且能被 3 整除但不能被 5 整除的三位自然数共有多少个,分别是哪些?
    Apple 已弃用 NavigationView,使用 NavigationStack 和 NavigationSplitView 实现 SwiftUI 导航
  • 原文地址:https://blog.csdn.net/weixin_51692487/article/details/133894619