【C++进阶】多态

在这里插入图片描述

个人主页:@Weraphael
✍作者简介:目前学习C++和算法
✈️专栏:C++航路
希望大家多多支持,咱一起进步!
如果文章对你有帮助的话
欢迎 评论 点赞 收藏 加关注✨


目录

  • 一、 多态的概念
  • 二、多态的定义及实现
      • 2.1 虚函数
      • 2.2 虚函数的重写(覆盖)
      • 2.3 多态的构成条件(重点)
      • 2.4 多态构成条件的两个例外
      • 2.5 析构函数的重写(面试常考)
  • 三、override和final(支持C++11)
      • 3.1 override
      • 3.2 final
  • 四、重载、覆盖(重写)、隐藏(重定义)的对比

一、 多态的概念

多态是面向对象三大基本特征中的最后一个。概念:通俗来说,就是多种形态,具体点就是当不同的对象去完成某个行为,就会产生出不同的状态。

比如在购买高铁票时,成人原价,学生半价,而军人可以优先购票,对于购票这一相同的动作,需要根据不同的对象提供不同的方法

#include 
using namespace std;

class Adult // 成人
{
public:
	virtual void Buyticket()
	{
		cout << "成人-原价" << endl;
	}
};

class Student : public Adult
{
public:
	virtual void Buyticket()
	{
		cout << "学生-半价" << endl;
	}
};

class Soldier : public Adult
{
public:
	virtual void Buyticket()
	{
		cout << "军人-优先" << endl;
	}
};

void Buyticket(Adult& At)
{
	At.Buyticket();
}

int main()
{
	Adult at;
	Student s;
	Soldier sd;

	Buyticket(at); // 成人
	Buyticket(s); // 学生
	Buyticket(sd); // 军人
	return 0;
}

【输出结果】

可以看到,不同对象调用同一函数,执行结果是不同

【C++进阶】多态_第1张图片

二、多态的定义及实现

2.1 虚函数

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

【C++进阶】多态_第2张图片

2.2 虚函数的重写(覆盖)

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

// 父类
class Adult // 成人
{
public:
	virtual void Buyticket()
	{cout << "成人-原价" << endl;}
};

// 子类
class Student : public Adult
{
public:
	virtual void Buyticket()
	{cout << "学生-半价" << endl;}
};

2.3 多态的构成条件(重点)

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

  1. 必须通过父类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数,且子类必须对父类的虚函数进行重写(返回值类型、函数名字、参数列表完全相同)

注意:上述两个构成多态的条件缺一不可!缺少其中任意一个条件,都不构成多态!

// 父类
class Adult // 成人
{
public:
	virtual void Buyticket()
	{cout << "成人-原价" << endl;}
};

// 子类
class Student : public Adult
{
public:
	// 子类必须对父类的虚函数进行重写
	virtual void Buyticket()
	{cout << "学生-半价" << endl;}
};

// 1. 通过父类的指针或者引用调用虚函数
// void Buyticket(Adult* At) - 指针
void Buyticket(Adult& At) // 引用
{
	// 2. 被调用的函数必须是虚函数
	At.Buyticket();
}

2.4 多态构成条件的两个例外

  • 例外一:子类虚函数可以不使用virtual修饰

【C++进阶】多态_第3张图片

虽然这个例外在语法上是支持的,但是建议不要省略,因为会破坏代码的可阅读性,可能无法让别人一眼看出多态。

  • 例外二:协变(父类与子类虚函数返回值类型可以不同)

子类重写基类虚函数时,与父类虚函数返回值类型不同。即 父类虚函数返回父类对象的指针或者引用,子类虚函数返回子类对象的指针或者引用时,称为协变。

第一种:返回各对象的指针

class Adult // 成人
{
public:
	// 父类虚函数返回父类对象的指针
	virtual Adult* Buyticket()
	{
		cout << "成人-原价" << endl;
		return 0;
	}
};
// 子类
class Student : public Adult
{
public:
	virtual Student* Buyticket()
	{
		cout << "学生-半价" << endl;
		return 0;
	}
};

第二种:返回各对象的引用

class Adult // 成人
{
public:
	// 父类虚函数返回父类对象的引用
	virtual const Adult& Buyticket()
	{
		cout << "成人-原价" << endl;
		return Adult(); // 返回匿名对象 
	}
};
// 子类
class Student : public Adult
{
public:
	virtual const Student& Buyticket()
	{
		cout << "学生-半价" << endl;
		return Student();
	}
};

注意:父子类关系的指针/引用,不是必须是自己的,也可以是其他类的,但是要对应匹配子类和父类。

class A  // 父类
{};

class B : public A // 子类
{};

class Adult // 成人
{
public:
	// 父类虚函数返回父类对象的引用
	virtual const A& Buyticket()
	{
		cout << "成人-原价" << endl;
		return A(); // 返回匿名对象 
	}
};
// 子类
class Student : public Adult
{
public:
	virtual const B& Buyticket()
	{
		cout << "学生-半价" << endl;
		return B();
	}
};

还有一点要注意的是,不可以一个是指针,一个是引用,必须同时是指针,或者同时是引用

2.5 析构函数的重写(面试常考)

有个问题:析构函数加上virtual是不是虚函数重写?

答案:是。虽然父类与子类析构函数名字不同(不满足三重),看起来违背了重写的规,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor

#include 
using namespace std;

class A
{
public:
	virtual ~A()
	{
		cout << "~A()" << endl;
	}
};

class B : public A
{
public:
	virtual ~B()
	{
		cout << "~B()" << endl;
	}
};

int  main()
{
	A a;
	B b;
	
	return 0;
}

【输出结果】

【C++进阶】多态_第4张图片

接下来就是面试官的连续“攻击”:为什么要这样处理呢?— 因为要构成重写

那么为什么要让它们构成重写呢?其实不加virtual关键字也是可以的

【C++进阶】多态_第5张图片

但如果不对析构重写的话,那么下面有一个场景是过不了的(记住此场景)

#include 
using namespace std;

class A // 父类
{
public:
	~A()
	{
		cout << "~A()" << endl;
	}
};

class B : public A // 子类
{
public:
	~B()
	{
		cout << "~B()" << endl;
		delete[] ptr;
	}
protected:
	int* ptr = new int[3];
};

int  main()
{
	A* a = new A;
	delete a;
	
	a = new B;
	delete a;

	return 0;
}

【输出结果】

【C++进阶】多态_第6张图片

我们发现,不加virtual没有调用子类的析构函数,发生了内存泄漏。那为什么没有调到子类的析构呢?第一次释放了a指向的空间,然后又改变了指向

在前面说过,类的析构函数都被处理成了destructor这个函数。而delete对于自定义类型的原理是:

  1. 在空间上执行析构函数,完成对象中资源的清理工作
  2. 调用operator delete函数释放对象的空间(operator delete本质就是调用free函数)

即对于delete a,先调用了析构函数a->destructor(),然后再调用operator delete函数释放对象的空间。

但由于编译器将析构函数名处理成一样的函数名destructor,因此构成了隐藏/重定义了。而且a刚好是A类型的指针,是一个普通的调用,不是多态调用。对于普通调用,看的是当前者的类型。因此delete a就会再次调用A类的析构函数。

但我们想的是指向什么类型,就去调用对应的析构函数,因此这是就得用到多态了。多态调用:看的是其指向的类型,指向什么类型,调用什么类型。

  • 因此,为什么要在 父类/基类 的析构函数中加上virtual修饰?

答案:为了构成多态,确保不同对象的析构函数能被成功调用,避免内存泄漏。

三、override和final(支持C++11)

从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重写,而这种错误在编译期间是不会报错的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了overridefinal两个关键字,可以帮助用户检测是否重写。

3.1 override

作用:修饰子类的虚函数,检查是否构成重写(是否满足重写的必要条件),若不满足,则报错

先以正确代码为例,override要写在子类函数括号的后面

#include 
using namespace std;

class A
{
public:
	virtual void Print()
	{cout << "class A" << endl;}
};

class B : public A
{
public:
	virtual void Print() override
	{cout << "class B : public A" << endl; }
};

void Print(A& a)
{
	a.Print();
}

int main()
{
	A a;
	B b;
	Print(a);
	Print(b);
	return 0;
}

【输出结果】

【C++进阶】多态_第7张图片

以下故意在子类的虚函数加个参数(不构成三重:子类虚函数与父类虚函数的返回值类型、函数名字、参数列表完全相同),看看是否会报错

【C++进阶】多态_第8张图片

3.2 final

作用:修饰父类的虚函数,不让子类的虚函数与其构成重写,即不构成多态

对父类的虚函数加上final:无法构成重写

【C++进阶】多态_第9张图片

除此之外,final在某些场景下很实用:final还可以修饰父类,修饰后,父类不可被继承。

【C++进阶】多态_第10张图片

注:final可以修饰子类的虚函数,因为子类也有可能成为父类;但override无法修饰父类的虚函数,因为父类之上没有父类了,自然无法构成重写。

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

面试题中也喜欢考这三者的区别

  • 重载:即函数重载。在同一个作用域中,通过参数的类型、个数或顺序不同来定义多个具有相同函数名但不同参数列表的方法。重载方法在编译时根据调用的参数匹配最合适的方法。

  • 重写(覆盖):发生在类中,当出现虚函数且符合重写的三同原则(函数名、参数列表和返回类型必须与父类中的方法一致)时,则会发生重写(覆盖)行为

  • 重定义(隐藏):发生在类中,当子类中的函数名与父类中的函数名起冲突时,会隐藏父类同名函数,默认调用子类的函数。如果想使用父类的同名成员,可以通过::指定调用。

重写和重定义比较容易记混,简言之 先看看是否为虚函数,如果是虚函数且三同,则为重写;若不是虚函数且函数名相同,则为重定义。注:在类中,仅仅是函数名相同(未构成重写的情况下),就能触发 重定义(隐藏)

【C++进阶】多态_第11张图片

你可能感兴趣的:(C++,c++,java,开发语言,c语言,visualstudio,list)