C++进阶:多态

◆博主名称:少司府

欢迎来到少司府的博客☆*: .。. o(≧▽≦)o .。.:*☆

数据结构系列个人专栏:

初阶数据结构_少司府的博客-CSDN博客

C++基础个人专栏:

C++初阶_少司府的博客-CSDN博客

⭐水滴石穿非一日,功不唐捐终可期

目录

一、多态的概念

二、多态的定义及实现

[2.1 多态的构成条件](#2.1 多态的构成条件)

[2.1.1 实现多态的两个必须条件](#2.1.1 实现多态的两个必须条件)

[2.1.2 虚函数](#2.1.2 虚函数)

[2.2 虚函数相关](#2.2 虚函数相关)

[2.2.1 虚函数的重写/覆盖](#2.2.1 虚函数的重写/覆盖)

[2.2.2 多态场景的一个选择题](#2.2.2 多态场景的一个选择题)

[2.2.3 虚函数重写的一些其他问题](#2.2.3 虚函数重写的一些其他问题)

[2.2.4 override 和 final 关键字](#2.2.4 override 和 final 关键字)

[2.2.5 重载/重写/隐藏的对比](#2.2.5 重载/重写/隐藏的对比)

三、纯虚函数和抽象类

[3.1 纯虚函数](#3.1 纯虚函数)

[3.2 抽象类](#3.2 抽象类)

四、多态的原理

[4.1 虚函数表指针](#4.1 虚函数表指针)

[4.2 多态的原理](#4.2 多态的原理)

[4.3 动态绑定和静态绑定](#4.3 动态绑定和静态绑定)

[4.4 虚函数表](#4.4 虚函数表)


一、多态的概念

多态及多种形态,分为编译时多态 (静态多态)和运行时多态 (动态多态)。静态多态主要是函数重载和函数模板,传入不同参数就可以调用不同函数,通过参数的不同达到多种形态。

动态多态,具体就是去完成某个行为(函数),可以传不同对象就会完成不同行为,达到多种形态。

二、多态的定义及实现

2.1 多态的构成条件

多态是一个继承关系下的类对象,去调用同一函数,不同对象产生了不同的行为。⽐如Student继承了Person。Person对象买票全价,Student对象优惠买票。

2.1.1 实现多态的两个必须条件

1)、必须是父类的指针或引用调用虚函数

2)、被调用的函数必须是虚函数,子类必须对父类的虚函数重写或者覆盖

2.1.2 虚函数

类成员函数前面加 virtual 修饰,那么称这个成员函数是虚函数。

cpp 复制代码
class Person 
{
public:
    virtual void BuyTicket() { cout << "买票-全价" << endl;}
};
2.2 虚函数相关
2.2.1 虚函数的重写/覆盖

子类当中有一个跟父类完全相同的虚函数(函数名、返回类型、参数类型完全一样),称子类的虚函数重写了父类的虚函数。

如图,调用的是各自的BuyTiket();

注意:子类的虚函数可以不加 virtual 关键字,但是这样写是不规范的。

2.2.2 多态场景的一个选择题
cpp 复制代码
class A
{
public:
	virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
	virtual void test() { func(); }
};

class B : public A
{
public:
	void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};

int main(int argc, char* argv[])
{
	B* p = new B;
	p->test();
	return 0;
}

如图,B继承A,test当中this指针实际上是A*,并不是B*(继承实际上是一个查找规则,而不是拷贝父类到子类中);this->func(),两个func() 构造多态,实现子类B的func()方法,但是,重写的本质是函数体内方法的替换,this->func() 中val的缺省值仍然是A类中的1,因此结果是 B->1。

2.2.3 虚函数重写的一些其他问题

析构函数的重写

父类的析构函数为虚函数,此时子类析构函数只要定义,无论是否加 virtual ,都与父类的析构函数构成重写。实际上,编译器对析构函数名称做了特殊处理,编译后析构函数名称统一处理成destructor。

cpp 复制代码
class A
{
public:
	virtual ~A()
	{
		cout << "~A()" << endl;
	}
};
class B : public A {
public:
	~B()
	{
		cout << "~B()->delete:" << _p << endl;
		delete _p;
	}
protected:
	int* _p = new int[10];
};
// 只有派⽣类Student的析构函数重写了Person的析构函数,下⾯的delete对象调⽤析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调⽤析构函数。
int main()
{
	A* p1 = new A;
	A* p2 = new B;
	delete p1;
	delete p2;
	return 0;
}

如果基类A的析构不加virtual,那么delete p2的时候就只能调用A的析构,没有调用B的析构,有内存泄漏的问题。

2.2.4 override 和 final 关键字

C++11提供这两个关键字。

1)、在派生类成员函数参数列表的后面加 override ,检查成员函数是否完成重写,若没有则报错

cpp 复制代码
class Car {
public:
    virtual void Dirve()
    {}
};
class Benz :public Car {
public:
    virtual void Drive() override { cout << "Benz-舒适" << endl; }
};

2)、如果不想让派生类重写这个虚函数,那么就可以用 final 修饰

cpp 复制代码
class Car
{
public:
    virtual void Drive() final {}
};
2.2.5 重载/重写/隐藏的对比

三、纯虚函数和抽象类

3.1 纯虚函数

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。纯虚函数不需要定义实现(可以但一般不实现),只要声明即可。

cpp 复制代码
class Car
{
public:
	virtual void Drive() = 0;
};
3.2 抽象类

包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象。如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。

纯虚函数某种程度上强制了派生类重写虚函数,因为不重写就不能实例化出对象。

cpp 复制代码
	// 编译报错:error C2259: "Car": ⽆法实例化抽象类 
	Car car;

四、多态的原理

4.1 虚函数表指针
cpp 复制代码
class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
protected:
	int _b = 1;
	char _ch = 'x';
};
int main()
{
	Base b;
	cout << sizeof(b) << endl;
	return 0;
}

在内存对齐中,一开始是一个虚函数表指针,4字节,一个int有4字节,一个char有1字节,最后补上3字节,整体是12字节。

虚函数表(virtual function table) 里面存放的是该类虚函数的函数指针,其本质就是一个函数指针数组。

4.2 多态的原理

满⾜多态条件后,底层 不再是编译时通过调⽤对象确定函数的地址,⽽是运⾏时到指向的对象的虚表中确定对应的虚函数的 地址,这样就实现了指针或引⽤指向基类就调⽤基类的虚函数,指向派⽣类就调⽤派⽣类对应的虚函 数。

cpp 复制代码
class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
private:
	string _name;
};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-打折" << endl; }
private:
	string _id;
};
class Soldier : public Person {
public:
	virtual void BuyTicket() { cout << "买票-优先" << endl; }
private:
	string _codename;
};
void Func(Person* ptr)
{
	// 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket 
	// 但是跟ptr没关系,⽽是由ptr指向的对象决定的。 
	ptr->BuyTicket();
}
int main()
{
	// 其次多态不仅仅发⽣在派⽣类对象之间,多个派⽣类继承基类,重写虚函数后 
	// 多态也会发⽣在多个派⽣类之间。 
	Person ps;
	Student st;
	Soldier sr;
	Func(&ps);
	Func(&st);
	Func(&sr);

	return 0;
}
4.3 动态绑定和静态绑定

1)、对不满⾜多态条件(指针或者引⽤+调⽤虚函数)的函数调⽤是在编译时绑定,也就是编译时确定调⽤ 函数的地址,叫做静态绑定

2)、满⾜多态条件的函数调⽤是在运⾏时绑定,也就是在运⾏时到指向对象的虚函数表中找到调⽤函数的地址,也就做动态绑定

cpp 复制代码
// ptr是指针+BuyTicket是虚函数满⾜多态条件。 
 // 这⾥就是动态绑定,编译在运⾏时到ptr指向对象的虚函数表中确定调⽤函数地址 
 ptr->BuyTicket();
00EF2001 mov eax,dword ptr [ptr] 
00EF2004 mov edx,dword ptr [eax] 
00EF2006 mov esi,esp 
00EF2008 mov ecx,dword ptr [ptr] 
00EF200B mov eax,dword ptr [edx] 
00EF200D call eax
 // BuyTicket不是虚函数,不满⾜多态条件。 
 // 这⾥就是静态绑定,编译器直接确定调⽤函数地址 
 ptr->BuyTicket();
00EA2C91 mov ecx,dword ptr [ptr] 
00EA2C94 call Student::Student (0EA153Ch)
4.4 虚函数表

1)、基类对象的虚函数表存放基类所有虚函数地址。

2)、同类型对象虚函数表共用,不同类型对象的虚函数表不同,因此基类和派生类有各自的虚函数表。

3)、派生类由两部分构成,继承下来的基类和自己的成员。一般情况下,继承的基类中有虚函数表指针,自己就不会生成虚函数表指针。但是注意的是继承下来的基类部分虚函数表指针和基类对象虚函数表指针不是同一个。

4)、虚函数和普通函数一样,编译好后就是一段指令存在代码段

本期的分享就到这里,如果觉得博主的文章比较对胃口的话,可以点一个小小的关注~

您的三连是我持续更新的动力~

相关推荐
并不喜欢吃鱼1 小时前
从零开始 C++----- 十三【C++ 数据结构】哈希表从原理到手撕实现(开放定址 + 链地址全覆盖)
数据结构·c++·散列表
:1211 小时前
Java泛型
java·开发语言
愿天垂怜1 小时前
【C++脚手架】etcd 的介绍与使用
java·linux·服务器·c语言·c++·中间件·etcd
喵了几个咪1 小时前
Headless 后端实践:基于Go的企业级多栈管理系统脚手架
开发语言·vue.js·后端·golang·reactjs·gowind
小则又沐风a1 小时前
进程篇: 进程概念的补充(了解环境变量和虚拟地址空间)
linux·运维·服务器·c++
枫叶丹41 小时前
【HarmonyOS 6.0】Map Kit瓦片图层深度解析:本地加载方式与瓦片数据缓存能力
开发语言·缓存·华为·harmonyos
郝学胜-神的一滴1 小时前
[简化版 GAMES 101] 计算机图形学 11:频域·卷积·抗锯齿
c++·unity·图形渲染·opengl·three·unreal
小小龙学IT1 小时前
Go 并发模式深度解析:Fan-out/Fan-in 高效处理大规模数据流
开发语言·后端·golang
a83331961 小时前
c语言课程设计小游戏,c语言小游戏设计案例
c语言·开发语言