C++ —— 关于多态

news2024/11/28 4:09:01

目录

1. 多态的概念

2.多态的定义及实现

3. 虚函数 

3.1 虚函数的重写/覆盖

3.2 关于多态的面试难题 

3.3 虚函数重写的⼀些问题

3.4 override 和 final关键字

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

 3.6 纯虚函数和抽象类

4.多态的原理 

4.1虚函数表指针

4.2 多态是如何实现的

4.3 动态绑定与静态绑定

4.4 虚函数表


1. 多态的概念


多态(polymorphism)的概念:通俗来说,就是多种形态

   

多态分为编译时多态(静态多态)和运⾏时多态(动态多态)

    

我们把编译时归为静态,运⾏时归为动态

编译时多态(静态多态)主要就是我们前⾯讲的函数重载和函数模板,他们传不同类型的参数就可以调⽤不同的函数,通过参数不同达到多种形态,之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在编译时完成的

    

运⾏时多态,具体点就是去完成某个⾏为(函数),可以传不同的对象就会完成不同的⾏为,就达到多种形态。⽐如买票这个⾏为,当普通⼈买票时,是全价买票;学⽣买票时,是优惠买票(5折或75折);军⼈买票时是优先买票。再⽐如,同样是动物叫的⼀个⾏为(函数),传猫对象过去,就是”(>^ω^<)喵“,传狗对象过去,就是"汪汪" 


2.多态的定义及实现

多态的构成条件:

   

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

 实现多态还有两个必须重要条件:

  
                                                        1. 必须是基类的指针或者引⽤调⽤虚函数

  
                                                        2.  被调⽤的函数必须是虚函数。

说明:要实现多态效果

                                    1.第⼀必须是基类的指针或引⽤去调用虚函数,因为只有基类的指针或引⽤才能既指向派⽣类对象又指向基类

                                     2.第⼆派⽣类必须对基类的虚函数进行重写/覆盖,只有重写或者覆盖了,派⽣类才能有不同的函数,多态的不同形态效果才能达到,并且派生类与派生类之间也可以实现多态

 

class Parent
{
public:
	void virtual Show()
	{
		cout << "基类" << endl;
	}
};
 
class Child : public Parent
{
public:
	void virtual Show()
	{
		cout << "派生类" << endl;
	}
};
 
//引用
//void Display(Parent& p)
//{
//	p.Show();
//}

//指针
void Display(Parent* p)
{
	p->Show();
}
 
 
int main()
{
	Parent _parent;
	Child _child;
 
	//引用
	//Display(_parent);
	//Display(_child);
 
	//指针
	Display(&_parent);
	Display(&_child);
 
	return 0;
}


3. 虚函数 

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

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


3.1 虚函数的重写/覆盖
 

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

  

重写的本质是重写虚函数的实现

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

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : 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;
	Func(&ps);
	Func(&st);
	return 0;
}

多个子类中的多态

class Animal
{
public:
	virtual void talk() const
	{}
};

class Dog : public Animal
{
public:
	virtual void talk() const
	{
		std::cout << "汪汪" << std::endl;
	}
};

class Cat : public Animal
{
public:
	virtual void talk() const
	{
		std::cout << "(>^ω^<)喵" << std::endl;
	}
};

void letsHear(const Animal& animal)
{
	animal.talk();
}

int main()
{
	Cat cat;
	Dog dog;
	letsHear(cat);
	letsHear(dog);
	return 0;
}

3.2 关于多态的面试难题 

这里引出一个新的概念:多态重写时绝不重新定义继承而来的缺省值

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


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

答案是:B

解析:这里p->test()就已经构成了多态,也就是说B类中的fun()已经重写了A中的fun()函数,那么此时调用的结果就是B->,那么之后的值如何判断呢,根据我们上面引出的概念,多态重写时绝不重新定义继承而来的缺省值,也就是说此时的val仍然是A类中的缺省值1,所以最后的结果就是B->1

当使用p->fun()时由于传的指针是派生类的指针,于是不构成多态,所以直接调用的是B类中的fun()函数,最后结果是B->0

//面试难题
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();
    p->fun();
  
	return 0;
}

3.3 虚函数重写的⼀些问题

协变 

  

派⽣类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引⽤派⽣类虚函数返回派⽣类对象的指针或者引⽤时,称为协变

class A {};
class B : public A {};
 
class Person {
 
public:
	virtual A* BuyTicket()
	{
		cout << "买票-全价" << endl;
		return nullptr;
	}
};
 
class Student : public Person {
 
public:
	virtual B* BuyTicket()
	{
		cout << "买票-打折" << endl;
		return nullptr;
	}
};
 
void Func(Person* ptr)
{
	ptr->BuyTicket();
}
 
int main()
{
	Person ps;
	Student st;
	Func(&ps);
	Func(&st);
 
	return 0;
}

析构函数的重写(重点

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

 

下⾯的代码我们可以看到,如果~A(),不加virtual,那么delete p2时只调⽤的A的析构函数,没有调⽤B的析构函数,就会导致内存泄漏问题,因为~B()中在释放资源。

注意:这个问题⾯试中经常考察,⼤家⼀定要结合类似下⾯的样例才能讲清楚,为什么基类中的析构函数建议设计为虚函数  

//析构的重写
class A
{
public:
	virtual ~A()
	{
		cout << "~A()" << endl;
	}
};
 
class B :public A
{
public:
	//构成重写
	~B()
	{
		cout << "~B()" << endl;
	}
private:
	int* b = new int[10];
};
 
int main()
{
	//传的均是基类的指针,构成多态
	A* p1 = new A;
	A* p2 = new B;
 
	//基类传递基类的析构
	delete p1;
	//子类则传递子类的析构,最后析构完子类再析构父类
	delete p2;
 
	return 0;
}


3.4 override 和 final关键字

从上⾯可以看出,C++对虚函数重写的要求⽐较严格,但是有些情况下由于疏忽,⽐如函数名写错参数写错等导致⽆法构成重载,⽽这种错误在编译期间是不会报出的,只有在程序运⾏时没有得到预期结果才来debug会得不偿失,因此C++11提供了override,可以帮助⽤⼾检测是否重写。如果我们不想让派⽣类重写这个虚函数,那么可以⽤final去修饰

override:检查是否完成重写  

//override检查是否重写
class Car {
 
public:
	virtual void Dirve()
	{}
};
 
class Benz :public Car {
 
public:
	virtual void Drive() override { cout << "Benz-舒适" << endl; }
};
 
int main()
{
	return 0;
}

final:避免基类的虚函数被重写  

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

 


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

注意:这个概念对⽐经常考,⼤家得理解记忆⼀下


 3.6 纯虚函数和抽象类

在虚函数的后⾯写上 =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;
}

 


4.多态的原理 

4.1虚函数表指针

 

虚函数表本质是⼀个存虚函数指针的指针数组,也就是一个数组,里面存放的都是虚函数的指针 

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


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

  

答案: D

  

解析:除了_b和_ch成员,还多⼀个_vfptr放在对象的前⾯(vftabl用来存放虚函数的地址)(注意有些平台可能会放到对象的最后⾯,这个跟平台有关)

  

对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。⼀个含有虚函数的类中都⾄少都有⼀个虚函数表指针,因为⼀个类所有虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也简称虚表

  

这时根据内存对齐原则指针类型占4字节,32位下最大对齐数为8,取二者较小值也就是4,整数类型占4字节,char类型占1字节,内存对齐4字节后总的内存是4+4+4=12byte

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.2 多态是如何实现的

从底层的⻆度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; }
protected:
	string _name;
};
 
class Student : public Person {
 
public:
	virtual void BuyTicket() { cout << "买票-打折" << endl; }
protected:
	int _id;
};
 
class Soldier : public Person {
 
public:
	virtual void BuyTicket() { cout << "买票-优先" << endl; }
protected:
	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. 满⾜多态条件的函数调⽤是在运⾏时绑定,也就是在运⾏时到指向对象的虚函数表中找到调⽤函数的地址,也就做动态绑定

从编译的角度来看 

 

// 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 虚函数表

基类对象的虚函数表存放基类所有虚函数的地址(同类型对象虚表共用,不同类型对象虚表各自独立)

  
派⽣类由两部分构成,继承下来的基类和⾃⼰的成员,⼀般情况下,继承下来的基类中有虚函数表指针,⾃⼰就不会再⽣成虚函数表指针。但是要注意的这⾥继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同⼀个,就像基类对象的成员和派⽣类对象中的基类对象成员也独⽴的

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

  
派⽣类的虚函数表中包含:基类的虚函数地址,派⽣类重写(覆盖)的虚函数地址,派⽣类⾃⼰的虚函数地址三个部分

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

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

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


完结撒花~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2218919.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

go压缩的使用

基础&#xff1a;使用go创建一个zip func base(path string) {// 创建 zip 文件zipFile, err : os.Create("test.zip")if err ! nil {panic(err)}defer zipFile.Close()// 创建一个新的 *Writer 对象zipWriter : zip.NewWriter(zipFile)defer zipWriter.Close()// 创…

如何使用DockerSpy检测你的Docker镜像是否安全

关于DockerSpy DockerSpy是一款针对Docker镜像的敏感信息检测与安全审计工具&#xff0c;该工具可以帮助广大研究人员在Docker Hub上检测和搜索自己镜像的安全问题&#xff0c;并识别潜在的泄漏内容&#xff0c;例如身份验证密钥等敏感信息。 功能介绍 1、安全审计&#xff1a…

linux一二三章那些是重点呢

第一章 静态库动态库的区别 什么是库 库文件是计算机上的一类文件&#xff0c;可以简单的把库文件看成一种代码仓库&#xff0c;它提供给使用者一些可以直接 拿来用的变量、函数或类。 如何制作 静态动态库 静态库&#xff1a; GCC 进行链接时&#xff0c;会把静态库中代码打…

2013 lost connection to MySQL server during query

1.问题 使用navicat连接doris&#xff0c;会有这个错误。 2.解决 换低版本的navicat比如navicat11。

linux运行openfoam并行会报错:attempt to run parallel on 1 processor

linux运行openfoam并行会报错&#xff1a;attempt to run parallel on 1 processor 步骤&#xff1a; 1.先在终端输入which mpirun,查看当前并行路径&#xff1b; 2.输入gedit ~/.bashrc&#xff0c;文本方式打开bashrc文件&#xff1b; 3.修改为export PATH/usr/bin:$PATH&am…

支持阅后即焚的笔记Enclosed

什么是 Enclosed &#xff1f; Enclosed 是一个简约的网络应用程序&#xff0c;旨在发送私人和安全的笔记。所有笔记均经过端到端加密&#xff0c;确保服务器和存储对内容一无所知。用户可以设置密码、定义有效期 (TTL)&#xff0c;并选择在阅读后让笔记自毁。 软件特点&#x…

第一年改考408的学校有炸过的吗?怎么应对突然改考408?

C哥专业提供——计软考研院校选择分析专业课备考指南规划 专业课改考 408 后&#xff0c;分数线不一定会暴涨&#xff0c;其变化受到多种因素影响&#xff1a; 可能导致分数线不暴涨甚至下降的因素&#xff1a; 考试难度增加&#xff1a;408 统考涵盖数据结构、计算机组成原理…

P2-1与P2-2.【C语言基本数据类型、运算符和表达式】第一节与第二节

讲解视频&#xff1a; P2-1.【基本数据类型、运算符和表达式】第一节 P2-2.【基本数据类型、运算符和表达式】第二节 必备知识与理论 1&#xff0e;数据类型概述 所谓数据类型&#xff0c;是按被定义变量的性质&#xff0c;表示形式&#xff0c;占据存储空间的多少&#xff0…

【分布式事务-04】分布式事务seata的XA模式

redis系列整体栏目 内容链接地址【一】分布式事务之2pc两阶段提交https://zhenghuisheng.blog.csdn.net/article/details/142406325【二】分布式事务seata的安装下载与环境搭建https://zhenghuisheng.blog.csdn.net/article/details/142893117【三】分布式事务seata的AT模式htt…

k8s ETCD数据备份与恢复

在 Kubernetes 集群中&#xff0c;etcd 是一个分布式键值存储&#xff0c;它保存着整个集群的状态&#xff0c;包括节点、Pod、ConfigMap、Secrets 等关键信息。因此&#xff0c;定期对 etcd 进行备份是非常重要的&#xff0c;特别是在集群发生故障或需要恢复数据的情况下。本文…

Axure科技感元件:打造可视化大屏设计的得力助手

Axure&#xff0c;作为一款专业的原型设计工具&#xff0c;凭借其强大的设计功能、丰富的组件库和灵活的交互能力&#xff0c;成为了许多设计师打造科技感设计的首选工具。其中&#xff0c;Axure科技感元件更是以其独特的魅力和实用性&#xff0c;在数据可视化大屏、登录界面、…

HarmonyOS开发(State模型)

一、State模型概述 FA&#xff08;Feature Ability&#xff09;模型&#xff1a;从API 7开始支持的模型&#xff0c;已经不再主推。 Stage模型&#xff1a;从API 9开始新增的模型&#xff0c;是目前主推且会长期演进的模型。在该模型中&#xff0c;由于提供了AbilityStage、Wi…

Leetcode—1114. 按序打印【简单】(多线程)

2024每日刷题&#xff08;179&#xff09; Leetcode—1114. 按序打印 C实现代码 class Foo { public:Foo() {firstMutex.lock();secondMutex.lock();}void first(function<void()> printFirst) {// printFirst() outputs "first". Do not change or remove t…

jupyter notebook远程连接服务器

jupyter notebook远程连接服务器 文章目录 jupyter notebook远程连接服务器jupyter是什么配置步骤安装jupyter生成jupyter配置文件编辑jupyter配置文件设置密码ssh隧道 启动顺序jupyter添加kernel下载ipykernel包添加kernel 测试遇到的问题 jupyter是什么 Jupyter Notebook是一…

fastStone Capture截图神器,你想要的功能它都有!

前言 大家好&#xff0c;我是小徐啊。从今天开始&#xff0c;小徐将介绍很多Java开发领域相关的软件工具资源&#xff0c;欢迎大家关注。今天&#xff0c;介绍一款非常小巧&#xff0c;但功能十分强大的图片软件&#xff0c;fastStone Capture。这款工具&#xff0c;主要是图片…

101、QT摄像头录制视频问题

视频和音频录制类QMediaRecorder QMediaRecorder 通过摄像头和音频输入设备进行录像。 注意: 使用Qt多媒体模块的摄像头相关类无法在Windows平台上进行视频录制&#xff0c;只能进行静态图片抓取但是在Linux平台上可以实现静态图片抓取和视频录制。 Qt多媒体模块的功能实现是依…

Git之代已修改文件的目录高亮设置

不管Android Studio或者Idea&#xff0c;进入Setting 选择如图所示&#xff0c;并进行勾选 就可以高亮了。

sentinel原理源码分析系列(四)-ContextEntry

启动和初始化完成后&#xff0c;调用者调用受保护资源&#xff0c;触发sentinel的机制&#xff0c;首先构建或获取Context和获取Entry&#xff0c;然后进入插槽链&#xff0c;决定调用是否通过&#xff0c;怎样通过 上图展示构建Context和获取Entry的类互动图 获取或构建Conte…

深度学习实战94-基于图卷积神经网络GCN模型的搭建以及在金融领域的场景

大家好,我是微学AI,今天给大家介绍一下深度学习实战94-基于图卷积神经网络GCN模型的搭建以及在金融领域的场景。文章首先介绍了GCN模型的原理及模型结构,随后提供了数据样例,并详细展示了实战代码。通过本文,读者可以深入了解GCN模型在金融场景下的应用,同时掌握代码的具…

keil5软件调试纪要

1&#xff0c;连接ST-LINK后查看连接信息。 2&#xff0c;除了printf调式外&#xff0c;keil5进行如下调式。 &#xff08;0&#xff09;进入调试界面 退出调式界面 &#xff08;1&#xff09; 打断点 &#xff08;2&#xff09;复位 &#xff08;3&#xff09;运行 &#xf…