• C++多态


    C++多态

    1. 多态的介绍

    多态是c++三大特性之一,多态的作用是使不同的对象调用同一函数有不同的效果。

    2. 重写

    2.1 一般重写

    重写(又称覆盖)是多态中的一个重要概念,重写是实现多态的条件之一。

    重写指两个函数在基类和派生类中,同时满足函数名相同、参数类型相同、返回值相同,且函数必须是虚函数。这样的两个函数构成重写。

    但要注意基类中的虚函数必须要使用virtual关键字,但派生类的虚函数不需要有virtual也能构成虚函数,**因为虚函数重写的是函数体的实现,而函数的结构(函数名、返回值、参数等)用的是基类的。**但在实际使用中建议派生类的虚函数也使用virtual关键字以增加代码的可读性。

    class A
    {
    public:
    	virtual int func(int x, int y)
    	{
    		return x + y;
    	}
    };
    
    class B:public A
    {
    public:
    	virtual int func(int x, int y)
    	{
    		return x - y;
    	}
    };
    

    func在A类和B类中构成函数重写。

    2.2 协变重写

    协变是重写的一种特殊情况,它是指在满足重写的其他条件下,两个函数的返回值不同。**协变重写的返回值必须是类对象的指针或引用。**即基类虚函数返回基类基类对象的指针或引用,派生类返回派生类对象的指针或引用。

    class A
    {
    public:
    	virtual A* func()
    	{
    		return new A(*this);
    	}
    };
    
    class B:public A
    {
    public:
    	virtual B* func()
    	{
    		return new B(*this);
    	}
    };
    

    3.3 析构函数的重写

    如果基类的析构函数为虚函数,此时派生类的析构函数只要定义,无论是否加关键字virtual,都与基类的析构函数构成函数重写。虽然基类和派生类的析构函数名字不同,但编译器对析构函数的名字做了特殊处理,程序编译后析构函数会被统一改名成destructor,这样析构函数的重写便满足了重写的规则。

    析构函数的重写在多态的使用中是很要必要的,如果派生类向内存申请了空间,在析构的时候如果不使用重写很容易造成内存泄漏。所以在使用多态的程序中建议析构函数定义为虚函数,防止发生内存泄漏。

    对比两段代码的运行结构:

    在这里插入图片描述
    在这里插入图片描述

    3. 多态的使用方法

    多态要在继承的前提下使用。

    条件:

    1. 基类和派生类要完成虚函数的重写。
    2. 父类的指针或者引用去调用虚函数。

    如有一个person类和一个student类,student类是person类的派生类。这两种对象在调用买火车票代码时,person类对象是全价票价,而student类是半价票价。实现这种情况的方法就是多态。

    #include  
    using namespace std;
    
    class person
    {
    public:
    	void BuyTicek()
    	{
    		cout << "普通人-全价票" << endl;
    	}
    };
    
    class student :public person
    {
    public:
    	virtual void BuyTicek()
    	{
    		cout << "学生-半价票" << endl;
    	}
    };
    
    void func(person* p)
    {
    	p->BuyTicek();
    }
    
    int main()
    {
    	person p;
    	student s;
    
    	func(&p);
    	func(&s);
    }
    

    在这里插入图片描述

    4. override 和final

    4.1 override

    override和final是c++11提供的功能。override能检查派生类中的虚函数是否和基类的虚函数构成重写,是一种检查代码是否编写正确的手段

    #include  
    using namespace std;
    
    class A
    {
    public:
    	 void func()
    	{
    
    	}
    
    };
    
    class B :public A
    {
    public:
    	virtual void func() override
    	{
    
    	}
    };
    
    int main()
    {
    	A a;
    	B b;
    
    	return 0;
    }
    

    在这里插入图片描述

    在这里插入图片描述

    4.2 final

    final可以使一个基类的虚函数不能被重写:

    #include  
    using namespace std;
    
    class A 
    {
    public:
    	 virtual void func() final{}
    };
    class B :public A
    {
    public:
    	virtual void func(){}
    };
    
    int main()
    {
    	A a;
    	B b;
    	return 0;
    }
    

    在这里插入图片描述

    final也可以使一个类不能被继承。在final之前c++98提供一种使类不能被继承的方法:

    把类的构造函数写在私有作用域,导致派生类无法访问基类的构造函数,无法实例化。但缺点是代码只有在实例化的时候才会报错。

    而final关键字使程序在编译的时候就能够产生报错并提供准确的错误信息。

    class A final
    {
    public:
    	 virtual void func(){}
    
    };
    
    class B :public A
    {
    public:
    	virtual void func(){}
    };
    int main()
    {
    	A a;
    	B b;	
    	return 0;
    }
    

    在这里插入图片描述

    5. 多态的原理

    一个类中如果有虚函数,那么这个类实例化时,内部除了成员变量,还会有一个**虚函数表指针,简称虚表指针。**虚表指针是一个函数指针数组,它指向这个类中所有的虚函数。同类型的对象共用一张虚表(指向的地址相同),不同类型的对象虚表不同。

    在这里插入图片描述

    在32位下,一个函数指针数组指向第一个元素的地址,所以占4个字节,int类型占4个字节,char类型占一个字节,共9个字节。又因为对齐值为4,最终大小为12

    在这里插入图片描述

    同类型的对象共用一张虚表

    因为派生类可以看作是特殊的基类,所以在使用多态的时候,派生类可以用基类的指针或引用来调用虚函数。在调用多态时,调用生成的指令就会去对应的虚表中找对应的虚函数来调用。

    上面已经提到,相同的类型使用同一张虚表,不同类型使用不同的虚表。派生类虽然可以看成特殊的基类,但本质与基类不同,所以基类和派生类的虚表不同。运行时,调用的指令指向基类就调用基类的虚表,指向派生类就调用派生类的虚表

    #include  
    using namespace std;
    
    class A
    {
    public:
    	virtual void func1(){}
    	virtual void func2(){}
    
    private:
    	int i;
    	char ch;
    };
    class B :public A
    {
    public:
    	virtual void func1(){}
    	virtual void func2(){}
    
    private:
    	int i;
    	char ch;
    };
    
    int main()
    {
    	A a;
    	B b;
    	return 0;
    }
    

    在这里插入图片描述

    基类和派生类的虚表不同,因为它们本质不是相同的类。

    6. 重载、重定义(隐藏)和重写(覆盖)的区别

    1. 函数重载,要求两个函数在同一作用域函数名相同,返回值相同,参数不同

    2. 重定义(隐藏),要求两个函数分别在基类和派生类的作用域,函数名相同

    3. 重写(覆盖),要求两个函数分别在基类和派生类的作用域函数名、参数、返回值相同(协变重写除外),且两个函数必须是虚函数。

    在基类和派生类中,如果有同名函数,如果它不构成重定义(隐藏)就一定构成重写(覆盖)。

    7. 抽象类

    7.1 抽象类的定义

    包含纯虚函数的类叫做抽象类。纯虚函数是一个只有声明没有定义的虚函数,它的声明方法为:

    在这里插入图片描述

    抽象类不能实例化出对象,它用来存放纯虚函数,而纯虚函数的意义是间接强制派生类重写虚函数。抽象类为基类时,它的派生类如果不重写纯虚函数的定义,那么基类的纯虚函数就被继承下来,导致派生类也无法实例化出对象。

    #include  
    class A
    {
        virtual void func() = 0;
    };
    

    在这里插入图片描述

    在这里插入图片描述

    7.2接口继承和实现继承

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

    8. 多态与虚函数

    1. 虚函数和普通函数一样,都是存在代码段。而类中指向虚函数的虚表存在常量区(vs下),在构造函数中完成初始化。

    2. 子类有虚函数,继承的父类也有虚函数,那么子类的虚表就在父类中,子类对象就不需要单独建立虚表。

    3. 多继承情况下,派生类的虚表在第一个父类的虚表中。

  • 相关阅读:
    如何在快应用中实现滑动操作组件
    串口服务器通信时间试验
    java计算机毕业设计小学教师课程管理系统源程序+mysql+系统+lw文档+远程调试
    【Linux】从零开始学习Linux基本指令(三)
    FPGA学习专栏-串口通信(xinlinx)
    ERP是数字化的底座,你数字化转型的巨人肩膀
    一文解读 SmartX 超融合虚拟化下的网络 I/O 虚拟化技术
    可灵王炸更新,图生视频、视频续写,最长可达3分钟!Runway 不香了 ...
    pandas read_json时ValueError: Expected object or value的解决方案
    Pandas:sort_index、sort_values方法的使用
  • 原文地址:https://blog.csdn.net/2301_80163789/article/details/138862864