• C++中的多态(下)


    🧸🧸🧸各位大佬大家好,我是猪皮兄弟🧸🧸🧸
    在这里插入图片描述

    一、C++11当中的final和override

    final

    1.修饰类,表示最终类,不能被继承
    2.修饰虚函数,表示该虚函数不能被重写

    这个场景是很少见的,因为虚函数写出来就是为了重写去构成多态

    class Car
    {
    public :
    	virtual void Drive() final
    	{}
    };
    class Benz:public Car
    {
    public:
    	virtual void Drive()
    	{
    		//报错,因为基类的函数(函数名,参数列表,返回值相同)被final修饰
    		//不能被重写
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    override

    检查子类的虚函数是否完成重写,没有完成就会报错
    检查的是该函数是否完成基类某个函数的重写
    如下面的代码,drive并没有完成某个函数的重写,即报错

    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
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    二、重载&重定义(隐藏)&重写(覆盖)

    三个概念的对比:

    重载:在同一作用域下,仅仅是参数列表不同的函数构成重载

    重定义(隐藏):派生类的同名函数和基类的同名函数构成隐藏(他们是在两个作用域的,因为子类和父类的作用域是单独的,其次,同名函数只要没有构成重写就是隐藏)

    重写(覆盖):子类通过虚函数重写来达成,此函数需要满足条件:返回值相同或构成重写的协变,函数名相同,参数列表相同(virtial通过在父类创建虚表,继承下来后子类重写来改变虚表当中的对应函数地址,构成多态调用的时候就去调用虚表(虚函数表)的函数

    三、抽象类(接口类)

    概念:在虚函数的后面写上=0,则这个函数为纯虚函数,包含纯虚函数的类叫做抽象类(也叫做接口类),抽象类不能够被实例化派生类继承后也不能直接实例化出对象,只有重写纯虚函数才能够被实例化。

    纯虚函数规范了:派生类想要实例化必须重写,另外纯虚函数更体现出了接口继承。

    在现实生活中没有对应实体的适合定义为抽象类,比如植物,植物是一个大类,再比如说车

    class Car
    {
    public:
    	virtual void Drive() =0;//纯虚函数
    };
    class BWM
    {
    public:
    	//不能够被实例化,实例化报错
    };
    class Benz
    {
    public:
    	virtual void Drive() 
    	{
    		cout<<"开车"<<endl;
    	}
    	//实例化成功
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    四、接口继承和实现继承

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

    例题

    class A
    {
    public:
    	virtual void func(int val =1){std::cout<<"A->"<<val<<std::endl;}
    	virtual void test(){func();}
    };
    class B:public A
    {
    public:
    	void func(int val =0){std::cout<<"B->"<<val<<std::endl;}
    };
    int main(int argc,char*argv[])
    {
    	B*p = new B
    	p->test();
    	return 0;
    }
    //答案是B->1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    解释:
    首先,p是子类的指针,不构成多态,但是调用test是需要传参的,参数类型是Athis,因为创建的是B对象,所以传过去的是Bthis,因为赋值兼容转换,所以,已经被隐式转换成立A*this,再去调用func,那么这里就构成了多态(父类指针或者引用去调用虚函数),所以调用的是B的func。

    那子类的缺省值不是0吗,为什么输出的是1???
    因为虚函数是接口继承,继承下来的是接口,我只是重写他的实现,那为什么子类可以不写virtual呢?除了virtual本来就是需要用来重写的之外,重要的原因就是虚函数是接口继承,直接继承下来了,所以不用写他其实也有。

    如果上面是A*p =new B;结果也是一样的,只是少了一层隐式类型转换。只要是构成了多态(重写),或者是用子类的xx去调用(隐藏/重定义),结果都是B->1,除了用A的对象来调用或者直接创建A的xx。

    五、如何打印虚函数表

    1.虚函数表的类型是函数指针数组

    typedef void(*VFPTR)();//这是函数指针类型重定义的格式
    //typedef void(*)() VFPTR//这是过不了的,函数指针优点特殊
    
    • 1
    • 2

    2.打印虚函数表中的函数地址

    typedef void(*VFPTR)();
    void PrintfVftable(VFPTR table[])
    {
    //因为虚表再对象的头四个字节或者头八个字节
    	int index =0;
    	while(index<=2)
    	//因为不知道虚函数表用什么结尾,暂时只能这样
    	{
    		printf("vft[%d]:%p\n",index,table[index]);
    		table[index]();
    		index++;
    	}
    }
    int main()
    {
    	Person p1;
    	Person p2;
    	Student s1;
    	Student s2;
    	PrintfVftable((VFPTR*)(*(int*)&s1));
    	//解释
    	//取到s1的地址,转成int*,32位下拿到前4个,然后解引用,因为地址int*的
    	//解引用就是一串数字,不然int*转换成VFPTR*转换不过去。
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    六、多继承当中的虚函数表

    多继承会有多个父类拷贝下来的多个虚表,那么我重写的话重写的谁的呢?

    多个虚表

    class Base1
    {
    public:
    	virtual void func1(){cout<<"Base1::func1"<<endl;}
    	virtual void func2(){cout<<"Base1::func2"<<endl;}
    private:
    	int b1=1;
    };
    class Base2
    {
    public:
    	virtual void func1(){cout<<"Base2::func1"<<endl;}
    	virtual void func2(){cout<<"Base2::func2"<<endl;}
    private:
    	int b2=2;
    };
    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 PrintfVftable(VFPTR table[])
    {
    	int index =0;
    	while(index<=2)
    	{
    		printf("vft[%d]:%p\n",index,table[index]);
    		table[index]();
    		index++;
    	}
    }
    int main()
    {
    	Derive d;
    	PrintfVftable((VFPTR*)(*(int*)&d));
    	cout<<sizeof d<<endl;
    	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

    在这里插入图片描述
    通过调试发现,d重写时,把每个虚表的对于函数指针都改变了,因为为了实现多态,不知道用哪个父类的指针或者引用来调用,所以只能都改。那么d新增的func3虚函数放在哪儿呢?我发现VS的调试窗口是看不见的。

    其实我们通过内存窗口可以发现,func3只进入第一个虚表,因为只有创建子类的xx才能够调用到func3,虚表的查找是按顺序来的,我能在第一个虚表中找到func3,我还放在其他虚表中干嘛呢

    如何打印第二个虚表

    步骤和上面类似,主要是传参问题

    //比如一个子类对象d,Derive继承父类Base1 Base2
    1.(VFPTR*)*(int*)((Base1*)&d+1);//跳过base1
    2.(VFPTR*)*((int*)&d+sizeof(Base1));//跳过base1
    3.Base2*ptr=&d;
    	(VFPTR*)*(int*)(ptr);//切片/切割
    
    • 1
    • 2
    • 3
    • 4
    • 5

    内存对齐

    这其中还有结构体内存对齐的问题,详见
    链接:结构体内存对齐
    这里简单进行一下说明
    每个成员进行对齐
    按照min(该成员大小,默认对齐数)
    都对齐完了之后还有一个整体的对齐
    按照min(结构体中最大成员,默认对齐数)

    在vs下,默认对齐数是8
    在linux下,默认对齐数是4
    可以通过#pragma pack(n)的方式来修改默认对齐数
    #pragma pack(1)就是不进行内存对齐

    指针偏移

    在这里插入图片描述
    我们发现,重写进两个虚表中的func1不一样,我同一个函数怎么会不一样呢??

    我们再通过打印函数地址来看看
    注意:
    打印普通函数地址:printf(“%p”,func1)即可
    打印成员函数地址: printf(“%p”,&类域::func1),这是语法规定
    cout不方便打印,即便是cout<<&类域::func1也不能成功打印

    在这里插入图片描述
    看出来存的地址和我们的func1地址并不相同
    解释:
    最终都是调用的func1,但是外包装不同
    普通调用也不是直接就存的函数地址,存储的只是调用的众多步骤中的某一句指令的地址而已

    指针偏移就是在后面的过程中会加以修正,最终会调用同一个函数
    因为有了指针偏移,所以存的地址不一样(存的是中间过程命令的地址)

    指针偏移需要结合汇编来看

    菱形继承–子类必须重写

    class A
    {
    public:
    	virtual void func()
    	{
    		//结果1
    	}
    };
    class B:public A
    {
    public:
    	virtual void func()
    	{
    		//结果2
    	}
    };
    class C:public A
    {
    public:
    	virtual void func()
    	{
    		//结果3
    	}
    };
    class D:public B,public C
    {
    public:
    
    };
    
    • 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

    虽然菱形继承可以通过虚继承的方式来完成对象的数据冗余和二义性的处理,但是对于成员函数来说,像上面这种情况,D d对象调用的时候又发了二义性,那么就要求菱形继承必须去发生隐藏。而一般为了接口的统一性,会把子类的写成三同,就重写了,所以附带的,也覆盖了多个虚表中的func函数,也能完成多态。

    所以,要多态的话,子类就必须重写

    在这里插入图片描述

  • 相关阅读:
    Python xlwings打开excel表格进行最大化
    修改文字对齐方式,居中改为底部对齐
    尚硅谷Vue3入门到实战,最新版vue3+TypeScript前端开发教程
    鸿蒙ArkTS声明式开发:跨平台支持列表【组件快捷键事件】
    openlayes + vue 最新版本 实现 轨迹移动动画
    Reactive反应式编程及使用介绍
    超详细的hadoop完全分布式安装及xsync等各个脚本
    生产者消费者模型
    C++学习笔记02-面向对象及类的引入
    Jenkinsfile 同时检出多个 Git 仓库
  • 原文地址:https://blog.csdn.net/zhu_pi_xx/article/details/128048207