• C++之多态二三事



    什么是多态
    所谓的多态就是就是有多种形态。对应的就是同一个行文,会出现不同的形态。具体到一个生活中的实例就是:在火锅店消费的时候。同样是结账,但是却有不同的结果

    普通用户:全价
    vip会员:88折
    学生:69折

    类似这样的例子还有很多。而在面向对象程序设计中也需要多态,所以c++语言中也引进了多态!


    重写的概念
    首先,在谈多态之前不得不提一下一个概念—>重写,而在谈到重写这个概念之前,就要提到隐藏。来看下面这段代码:

    class A
    {
    public:
    	void func()
    	{
    		cout << "A::func()" << endl;
    	}
    };
    class B :public A
    {
    public:
    	void func()
    	{
    		cout << "B::func()" << endl;
    	}
    };
    int main()
    {  
    	B b;
    	b.func();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    先看运行结果
    在这里插入图片描述
    很显然,这里的func和父类中的同名函数func构成的是隐藏的关系 再来看下面这一段代码

    class A
    {
    public:
    //virtual关键字声明函数,此时的函数就变成了虚函数
    	virtual void func()
    	{
    		cout << "A::func()" << endl;
    	}
    };
    class B :public A
    {
    public:
    	virtual void func()
    	{
    		cout << "B::func()" << endl;
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    此时的B类的func和A类的func不再是隐藏关系了,而是更加特殊的重写关系 两个函数构成重写有如下的要求:

    1.两个函数要分别在两个类的作用域,并且这两个类必须要有继承关系
    2.父类的同名函数必须是虚函数
    3.构成重写关系的两个函数返回值,函数。参数名相同
    4.父子函数构成协变也构成重写

    满足以上四个条件,子类和父类的同名函数构成的关系就是重写,而重写关系和隐藏关系是互斥的,也就是说如果两个函数构成重写,那么这两个函数就不是隐藏关系!
    那么前面三个条件已经演示过了,那么我们重点来看最后一个重写的特例:两个函数构成协变也是重写的关系 首先,我们通过一段代码来看看什么是协变

    class A
    {
    public:
    	virtual A* func()
    	{  
    		cout << "A::func()" << endl;
    		return nullptr;
    	}
    };
    class B :public A
    {
    public:
    	virtual B* func()
    	{   
    		cout << "B::func()" << endl;
    		return nullptr;
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    在上面的代码里面,这里的返回值一个是A类的指针,一个是B类的指针,不符合我们重写的第二条的规则:虚函数的返回值的类型不同。但是这里同样构成重写关系 但是前提是,这两个函数仅仅只能是返回值不一样,并且返回值分别是必须是指针或者引用,并且返回的指针或者引用必须构成父子关系!
    假设把返回类型改成下面这样:

    class A
    {
    public:
    	virtual A func()
    	{  
    		cout << "A::func()" << endl;
    		return A();
    	}
    };
    class B :public A
    {
    public:
    	virtual B func()
    	{   
    		cout << "B::func()" << endl;
    		return B();
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    在这里插入图片描述
    那么,在实际的应用中,协变应用相对较少。这也可以认为是c++语法设计的一个"坑",所以我们只要了解有协变这种特殊的情况就可以了。尽量在实际的设计中严格按照前面的3个条件设计重写。
    值得一提的是,如果父类函数里面的虚函数加了virtual,即使子类的函数前面不加vitual也是虚函数。这也是c++设计的一个不足的地方,虽然可以不加,但是良好的习惯还是在子函数前面也加上virtual。


    C++11新增的两个关键字
    虽然继承可以实现代码的复用,但是在有的实际的场景下,有一些类不适合设计继承,所以为了解决这个问题。c++11引进了final关键字,接下来我们就通过代码来看看final关键字。

    //final关键字
    //final关键字,修饰类的时候,这个类不能被继承
    class A final
    {
    public:
    	void func()
    	{
    		cout << "A::func()" << endl;
    	}
    };
    class B :public A
    {
    public:
    };
    int main()
    {   
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    在这里插入图片描述
    final不仅可以修饰类,还可以修饰父类的虚函数,表示这个虚函数不能被继承。

    class A
    {
    public:
    //final修饰虚函数,这个函数无法被子类继承
    	virtual void func() final
    	{
    		cout << "A::func()" << endl;
    	}
    };
    class B:public A
    {
    public:
    	virtual void func()
    	{
    		cout << "B::func()" << endl;
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    在这里插入图片描述
    这是c++11新增的final关键字。但是又有一种情况,有的时候父类的虚函数必须被子类重写,但是由于各种原因没有进行重写,造成了严重的错误。为了避免这种情况发生带来的损失。 c++11引入了一个新的关键字—>override

    //override的作用:强制检查子类是否重写父类的虚函数
    class A
    {
    public:
    	 void func() 
    	{
    		 cout << "A::func()" << endl;
    	}
    };
    class B :public A
    {
    public:
    //父类同名函数不是虚函数,所以这里并没有重写父类的func
    	virtual void func() override
    	{
    		cout << "B::func()" << endl;
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    在这里插入图片描述
    对比final和override我们不难可以得出下面的结论

    1.final是在父类中使用,限制子类不能继承父类
    2.override是在子类中使用,强制子类要重写父类中的虚函数


    重载,隐藏,重写三者的区别
    结合前面的对于重写的分析,接下来我们来对比一下三个概念:

    1.重载:两个函数在同一个作用域,两个函数根据不同的参数的类型和顺序修饰形成不同的函数地址.
    2.隐藏:两个函数分别在在基类和派生类的作用域,当前作用域的同名函数隐藏外部作用域同名的函数,隐藏也叫做重定义.
    3.重写:两个同名虚函数在基类和派生类的作用域,函数的返回值,参数完全相同(协变例外).这时候两个函数构成重写关系.


    多态如何触发
    接下来,我们就来谈一谈在c++里面怎么触发多态.要触发多态有如下两个条件:

    1.基类的指针或者引用指向派生类对象
    2.派生类的虚函数完成了对基类虚函数的重写.

    这两个条件都很重要,缺少任何一个都不能构成多态的调用
    接下来,我们用代码来验证多态的触发条件.

    class A
    {
    public:
    	virtual void func()
    	{
    		cout << "A::func()" << endl;
    	}
    };
    class B:public A
    {
    public:
    	virtual void func()
    	{
    		cout << "B::func()" << endl;
    	}
    };
    int main()
    {
    	A a;
    	B b;
    	A* pa = &a;
    	pa->func();
    	cout << endl;
    	B* pb = &b;
    	pb->func();
    	cout << endl;
    	pa = pb;
    	pa->func();
    	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

    先来看对应的调用结果:
    在这里插入图片描述
    首先,第1和第2个易于理解,A类的指针指向A类对象的地址调用的自然就是A类的func,对于pb也是如此.而当pa指向B类对象的地址时,首先满足多态的第一个条件:基类的指针或引用指向派生类对象,接着派生类完成了对基类虚函数的重写,满足多态的两个条件!所以最后调用的是B类的重写的虚函数.


    抽象类的概念
    有的时候,一些东西是没办法具体化出实际对象的。而在c++语言里面也提供了抽象类的这一个概念.
    所谓的抽象类,就是没办法实例化出对象的类!接下来我们来看怎么从语法上定义一个抽象类.

    //}
    //抽象类:只有纯虚函数的类是抽象类
    //纯虚函数:虚函数声明后面=0
    class A
    {
    public:
    	virtual void func() = 0;
    };
    //抽象类不能创建对象
    int main()
    {
          A a;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    在这里插入图片描述
    假如这个时候有一个类B来继承这个抽象类A,但是没有重写里面的纯虚函数,那么这个B也是抽象类.

    /抽象类:只有纯虚函数的类是抽象类
    //纯虚函数:虚函数声明后面=0
    class A
    {
    public:
    	virtual void func() = 0;
    };
    class B:public A
    {  
    public:
    };
    int main()
    {
    	//A a;
    	B b;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    在这里插入图片描述
    而当子类重写纯虚函数以后,子类才能创建对象!

    /抽象类:只有纯虚函数的类是抽象类
    //纯虚函数:虚函数声明后面=0
    class A
    {
    public:
    	virtual void func() = 0;
    };
    //B类必须重写才能创建对象!
    class B:public A
    {  
    public:
    	virtual void func()
    	{
    		cout << "B::func()" << endl;
    	}
    };
    int main()
    {
    	//A a;
    	B b;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    值得一提的是:纯虚函数是可以有函数体的,但是实际意义并不大,因为纯虚函数是一定要被重写的!而g++下会直接报错
    对比override我们可以得到如下的结论;

    1.override只是检查是否重写,而抽象类是强迫子类必须重写
    2.override修饰函数,子类依旧可以创建对象


    虚拟构函数
    前面我们说过,父子析构函数会构成隐藏关系.但是有的时候,子类直接管理堆上的动态资源.单纯只用父类的析构函数没办法解决问题!所以这时候就需要使用子类特定的析构函数,所以我们把父类的析构定义成virtual

    class A
    {
    public:
    	A(int a)
    		:_a(a)
    	{}
    	~A()
    	{
    		cout << "~A" << endl;
    	}
    protected:
    	int _a;
    };
    class B :public A
    {
    public:
    	B()
    		:A(0)
    		,pb(new int(4))
    	{}
    	~B()
    	{  
    		cout << "~B" << endl;
    		delete pb;
    		pb = nullptr;
    	}
    private:
    	int* pb;
    };
    int main()
    {  //赋值兼容的转换
    	A* pa = new B;
    	//delete会调用析构函数
    	delete pa;
    	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

    运行结果:
    在这里插入图片描述
    可以看到这里只调用了A的析构函数所以这里存在严重的内存泄露问题! 所以我们希望调用到B的析构函数,所以我们就要把父类的析构函数声明成虚函数

    class B :public A
    {
    public:
    	B()
    		:A(0)
    		,pb(new int(4))
    	{}
    	virtual ~B()
    	{  
    		cout << "~B" << endl;
    		delete pb;
    		pb = nullptr;
    	}
    private:
    	int* pb;
    };
    int main()
    {  
    	A* pa = new B;
    	delete pa;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    在这里插入图片描述
    可以看到,这里我们调用到了子类的析构函数处理了资源,所以我们的建议是基类的析构函数尽量声明成虚函数,基类的成员的访问控制符尽量用保护!


    以上就是本文的主要内容,希望大家可以一起进步.

  • 相关阅读:
    零基础学Python--机器学习(一):人工智能与机器学习概述
    刷爆LeetCode 字节技术官亲码算法面试进阶神技太香了
    【Docker容器】Docker容器日志查询工具dozzle的安装与使用
    tomcat为什么要自定义类加载器?
    C专家编程 第7章 对内存的思考 7.3 虚拟内存
    C++ Balanced Braces
    球谐函数实现环境光照漫反射实践
    vue响应式原理
    除了「加机器」,其实你的微服务还能这样优化
    浅记录一下MATLAB安装心得
  • 原文地址:https://blog.csdn.net/qq_56628506/article/details/126390179