当前位置: 首页 > article >正文

C++_详解多态

18fde01fee5e4278981004762ce48cc4.png

✨✨ 欢迎大家来到小伞的大讲堂✨✨

🎈🎈养成好习惯,先赞后看哦~🎈🎈

所属专栏:C++学习
小伞的主页:xiaosan_blog

1.多态的概念

通俗来说,就是多种形态,具体点就是去完成某个行为(函数),通过传不同的参数调用不同的函数,达到多种形态。也就是说,当不同的对象去完成时会 产生出不同的状态。

举个例子:
当普通人去买票(某个行为),是全价买票;学生买票(同样满足这个行为),是优惠票价;军人买票时,则是优先买票;
同样是动物叫的一个行为(函数),传猫对象过去,就是”(>^ω^<) 喵“,传狗对象过去,就是"汪汪"。

 2.多态的定义与实现

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

 2.1 实现多态必须满足的两个重要条件

1.必须指针或者引用调用**虚函数**
2.被调用的必须是虚函数,且派生类必须对基类的虚函数进行重写

 说明:要实现多态效果,第一必须是基类的指针或引用,因为只有基类的指针或引用才能既指向派生类对象;第二派生类必须对基类的虚函数重写/覆盖,重写或者覆盖了,派生类才能有不同的函数,多态的不同形态效果才能达到。

2.1.1 虚函数

类成员函数前面加virtual修饰,那么这个成员函数被称为虚函数。注意非成员函数不能加virtual修饰。

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


class student :public person {
public:
	void BuyTicket() {
		cout << "买票半价" << endl;
	}
};
2.1.2 虚函数的重写/覆盖 

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

注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用,不过在考试选择题中,经常会故意买这个坑,让你判断是否构成多态。

让我们历练历练一下吧

小练:

以下程序输出结果是什么()

A: A->0   B: B->1   C: A->1   D: B->0   E: 编译出错   F: 以上都不正确

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,恭喜你答对了,这题出错点主要在以下几个问题:

1、对继承条件的判断,有相当一部分人会因为基类func与派生类func的val值不同,或者因为这里调用A::的函数 而认为其不构成继承。

2、认为此时的val为B类中的val=0,这部分弄错了派生类对基类重写的部分,与继承的部分。

如下图解释的那样,当我们调用基类的test函数时,此时的*this指针是A*(B)类型,此时满足继承的条件,构成重载。然后函数调用func函数,由于其构成重载,所以调用基类继承下来的函数,机,此时的val为继承基类的val,派生类的重写是对基类函数实现部分的重写,其余都继承父类。

2.2 析构函数的重写

基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor,所以基类的析构函数加了vialtual修饰,派生类的析构函数就构成重写。
 

class A
{
	public :
	virtual ~A()
	{
    //如果~A(),不加virtual,那么delete p2时只调⽤的A的析构函数,没有调⽤
    //B的析构函数,就会导致内存泄漏问题,因为~B()中在释放资源。 
		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;
}

有什么方法检测函数是否重写了呢

2.3 override 和 final关键字

C++11提供了override,可以帮助用户检测是否重写。如果我们不想让派生类重写这个虚函数,那么可以用final去修饰。

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

2.4 重载/重写/隐藏的对比

重载:

1. 两个函数在同一作用域

2.函数相同,参数不同,参数类型或者个数不同,返回值可同,可不同

重写:

1.两个函数分别继承在父类和子类不同作用域

2.函数名,参数,返回值必须相同,协变除外

3.必须是虚函数

隐藏:

1.两个函数分别继承在父类和子类不同作用域

2.函数名相同

3.两个函数不构成重写,就是隐藏

4.父类的成员函数相同也叫隐藏

2.5 纯虚函数和抽象类

在虚函数的后⾯写上 =0 ,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义因为要被派生类重写,但是语法上可以实现),只要声明即可。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。纯虚函数某种程度上强制了派生类重写虚函数,因为不重写实例化不出对象。

class Car
{
	public :
	virtual void Drive() = 0;
};
class Benz :public Car
{
public:
	virtual void Drive()
	{
		cout << "Benz-舒适" << endl;
	}
};
class BMW :public Car
{
	public :
	virtual void Drive()
	{
		cout << "BMW-操控" << endl;
	}
};
int main()
{
	// 编译报错:error C2259: “Car”: ⽆法实例化抽象类
	Car car;
	Car* pBenz = new Benz;
	pBenz->Drive();
	Car* pBMW = new BMW;
	pBMW->Drive();
	return 0;
}

3.多态的原理

下⾯编译为32位程序的运行结果是什么()

A. 编译报错 B. 运行报错 C. 8 D. 12

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

上面题目运行结果12bytes,除了_b和_ch成员,我们发现还多⼀个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。⼀个含有虚函数的类中都至少都有一个虚函数表指针,因为一个类所有虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也简称虚表。

3.1 多态是如何实现

从底层的角度Func函数中ptr->BuyTicket(),是如何作为ptr指向Person对象调用Person:BuyTicket,ptr指向Student对象调用Student::BuyTicket?满足多态条件后,底层不再是编译时通过调用对象确定函数的地址,而是运行时到指向的对象的虚表中确定对应的虚函数的地址,这样就实现了指针或引用指向基类就调用基类的虚函数,指向派生类就调用派生类对应的虚函数。
 

第一张图,ptr指向的Person对象,调用的是Person的虚函数; 我们发现他们虚函数地址接近,是否可以提出一个猜想呢,他们的虚函数的地址存放在虚表中,当满足多态时,编译器会到虚表中查找对应虚函数地址。

 第二张图,ptr指向的Student对象,调用的是Student的虚函数。

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

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

3.2 动态绑定与静态绑定

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

2.满足多态条件的函数调用是在运行时绑定,也就是在运行时到指向对象的虚函数表中找到调用函数的地址,也就做动态绑定。

// 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)

3.3 虚表

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

2.派生类由两部分构成,继承下来的基类和自己的成员,一般情况下,继承下来的基类中有虚函数表指针,自己就不会再生成虚函数表指针。但是要注意的这里继承下来的基类部分虚函数表指针基类对象的虚函数表指针不是同一个,就像基类对象的成员和派生类对象中的基类对象成员也独立的。

3.派生类中重写的基类的虚函数,派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址。

4.派生类的虚函数表中包含,基类的虚函数地址,派生类重写的虚函数地址,派生类自己的虚函数地址三个部分。

5.虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个0x00000000标记。(这个C++并没有进行规定,各个编译器自行定义的,vs系列编译器会再后面放个0x00000000标记,g++系列编译不会放)

6.虚函数存在哪的?虚函数和普通函数一样的,编译好后是一段指令,都是存在代码段的,只是虚函数的地址又存到了虚表中。

7.虚函数表存在哪的?这个问题严格说并没有标准答案C++标准并没有规定,我们写下面的代码可以对比验证一下。vs下是存在代码段(常量区)

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()
//{
//	Base b;
//	Derive d;
//	return 0;
//}
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;
}


http://www.kler.cn/a/419733.html

相关文章:

  • SpringBoot集成Kafka和avro和Schema注册表
  • 前端跳转路由的时候,清掉缓存
  • 【机器学习】分类任务: 二分类与多分类
  • 学习threejs,使用VideoTexture实现视频Video更新纹理
  • oracle数据库日常操作
  • vue+mars3d给影像底图叠加炫酷效果
  • Base64.cv:高效安全的在线Base64转换工具详解
  • 高效集成:将聚水潭数据导入MySQL的实战案例
  • PostgreSQL17.x创建用户与授权命令
  • 具身智能高校实训解决方案——从AI大模型+机器人到通用具身智能
  • Oracle DataGuard 主备正常切换 (Switchover)
  • 《沉积与特提斯地质》
  • PD虚拟机启动Windows系统突然黑屏的解决方法
  • 小程序-基于java+SpringBoot+Vue的养老院管理系统设计与实现
  • 【datasheet】LTC4412 (2)
  • 阿里重磅开源 Fluss: Flink Unified Streaming Storage
  • 宠物领养网络:SpringBoot的实现之道
  • Android studio 签名加固后的apk文件
  • YOLOv1 (You Only Look Once)
  • 如何使用 Jenkins 集成 Docker 以实现自动化 CI/CD 流程
  • 递归1——递归入门
  • 计算机网络复习2——物理层
  • C++多线程——原子操作(atomic)
  • Ardusub源码剖析——control_manual.cpp
  • 【网络安全设备系列】1、防火墙
  • Electron-vue 框架升级 Babel7 并支持electron-preload webapck 4 打包过程记录