【C++】“多态”特性

发布于:2025-05-30 ⋅ 阅读:(16) ⋅ 点赞:(0)

在这里插入图片描述

文章目录

  • 一、多态的概念
  • 二、多态的定义实现
    • 1. 多态的构成条件
      • 1.1 虚函数
      • 1.2 虚函数的重写
    • 2. 多态的调用
    • 3. 虚函数重写的其他问题
      • 3.1 协变
      • 3.2 析构函数的重写
  • 三、override和final关键字
  • 四、重载/重写/隐藏的对比
  • 五、纯虚函数和抽象类
  • 六、多态的原理

C++的三大主要特性:封装(类和对象)、继承、多态。前两者我们已经学习过了,今天最后来认识一下“多态”特性。

一、多态的概念

多态,就是“多种形态”,指函数的行为可以有多种形态。多态一般分为编译时多态(静态多态)和运行时多态(动态多态)。其中,编译时多态主要就是之前讲的函数重载和函数模板,它们传不同类型的参数就可以调用不同的函数,通过参数的不同达到不同的形态效果,之所以叫编译时多态,是因为传参匹配这一过程是在编译时期完成的。
运行时多态,具体指一个函数传不同的对象就会完成不同的行为,达到多种形态。比如写一个买火车票行为,传普通人类对象是“全价”,传学生类对象是“打折”,传军人类对象就是“优先”,一个函数(行为),根据传的对象类型不同,就有不同作用。今天谈的“多态”指的主要就是运行时多态。

二、多态的定义实现

1. 多态的构成条件

(运行时)多态是一个继承体系下的类对象,去调用同一函数,而产生不同的行为。比如,Person类为基类,Student类继承了Person类,Soldier类继承了Person类,那么这三类的对象买票行为就有不同的效果。

实现多态还有两个重要的条件:

  • 必须是基类的指针类型或引用类型去调用虚函数。
  • 被调用的函数必须是虚函数,并且完成了虚函数的重写(覆盖)。

我们依次来说明:
要实现多态效果,首先必须是基类的指针或引用去调用,因为只有这样才既能指向基类对象又能指向派生类对象(的基类的切片)。
第二,派生类必须对基类的虚函数完成重写,重写了,基类和派生类才能有这个函数的不同实现方法,多态的不同形态效果才能达到。

1.1 虚函数

类的成员函数前加关键字virtual修饰,那么这个成员函数被称为虚函数,非成员函数不能加virtual修饰。如:

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "全价" << endl;
	}
};

虚函数的存在就是为了给多态服务的。

1.2 虚函数的重写

虚函数的重写指:派生类中有一个跟基类虚函数“三同”的虚函数,则称派生类的这个虚函数完成了对基类虚函数的重写。“三同”指的是,函数名相同、返回类型相同、参数类型。
在派生类中重写基类虚函数时,派生类的虚函数前可以不加virtual修饰,也可以构成重写,因为继承后基类的虚函数在派生类中仍保持虚函数属性。但是这种写法不规范,实际使用还是建议派生类的虚函数前写上virtual(但基类的虚函数前是必须写virtual的)。不过在考试中也有可能故意埋这个坑,注意判断。
举例:

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "全价" << endl;
	}
};

class Student : public Person
{
public:
    // 注意保证“三同”
	virtual void BuyTicket()
	{
		cout << "打折" << endl;
	}
};

2. 多态的调用

利用多态的特性,我们可以模拟出简单的买火车票行为:传普通人类对象是“全价”,传学生类对象是“打折”,传军人类对象就是“优先”:

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "全价" << endl;
	}
};

class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "打折" << endl;
	}
};

class Soldier : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "优先" << endl;
	}
};

但是记得刚才说的多态的第一个条件,必须是基类的指针类型或引用类型去调用虚函数,比如:
在这里插入图片描述

3. 虚函数重写的其他问题

3.1 协变

派生类重写基类虚函数时,重写时虚函数返回类型也可以不一样。基类虚函数可以返回自己或其他基类对象的指针或引用,派生类虚函数可以返回自己或其他派生类对象的指针或引用。这种方式称为卸变,但是它的实际意义并不大,简单了解即可。

class Person
{
public:
	virtual Person* BuyTicket()
	{
		cout << "全价" << endl;
		return nullptr;
	}
};

class Student : public Person
{
public:
	virtual Student* BuyTicket()
	{
		cout << "打折" << endl;
		return nullptr;
	}
};

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;
	}
};

3.2 析构函数的重写

析构函数十分特殊,编译器会对析构函数的名字进行特殊处理——编译后析构函数的名称会统一处理成destructor,再加上析构函数都是没有返回、没有参数的。因此,基类和派生类的析构函数总会构成“三同”,如果不加virtual修饰成虚函数,那么基类和析构函数既然是“同名”的,就构成隐藏关系了所以,基类的析构函数一定要加virtual修饰成虚函数,派生类析构函数的virtual可写可不写

比如:

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

class B : public A
{
public:
	~B()
	{
		cout << "~B()" << endl;
		delete p;
	}
protected:
	int* p = new int[10];
};

在这里插入图片描述
可见,如果~A()不加virtual,那么delete p2时只会调用A的析构函数,没有调用B的析构函数,导致了内存泄漏问题。

这样就没问题了。
在这里插入图片描述

总而言之,基类的析构函数建议设计为虚函数

三、override和final关键字

C++提供了两个新的关键字:

  • override:
    C++对虚函数重写的要求是很严格的,但是有时候我们可能粗心没有满足多态的要求,但是语法没问题编译也不会有问题,只有在运行结果不是预期情况下我们才能发现问题。因此C++11提供了override关键字,写在派生类的想要重写的虚函数后,帮助用户检测是否完成了重写
    在这里插入图片描述
  • final
    之前继承提到过,如果一个类不想被继承,就在类名后加final修饰。除此之外final还有一个功能,如果我们不想让基类的虚函数被重写,就在这个虚函数()后加final修饰在这里插入图片描述

四、重载/重写/隐藏的对比

这三个概念比较相近,要注意区别:

函数重载:

  • 两个函数在同一作用域
  • 函数名相同,参数的类型或个数不同,返回值可同可不同

虚函数重写:

  • 两个函数分别在一个继承体系的基类和派生类中
  • 函数名、参数、返回值相同,协变除外
  • 两个函数必须都是虚函数

隐藏:

  • 两个函数分别在一个继承体系的基类和派生类中
  • 函数名相同
  • 两个函数只要不构成重写,就是隐藏关系
  • 基类和派生类的成员变量相同也构成隐藏关系

五、纯虚函数和抽象类

如果在一个虚函数的()后写上= 0,则这个虚函数称为纯虚函数,纯虚函数不需要定义实现(可以实现但是没有意义),只要声明即可。
包含纯虚函数的类称为抽象类,抽象类不能实例化出对象,如果一个抽象类的派生类继承后不重写纯虚函数,则派生类也是抽象类。可以认为,纯虚函数一定程度上强制派生类重写虚函数,因为不重写不实例化出对象。

在这里插入图片描述

六、多态的原理

还是看一开始的例子:

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "全价" << endl;
	}
};

class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "打折" << endl;
	}
};

class Soldier : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "优先" << endl;
	}
};
int main()
{
    Person per;
	Student stu;
	Soldier sol;
	Person* ptr;

	ptr = &per;
	ptr->BuyTicket(); 

	ptr = &stu;
	ptr->BuyTicket();

	ptr = &sol;
	ptr->BuyTicket();
	return 0;
}

调试观察,可以发现:
在这里插入图片描述
每一个对象的第一个成员都是一个叫__vfptr的指针(有些平台可能会把它放在最后一个),这个指针叫做虚函数表指针,指向这个类的虚函数表。一个含有虚函数的类中都至少有一个虚函数表指针,一个类所有虚函数的地址都会存在一个虚函数表中,虚函数表实际上就是函数指针数组,也称虚表。

  • 基类和每个派生类都有自己独立的虚函数表。派生类会继承基类的虚函数表指针,但是这个虚函数表指针和基类的虚函数表指针不是同一个指针,指向的虚表也就不是同一个。
  • 派生类重写了基类的虚函数后,派生类的虚表中对应的原基类虚函数就会被覆盖成派生类重写的虚函数地址。这也是为什么重写也可以称为覆盖。

基于这样的原理,基类的指针或引用调用基类和派生类的同一个虚函数时,其实就是从它们各自的__vfptr找到各自的虚函数表,再找到这个虚函数。但由于派生类的虚函数表中这个虚函数已经被重写(覆盖),调用后也就有不同的结果了。这就是多态实现的原理。

本篇完,感谢阅读。


网站公告

今日签到

点亮在社区的每一天
去签到