(详细)一篇认识C++面向对象特性 —— 多态

之前介绍了C++面向对象的一大特性 —— 继承, 今天我们就来看看另外的一大特性 —— 多态. 话不多说, 直接进入正题.



多态的概念

简单来说,多态就是一个行为、多种状态。

举个栗子:买火车票,都是买票这一行为,普通人只能买成人票,而我们大学生持有学生证就可以买学生票。

再比如:想必大家都玩过抽卡的游戏吧,非酋和欧皇都懂吧,那我就不多说了,这也是多态。



多态的定义及实现

1.实现多态的条件

多态的前提是继承

调用函数产生多态的行为 :
1. 函数必须为虚函数 virtual 函数
2. 虚函数需要在子类中重写
3. 调用虚函数时, 必须用父类指针或者引用调用

上述是三个多态实现的条件,缺一不可


2. 虚函数及虚函数的重写

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

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


3. 代码示例

父类和子类的定义:

//父类
class Person {
public:
	virtual void buyTicket() {
		cout << "普通票 -- 全价" << endl;
	}
};

//子类
class Student : public Person {
public:
	//子类进行虚函数的重写
	virtual void buyTicket() {
		cout << "学生票 -- 半价" << endl;
	}
};

多态和非多态的对比. 父类引用也可,由于引用和普通对象作为参数时, 传参会发生矛盾,故没有给出引用类型

//父类指针调用虚函数, 构成多态
void func(Person* p) {
	p->buyTicket();
}

//父类对象调用虚函数, 不构成多态
void func(Person p) {
	p.buyTicket();
}

测试函数:

void test() {
	Person p;
	Student s;

	Person* pp = &p;
	Student* ps = &s;

	//这就是用指针调了一下对应的函数而已, 不是多态
	pp->buyTicket();
	ps->buyTicket();
	cout << endl;

	// 多态 : 看对象
	func(&p); 
	func(&s);
	cout << endl;

	// 非多态 : 看类型
	func(p);
	func(s);
	cout << endl;
}

运行结果如下:
在这里插入图片描述
可见三组结果, 第一组结果是用对应的指针调用了一下函数而已,与多态无关, 第二组结果是调用func(Person*)函数, 参数不同结果也不同, 明显构成多态. 第三组调用func(Person)函数, 参数不同, 结果相同, 不构成多态.

多态 --- 看对象
func(Person*)函数构成多态
传入父类对象, 就执行父类函数逻辑
传入子类对象, 就执行子类函数逻辑

非多态 --- 看类型
func(Person)函数不构成多态
传入父类对象, 参数匹配, 执行父类函数逻辑
传入子类对象, 参数不匹配, 发生切片操作, 得到的结果还是父类对象, 所以还是执行父类函数逻辑

通过这个栗子想必大家对多态也有了一定的了解

这里说一个不规范的例子:
父类加virtual, 子类不加, 子类构成多态
这是不规范的, 但是我们要知道它也构成多态, 题中人家可能这么写

还有一种情况:
父类不加virtual, 子类加了, 这时子类不构成多态
这里应该没有什么疑问吧, 父类都没有虚函数那必定不构成多态
这里子类加上virtual是为了后面还有类继承它的时候可以构成多态

所以我们自己写代码的时候, 尽量都写出来, 出题人给我们挖坑就算了, 我们自己不要给自己挖坑


4. 虚函数重写的的特殊形式(协变)

上面我们说虚函数重写要满足 — 子类虚函数和父类接口完全相同的函数 (函数名, 参数, 返回值都相同)

但是这里存在一种特殊情况 — 协变(返回值可以不同), 但返回值必须是构成父子继承关系的指针/引用

光说概念不好理解, 我们直接上一段代码, 让大家更好的理解

//先有一对父子继承关系的类
class AA { };
class BB : public AA { };

class Person {
public:
	//返回值AA* , 是父类的指针
	virtual AA* buyTicket() {
		cout << "普通票 -- 全价" << endl;
		return &AA();
	}

};

class Student : public Person {
public:
	//返回值BB*, 是子类的指针
	virtual BB* buyTicket() {
		cout << "学生票 -- 半价" << endl;
		return &BB();
	
};

测试函数:

void func(Person* p) {
	p->buyTicket();
}

void func(Person p) {
	p.buyTicket();
}

void test() {
	Person p;
	Student s;

	Person* pp = &p;
	Student* ps = &s;

	// 多态 : 看对象
	func(&p); 
	func(&s);
	cout << endl;
}

测试结果如下 :
在这里插入图片描述
可以看到, 也是可以构成多态的

值得一提的是, 一般情况下也不使用协变来构成多态, 但是我们要知道这个知识点


5. 关键字 final & override

final关键字 : 修饰的虚函数, 在子类中不能再被重写 (体现了实现继承, 我继承下来直接用, 不必重写)

override : 强制子类的函数必须重写父类的一个虚函数 (体现了接口继承, 我只用你的接口, 不用你的逻辑)
一般用于重写函数, 加一个以便自己区分是隐藏还是重写

class A {
public:
	//final关键字: 修饰的虚函数,在子类中不能再被重写(体现了实现继承)
	virtual void func() final {
		cout << "A::func()" << endl;
	}

	virtual void func2() {
		cout << "A::func2()" << endl;
	}
};

class B : public A {
public:
	//override: 强制子类的函数必须重写父类的一个虚函数(体现了接口继承)
	virtual void func2() override {
		cout << "B::func2()" << endl;
	}
};



析构函数与虚函数

首先先来看一个概念

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。

大概意思就是: 只要父类的析构函数定义为虚函数, 无论如何子类的析构一定是重写了父类的析构, 虽然函数名不尽相同, 编译器在底层会把他们的函数名处理成一样的.
这么做的目的就是为了实现多态.

那为什么非要构成多态呢? 下面会告诉大家 !

这里我们就是回顾一下继承的知识了
首先: 调用子类的构造函数时, 会自动先调用父类的构造, 构造父类.
析构与构造正好相反, 调用子类的析构时, 先调用子类析构, 再调用父类析构, 释放各自的资源

当然, 正常情况下的析构函数调用是没有问题的.
问题就发生在切片的时候, 我们想用父类指针/引用指向子类
如果不把父类的析构函数定义为虚函数, 这时候只能调用父类的析构函数, 但凡子类中有资源, 就会造成内存泄漏.
反之, 如果定义成虚函数, 那么构成多态, 我们知道多态看对象, 此时是父类指针指向子类对象, 实际还是子类对象, 这时就会调用子类的析构. (调用子类析构之后会自动调用父类析构, 保证不会出现内存泄漏)
多态: 看对象 --> 子类对象 --> 子类析构

下面通过代码理解

class A {
public:
	//析构尽量写成虚函数
	virtual ~A() {
		cout << "~A()" << endl;
	}
};

class B : public A {
public:
	//重写父类的析构函数 :  因为底层的函数名是相同的, 接口完全一致  (这么设计就是为了实现多态)
	virtual ~B() {
		cout << "~B()" << endl;
	}
};

void test2() {
	//切片: 有内存泄漏的风险 (析构不是多态)
	//多态: 看对象  --> 子类对象  -->  子类析构
	//用父类指针/引用 指向继承体系中的对象
	A* pc = new B;
	delete pc;   //只调用父类析构, 如果子类中有资源, 会造成内存泄漏, 子类的资源不会被释放
				 // 如果父类析构写成虚函数, 就解决了问题 ()
}

在这里插入图片描述
我们可以看到调用了子类析构之后又调用了父类的析构

如果不把析构函数写成虚函数, 在进行测试

在这里插入图片描述
可以看到只调用了父类的析构, 这时如果子类中有资源, 就会造成内存泄漏



纯虚函数与抽象类

纯虚函数: 虚函数没有函数体, 在参数列表后加 = 0

抽象类 : 包含纯虚函数的类

  • 抽象类不能实例化 (因为成员不完整)
  • 子类继承了抽象类, 必须实现纯虚函数, 如果不实现, 也是一个抽象类, 不能实例化
class A {
public:
	//纯虚函数   (没有函数体)
	//抽象类:  包含纯虚函数的类
	virtual void func4() = 0;
};

class B : public A {
public:
	//重写父类中的纯虚函数
	virtual void func4() {
		cout << "B::func4()" << endl;
	}
};


接口继承与实现继承

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



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

重载

1. 两个函数在同一作用域
2. 函数名相同, 参数不同

重写(覆盖)

1. 两个函数分别在一个继承体系(父类和子类)中
2. 都是虚函数
3. 函数名, 参数, 返回值都必须相同 (协变除外)

重定义(隐藏)

1. 两个函数分别在一个继承体系(父类和子类)中
2. 函数名相同

可以看到, 分别在父类和子类中的同名函数, 要么构成覆盖, 要么构成隐藏

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

殇&璃

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值