​​C++多态​​


目录

1. 多态的概念

2. 多态的定义及实现

多态的构成条件

虚函数

虚函数的重写

特例

override 和 final

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

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

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

3. 抽象类

概念

接口继承和实现继承

4. 多态的原理

虚函数表

多态的原理

动态绑定与静态绑定

5. 单继承和多继承关系中的虚函数表

6. 继承和多态常见的面试问题


1. 多态的概念

具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。

2. 多态的定义及实现

多态的构成条件

1. 必须通过基类的指针或者引用调用虚函数

2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

虚函数

被virtual修饰的类成员函数称为虚函数
class Person {
public:
	virtual void BuyTicket() //BuyTicket()就是虚函数
	{
		cout << "买票-全价" << endl;
	}
};

虚函数的重写

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

class Person {
public:
	virtual void BuyTicket() //BuyTicket()就是虚函数
	{
		cout << "买票-全价" << endl;
	}
};

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

特例

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

class A {};
class B : public A {};
class Person 
{
public:
	virtual A* f() 
	{
		return new A;
	}
};
class Student : public Person 
{
public:
	virtual B* f() 
	{ 
		return new B; 
	}
};
协变: 派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或引用

2. 析构函数的重写(基类与派生类析构函数的名字不同)

class Person
{
public:
	virtual ~Person() 
	{
		cout << "~Person()" << endl; 
	}
};
 
class Student : public Person
{
public:
	virtual ~Student()
	{ 
		cout << "~Student()" << endl;
	}
};
编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理为destr uctor。

override 和 final

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

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

class Traffic
{
public:
	virtual void Way() 
	{}
};
class Car :public Traffic
{
public:
	virtual void Way()override { cout << "自驾" << endl; }
};

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

3. 抽象类

概念

包含纯虚函数的类叫做抽象类(也叫接口类),抽象类 不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。
class Animal
{
public:
	virtual void Play() = 0;
};
class Cat :public Animal
{
public:
	virtual void Play()
	{
		cout << "睡大觉" << endl;
	}
};
class Dog :public Animal
{
public:
	virtual void Play()
	{
		cout << "出去溜达" << endl;
	}
};
void test2()
{
	Animal* pCat = new Cat;
	pCat->Play();
	Animal* pDog = new  Dog;
}

接口继承和实现继承

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

4. 多态的原理

虚函数表

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

int main()
{
	cout << sizeof(Base) << endl;
	return 0;
}

//64位下是16
//32位下是8
提问: 是不是虚函数数量越多,这个值就越大呢?(很显然不是),这个值来源究竟是什么呢?
我们通过下面这个代码来观察:
class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	virtual void Func3()
	{
		cout << "Base::Func3()" << endl;
	}
	
private:
	int _b = 1;
};
class Derive : public Base
{
public:
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
private:
	int _d = 2;
};

int main()
{
	Base b;
	Derive d;

	cout << sizeof(b)<<"  "<<sizeof(Base) << endl;
	return 0;
}

1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就
是存在部分的另一部分是自己的成员。
2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重
写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的
叫法,覆盖是原理层的叫法。
3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会
放进虚表。
4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
5. 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基
类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在
派生类中的声明次序增加到派生类虚表的最后。
6. 这里还有一个童鞋们很容易混淆的问题:虚函数存在哪的?虚表存在哪的? 答:虚函数存在虚表,虚表
存在对象中。注意上面的回答的错的。但是很多童鞋都是这样深以为然的。注意虚表存的是虚函数指
针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外
对象中存的不是虚表,存的是虚表指针。

多态的原理

通过下面这段代码我们再来简单分析一下:

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

void Func(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person ps;
	Student st;

	Func(ps);
	Func(st);
}

1. 观察下图的红色箭头我们看到, p是 ps 对象的引用时, p->BuyTicket 在ps 的虚表中找到虚函数是
Person::BuyTicket
2. 观察下图的蓝色箭头我们看到, p 是st 对象的引用时, p->BuyTicket 在st 的虚表中找到虚函数
Student::BuyTicket
3. 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。
4. 反过来思考我们要达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调用虚函数。 反思一下为什么?
虚函数覆盖是提供了重写的方式,指针或者引用是为了寻址。

动态绑定与静态绑定

1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载

2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行

为,调用具体的函数,也称为动态多态。

5. 单继承和多继承关系中的虚函数表

单继承:

多继承:


class Base1 {
public:
	virtual void func1() 
	{ 
		cout << "Base1::func1" << endl; 
	}
	virtual void func2() 
	{
		cout << "Base1::func2" << endl; 
	}
private:
	int b1;
};
class Base2 {
public:
	virtual void func1() 
	{ 
		cout << "Base2::func1" << endl;
	}
	virtual void func2() 
	{ 
		cout << "Base2::func2" << endl;
	}

private:
	int b2;
};
class Derive1 : public Base1, public Base2 {
public:
	virtual void func1()
	{ 
		cout << "Derive::func1" << endl; 
	}
	virtual void func3() 
	{
		cout << "Derive::func3" << endl;
	}
private:
	int d1;
};

typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
	cout << " 虚表地址>" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
		VFPTR f = vTable[i];
		f();
	}
	cout << endl;
}
int main()
{
	Derive1 d;
	VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
	PrintVTable(vTableb1);
	VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
	PrintVTable(vTableb2);
	return 0;
}

观察下图可以看出: 多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中

6. 继承和多态常见的面试问题

1. 下面哪种面向对象的方法可以让你变得富有( ) 

A: 继承 B: 封装 C: 多态 D: 抽象 

2. ( )是面向对象程序设计语言中的一种机制。这种机制实现了方法的定义与具体的对象无关,而对方法的 

调用则可以关联于具体的对象。 

A: 继承 B: 模板 C: 对象的自身引用 D: 动态绑定 

3. 面向对象设计中的继承和组合,下面说法错误的是?() 

A:继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复用,也称为 

白盒复用 

B:组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动态复用,也 

称为黑盒复用 

C:优先使用继承,而不是组合,是面向对象设计的第二原则 

D:继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封装性的表现 

4. 以下关于纯虚函数的说法,正确的是( ) 

A:声明纯虚函数的类不能实例化对象 B:声明纯虚函数的类是虚基类 

C:子类必须实现基类的纯虚函数 D:纯虚函数必须是空函数 

5. 关于虚函数的描述正确的是( ) 

A:派生类的虚函数与基类的虚函数具有不同的参数个数和类型 B:内联函数不能是虚函数 

C:派生类必须重新定义基类的虚函数 D:虚函数可以是一个static型的函数 

6. 关于虚表说法正确的是( ) 

A:一个类只能有一张虚表 

B:基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表 

C:虚表是在运行期间动态生成的 

D:一个类的不同对象共享该类的虚表 

7. 假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则( ) 

A:A类对象的前4个字节存储虚表地址,B类对象前4个字节不是虚表地址 

B:A类对象和B类对象前4个字节存储的都是虚基表的地址 

C:A类对象和B类对象前4个字节存储的虚表地址相同 

D:A类和B类虚表中虚函数个数相同,但A类和B类使用的不是同一张虚表 
8. 以下程序输出结果是什么() 
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; 

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

6.2 问答题 

1. 什么是多态?
具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。

2. 什么是重载、重写(覆盖)、重定义(隐藏)?
函数重载是函数的一种特殊情况,c++允许在同一作用域中声明几个功能类似的同名函数,这些函数的形参列表(参数个数或类型或顺序)必须不同,对返回值不做要求,因为无法通过改变参数列表来判断调用的是那一个重载函数。
 

覆盖也叫做重写,指的是子类与父类中返回值类型,参数,名称均相同的虚函数,此时构成虚函数的覆盖(覆盖是原理层的叫
法,在语法上叫做重写)

子类和父类中含有同名函数,此时子类成员将屏蔽父类对同名成员的直接访问,这种情况叫做隐藏。隐藏对函数参数和返回值不做要求,只需要名称相同就可以

3. 多态的实现原理?
上面有分析。

4. inline函数可以是虚函数吗?
答:可以,不过编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去。 

5. 静态成员可以是虚函数吗?
答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式 无法访问虚函数表,所以静态成员函数无法放进虚函数表。 

6. 构造函数可以是虚函数吗?
答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。 

7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
答:可以,并且最好把基类的析构函数定义 成虚函数。

8. 对象访问普通函数快还是虚函数更快?
答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。 

9. 虚函数表是在什么阶段生成的,存在哪的?
答:虚函数表是在编译阶段就生成的,一般情况下存在代码 段(常量区)的。 

参考答案: 

1. A 2. D 3. C 4. A 5. B 

6. D 7. D 8.B 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

档案库房智能管理系统的功能有哪些呢?

档案库房智能管理系统是一个基于人工智能技术的综合性档案管理解决方案&#xff0c;通过自动化、智能化的方式&#xff0c;优化了档案管理流程&#xff0c;提高了工作效率和信息安全性。 1.档案入库管理&#xff1a; 档案信息录入&#xff1a;系统可以通过扫描、识别和自动填写…

Qt应用开发(基础篇)——框架类 QFrame

一、前言 QFrame继承于QWidget&#xff0c;被QLCDNumber、QToolBox、QLabel、QListView等部件继承&#xff0c;是一个拥有矩形框架的基类。 QFrame可以直接创建成一个没有内容的的矩形框架&#xff0c;框架的样式由边框厚度(lineWidth)、框架形状(QFrame::Shape)和阴影样式(QFr…

在vue中使用swiper轮播图(搭配watch和$nextTick())

在组件中使用轮播图展示图片信息&#xff1a; 1.下载swiper,5版本为稳定版本 cnpm install swiper5 2.在组件中引入swiper包和对应样式&#xff0c;若多组件使用swiper&#xff0c;可以把swiper引入到main.js入口文件中&#xff1a; import swiper/css/swiper.css //引入swipe…

玩转IndexedDB,比localStorage、cookie还要强大的网页端本地缓存

随着浏览器的功能不断增强&#xff0c;越来越多的网站开始考虑&#xff0c;将大量数据储存在客户端&#xff0c;这样可以减少从服务器获取数据&#xff0c;直接从本地获取数据。 现有的浏览器数据储存方案&#xff0c;都不适合储存大量数据&#xff1a;Cookie 的大小不超过 4K…

安科瑞电力监控系统在某区块页岩气地面集输工程中的应用

摘要&#xff1a;Acrel-2000Z电力监控系统适用于35kV及以下电压等级的各类变电站&#xff0c;可以帮助用户掌握配电系统实时运行状态&#xff0c; 获取预警、告警等各类事件&#xff0c;实现区域的无人值守&#xff0c;提高监管水平。本文介绍了安科瑞电力监控系统Acrel-2000在…

QEMU源码全解析37 —— Machine(7)

接前一篇文章&#xff1a;QEMU源码全解析36 —— Machine&#xff08;6&#xff09; 本文内容参考&#xff1a; 《趣谈Linux操作系统》 —— 刘超&#xff0c;极客时间 《QEMU/KVM》源码解析与应用 —— 李强&#xff0c;机械工业出版社 特此致谢&#xff01; 上回书讲完了q…

java 加载商户API私钥 (pem证书私钥)

1. pem证书放在resources目录下 2. 加载证书的工具类 import com.wechat.pay.contrib.apache.httpclient.util.PemUtil; // 商户API私钥 (把证书放在项目路径下, 然后加载出来), 加载证书的工具类PrivateKey merchantPrivateKey PemUtil.loadPrivateKey(new FileInp…

使用FTP文件传输协议的潜在风险

数据&#xff08;事实&#xff0c;数字&#xff0c;价值&#xff09;是当今业务运行的核心要素。但是&#xff0c;如果数据没有得到有效的存储和传输&#xff0c;它们就会成为阻碍业务发展的障碍。如果企业不能及时地把数据送到合适的地方&#xff0c;就会造成严重的经济损失。…

8.10 用redis实现缓存功能和Spring Cache

什么是缓存? 缓存(Cache), 就是数据交换的缓冲区,俗称的缓存就是缓冲区内的数据,一般从数据库中获取,存储于本地代码。 通过Redis来缓存数据&#xff0c;减少数据库查询操作; 逻辑 每个分类的菜品保存一份缓存数据 数据库菜品数据有变更时清理缓存数据 如何将商品数据缓存起…

Word 2019打开.doc文档后图片和公式不显示(呈现为白框)的解决办法

Word 2019打开.doc文档后图片和公式不显示&#xff08;呈现为白框&#xff09;的解决办法 目录 Word 2019打开.doc文档后图片和公式不显示&#xff08;呈现为白框&#xff09;的解决办法一、问题描述二、解决方法1.打开 WORD 2019&#xff0c;点击菜单中的“文件”&#xff1b;…

买前必看!蓝牙耳机哪些牌子值得买?蓝牙耳机热销排行榜

蓝牙耳机作为现代生活必备的电子产品之一&#xff0c;我们在选购时的选择就显得尤为重要。随着各大科技公司对蓝牙耳机功能的不断完善&#xff0c;用户对于耳机的期望也越来越高&#xff0c;音质、性能、降噪、舒适度等方面都成为了用户选择蓝牙耳机时考虑的因素。接下来我们一…

Redis复制

在Redis中&#xff0c;用户可以通过执行SLAVEOF命令或者设置slaveof选项&#xff0c;让一个服务器去复制(replicate) 另一个服务器&#xff0c;我们称呼被复制的服务器为主服务器(master)&#xff0c;而对主服务器进行复制的服务器则被称为从服务器(slave)&#xff0c;如下图所…

网络协议栈-基础知识

1、分层模型 1.1、OSI七层模型 1、OSI&#xff08;Open System Interconnection&#xff0c;开放系统互连&#xff09;七层网络模型称为开放式系统互联参考模型 &#xff0c;是一个逻辑上的定义&#xff0c;一个规范&#xff0c;它把网络从逻辑上分为了7层。 2、每一层都有相关…

工厂方法模式-java实现

介绍 工厂方法模式&#xff0c;通过把工厂抽象为一个接口&#xff0c;这样当我们新增具体产品的时候&#xff0c;就只需要实现一个新的具体工厂类即可。一个具体工厂类&#xff0c;对应着一个产品。 请注意&#xff1a;在工厂方法模式中&#xff0c;一个具体工厂类只对应生产…

FPGA + WS2812采灯控制

文章目录 一、WS2812C-2020-V11、产品概述2、引出端排列及功能3、数据传输时间4、数据传输方法 二、使用WS2812C显示图片1、静态显示2、动态显示 一、WS2812C-2020-V1 1、产品概述 WS2812C-2020-V1是一个集控制电路与发光电路于一体的智能外控LED光源&#xff1b;其外型采用最…

谷粒商城第十一天-完善商品分组(主要添上关联属性)

目录 一、总述 二、前端部分 2.1 改良前端获取分组列表接口及其调用 2.2 添加关联的一整套逻辑 三、后端部分 四、总结 一、总述 前端部分和之前的商品品牌添加分类差不多。 也是修改一下前端的分页获取列表的接口&#xff0c;还有就是加上关联的那一套逻辑&#xff0c;…

7.3 详解NiN模型--首次使用多层感知机(1x1卷积核)替换掉全连接层的模型

一.前提知识 多层感知机&#xff1a;由一个输入层&#xff0c;一个或多个隐藏层和一个输出层组成。&#xff08;至少有一个隐藏层&#xff0c;即至少3层&#xff09; 全连接层&#xff1a;是MLP的一种特殊情况&#xff0c;每个节点都与前一层的所有节点连接&#xff0c;全连接…

7.5.tensorRT高级(2)-RAII接口模式下的生产者消费者多batch实现

目录 前言1. RAII接口模式封装生产者消费者2. 问答环节总结 前言 杜老师推出的 tensorRT从零起步高性能部署 课程&#xff0c;之前有看过一遍&#xff0c;但是没有做笔记&#xff0c;很多东西也忘了。这次重新撸一遍&#xff0c;顺便记记笔记。 本次课程学习 tensorRT 高级-RAI…

前后端分离------后端创建笔记(05)用户列表查询接口(上)

本文章转载于【SpringBootVue】全网最简单但实用的前后端分离项目实战笔记 - 前端_大菜007的博客-CSDN博客 仅用于学习和讨论&#xff0c;如有侵权请联系 源码&#xff1a;https://gitee.com/green_vegetables/x-admin-project.git 素材&#xff1a;https://pan.baidu.com/s/…

City Walk带动茶饮品牌售1200万,媒介盒子带你探究奥秘

年轻人生活趋势又出现了一个新鲜词——City Walk&#xff0c;简单来说&#xff0c;City Walk就是没有目的地&#xff0c;没有目标&#xff0c;只是出行&#xff0c;填充自己的生活。 其实这个词源于gap year&#xff0c;而这个说法一直是国外的一些毕业生&#xff0c;大多会在…