• [C++]多态(下)



    一、抽象类

    1.1 概念

    在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口(函数的声明就相当于是接口,与别人对接)继承。
    纯虚函数一般是只声明,不实现,因为实现没有价值,但是是可以实现的

    class Car
    {
    public:
    	// 纯虚函数是只声明,不实现
    	// virtual void Drive() = 0;
    	virtual void Drive() = 0
    	{
    
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    int main()
    {
    	// Car c; 不行
    	Car* pc; // 可以用指针或是引用
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    对比下图两种情况
    在这里插入图片描述
    在这里插入图片描述
    为什么p是空指针/野指针,还可以去调用f()?——因为函数名没有放在对象里面,对象里面只有成员变量;因此p->f();并没有解引用,只是把p传给了this,传递一个空指针是没问题的,那么为什么第一种情况会出错?——虚函数的调用要去虚表中查找
    第一种虚函数是放在虚表里,p->Drive调用需要去虚表中找,虚表和虚表的指针在对象里面
    第二种在公共的代码区,没在对象里面,就不存在对空指针的解引用

    class Car
    {
    public:
    	virtual void Drive() = 0
    	{
    		cout << "virtual void Drive() = 0" << endl;
    	}
    };
    
    class Benz :public Car
    {
    public:
    	virtual void Drive()
    	{
    		cout << "Benz-舒适" << endl;
    	}
    };
    
    int main()
    {
    	// Car* p = new Car; // 不能实例化出这个对象
    	Car* p = new Benz; 
    	p->Drive(); // 重写虚函数后,就可以实例化出对象,调用的就是子类的
    	// 指向谁就调用谁
    	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

    因此,纯虚函数一般只声明,不实现,实现没有价值

    抽象一般指的是在现实世界中没有对应的实物,就叫做抽象,一个类型,如果一般在现实世界中没有对应的具体实物,就定义为抽象类比较好

    纯虚函数的类,本质上是强制子类去完成虚函数的重写,override只是在语法上检查了是否完成重写

    1.2 接口继承与实现继承

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

    继承体现的是实现继承


    二、多态的原理

    2.1 虚函数表

    class Base
    {
    public:
    	virtual void Func1()
    	{
    		cout << "Func1()" << endl;
    	}
    	virtual void Func2()
    	{
    		cout << "Func2()" << endl;
    	}
    private:
    	int _b = 1;
    	char _ch = 'A';
    };
    int main()
    {
    	cout << sizeof(Base) << endl; // 这里为多少?12/16
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    有了虚函数后,这个对象里面就多了一个成员
    在这里插入图片描述
    这个成员叫虚函数表指针(指向虚函数),之后按对齐数计算大小

    在这里插入图片描述
    现在就可以解释刚刚的问题,p->Drive为什么会报错
    在这里插入图片描述

    2.2 多态的原理

    class Person {
    public:
    	virtual void BuyTicket() { cout << "买票-全价" << endl; }
    	void f()
    	{
    		cout << "f()" << endl;
    	}
    protected:
    	int _a = 0;
    };
    
    class Student : public Person {
    public:
    	virtual void BuyTicket() { cout << "买票-半价" << endl; }
    
    protected:
    	int _b = 0;
    };
    
    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
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    上述代码如何实现出多态的?

    在这里插入图片描述

    在这里插入图片描述
    在这里插入图片描述
    因此他们两是调用的不同函数

    多态的原理:基类的指针或是引用指向谁,就去谁的虚函数表中找到对应的位置的虚函数进行调用

    那为什么要传指针和引用,而不能是对象?
    在这里插入图片描述
    因此其中一个原因是必须要为指针或是引用

    int main()
    {
    	Person p1;
    	Person p2;
    	Student s1;
    	Student s2;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    同类型的对象,虚表指针是一样的

    在这里插入图片描述
    在这里插入图片描述
    普通函数和虚函数存的位置一样吗?
    一样,都在公共的代码段,只是虚函数要把地址存到虚表,方便实现多态

    void Func(Person& p)
    {
    	p.BuyTicket();
    	// 运行时,是去p指向的对象的虚表中,找到虚函数的地址
    	p.f();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    只有符合多态的条件,才会去虚函数的表中去找,就算是虚函数,但不构成多态也不会去虚函数的表中去找,而是直接确认了函数的地址

    多态调用在编译时是不能确定调用的哪个函数,指向谁是不知道的

    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

    在这里插入图片描述

    1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
    2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
    3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。
    4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
    5. 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
    6. 这里还有一个容易混淆的问题:虚函数存在哪的?虚表存在哪的? 虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?vs下是存在代码段

    在这里插入图片描述


    三、单继承和多继承关系的虚函数表

    3.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;
    };
    
    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

    在这里插入图片描述
    可以观察到虚函数表中只有func1和func2,但是func3和4在哪里?

    在这里插入图片描述

    3.2 多继承中的虚函数表

    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 { // 只存放第一个继承的虚表base1
    public:
    	virtual void func1() { cout << "Derive::func1" << endl; }
    	virtual void func3() { cout << "Derive::func3" << endl; }
    private:
    	int d1;
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    在这里插入图片描述

    可以发现,有两个虚表指针,都存的func1和func2,那么func3在哪里?以及两个func1虽然地址不同,但是最后指向的位置是相同的

    在这里插入图片描述
    在这里插入图片描述
    又转到这里
    在这里插入图片描述
    又进行jmp,多次jmp是要修正this指针的值以及指针的偏移
    在这里插入图片描述
    最后两者的地址一样,第一个是直接调到00C3122B,第二个jmp多次后才跳到;虽然两个fun1()地址不同,但是虚表中存的地址不是函数真正的地址,最终调用的都是同一个函数

    多继承时,子类重写了Base1和Base2虚函数func1,但是虚表中重写的fun1地址的确不一样,但是这没有什么关系,因为最后还是会调到同一个函数

    在这里插入图片描述
    构成多态:运行时决定去调用谁
    不构成多态:编译时决定去调用谁

  • 相关阅读:
    [Leetcode学习-C语言]Two Sum
    ​一文梳理ICML 2022中图机器学习热点和趋势
    广西厂家直销建筑模板,工程用木工板,多层胶合板批发
    Pandas+Pyecharts | 2022年世界500强数据分析可视化
    ✪✪✪宁波国家实验室认可✪✪✪
    【CSS】5分钟带你彻底搞懂 W3C & IE 盒模型
    Android Jetpack Compose之确定重组范围并优化重组
    详解csrf(跨站请求伪造)
    【设计模式】从女娲娘娘到取媳妇
    Cholesterol-PEG-Maleimide,CLS-PEG-MAL,胆固醇-聚乙二醇-马来酰亚胺供应
  • 原文地址:https://blog.csdn.net/weixin_51304981/article/details/126037341