• [C++]多态(上)



    一、多态的概念

    多态:通俗来说就是多种形态
    多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态
    静态的多态:函数重载(同一个函数可以实现多种不同的形态,看起来调用同一个函数有不同的行为,实际上调用的都是不同的函数)这里的静指的是编译时实现的(原理)
    动态的多态:一个父类的引用或是指针去调用同一个函数,如果传递不同的对象,会调用不同的函数。动态:运行时实现(原理)
    本质:不同人做同一件事,结果不同

    class Person {
    public:
    	virtual void BuyTicket() { cout << "买票-全价" << endl; }
    };
    class Student : public Person {
    public:
    	virtual void BuyTicket() { cout << "买票-半价" << endl; }
    	/*
    	这里函数名相同也不一定构成隐藏
    	子类中满足三同(函数名、参数、返回值)的虚函数叫做重写(覆盖)
    	继承里的虚继承virtual和这里用的virtual虚函数没有关系
    	*/ 
    };
    void Func(Person& p) // 父类的引用
    {
    	p.BuyTicket(); // 多态
    }
    int main()
    {
    	Person ps;
    	Student st;
    	Func(ps);
    	Func(st); // 子类对象也可以给父类对象
    	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

    swap函数模板实际上不是多态,模板只是实例化成了多个函数,函数构成了重载;而是函数模板实例化生成了多份函数,这时候才是多态


    二、多态的定义及实现

    2.1 多态构成条件

    在继承中要构成多态需要两个条件

    1. 必须通过基类的指针或者引用调用虚函数(只有这样子才可以接受父类对象或是子类对象)
    2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
    // 传对象呢?——这时候就不构成多态了
    void Func(Person p) // 原本是void Func(Person& p)
    {
    	p.BuyTicket(); // 多态
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    构成多态:传的是哪一个类型的对象,调用的就是这个类型的虚函数——与对象有关
    不构成多态:调用的就是p类型的函数——与类型有关

    void Func(Person* p) // 父类的引用
    {
    	p->BuyTicket(); // 多态
    }
    
    • 1
    • 2
    • 3
    • 4

    传指针也是可以的

    2.2 虚函数

    虚函数:即被virtual修饰的类成员函数称为虚函数。

    class Person {
    public:
    	virtual void BuyTicket() { cout << "买票-全价" << endl; }
    };
    
    • 1
    • 2
    • 3
    • 4

    静态成员函数不能加virtual,只能是类的非静态成员函数才能是虚函数

    2.3 虚函数的重写

    虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。

    子类中满足虚函数的重写要求:首先是虚函数,而且要求三同(函数名、参数、返回值)
    虚函数是重写的条件之一

    class Person {
    public:
    	void BuyTicket() { cout << "买票-全价" << endl; }
    };
    class Student : public Person {
    public:
    	void BuyTicket() { cout << "买票-半价" << endl; }
    };
    void Func(Person& p)
    {
    	p.BuyTicket(); // 这里构成隐藏,为什么还能调用这个函数?
    }
    int main()
    {
    	Person ps;
    	Student st;
    	Func(ps);
    	Func(st); // 子类对象也可以给父类对象
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    为什么这里构成了隐藏,还是能够调用全票函数?
    在这里插入图片描述

    隐藏与父类无关,隐藏是影响的子类
    子类中有两个BuyTicket()函数,想让子类去调用父类的需要加域限定符
    
    • 1
    • 2

    在这里插入图片描述
    若参数不同,则不会实现多态
    在这里插入图片描述

    class Person {
    public:
    	virtual void BuyTicket(char ch) { cout << "买票-全价" << endl; }
    };
    class Student : public Person {
    public:
    	virtual void BuyTicket(int i) { cout << "买票-半价" << endl; }
    };
    void Func(Person& p) // 父类的引用
    {
    	p.BuyTicket(10); 
    	// 这里与隐藏不隐藏无关,因为隐藏是子类对象需要考虑的,这里只考虑是否会构成多态
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这里插入图片描述

    这种情况会调用哪一个?——还是父类的(因为不构成多态了)
    不构成多态,调用的就是p的类型——与类型有关系
    构成多态后,与p的类型就无关了,传哪一个类型的对象,就会去调用哪一个类型的虚函数

    2.3.1 虚函数重写的两个意外

    重写要求返回值相同有一个例外:协变
    协变:要求返回值是父子关系的指针或引用

    // 当返回值是父子关系的对象
    class A
    {};
    class B : public A
    {};
    class Person {
    public:
    	virtual A BuyTicket() { cout << "买票-全价" << endl; return A(); }
    };
    class Student : public Person {
    public:
    	virtual B BuyTicket() { cout << "买票-半价" << endl; return B(); }
    };
    void Func(Person& p) // 父类的引用
    {
    	p.BuyTicket(); // 多态
    }
    int main()
    {
    	Person ps;
    	Student st;
    	Func(ps);
    	Func(st); 
    	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

    在这里插入图片描述
    匿名对象不能用引用返回

    // 当返回的是指针或是引用就是多态了
    class A
    {};
    class B : public A
    {};
    class Person {
    public:
    	virtual A* BuyTicket() { cout << "买票-全价" << endl; return nullptr; }
    };
    class Student : public Person {
    public:
    	virtual B* BuyTicket() { cout << "买票-半价" << endl; return nullptr; }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这里插入图片描述
    因此遇到虚函数重写必须要求返回值相同这句话是要看有没有更适合的选项,如果没有就认为它是错的

    class A
    {};
    class B : public A
    {};
    class Person {
    public:
    	virtual Person* BuyTicket() { cout << "买票-全价" << endl; return nullptr; }
    };
    class Student : public Person {
    public:
    	virtual Student* BuyTicket() { cout << "买票-半价" << endl; return nullptr; }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    自己的父亲关系指针也可以
    在这里插入图片描述
    析构函数的重写(基类与派生类析构函数名字不同)
    析构函数是虚函数构成重写吗?——构成

    class Person {
    public:
    	virtual ~Person() { cout << "~Person()" << endl; }
    };
    class Student : public Person {
    public:
    	virtual ~Student() { cout << "~Student()" << endl; }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    他们俩如果不是虚函数构成隐藏——析构函数名被特殊处理成了destructor
    在这里插入图片描述
    他们有完成正常的析构,s对象有两个析构,先析构子,再析构父
    在这里插入图片描述
    普通对象:析构函数是否是虚函数,是否完全重写都正确调用了,因此普通对象不需要写为虚函数,下面的情况才要写为虚函数

    class Person {
    public:
    	~Person() { cout << "~Person()" << endl; }
    };
    class Student : public Person {
    public:
    	~Student() { cout << "~Student()" << endl; }
    };
    // 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
    int main()
    {
    	Person* p1 = new Person; // new是由operator new 开空间  +   构造函数
    	Person* p2 = new Student;
    
    	// delete是由 析构函数 + operator delete构成
    	//            p1->destructor()
    	delete p1; // 父类对象调用父类析构函数
    	//            p2->destructor()
    	delete p2; // 子类对象调用父类析构函数(子类有子类和父类两部分,这时候只销毁了父类)
    	return 0; 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    在这里插入图片描述
    因为他不符合多态,所以没去调用子类的(都调用父类的析构是因为p1、p2类型都是Person),想要让它符合多态,就需要去加上虚函数

    class Person {
    public:
    	virtual ~Person() { cout << "~Person()" << endl; }
    };
    class Student : public Person {
    public:
    	virtual ~Student() { cout << "~Student()" << endl; }
    };
    int main()
    {
    	Person* p1 = new Person; 
    	Person* p2 = new Student;
    	delete p1; 
    	delete p2; 
    	return 0; 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在这里插入图片描述
    这时候因为构成了多态,p2指向的student,就可以去调用student了

    int main()
    {
    	Person* p1 = new Person;
    	Student* p2 = new Student; // 这里就不需要实现虚函数,因为没有给父类指针管理,而且这里也不是多态
    	delete p1; 
    	delete p2;
    	return 0; 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    动态申请的父子对象,如果给了父类指针管理,那么需要析构函数是虚函数,完成重写,构成多态,才能正确的调用析构函数,在其他场景析构函数是不是虚函数都可以正确调用析构函数

    class Person {
    public:
    	virtual void BuyTicket() { cout << "买票-全价" << endl; }
    	virtual ~Person() { cout << "~Person()" << endl; }
    };
    class Student : public Person {
    public:
    	virtual void BuyTicket() { cout << "买票-半价" << endl; }
    	virtual ~Student() { cout << "~Student()" << endl; }
    };
    void Func(Person& p)
    {
    	p.BuyTicket();
    }
    int main()
    {
    	Person* p1 = new Person; 
    	Student* p2 = new Student;
    
    	delete p1; 
    	delete p2;
    
    	Person ps;
    	Student st;
    	Func(ps);
    	Func(st);
    	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

    在这里插入图片描述
    当父子都不是虚函数的时候,不构成多态
    在这里插入图片描述
    当父类不是虚函数,子类是虚函数也不构成多态(上图)
    在这里插入图片描述
    当父是虚函数,子不是虚函数构成多态,虽然子类没写virtual,但是他是先继承父类虚函数的属性、再完成的重写,那么他也算是虚函数

    虚函数的重写允许两个都是虚函数或者父类是虚函数再满足三同就构成了重写
    这里是C++不是很规范的地方,建议都要写上virtual

    当修改访问限定符后呢?

    class Person {
    public:
    	virtual void BuyTicket() { cout << "买票-全价" << endl; }
    	virtual ~Person() { cout << "~Person()" << endl; }
    };
    
    
    class Student : public Person {
    	void BuyTicket() { cout << "买票-半价" << endl; }
    
    public:
    	virtual ~Student() { cout << "~Student()" << endl; }
    };
    void Func(Person& p)
    {
    	p.BuyTicket();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    这里还构成多态吗?
    在这里插入图片描述
    虽然是私有,因为多态,所以这里还是能够调用到,因为继承下来了属性

    为什么子类可以不去写virtual,本质上是因为析构函数,设计初衷是父类析构函数+virtual就不构成多态,没有调用到子类析构函数,内存泄漏的场景

    建议我们自己写的时候都加上virtual,这样子肯定没问题

    2.4 C++11 override和final

    若要设计一个不能被继承的类如何做?
    C++98是将构造函数弄为私有

    class A
    {
    private:
    	A(int a = 0)
    		:_a(a)
    	{}
    
    protected:
    	int _a;
    };
    class B : public A
    {
    
    };
    int main()
    {
    	B bb; // 子类要初始化父类,需要去调用父类的构造函数——私有是不可见的所以子类就没办法去构造了
    	A aa; // A也构造不了了——这时候是有方法(单例方法,之后会讲)
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    这样子就可以创建A类的对象

    class A
    {
    private:
    	A(int a = 0)
    		:_a(a)
    	{}
    
    public:
    	static A CreateObj(int a = 0)
    	{
    		// new A;
    		return A(a);
    	}
    
    protected:
    	int _a;
    };
    class B : public A
    {
    
    };
    int main()
    {
    	// B bb; // 子类要初始化父类,需要去调用父类的构造函数
    	A aa = A::CreateObj(10); // 如果不是静态的就调用不到了,需要用对象来调用这个函数,但是调用这个函数就需要是对象,不需要对象就可以去调用静态的函数
    	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

    C++11后就解决了这种问题
    使用final表明这个类为最终类,这样子就不能去继承了

    class A final
    {
    protected:
    	int _a;
    };
    class B : public A
    {
    
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这里插入图片描述
    final还可以去限制重写

    class C
    {
    public:
    	virtual void f() final
    	{
    		cout << "C::f()" << endl;
    	}
    };
    class D : public C
    {
    public:
    	virtual void f()
    	{
    		cout << "D::f()" << endl; // 这里就会报错,无法完成重写
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    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
    • 10

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

    重载

    1. 要求两个函数在同一个作用域内
    2. 函数名相同,参数不同,对返回值没有要求

    重写(覆盖)

    1. 两个函数分别在基类和派生类的作用域
    2. 函数名、参数、返回值都必须要求相同(协变例外)
    3. 两个函数都必须是虚函数

    重定义(隐藏)

    1. 两个函数分别在基类和派生类的作用域
    2. 函数名相同
    3. 两个基类和派生类的同名函数不构成重写就是重定义
  • 相关阅读:
    【ManageEngine】什么是SIEM
    Leetcode2760. 最长奇偶子数组
    基于单片机火灾报警器仿真设计
    rxjs 关于防抖的方法列举说明
    大数据项目之电商数仓、实时数仓同步数据、离线数仓同步数据、用户行为数据同步、日志消费Flume配置实操、日志消费Flume测试、日志消费Flume启停脚本
    【C语言】.c源文件从编译到链接生成可.exe执行程序的过程
    数据结构刷题:第四天
    BadTokenException: Unable to add window -- token null is not valid
    SpringBoot Filter过滤器的使用篇
    面向对象建模的三种模型是什么,各自的主要功能有哪些,分别可用UML的哪些图来描述?
  • 原文地址:https://blog.csdn.net/weixin_51304981/article/details/126012139