c++ 多态详解

1.多态的概念

多态,通俗来讲就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。比如,在买票这一行为,普通人买票是全价买票,学生买票是半价买票,而军人买票是优先买票;再比如动物园的动物叫这个行为,不同的动物叫声是不一样的。这些都是生活中多态的例子。

2.c++中多态的分类

1.静态多态

静态多态是指在编译时实现的多态,比如说函数重载

int Add(int left, int right)
{
    return left + right;
}
double Add(double left, int right)
{
    return left + right;
}

int main()
{
    Add(10, 20);
    //Add(10.0, 20.0);  //这是一个问题代码
    Add(10.0,20);  //正常代码
    return 0;
}

这里跟据参数类型的不同调用不同的函数。

2.2 动态多态

动态也就是我们常说的多态,动态多态是在运行中实现的。根据父类的指针或引用接收不同对象,来确定自己会调用哪个类的虚函数。
那么构成多态形成的条件有哪些呢?

3.多态的构成条件

3.1两个概念的介绍

3.1.1 虚函数

虚函数,即被virtual修饰的类成员函数称为虚函数。
关于虚函数需要注意几点:
1.普通的函数不能是虚函数,只能是类中的函数。
2.静态成员函数不能加virtual

3.1.2 虚函数的重写

讲到重写,那么我们就得弄清楚3个概念,即重载,重写(覆盖),重定义(隐藏)
这是说三个定义的区别:
1.重载:重载函数处在同一作用域。
函数名相同,函数列表必须不同。
2.重写(覆盖):必须是虚函数,且处在父类和子类中。
返回值,参数列表,函数名必须完全相同(协变除外)。
3.重定义:子类和父类的成员变量相同或者函数名相同,子类隐藏父类的对应成员。
子类和父类的同名函数不是重定义就是重写。
在这里插入图片描述

虚函数的重写又称为虚函数的覆盖(重写是表面意义上的,覆盖是指原理上的):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型,函数名,参数列表完全相同),称子类的虚函数重写了父类的虚函数。
注意,只有虚函数才能构成重写。

3.2 多态构成条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象买票半价。
那么在继承中要构成多态还有两个条件:
1. 必须通过基类的指针或者引用调用虚函数
2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
接下来看一个例子

#include<iostream>
#include<string>
using namespace std;
class Person
{
public: 
	virtual void BuyTicket()
	{
		cout << "全价买票" << endl;
	}
};
class Student :public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "半价买票" << endl;
	}
};
void Func(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person p1;
	Student p2;
	Func(p1);
	Func(p2);
}

上面程序Student继承了父类,并对父类中的虚函数进行了重写。
程序运行的结果如下所示:
在这里插入图片描述
可以看到当传父类对象时调用的是父类的函数,传的是子类对象时调用的是子类的对象。这就是多态的应用。

3.3 虚函数重写的两个例外

(1)协变
如果我们将父类和子类中的虚函数的返回值设为不同,可能会发生报错;

协变指的是:派生类重写基类虚函数时,与基类虚函数返回值类型不同,且基类的虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
简单来说就是两者的返回值必须是父子关系的指针或者引用。
举个例子

class A{};
class B:public A
	{};
class Person
{
public: 
	virtual A* BuyTicket()
	{
		A a;
		cout << "全价买票" << endl;
		return &a;
	}
};
class Student :public Person
{
public:
	virtual B* BuyTicket()
	{
		B b;
		cout << "半价买票" << endl;
		return &b;
	}

我们将上一段代码进行了改写,定义了B继承A,而在Person和Student两个类中的虚函数中将返回值分别置为A和B,由于A和B是继承关系,所以仍然可以构成多态,我们称派生类的虚函数为基类的虚函数的协变。
注意返回值必须是指针或者引用,否则对象不会构成协变。

(2) 析构函数的重写(基类与派生类析构函数的名字不同)
首先我们先回顾一下没有构成多态的析构函数调用:只需要子类对象销毁时无需手动销毁父类对象,会自动调用父类对象的析构函数。
1.如果基类的析构函数为虚函数,此时子类的析构函数无论加不加virtual,都是对父类的析构函数的重写。
2.虽然子类和父类的析构函数的函数名不同,但其实编译器对析构函数的名称进行了特殊的处理,都处理成了destructor。

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

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

// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
 Person* p1 = new Person;
 Person* p2 = new Student;
 delete p1;
 delete p2;
 return 0;
}

这是程序运行的结果:
在这里插入图片描述
当析构父类对象时,调用父类的析构函数,当析构子类对象时,调用的是子类的析构函数和父类的析构函数。

4.final与override

(1)final
final关键字的作用是限制类不能被继承和限制虚函数不被重写
1.限制类不能被继承
当我们想要设计一个不被继承的类时,目前我们知道的有一种方法,就是将父类的构造函数设为私有(因为子类需要调用父类的构造函数来进行初始化)。
final提供了另一种方式来限制一个类不会被继承。 只需要在不想被继承的类后加一个final即可

class Person final
{
public: 
	virtual A* BuyTicket()
	{
		A a;
		cout << "全价买票" << endl;
		return &a;
	}
	virtual ~Person()
   {
		cout << "~Person" << endl;
	}
};

如果此时子类去继承Person的话就会报错。
2.限制虚函数不被继承
当我们在虚函数后面加上final的时候,该虚函数将不能被子类中的虚函数重写,否则会报错。
(2)override
将override放在子类的重写的下虚函数后,判断是否完成重写(重写的是否正确);

class Car{
public:
 virtual void Drive(){}
};
class Benz :public Car {
public:
 virtual void Drive() override
  {
  	cout << "Benz-舒适" << endl;
  }
};

注意:final关键字放在父类的位置,override关键字放在子类的位置。

5.抽象类

在虚函数的后面加上=0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫做接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象。只有重写虚函数,派生类才能实例化出对象。注意虽然不能实例化出对象,但是可以定义指针。
抽象类的存在本质上来说就是希望我们在派生类中重写父类的虚函数。抽象类中的虚函数一般只声明,不实现,因为没有意义。我们可以搭配override来使用。
子类必须只有重写虚函数才能定义对象。通常情况下现实中没有的事物,定义成抽象类会比较合适。
虽然我们不能使用抽象类来定义对象,但是我们可以使用抽象类来定义指针。

class Car
{
public:
	virtual void Drive() = 0
	{
		cout << " Car" << endl;
	}
};
class Benz :public Car
{
public:
	virtual void Drive()
	{
		cout << "Benz" << endl;
	}
	void f()
	{
		cout << "f()" << endl;
	}
};
int main()
{
	//Car* p = nullptr;
	//p->Drive();//程序会崩溃
	Car* a = new Benz;
	a->Drive();
}

我们可以使用父类指针去接收子类对象,同时调用函数。但是不能使用父类去创建对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

不倒翁*

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

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

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

打赏作者

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

抵扣说明:

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

余额充值