C++_进阶:多态详解

1. 多态的概念

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个同一个行为,当不同的对象去完成时会产生出不同的状态
1️⃣ 个例子:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。

2. 多态的定义及实现

2.1多态的构成条件

📝多态在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象买票半价。

那么在继承中要构成多态有十分重要❗的两个条件

  1. 必须通过基类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数且派生类必须对基类的虚函数进行重写
class Person {
public:
	virtual int BuyTicket() { 
		cout << "买票-全价" << endl; 
		return 0;
	}
};

class Student : public Person {
public:
	// 重写/覆盖
	virtual int  BuyTicket() { 
		cout << "买票-半价" << endl;
		return 0;
	}
};
// 多态条件:
// 1、虚函数重写
// 2、父类指针或者引用调用虚函数

void Func1(Person* p)//父类指针调用虚函数
{
	p->BuyTicket();
}

void Func2(Person& p)//父类引用调用虚函数
{
	p.BuyTicket();
}

int main()
{
	Person p;
	Student st;

	Func(p);
	Func(st);
}

在这里插入图片描述

2.2 虚函数

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

class Person {
public:
	//👇虚函数
	virtual int BuyTicket() { 
		cout << "买票-全价" << endl; 
		return 0;
	}
};

2.3虚函数的重写(覆盖)

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

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
	// 子类中不加“virtual” 虽然也可以,但是不规范。👇
	// void BuyTicket() { cout << "买票-半价" << endl; } 
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
	
};

📝备注:派生类的重写虚函数可以不加virtual,也能构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),❗但是该种写法不是很规范,不建议这样使用。

❗虚函数重写的两个例外

  1. 📋协变(此时基类与派生类虚函数返回值类型不同)

重写虚函数的返回值可以允许不同,但这父类和子类的返回值必须也是继承关系

//A是父类  B是子类 是继承关系
class A {};
class B : public A {};

//Person和Student的f()返回值不同
//但是Person的返回值是A,Student是B,A与B也是父子关系
//构成协变,满足虚函数重写的条件,也就可以多态 
class Person {
public:
	virtual A* f() { return new A; }
};
class Student : public Person {
public:
	virtual B* f() { return new B; }
};
  1. 📋析构函数的重写(基类与派生类析构函数的名字不同)

1️⃣基类的析构函数加了virtual后子类的析构函数只要定义就可以构成 重写

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

2️⃣基类的析构函数子类的析构函数名称不相同,看似破坏了重写的条件,但实际上他们名称是相同的,编译后析构函数的名称统一处理成destructor + operator delete

3️⃣析构函数的重写其实相当重要,我们先来看一个场景:

class Person {
public:
	 ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
	~Student() { cout << "~Student()" << endl; }
private:
	int* a = new int[10];
};

int main()
{
	// 父类指针指向子类空间,此行为是切片
	Person* ptr = new Student();
	delete ptr;
}

🔎ptr是一个Person关于Students切片,按理来说,我们delete ptr,会希望把整个 Student的内存删除(同时调用Person与Students的析构),然而:

在这里插入图片描述
❗delete ptr后,程序只是调用了Person的析构,而没有调用子类的析构,这就会导致子类的内存泄漏

💡解决方法就是析构函数的重写,也就是在基类的析构函数加上virtual,这样就能让析构函数形成多态,指向谁,调用谁的析构函数。指向父类,调用父类的析构,指向子类,调用子类的析构与父类的析构。

class Person {
public:
	virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
	~Student() { cout << "~Student()" << endl; }
private:
	int* a = new int[10];
};

int main()
{
	Person* ptr = new Student();
	delete ptr;
	//输出  ~Person()  ~Student()
}

2.4 C++11 override 和 final

🔎从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而]无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失。

💡因此:C++11提供了overridefinal两个关键字,可以帮助用户检测是否重写。

1️⃣ final:修饰虚函数,表示该虚函数不能再被重写

class Car
{
public:
	virtual void Drive() final {}
};

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

在父类添加final后,如果重写成功就会报错,提醒自己已经完成重写

2️⃣ override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错

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

没有重写成功会报错

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

在这里插入图片描述
💡单独论隐藏和重写:
在这里插入图片描述

3. 抽象类

3.1 概念

📋纯虚函数在虚函数的后面写上 =0 ,则这个函数为纯虚函数。
📋抽象类:包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

class Car
{
public:
	//纯虚函数
	virtual void Drive() = 0;
};
class Benz :public Car
{
public:
	//子类必须重写 否则也会成为抽象类
	virtual void Drive()
	{
		cout << "Benz-舒适" << endl;
	}
};

int main()
{
	Car s;//抽象类不能实例化
	Benz b;//重写纯虚函数的子类可以实例化
}

3.2 接口继承和实现继承

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

下面有一个场景:
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传输出的结果是 B-> 1,原因就如上文所说,虚函数的继承是继承的是接口,B从A继承下来的是A的接口void func(int val = 1) ,重写的内容则是B函数体的内容。所以结果就为从A接口继承下来的缺省值 val = 1,所用的函数体为在B重写的函数内容。

4.多态的原理

4.1虚函数表

// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
};

通过观察测试我们发现Base对象的大小是8bytes(32位)****,除了_b成员,还多一个__vfptr,对象中的这个指针我们叫做📋虚函数表指针(v代表virtual,f代表function)。

一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,📋虚函数表也简称虚表

🔎在以上的Base对象中加多两个成员函数看看:

class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}
private:
	int _b = 1;
};

在这里插入图片描述
🔎那么派生类中这个表放了些什么呢?我们接着往下分析:

//继承的是上文的Base类
class Derive : public Base
{
public:
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
private:
	int _d = 2;
};

在这里插入图片描述
🔎我们能看到的是,Derive类的虚表是在其Base类中的,但是,因为Derive类重写了Func1函数,则虚表中原本放Base::Func1函数的位置被Derive::Func1替代了
📝备注:

  1. Derive(派生类)对象中也有一个虚表指针,Derive对象由两部分构成,一部分是父类继承下来的成员虚表指针存在这一部分中,另一部分就是自己的成员。
    在这里插入图片描述

  2. 基类Base对象和派生类Derive对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,📋所以虚函数的重写也叫作覆盖覆盖就是指虚表中虚函数的覆盖。📋重写是语法的叫法,覆盖是原理层的叫法。
    在这里插入图片描述

  3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表

  4. ❗虚函数表本质是一个存虚函数指针的函数指针数组,一般情况这个数组最后面放了一个nullptr。

  5. 总结一下派生类的虚表生成:1️⃣先将基类中的虚表内容拷贝一份到派生类虚表中 2️⃣如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 3️⃣派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后

  6. 虚表存的是虚函数指针,不是虚函数虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。

4.2多态的原理

多态在实际的表现中,就是指向谁调用谁,那么它是怎么做到的呢?

我们看看下面的例子:

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 Mike;
	Func(Mike);//传入Mike(父类节点)
	Student Johnson;
	Func(Johnson);//传入Mike(子类节点)
	return 0;
}

1️⃣在以上的例子中,Func函数以 Person&类型作为参数类型,

void Func(Person& p)

之后传入Person类型的Mike,就是正常的给一个Person对象起别名,

Person& p = Mike;

之后调用BuyTicket(),这里并不是单纯的直接调用,虚函数的调用是先要在虚表中查找的,这里找到的是Person::BuyTicket()

p.BuyTicket();//Person::BuyTicket()

在这里插入图片描述


2️⃣然后传入Student类型的Johnson ,这里 让父类别名接收子类,
Person& p = Johnson;

这里的p是Johnson中的Person类的别名,
在这里插入图片描述
而我们知道,Student类对BuyTicket进行了重写,而重写后的函数的地址会取代被重写的地址,放进虚表中,
在这里插入图片描述

而虚表是在子类中是属于基类的成员,所以,p会切到修改后的虚表。
在这里插入图片描述
所以,在这条代码下,p又会使用Student::Buyticket了

	p.BuyTicket();//Student::Buyticket


结论:1️⃣📝多态的原理就是接收不同的虚表,按着虚表对应的位置调用函数。2️⃣虚函数的重写会覆盖虚表的中的对应函数,由此,不同的类的虚表会有不同的内容,这样就实现出了不同对象去完成同一行为时,展现出不同的形态

反过来思考我们要达到多态,有两个条件,1️⃣一个是虚函数覆盖,2️⃣一个是对象的指针或引用调用虚函数。反思一下为什么?1️⃣如果不实现虚函数覆盖,虚表中一直存还是基类函数的地址,就调不到对应的其他函数;2️⃣因为如果不是对象的指针或引用调用虚函数,如Person p = Johnson,就只能切到Johnson中的Person的内容,调不到Johnson关于虚函数重写部分的函数。(切片不会切虚表指针)

4.3 动态绑定与静态绑定

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
  3. 本小节之前(5.2小节)买票的汇编代码很好的解释了什么是静态(编译器)绑定和动态(运行时)绑定。

本文就到这里,感谢你看到这里❤️❤️!
我知道一些人看文章喜欢静静看,不评论🤔,但是他会点赞😍,这样的人,帅气低调有内涵😎,美丽大方很优雅😊,明人不说暗话,要你手上的一个点赞😘!

  • 27
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值