【C++】—— 多态常见的笔试和面试问题
序言:
在上期,我们对多态进行了详细的讲解。本期,我给大家带来的是关于有关多态常见的笔试和面试问题,帮助大家理解记忆相关知识点。
目录
(一)概念查考
(二)问答题
1、简述一下 C++ 中的多态
2、什么是重载、重写(覆盖)、重定义(隐藏)?
3、C++ 的重载和重写是如何实现的
4、inline函数可以是虚函数吗?
5、静态成员可以是虚函数吗?
6、为什么要虚析构,而不能虚构造?
7、对象访问普通函数快还是虚函数更快?
8、虚函数表里存放的内容是什么时候写进去的?
9、C++ 中哪些函数不能被声明为虚函数?
(一)概念查考
- A: 继承 B: 模板 C: 对象的自身引用 D: 动态绑定
【答案】:D
- A:继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复 用,也称为白盒复用
- B:组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动 态复用,也称为黑盒复用
- C:优先使用继承,而不是组合,是面向对象设计的第二原则
- D:继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封 装性的表现
【答案】:C
在面向对象设计中,并没有规定继承应该优先于组合。事实上,面向对象设计原则中的一个重要原则是「优先使用组合,而不是继承」,也被称为组合优于继承原则
这个原则建议使用组合(或聚合)关系来构建对象之间的关联,而不是过度使用继承。通过组合,可以更灵活地构建对象之间的关系,避免了过度依赖继承关系导致的紧耦合和复杂性增加。
因此,选项C中的说法是错误的。
- A:声明纯虚函数的类不能实例化对象
- B:声明纯虚函数的类是虚基类
- C:子类必须实现基类的纯虚函数
- D:纯虚函数必须是空函数
【答案】:A
纯虚函数是在基类中声明的虚函数,它没有提供默认的实现,而是由派生类来实现。这个函数在基类中被声明为纯虚函数,可以通过在函数声明的末尾使用 "= 0" 来表示。
根据C++语言规范,含有纯虚函数的类被称为抽象类,不能被直接实例化对象。只能通过派生类来创建对象,派生类需要实现基类中的纯虚函数,才能够被实例化。
选项B、C、D的说法是不正确的:
- B:声明纯虚函数的类并不一定是虚基类,虚基类是指在多重继承中被标记为虚继承的类。
- C:子类可以选择是否实现基类的纯虚函数,如果子类不实现基类中的纯虚函数,则它也会成为一个抽象类,不能直接实例化对象。
- D:纯虚函数可以有自己的实现代码,不一定是空函数。只要在基类中将该函数声明为纯虚函数即可。
4.关于虚函数的描述正确的是( )
- A:派生类的虚函数与基类的虚函数具有不同的参数个数和类型
- B:内联函数不能是虚函数
- C:派生类必须重新定义基类的虚函数
- D:虚函数可以是一个static型的函数
【答案】:B
选项A是错误的描述。派生类的虚函数与基类的虚函数具有相同的参数个数和类型。在C++中,虚函数实现了动态绑定,允许基类指针或引用在运行时动态地调用派生类的虚函数。
选项C也是错误的描述。派生类不一定必须重新定义基类的虚函数。如果派生类没有重新定义基类的虚函数,那么它将继承基类的虚函数实现。
选项D也是错误的描述。虚函数不能是一个static型的函数。虚函数通过对象的动态类型来确定调用的函数,而static函数是与类本身相关联的,不涉及对象的特定实例。
因此,正确的描述是 B:内联函数不能是虚函数。
5.关于虚表说法正确的是( )
- A:一个类只能有一张虚表
- B:基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表
- C:虚表是在运行期间动态生成的
- D:一个类的不同对象共享该类的虚表
【答案】:D
A:一个类的每个实例都有自己的虚函数表指针(vptr),即每个对象都有一个指向虚函数表的指针。每个类只有一个虚函数表,但每个对象都有自己的vptr,可以指向该类的虚函数表。
B:当派生类继承基类的虚函数并在派生类中没有重写时,子类将共享基类的虚函数表,并且派生类对象的vptr指针将指向基类的虚函数表。
C:虚表是在编译阶段由编译器生成的静态数据结构。编译器根据类的层次结构和虚函数的声明创建虚函数表,并在每个对象的vptr中存储对应的虚函数表。
因此,选项 B、C 和 A 都是错误的。只有选项 D 是正确的,即一个类的不同对象共享该类的虚表。
6.假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则( )
- A:A类对象的前4个字节存储虚表地址,B类对象前4个字节不是虚表地址
- B:A类对象和B类对象前4个字节存储的都是虚基表的地址
- C:A类对象和B类对象前4个字节存储的虚表地址相同
- D:A类和B类虚表中虚函数个数相同,但A类和B类使用的不是同一张虚表
【答案】:D
在这种情况下,类A和类B各自会有自己的虚函数表,虽然它们的虚函数个数相同,但它们使用的是不同的虚表。类B继承自类A,当类B重写了类A中的虚函数时,会在类B的虚函数表中将该虚函数替换为类B的实现。因此,类A和类B的虚表中存储的是不同的虚函数的地址。
选项A、B和C均与这种情况不符,因此是错误的。正确选项是D。
7.下面程序输出结果是什么? ()
class A{
public:A(char *s) { cout<<s<<endl; }~A(){}
};
class B:virtual public A
{
public:B(char *s1,char*s2):A(s1) { cout<<s2<<endl; }
};
class C:virtual public A
{
public:C(char *s1,char*s2):A(s1) { cout<<s2<<endl; }
};
class D:public B,public C
{
public:D(char *s1,char *s2,char *s3,char *s4):B(s1,s2),C(s1,s3),A(s1){ cout<<s4<<endl;}
};
int main() {D *p=new D("class A","class B","class C","class D");delete p;return 0;
}
- A:class A class B class C class D
- B:class D class B class C class A
- C:class D class C class B class A
- D:class A class C class B class D
【答案】:A首先,通过创建D类的对象p并传递相应的字符串参数,会触发类的构造函数的调用。因为D类是继承自B和C类,并且B和C类都是虚继承自A类的,所以在创建D对象时,会按照虚继承的顺序先调用A类的构造函数。因此,首先输出 "class A"。
接下来,B类和C类的构造函数会被调用,分别输出 "class B" 和 "class C"。
最后,D类自身的构造函数被调用,输出 "class D"。
在程序结束时,会执行delete操作释放对象p所占用的内存,同时会按照与构造函数相反的顺序调用析构函数,所以会先输出 "class D"、"class C"、"class B"、"class A"。
(二)问答题
1、简述一下 C++ 中的多态
由于 派生类重写基类方法,然后用基类引用指向派生类对象,调用方法时候会进行动态绑定,这就是多态 。
- 1. 静态多态:编译器在编译期间完成的,编译器会根据实参类型来推断该调用哪个函数,如果有对应 的函数,就调用,没有则在编译时报错。
include<iostream>
using namespace std;int Add(int a,int b)//1
{return a+b;
}
char Add(char a,char b)//2
{return a+b;
}
int main()
{cout<<Add(666,888)<<endl;//1cout<<Add('1','2');//2return 0;
}
- 显然,第一条语句会调用函数1,而第二条语句会调用函数2,这绝不是因为函数的声明顺序,不信你可以将顺序调过来试试。
2.动态多态:其实要实现动态多态,需要几个条件——即动态绑定条件:
- 1. 虚函数。基类中必须有虚函数,在派生类中必须重写虚函数。
- 2. 通过基类类型的指针或引用来调用虚函数。
2、什么是重载、重写(覆盖)、重定义(隐藏)?
具体见我上一篇博客:C++ 的重载和重写,以及它们的区别
3、C++ 的重载和重写是如何实现的
- 用virtual关键字申明的函数叫做虚函数,虚函数肯定是类的成员函数。
- 存在虚函数的类都有一个一维的虚函数表叫做虚表,类的对象有一个指向虚表开始的虚指针。
- 虚表是和类对应的,虚表指针是和对象对应的。
- 多态性是一个接口多种实现,是面向对象的核心,分为类的多态性和函数的多态性。
- 重写用虚函数来实现,结合动态绑定。
- 纯虚函数是虚函数再加上 = 0。
- 抽象类是指包括至少一个纯虚函数的类。
4、inline函数可以是虚函数吗?
在C++中,虚函数是用于实现运行时多态性的概念,而虚函数的调用是通过指针或引用来完成的。然而,内联函数(inline function)在编译时被展开,将函数的代码插入到调用处,以减少函数调用的开销。
由于内联函数的展开是在编译时完成的,而虚函数的调用是在运行时动态确定的,因此二者的机制存在冲突。虚函数的调用需要根据对象的实际类型确定调用的函数,而内联函数的展开则需要在编译时就确定函数体的具体代码。
class Base {
public:virtual inline void foo() {cout << "Base::foo()" << endl;}
};class Derived : public Base {
public:inline void foo() override {cout << "Derived::foo()" << endl;}
};int main()
{Base* ptr = new Derived();ptr->foo();delete ptr;return 0;
}
【说明】
- 在这个示例中,我们将基类
Base
中的虚函数foo
声明为内联函数,并在派生类Derived
中进行重写。然后,我们通过基类的指针调用虚函数foo
。 - 尽管我们在虚函数声明中加上了
inline
关键字,但编译器会忽略它,将其视为普通的成员函数。因此,在运行时仍然会根据实际类型进行动态绑定。
- 这表明虚函数
foo
被正确地重写,并且运行时根据实际对象类型进行了动态绑定。
5、静态成员可以是虚函数吗?
静态成员函数不能被声明为虚函数。虚函数的多态性是通过在运行时动态绑定来实现的,而静态成员函数是与类本身相关联的,不依赖于特定的对象,因此无法进行动态绑定。
虚函数依赖于对象的虚表,通过对象的指针或引用来调用虚函数时,会根据对象的实际类型确定要调用的具体函数。而静态成员函数是与类关联,而不是与对象关联的,它可以通过类名直接调用,不依赖于特定的对象。
另外,静态成员函数没有 this 指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
综上所述,静态成员函数不能被声明为虚函数。
以下是一个示例代码,演示了将静态成员函数声明为虚函数将导致编译错误:
class Base {
public:virtual void foo() {std::cout << "Base::foo()" << std::endl;}static virtual void bar() // 尝试将静态成员函数声明为虚函数{std::cout << "Base::bar()" << std::endl;}
};int main() {Base::bar(); // 直接通过类名调用静态成员函数return 0;
}
上述代码中,我们尝试将静态成员函数bar()
声明为虚函数。如果我们尝试编译这段代码,编译器将给出一个错误。
这个错误告诉我们无法将静态成员函数声明为虚函数。
6、为什么要虚析构,而不能虚构造?
- 1. 用派生类类型指针绑定派生类实例,析构的时候,不管基类析构函数是不是虚函数,都会正常析构
- 2. 用基类类型指针绑定派生类实例,析构的时候,如果基类析构函数不是虚函数,则只会析构基 类,不会析构派生类对象,从而造成内存泄漏。为什么会出现这种现象呢,个人认为析构的时 候如果没有虚函数的动态绑定功能,就只根据指针的类型来进行的,而不是根据指针绑定的对 象来进行,所以只是调用了基类的析构函数;如果基类的析构函数是虚函数,则析构的时候就要根据指针绑定的对象来调用对应的析构函数了。
代码说明:
class TimeKeeper
{
public:TimeKeeper() {}~TimeKeeper() {} //非virtual的
};
//都继承与TimeKeeper
class AtomicClock :public TimeKeeper{};
class WaterClock :public TimeKeeper {};
class WristWatch :public TimeKeeper {};
TimeKeeper* getTimeKeeper()
{//返回一个指针,指向一个TimeKeeper派生类的动态分配对象
}
TimeKeeper* ptk = getTimeKeeper();delete ptk;
class TimeKeeper
{
public:TimeKeeper() {}virtual ~TimeKeeper() {}
};
- 1. 从存储空间角度:虚函数对应一个vtale,这个表的地址是存储在对象的内存空间的。如果将构造函数设置为虚函数,就需要到vtable 中调用,可是对象还没有实例化,没有内存空间分 配,如何调用。(悖论)
- 2. 从实现上看:vbtl 在构造函数调用后才建立,因而构造函数不可能成为虚函数。
7、对象访问普通函数快还是虚函数更快?
- 首先如果是普通对象,是一样快的;
- 如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
8、虚函数表里存放的内容是什么时候写进去的?
- 1. 虚函数表是一个存储虚函数地址的数组,以NULL结尾。虚表(vftable)在编译阶段生成,对象内存空间开辟以后,写入对象中的 vfptr,然后调用构造函数。即:虚表在构造函数之前写入
- 2. 除了在构造函数之前写入之外,我们还需要考虑到虚表的二次写入机制,通过此机制让每个对象的虚表指针都能准确的指向到自己类的虚表,为实现动多态提供支持。
9、C++ 中哪些函数不能被声明为虚函数?
常见的不不能声明为虚函数的有: 普通函数(非成员函数),静态成员函数,内联成员函数,构造函数,友元函数。
- 普通函数(非成员函数)只能被overload,不能被override,声明为虚函数无意义,因此编译器会在编译时绑定函数。
- 因为C++不支持友元函数的继承,对于没有继承特性的函数没有虚函数的说法。
其余的上述我都有讲解。
总结
多态作为笔试重点考察对象,希望大家能够勤加练习,掌握相关的知识。
相关文章:

【C++】—— 多态常见的笔试和面试问题
序言: 在上期,我们对多态进行了详细的讲解。本期,我给大家带来的是关于有关多态常见的笔试和面试问题,帮助大家理解记忆相关知识点。 目录 (一)概念查考 (二)问答题 1、简述一下…...

探寻AI大模型平台之巅——文心千帆
目录 前言1. 何为文心千帆2. 核心亮点2.1 第三方大模型2.2 Prompt模板2.3 安全可靠 3. 一站式服务3.1 数据管理3.2 数据标注3.3 数据处理3.4 数据训练3.5 模型纳管3.5.1 模型评估3.5.2 模型压缩 3.6 服务发布 总结 前言 众多AI大模型不断涌现,一时不知如何挑选&…...

【springboot】RestTemplate配置HttpClient连接池
在Java开发中,访问第三方HTTP协议的网络接口,通常使用的连接工具为JDK自带的HttpURLConnection、HttpClient(现在应该称之为HttpComponents)和OKHttp。 这些Http连接工具,使用起来都比较复杂,如果项目中使…...

MySQL内置函数使用说明
MySQL函数使用说明 MySQL 是一个流行的关系型数据库管理系统,它提供了许多内置函数来处理和操作数据。这些函数可以简化数据库查询和操作的过程,提高代码的可读性和效率。以下是一些常见的 MySQL 内置函数及其使用说明和示例。 数值函数 ABS() 函数原…...

java后端富文本转word,再传递到浏览器下载。
思路参考,以及所有的工具类都使用了》牧羊人大佬的代码《 有帮助的话不用给到我点赞,给大佬点赞即可 这是前端代码,必须使用get。 post后端返回的流浏览器接收不到(具体原因不详)。get无法传递requestBody,…...

【动态规划算法】-回文串问题题型(34-40题)
💖作者:小树苗渴望变成参天大树🎈 🎉作者宣言:认真写好每一篇博客💤 🎊作者gitee:gitee✨ 💞作者专栏:C语言,数据结构初阶,Linux,C 动态规划算法🎄 如 果 你 …...

STM32基础回顾
文章目录 单片机编程的原理GPIO中断EXTI外部中断定时器中断、串口中断 定时器定时器中断配置过程通用定时器输出比较功能:PWM波的生成定时器的输入捕获功能主从触发模式PWMI模式 定时器的编码器接口 DMA简介通信接口USART软件配置流程:1、仅发数据的配置…...

如何解决电脑无声问题:排除故障的几种常见方法
大家好,今天我们来讨论一下处理电脑没有声音的故障。当你突然发现电脑静音无声时,需要逐步排除可能的问题,但总体而言,声音故障是相对容易解决的。接下来,我们将介绍一些排除电脑无声问题的方法。 第一步:…...

Apache RocketMQ 命令注入
漏洞简介 RocketMQ 5.1.0及以下版本,在一定条件下,存在远程命令执行风险。RocketMQ的NameServer、Broker、Controller等多个组件外网泄露,缺乏权限验证,攻击者可以利用该漏洞利用更新配置功能以RocketMQ运行的系统用户身份执行命令…...

二、搜索与图论6:Dijkstra 模板题+算法模板(Dijkstra求最短路 I, Dijkstra求最短路 II,1003 Emergency)
文章目录 算法模板Dijkstra题目代码模板朴素dijkstra算法堆优化版dijkstra 树与图的存储(1) 邻接矩阵:(2) 邻接表:关于e[],ne[],h[]的理解 关于堆的原理与操作 模板题Dijkstra求最短路 I原题链接题目思路题解 Dijkstra求最短路 II原题链接题目思路题解 1…...

ROS2学习(四)进程,线程与节点的关系
节点与节点执行器 节点,英文是node,在ROS2中,节点是一个抽象的实体,它可以代表某种或某类特定功能的抽象集合体,它可以存在于进程中,也可以存在于线程中。所有ROS2的基础功能最基础的载体是节点,所有的通信…...

【物联网】DMA传输原理与实现详解(超详细)
DMA(Direct Memory Access,直接内存访问)是一种计算机数据传输方式,允许外围设备直接访问系统内存,而无需CPU的干预。 文章目录 Part 1: DMA的工作原理配置阶段:数据传输阶段: Part 2: DMA数据…...

Java类集框架(二)
目录 1.Map(常用子类 HashMap,LinkedHashMap,HashTable,TreeMap) 2.Map的输出(Map.Entry,iterator,foreach) 3.数据结构 - 栈(Stack) 4.数据结构 - 队列(Q…...

爬虫008_流程控制语句_if_if else_elif_for---python工作笔记026
然后我们再来看一下这里的,判断,可以看到 再看一个判断,这里的布尔类型 第二行有4个空格,python的格式 注意这里,输入的age是字符串,需要转一下才行 int可以写到int(intput("阿斯顿法师打发地方")) 这样也可以...

【随笔】五周年创作纪念日
今天收到了 CSDN 的创作五周年提示,正好前几天(7.31)我也成功申请了 CSDN 博客专家,趁这个机会分享一下这几年写博客的感受吧 机缘 关注我比较久的读者应该知道我是从学传统工科半路出家搞计算机的,这里的经历还是比…...

7_分类算法—逻辑回归
文章目录 逻辑回归:1 Logistic回归(二分类问题)1.1 sigmoid函数1.2 Logistic回归及似然函数(求解)1.3 θ参数求解1.4 Logistic回归损失函数1.5 LogisticRegression总结 2 Softmax回归(多分类问题࿰…...

【计算机网络】应用层协议 -- DNS协议
文章目录 1. DNS背景2. 域名简介3. 域名解析过程4. 使用dig查看DNS过程 1. DNS背景 DNS(Domain Name System,域名系统)协议,是一个用来将域名转化为IP地址的应用层协议。 TCP/IP当中通过IP地址和端口号的方式,来确定…...

ES6 - 数组新增的一些常用方法
文章目录 1,Array.from()2,Array.of()3,find(),findIndex(),findLast()和findLastIndex()4,Array.fill()5,keys(),values() 和 entries()6,Array.includes()7,…...

【BEV感知】3-BEV开源数据集
3-BEV开源数据集 1 KITTI1.1 KITTI数据怎么采集?1.2 KITTI数据规模有多大?1.3 KITTI标注了哪些目标?1.4 转换矩阵1.5 标签文件 2 nuScenes2.1 nuScenes Vs KITTI2.2 标注文件 1 KITTI KITTI 1.1 KITTI数据怎么采集? 通过车载相机、激光雷达等传感器采集。 只提供了相机正…...

Kafka-Broker工作流程
kafka集群在启动时,会将每个broker节点注册到zookeeper中,每个broker节点都有一个controller,哪个controller先在zookeeper中注册,哪个controller就负责监听brokers节点变化,当有分区的leader挂掉时,contro…...

第八篇-Tesla P40+ChatGLM2+LoRA
部署环境 系统:CentOS-7CPU: 14C28T显卡:Tesla P40 24G驱动: 515CUDA: 11.7cuDNN: 8.9.2.26目的 验证P40部署可行性,只做验证学习lora方式微调创建环境 conda create --name glm-tuning python3.10 conda activate glm-tuning克隆项目 git clone http…...

调用feign返回错误的数据
bug描述: 在一个请求方法中会调用到feign去获取其他的数据。 List<Demo> list aaaFeignApi.getData(personSelectGetParam);在调用的时候,打断点到feign的地方,数据是存在的,并且有15条。但是返回到上面代码的时候数据就…...

【Spring】(二)从零开始的 Spring 项目搭建与使用
文章目录 前言一、Spring 项目的创建1.1 创建 Maven 项目1.2 添加 Spring 框架支持1.3 添加启动类 二、储存 Bean 对象2.1 创建 Bean2.1 将 Bean 注册到 Spring 容器 三、获取并使用 Bean 对象3.1 获取Spring 上下文3.2 ApplicationContext 和 BeanFactory 的区别3.3 获取指定的…...

redis五种数据类型介绍
、string(字符串) 它师最基本的类型,可以理解为Memcached一模一样的类型,一个key对应一个value。 注意:一个键最大能存储 512MB。 特性:可以包含任何数据,比如jpg图片或者序列化的对象,一个键最大能存储512…...

【JavaEE】Spring Boot - 项目的创建和使用
【JavaEE】Spring Boot 开发要点总结(1) 文章目录 【JavaEE】Spring Boot 开发要点总结(1)1. Spring Boot 的优点2. Spring Boot 项目创建2.1 下载安装插件2.2 创建项目过程2.3 加载项目2.4 启动项目2.5 删除一些没用的文件 3. Sp…...

Git reset、revert用法
reset reset是删除之前的提交记录,所有的提交点都会被清除,我们看下执行前后的git log区别 D:\workspace\android>git log commit 87c1277a57544c53c603b04110e3dde100da8f57 (HEAD -> develop_main) Author: test <test.com> Date: Wed…...

Redis-1
Redis 理论部分 redis 速度快的原因 1、纯内存操作 2、单线程操作,避免了频繁的上下文切换和资源争用问题,多线程需要占用更多的 CPU 资源 3、采用了非阻塞 I/O 多路复用机制 4、提供了非常高效的数据结构,例如双向链表、压缩页表和跳跃…...

【Linux】Linux服务器连接百度网盘:实现上传下载
【Linux】Linux服务器连接百度网盘:实现上传下载 文章目录 【Linux】Linux服务器连接百度网盘:实现上传下载1. 前言2. 具体过程2.1 pip 安装所需包2.2 认证(第一次连接需要认证)2.3 下载所需文件或者目录2.4 其他指令使用2.5 注意…...

ADC模拟看门狗
如果被ADC转换的模拟电压低于低阀值或高于高阀值,AWD模拟看门狗状态位被设置。阀值位 于ADC_HTR和ADC_LTR寄存器的最低12个有效位中。通过设置ADC_CR1寄存器的AWDIE位 以允许产生相应中断。通过以下函数可以进行配置 void ADC_AnalogWatchdogCmd(ADC_TypeDef* ADCx…...

google谷歌gmail邮箱账号注册手机号无法进行验证怎么办?此电话号码无法用于进行验证 或 此电话号码验证次数太多
谷歌gmail邮箱账号注册手机号无法进行验证怎么办? 使用手机号码注册谷歌gmail邮箱账号时会遇到:此电话号码无法用于进行验证 或 此电话号码验证次数太多。造成注册google谷歌gmail邮箱账号受阻,无法正常完成注册。 谷歌Gmail邮箱账号正确的注册方法与教…...