C++进阶——多态

目录

一、多态的概念

二、多态的实现

1.逻辑条件

2.代码层面

3.一个经典题目

4.虚函数重写的其它问题

4.1协变(了解)

4.2析构函数重写

[4.3 override和final](#4.3 override和final)

4.4重载、重写(覆盖)和隐藏的对比

5.纯虚函数和抽象类

三、多态的原理

1.虚函数表指针

2.多态的原理

3.动态绑定和静态绑定

4.虚函数表(细节)


一、多态的概念

多态,如它本身所表达的意思,就是一种事物多种状态、形态。

多态分为编译时多态和运行时多态。

编译时多态(也称静态多态)其实我们之前已经了解很多了,函数模板和函数重载就是,通过函数模板和函数重载,我们可以达到一种传不同的参数使"同一个函数"进行对应的不同的操作的状态,由于这种多态在编译时函数所需要传递的参数就已经确定了,所以叫做编译时多态,也称作静态多态。

运行时多态(也称动态多态)具体点其实就是我们的程序需要去完成某个行为,比如购票,如果传学生对象,那么就打折,如果传军人对象,那么就优先购票,如果传普通人,那么就是没有任何优惠。程序会根据我们传递的对象而在运行时展现出不同的行为,所以叫做运行时多态,也叫动态多态。

二、多态的实现

1.逻辑条件

多态其实就是同一基类的派生类对象,调用同一函数,而产生了不同的结果。

这就是多态实现的逻辑条件,比如军人和学生继承自Person,都调用购票函数,但是产生的结果不同。

2.代码层面

根据C++的设计,代码层面要实现多态需要做到两点:

1.基类具有虚函数且必须用基类的指针或引用调用虚函数

2.派生类对基类的虚函数进行重写(覆盖)

cpp 复制代码
#include <iostream>
using namespace std;

class Person
{
public:
	// 实现多态基类必须有虚函数
	virtual void buy_ticket()
	{
		cout << "购票-正常" << endl;
	}
};

class Student : public Person
{
public:
	// 实现多态必须让派生类重写基类虚函数
	// 重写:"三同"函数名、参数列表、返回类型
	virtual void buy_ticket()
	{
		cout << "购票-打折" << endl;
	}
};

// 为什么必须传基类的指针或引用?
// 因为这样不管是传派生类对象还是基类对象,都会统一切片成基类对象
// 也就是说这样的方式可以保证基类对象和派生类对象都可以调用此函数
// 而如果形参是派生类的指针或引用,传基类的对象就不得行
void func(Person *p)
{
	// 这里Student不是被切片成基类对象了吗,怎么调用出来会显示打折?
	// 后面的虚函数表会解释,大家先别急,现在只需要知道这么可以实现多态即可
	p->buy_ticket();
}

void func(Person& p)
{
	p.buy_ticket();
}

int main()
{
	Student s;
	Person p;
	func(&s);  //打折
	func(s);	    //打折
	func(p);	//正常

	return 0;
}

3.一个经典题目

猜编译结果:

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()
{
	B* p = new B;
	p->test();
	return 0;
}

输出:B->1

没错!!!!本博主没敲错!!!!!!

看我的注释的解释:

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:
	// 在重写基类虚函数时,派⽣类的虚函数在不加virtual关键字时,虽然也可以构成重写
	// 因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性
	// 但是该种写法不是很规范,不建议这样使用,不过在考试选择题中,经常会故意埋这个坑,让你判断是否构成多态。
	// 所以这里其实是构成多态的
	void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};
int main()
{
	B* p = new B;
	p->test();

	// test没有重写,test不构成多态,test的本质是virtual void test(A* this){}
	// 参数列表中A是基类,A*是基类对象的指针,所以构成了多态
	// 所以调用test()时,会走到B重写的func中,所以是B->?
	// 其实很多人都误以为是B->0,但其实是B->1
	// 虚函数的重写,缺省值确实不用一样,但是其实本质上是对函数体的重写,而没有重写参数列表,更没有重写缺省值
	// 所以这里多态的缺省值应该是1

	p->func(); 
	// 这个的输出结果是B->0
	// 为什么不是B->1呢?
	// 因为这里不是多态的用法,func的参数缺省值就是0,只有当多态时才和基类一样
	// 所以说,这里的细节太多,容易失误,在具体写代码的时候要避免在派生类重写的虚函数中定义或修改缺省值

	return 0;
}

4.虚函数重写的其它问题

4.1协变(了解)

简单说,因为协变没什么实际意义,可能是祖师爷多喝了几杯......

派生类重写的虚函数,与基类的虚函数返回值可以不同,但是必须返回该类的指针或引用。

派生类返回派生类的指针或引用,基类返回基类的指针或引用。

4.2析构函数重写

基类的析构函数默认就是虚函数,而派生类的析构函数一旦定义,无论是否显示写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];
};

int main()
{
	A* p1 = new A;
	A* p2 = new B;
	// 回忆一下delete:先调用对象的析构函数,再释放申请的空间
	delete p1;
	delete p2;
	// 如果A的析构函数不是虚函数
	// p1和p2都是A*类型,调用析构函数都是调用的~A()
	// 但是delete p2是想释放一个B对象的空间,B对象的空间里有需要B的析构函数释放的资源
	// 所以就会造成内存泄漏

	// 如果A的析构函数是虚函数
	// 那么p2调用析构函数,A和B的析构函数构成了多态,所以会调用B的析构函数,释放B对象申请的资源
	// 而且B的析构函数调用完成之后也会自动调用A的析构函数,不会造成内存泄漏

	// 所以综上所述,我们在写基类的构造函数时最好还是将析构函数定义成虚函数

	return 0;
}

4.3 override和final

override关键字:有些时候,我们由于自身的疏忽,比如写错函数名等,以至于虚函数并没有重载,但是这个错误如果一开始没发现,我们在后面的运行阶段才能发现错误,再去debug,就会很浪费时间,所以C++11新增了override关键字,在函数的参数列表后面加上override关键字,编译器就会帮我们检查该函数是否重写了其它的虚函数,如果没有,那么就会直接报错。

cpp 复制代码
// error C3668: "Benz::Drive": 包含重写说明符"override"的⽅法没有重写任何基类⽅法
class Car {
public:
	virtual void Dirve()
	{}
};
class Benz :public Car {
public:
	// 如果函数名写错了,预编译阶段就会直接报错
	virtual void Dirve() override { cout << "Benz-舒适" << endl; }
};
int main()
{
	Benz b;
	Car* p = &b;
	p->Dirve();
	return 0;
}

final关键字:之前在学继承的时候,如果这个类不想被继承,就可以在类名后面加个final修饰,这样如果有继承了该类的类,在编译阶段就会报错。这里也是一样,在虚函数的参数列表后面加上final这个关键字修饰,该虚函数就不能被重写,如果重写了,就会直接报错。

cpp 复制代码
// error C3248: "Car::Drive": 声明为"final"的函数⽆法被"Benz::Drive"重写
class Car
{
public:
	virtual void Drive() final {}
};

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

	return 0;
}

4.4重载、重写(覆盖)和隐藏的对比

1.重载是同一作用域中,同名函数的多中实现方式,函数名相同,但是参数的传递不同,传不同的参数会调用不同的函数(参数类型、数量可能不同),返回值无吊所谓。

2.重写(覆盖)是在不同作用域下,派生类对基类中虚函数的一种类似于重新定义。两个函数都是虚函数,函数名,参数列表和返回值都相同(除了协变和析构函数这种特殊情况),实现不同。

3.隐藏是在不同作用域下,函数名或者变量名相同即可(当然前提也得是继承)。

5.纯虚函数和抽象类

纯虚函数:在虚函数的参数列表后面加上" = 0",这个虚函数就变成了纯虚函数,纯虚函数不需要被实现,(实现了也没啥意义),主要是为了让派生类重写。

抽象类:具有纯虚函数的类,就是抽象类,抽象类不能实例化出对象,作用是供别的类继承。

注意:如果派生类继承了抽象类而没有重写抽象类中的纯虚函数,那么这个派生类也是抽象类,因为基类中的纯虚函数被继承了下来。

cpp 复制代码
class Person
{
public:
	// 纯虚函数,不需要实现
	virtual void buy_ticket() = 0;
	// 拥有了纯虚函数,Person就成了抽象类
};

class Student : public Person
{
public:
	// 如果不重写Person的纯虚函数,那么Student就会从Person继承纯虚函数,也会变成抽象类
	virtual void buy_ticket()
	{
		cout << "购票-打折" << endl;
	}
};

class Soldier : public Person
{
public:
	virtual void buy_ticket()
	{
		cout << "购票-优先" << endl;
	}
};

void func(Person *p)
{
	p->buy_ticket();
}

void func(Person& p)
{
	p.buy_ticket();
}

int main()
{
	//Person p;
	// "Person" : 无法实例化出抽象类

	//Student s;
	//Soldier soldier;
	//Person* p1 = &s;
	//Person* p2 = &soldier;

	// 上面的可以一次性写成下面这种形式

	Person* p1 = new Student;
	Person* p2 = new Soldier;
	func(p1);
	func(p2);

	return 0;
}

三、多态的原理

1.虚函数表指针

对一个具有虚函数的类,调试时进入监视窗口就会发现,有个我们不认识的东西:

虚函数表指针,指向的是一个函数指针数组,这个数组中存了该类中所有虚函数的指针,这个数组叫虚表(虚函数表)。

2.多态的原理

看图说话:

3.动态绑定和静态绑定

静态绑定:不满足多态条件的虚函数调用(使用指针或引用),在编译阶段就会确定虚函数的地址,所以是静态绑定。
动态绑定:满足多态条件的虚函数调用(使用指针或引用),在运行时在指向对象的虚表中寻找虚函数的地址,所以时动态绑定。

4.虚函数表(细节)

派生类在刚开始会和基类有一个相同的虚函数表。

1.当派生类对基类中的虚函数进行重写时,派生类的虚函数表会发生改变(重写的虚函数覆盖掉原来的虚函数地址)

2.当派生类增添新的虚函数时,派生类的虚函数表会增添一个虚函数地址

综合1、2,可以知道,派生类的虚函数表包括三部分的地址:基类的虚函数(除重写)、重写的基类的虚函数、派生类新添加的虚函数。

3.虚函数表一般情况下会放一个0x00000000的标志表明虚函数表的末尾,但是C++标准并没有规定。(vs是这样的,gcc不是)。

4.虚函数跟普通函数一样,编译后是一段指令,都存在代码段,只不过是地址存到了虚表。

5.虚函数表的存储位置,C++没有明确规定,但是可以通过这段代码,来通过比较看出来(相同区域地址类似,况且这只是个小程序而已,挨得都比较近)

cpp 复制代码
class Base {
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
	void func5() { cout << "Base::func5" << endl; }
protected:
	int a = 1;
};
class Derive : public Base
{
public:
	// 重写基类的func1
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func1" << endl; }
	void func4() { cout << "Derive::func4" << endl; }
protected:
	int b = 2;
};

int main()
{
	int i = 0;
	static int j = 1;
	int* p1 = new int;
	const char* p2 = "xxxxxxxx";
	printf("栈:%p\n", &i);
	printf("静态区:%p\n", &j);
	printf("堆:%p\n", p1);
	printf("常量区:%p\n", p2);
	Base b;
	Derive d;
	Base* p3 = &b;
	Derive* p4 = &d;
	printf("Person虚表地址:%p\n", *(int*)p3);
	printf("Student虚表地址:%p\n", *(int*)p4);
	printf("虚函数地址:%p\n", &Base::func1);
	printf("普通函数地址:%p\n", &Base::func5);
	return 0;
}

所以vs2022中虚表是存在常量区的。


以上就是本博客的所有内容啦!

完结撒花~~~~~~~~~~~~~~~~~~~~~~~~~~

(´。✪ω✪。`)

相关推荐
程序猿阿伟1 分钟前
《C++中的魔法:实现类似 Python 的装饰器模式》
java·c++·装饰器模式
Mr. zhihao3 分钟前
装饰器模式详解:动态扩展对象功能的优雅解决方案
java·开发语言·装饰器模式
zyhomepage4 分钟前
科技的成就(六十四)
开发语言·人工智能·科技·算法·内容运营
Ethan Wilson10 分钟前
C++/QT可用的websocket库
开发语言·c++·websocket
小宇1 小时前
The valid characters are defined in RFC 7230 and RFC 3986
java·开发语言·后端·tomcat
尘浮生1 小时前
Java项目实战II基于Spring Boot的美食烹饪互动平台的设计与实现(开发文档+数据库+源码)
java·开发语言·数据库·spring boot·微信小程序·小程序·美食
杨荧1 小时前
【JAVA毕业设计】基于Vue和SpringBoot的校园美食分享平台
java·开发语言·前端·vue.js·spring boot·java-ee·美食
糊涂君-Q1 小时前
Python小白学习教程从入门到入坑------第十九课 异常模块与包【下】(语法基础)
开发语言·python·学习·程序人生·改行学it
ergevv1 小时前
类的变量的初始化:成员初始化列表、就地初始化
c++·初始化·
爱编程的小新☆1 小时前
Java篇图书管理系统
java·开发语言·学习