嵌入式八股-C++面试91题(20240809)

1. 讲一讲封装、继承、多态是什么?

封装:将具体实现过程和数据封装成一个类,只能通过接口进行访问,降低耦合性,使类成为一个具有内部数据的自我隐藏能力、功能独立的软件模块。

  • 意义:保护代码防止被破坏,保护类中的成员,通过提供的公共接口进行访问。

继承:子类继承父类的特征和行为,复用基类的成员数据和成员函数,具有从基类复制而来的数据成员和成员函数(基类私有成员可被继承但无法被访问)。构造函数、析构函数、友元函数、静态数据成员、静态成员函数不能被继承。基类成员的访问方式决定派生类能否访问它们。

  • 意义:基类程序代码可被派生类复用,提高软件复用效率,缩短开发周期。

多态:不同继承类的对象对同一消息做出不同响应,基类的指针指向或绑定到派生类的对象,使得基类指针呈现不同表现形式。

  • 意义:代码具有可替代性和可扩展性,新增子类不影响现有类,提高使用效率,简化代码编写和修改过程。

2. 多态的实现原理(实现方式)是什么?以及多态的优点(特点)?

实现方式

  • 动态多态:通过虚函数实现,编译时不确定调用哪个函数,运行时确定。虚函数存在虚函数表中,子类调用虚函数时,通过虚表找到对应函数指针,实现多态。

  • 静态多态:通过函数重载、运算符重载和重定义(隐藏)实现,编译时确定调用哪个函数。

优点:增强代码可扩展性、可替换性,程序更加灵活,提高使用效率,简化代码编写和修改。

3. final标识符的作用是什么?

  • :表示该类无法被继承,阻止类继承。

  • 虚函数:表示该虚函数无法被重写,阻止虚函数重载。

4. 虚函数是怎么实现的?它存放在哪里在内存的哪个区?什么时候生成的?

实现原理

  • 虚函数表:存储类中所有虚函数的地址。

  • 虚函数指针:在对象内存布局中指向虚函数表,动态调用虚函数。

虚函数在编译阶段生成,存放在代码段,虚表指针存放在对象内存布局中。

5. 智能指针的本质是什么,它们的实现原理是什么?

本质:封装了一个原始C++指针的类模板,确保动态内存的安全性。

实现原理:通过对象存储自动释放的资源,依靠对象的析构函数释放资源。

6. 匿名函数的本质是什么?他的优点是什么?

本质:一个对象,定义时创建一个栈对象,内部通过重载()符号实现函数调用。

优点:免去函数的声明和定义,仅在调用时创建对象,调用结束后立即释放,节省空间。

7. 右值引用是什么,为什么要引入右值引用?

右值引用:为临时变量取别名,绑定到临时变量或表达式上,允许对右值进行修改。

引入原因

  1. 移动语义:避免昂贵的复制操作,实现高效的移动语义。

  2. 完美转发:绑定任何类型的右值,作为参数传递并转发,实现完美转发。

  3. 灵活的模板编程:拓展可变参数模板。

8. 左值引用和指针的区别?

  • 是否初始化:指针可以不初始化,引用必须初始化。

  • 性质:指针是变量,引用是对象的别名。

  • 内存单元:指针有独立地址,引用与对象共用一个地址。

9. 指针是什么?

指针:存储内存地址的变量,用于访问存储在特定地址上的数据。

10. weak_ptr真的不计数?是否有计数方式,在哪分配的空间。

计数:控制块中有强弱引用计数。

  • makeshared初始化:控制块与sharedptr同一块空间。

  • new初始化:控制器内存与shared_ptr不同块内存。

11. malloc的内存分配的方式,有什么缺点?

分配方式

  1. brk()系统调用:分配小于128KB内存,堆顶指针移动获取内存,free后内存缓存不归还系统。

  2. mmap()系统调用:分配大于128KB内存,使用私有匿名映射分配内存,free后内存归还系统。

缺点:容易造成内存泄漏和碎片,影响系统运行,需要判断内存分配成功,释放后指针需置NULL防止野指针。

11.1 为什么不全部使用mmap来分配内存?

原因:系统调用耗时,频繁切换状态影响效率。mmap分配的内存每次释放归还系统,触发缺页中断。

11.2 为什么不全部都用brk?

原因:频繁调用malloc和free产生内存碎片,影响效率。

12. 传入一个指针,它如何确定具体要清理多少空间呢?

申请内存时多分配16字节,保存内存块详细信息,free时对内存地址左偏移16字节,分析内存块大小。

13. define和const的区别是什么?

  • 编译阶段:define在编译预处理阶段简单文本替换,const在编译阶段确定值。

  • 安全性:define没有数据类型,无类型检查,const有类型检查。

  • 内存占用:define宏常量多次替换占用多个备份,const常量占用静态存储区域空间。

  • 调试:define宏常量不能调试,const常量可调试。

14. 程序运行的步骤是什么

  1. 预编译:头文件编译,宏替换,输出.i文件。

  2. 编译:转化为汇编语言文件,词法分析,语义分析,错误检查,生成.s文件。

  3. 汇编:汇编器翻译为机器语言,生成.o文件。

  4. 链接:目标文件和库链接,生成可执行文件.exe。

15. 锁的底层原理是什么?

底层原理:通过CAS和atomic机制实现。

  • CAS机制:比较相同再交换,确保原子操作。

  • atomic机制:提供原子操作。

16. 原子操作是什么?

原子操作:不会被线程调度打断的操作,执行期间不会切换线程。 原理:CPU提供对总线加锁手段,指令前加LOCK前缀锁住总线,保证原子性。

17. class与struct的区别

  • 继承权限:class默认private继承,struct默认public继承。

  • 模板参数:class用于定义模板参数,struct不能定义模板参数,C++保留struct兼容C语言。

18. 内存对齐是什么?为什么要进行内存对齐?内存对齐有什么好处?

内存对齐:处理器为提高性能对存取数据起始地址的要求。

  • 原因:不同硬件平台具有差异性,内存对齐提高平台移植性。

  • 好处:减少内存访问次数,提高性能,增强程序可移植性。

19. 进程之间的通信方式有哪些?

  • 管道:匿名管道和命名管道,半双工通信,适合简单通信。

  • 消息队列:边发边收,消息长度和总数量有限。

  • 共享内存:解决内核态和用户态数据拷贝问题。

  • 信号量:实现进程间互斥和同步。

  • 信号

  • 套接字

20. 线程之间的通信方式有哪些?

  • 信号量

  • 条件变量

  • 互斥量

21. 介绍一下socket中的多路复用,及其他们的优缺点,epoll的水平和边缘触发模式

IO多路复用selectpollepoll都是IO多路复用机制,可以监视多个文件描述符,一旦某个文件描述符进入读或写就绪状态,系统就能进行相应的读写操作。

Select

优点

  • 可移植性好,因为在某些Unix系统中并不支持poll和epoll。

  • 对于超时时间提供了更好的精度:微秒,而poll和epoll都是毫秒级。

缺点

  • 支持监听的文件描述符fd的数量有限制,最大数量默认是1024个。

  • 需要维护一个用来存放文件描述符的数据结构,每次调用select都需要把fd集合从用户区拷贝到内核区,select系统调用后有需要把fd集合从内核区拷贝到用户区,这个系统开销在fd数量很多的时候会很大。

Poll

优点

  • 没有最大文件描述符数量的限制,poll基于链表存储,主要解决了文件描述符数量的限制问题。

  • 优化了编程接口,减少了函数调用参数。

缺点

  • 与select一样,需要维护一个用来存放文件描述符的数据结构,当注册的文件描述符数量很多时,会使得用户态和内核区之间传递该数据结构的复制开销很大。
Epoll

优点

  • 没有最大文件描述符数量的限制。

  • 只需要将文件描述符数据结构拷贝一次,不需要重复拷贝,在调用epollctl系统调用时拷贝一次要监听的文件描述符数据结构到内核区,在调用epollwait时不需要再重复拷贝。

  • 提供水平触发和边缘触发两种模式。

缺点

  • 目前只有Linux操作系统支持epoll,不支持跨平台使用,而Unix操作系统上是使用kqueue。
Epoll的触发模式

水平触发(LT)

  • 对于读操作,只要缓冲区内容不为空,LT模式返回读就绪。

  • 对于写操作,只要缓冲区还不满,LT模式返回写就绪。

边缘触发(ET)

  • 对于读操作,当缓冲区由不可读变为可读的时候,进程会被唤醒;缓冲区数据变少时不会再次唤醒进程。

  • 当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完,ET模式下不会再次通知,直到出现第二次可读写事件。

22. 什么是重载、重写、隐藏?

重载:函数名相同,参数不同,处于同一作用域。

重写:子类和父类中函数名、返回值、参数相同,必须为虚函数。

隐藏:子类实现了一个与父类同名的函数,隐藏父类同名函数。

23. 什么是this指针,为什么存在this指针?

this指针:指向调用成员函数的当前对象的指针。

原因:类和对象的成员函数存储在公共代码段,不同对象调用成员函数时,编译器通过 this 指针知道具体操作的对象。

24. 类的生命周期

类从被加载到内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。其中验证,准备,解析三个部分统称为连接。

  • 全局对象在main开始前被创建,main退出后被销毁。

  • 静态对象在第一次进入作用域时被创建,在main退出后被销毁。

  • 局部对象在进入作用域时被创建,在退出作用域时被销毁。

  • new创建的对象直到内存被释放的时候都存在。

25. 父类的构造函数和析构函数是否能为虚函数?这样操作导致的结果?

  • 构造函数不能为虚函数,因为虚函数表由类的实例化对象的vptr指针指向,该指针存放在对象内部空间中,需要调用构造函数完成初始化,如果构造函数为虚函数,调用构造函数时vptr还未初始化,导致无法构造对象。

  • 析构函数可以且经常为虚函数,当使用父类指针指向子类时,调用析构函数只会调用父类的析构函数,子类的析构函数不会被调用,容易造成内存泄漏。

26. 多线程为什么会发生死锁,死锁是什么?死锁产生的条件,如何解决死锁?

死锁:在多进程中易发生多进程对资源的竞争,如果一个进程集合中的每个进程都在等待集合中的其他一个进程才能继续执行,若无外力,他们将无法推进,这种情况就是死锁。

产生死锁的四个条件

  1. 互斥条件

  2. 请求和保持条件

  3. 不可剥夺条件

  4. 环路等待条件

解决死锁的方法

  • 破坏上述任意一种条件。

27. 描述一下面向过程和面向对象

面向对象

  • 将问题分解为各个对象,建立对象的目的是描述某个事物在解决问题步骤中的行为。

  • 代码更易维护和复用,但代码效率相对较低。

面向过程

  • 分析问题的步骤,然后一步步实现,使用时按顺序调用即可。

  • 代码效率高,但复用率低,不易维护。

28. C++中左值和右值是什么?++i是左值还是右值,++i和i++哪个效率更高?

左值:指表达式结束后依然存在的值。 右值:指表达式结束后不再存在的值。

++i是左值,因为它返回自身,i++是右值,因为它返回一个临时变量。

效率:++i更高,因为++i直接对变量进行自增操作,而i++会产生一个临时变量保存旧值。

29. 介绍一下vector、list的底层实现原理和优缺点

Vector

  • 优点:可使用下标随机访问,尾插尾删效率高。

  • 缺点:前面部分的插入删除效率低,扩容有消耗,可能存在一定的空间浪费。

  • 底层:由连续的内存空间组成,由头指针、尾指针和可用空间尾指针实现。

List

  • 优点:按需申请内存,不需要扩容,不会造成内存空间浪费。任意位置插入删除效率高。

  • 缺点:不支持下标随机访问。

  • 底层:由双向链表实现。

30. 静态变量在哪里初始化?在哪一个阶段初始化?

静态变量、全局变量、常量在编译阶段完成初始化和内存分配,存放在全局区域。

31. 如何实现多进程?

  • Linux:使用fork函数创建进程。

  • Windows:使用CreateProcess函数创建进程。

32. 空对象指针为什么能调用函数?

在类初始化时,编译器将函数分配到类的外部,包括静态成员函数。调用类中的成员函数时,如果不使用类中的任何成员变量,不会使用到this指针,所以可以正常调用。

33. shared_ptr线程安全吗?

智能指针中的引用计数是线程安全的,但智能指针指向的对象线程安全问题不保障。智能指针管理资源的生命周期是线程安全的,但资源的访问不是线程安全的。

34. push_back()左值和右值的区别是什么?

  • 左值:使用拷贝构造新对象。

  • 右值:使用移动构造新对象。

35. move底层是怎么实现的?

move功能:将一个左值引用强制转化为右值引用,使用右值引用实现移动语义,避免拷贝构造,将对象状态所有权从一个对象转移到另一个对象,没有内存搬迁或内存拷贝。

36. 完美转发的原理是什么?

完美转发:函数模板可以将参数完美转发给内部调用的其他函数,不仅能转发参数的值,还能保持参数的左、右值属性不变。使用引用折叠规则,将传递进来的左值以左值传递,将右值以右值传递。

37. 空类中有什么函数?

  • 默认构造函数

  • 默认拷贝构造函数

  • 默认析构函数

  • 默认赋值运算符

  • 取值运算符

  • const取值运算符

38. explicit用在哪里?有什么作用?

explicit只能用于修饰只有一个参数的类构造函数(当其他参数有默认值时也有效),表示该构造函数是显式的,防止隐式转换。

39. 成员变量初始化的顺序是什么?

  • 成员变量在使用初始化列表初始化时,与构造函数中初始化成员列表的顺序无关,只与定义成员变量的顺序有关。

  • 不使用初始化列表时,与构造函数内的顺序有关。

  • const成员必须在初始化列表中初始化。

  • static成员变量只能在类外初始化。

顺序

  1. 基类静态变量或全局变量

  2. 派生类静态变量或全局变量

  3. 基类成员变量

  4. 派生类成员变量

40. 指针占用的大小是多少?

  • 64位系统:8字节

  • 32位系统:4字节

计算机的位数指CPU一次性处理、传输、保存的信息的最大长度。CPU访问内存的地址由位数决定,因此指针的大小取决于系统的位数。

41. 野指针和内存泄漏是什么?如何避免?

内存泄漏:指程序中动态分配的堆内存由于未释放或无法释放,造成系统内存的浪费,可能导致程序运行速度减慢甚至系统崩溃。

避免方法

  • 使用智能指针管理资源。

  • 在释放对象数组时使用 delete[]

  • 尽量避免在堆上分配内存。

野指针:指向一个已删除对象或未申请访问受限内存区域的指针。

避免方法

  • 对指针进行初始化。

  • 用合法的可访问内存地址对指针初始化。

  • 指针用完释放内存,将指针赋值为 nullptr

42. malloc和new的区别是什么?

  • 类型malloc/free 是标准库函数,new/delete 是 C++ 运算符。

  • 失败处理malloc 分配内存失败返回 NULLnew 失败抛异常。

  • 构造和析构new/delete 会调用构造和析构函数,malloc/free 不会。

  • 返回类型new 返回有类型的指针,malloc 返回无类型的指针。

  • 内存位置malloc 从堆上分配内存,new 从自由存储区分配内存(依赖于 operator new 的实现)。

43. 多线程会发生什么问题?线程同步有哪些手段?

问题

  • 资源竞争。

  • 频繁上锁导致程序效率低下。

  • 死锁。

线程同步手段

  • 使用 atomic 原子变量。

  • 使用互斥量(上锁)。

  • 使用条件变量或信号量制约对共享资源的并发访问。

44. 什么是STL?

STL 是 C++ 标准库的重要组成部分,不仅是一个可复用的组件库,也是一个包含数据结构与算法的软件架构。STL 拥有六大组件:仿函数、算法、迭代器、空间配置器、容器、配接器。

45. 对比迭代器和指针的区别

  • 迭代器:不是指针,是一个模板类,通过重载指针的一些操作符模拟指针功能,迭代器返回的是对象引用而不是对象的值。

  • 指针:能够指向函数,而迭代器不行,迭代器只能指向容器。

46. 线程有哪些状态,线程锁有哪些?

线程状态:创建、就绪、运行、阻塞、死亡。

线程锁的种类:互斥锁、条件锁、自旋锁、读写锁、递归锁。

47. 解释说明一下map和unordered_map

  • map:内部实现是红黑树,所有元素有序。

  • unordered_map:内部实现是哈希表,元素无序。

优缺点

  • map

  • 优点:有序,红黑树实现的操作复杂度为 O(log n),效率高。

  • 缺点:空间占用率高。

  • unordered_map

  • 优点:查找效率高。

  • 缺点:哈希表建立比较费时间。

48. vector中的pushback()和emplaceback()的区别、以及使用场景

  • push_back():调用类的有参构造函数创建一个临时变量,再将元素拷贝或移动到容器中。

  • emplace_back():直接在容器尾部构造元素,少一次构造函数调用。

使用场景

  • emplace_back():用于直接在容器中构造新元素。

  • push_back():用于将现有对象添加到容器中。

49. 如何实现线程安全,除了加锁还有没有其他的方式?

除了锁之外还可以使用:

  • 互斥量:防止多个线程同时访问共享资源,避免数据竞争。

  • 原子操作:确保多线程环境中操作的安全性。

  • 条件变量:协调线程之间的协作,用于在线程之间传递信号,控制线程的执行流程。

50. vector扩容,resize和reserve的区别

  • resize:改变 vector 的大小(size),可能会添加或删除元素。

  • reserve:改变 vector 的容量(capacity),不改变当前元素的数量,仅优化内存使用和性能。

51. vector扩容为了避免重复扩容做了哪些机制?

  • 当 vector 内存不够时,本身内存会以1.5倍或者2倍的增长,以减少扩容次数。

  • 引入了 reserve 方法,自定义 vector 最大容量。

52. C++中空类的大小是多少?

1字节。

53. weak_ptr是怎么实现的?

实现:依赖于计数器和寄存器。计数器记录弱引用的数量,寄存器存储 shared_ptr

54. 虚函数的底层原理是什么?

虚函数的底层原理是虚函数表和虚表指针。

55. 一个函数f(int a, int b),其中a和b的地址关系是什么?

a和b的地址是相邻的。

56. 移动构造和拷贝构造的区别是什么?

  • 移动构造:基于指针的拷贝,实现对堆区内存所有权的移交,减少不必要的拷贝。

  • 拷贝构造:将传入的对象复制一份,然后放进新的内存中。

57. lamda表达式捕获列表捕获的方式有哪些?如果是引用捕获要注意什么?

  • 捕获方式:按值捕获和引用捕获。

  • 注意事项:默认的引用捕获可能会导致悬挂引用,闭包包含局部变量的引用或形参的引用,如果闭包生命周期超过了局部变量或形参的生命期,引用将会空悬。解决方法是对个别参数使用值捕获。

58. 哈希碰撞的处理方法

  • 开放定址法:遇到哈希冲突时,寻找新的空闲哈希地址。

  • 再哈希法:构造多个哈希函数,发生冲突时使用其他哈希函数。

  • 链地址法:将所有哈希地址相同的记录链接在同一链表中。

  • 建立公共溢出区:将哈希表分为基本表和溢出表,将冲突的记录存放在溢出表中。

59. unordered_map的扩容过程

unordered_map 中的元素数量达到桶的负载因子(通常是0.75)时,会重新分配桶的数量(通常按原有桶数量的2倍进行扩容),并将所有元素重新哈希到新的桶中。

60. vector如何判断应该扩容?(size和capacity)

通过当前容器内元素数量(size)和容器最大大小(capacity)进行比较,如果二者相等,vector 会进行扩容,一般为1.5倍,部分情况下为2倍。

61. 构造函数是否能声明为虚函数?为什么?什么情况下为错误?

构造函数不能为虚函数。虚函数的调用是通过虚函数表来查找的,而虚函数表由类的实例化对象的vptr指针指向,该指针存放在对象的内部空间之中。调用构造函数时,vptr 还没有初始化,因此无法找到虚函数表,导致无法构造对象。

62. 类中 static 函数是否能声明为虚函数?

不能。类中的 static 函数是所有类实例化对象所共有的,没有 this 指针,而虚函数依靠 vptr 和 vtable 来处理。vptr 是一个指针,在类的构造函数中生成,并且只能通过 this 指针访问。对于静态成员函数来说,没有 this 指针,无法访问 vptr,因此 static 函数无法声明为虚函数。

63. 哪些函数不能被声明为虚函数?

  • 构造函数

  • 内联函数(内联函数在编译时展开,没有 this 指针)

  • 静态成员函数

  • 友元函数(C++ 不支持友元函数的继承)

  • 非类成员函数

64. 如何保证类的对象只能被开辟在堆上?(将构造函数声明为私有、单例)

将构造函数设置为私有,这样只能使用 new 运算符来创建对象。需要准备一个 destroy 函数来进行内存的释放,然后将析构函数设置为 protected,提供一个 publicstatic 函数来完成构造,类似于单例模式。

如果需要在栈上分配,可以重载 new 操作符,使得 new 操作符功能为空,这样外部程序无法在堆上分配对象,只能在栈上分配。

65. 讲讲你理解的虚基类

虚基类是 C++ 中一种特殊的类,用于解决多继承所带来的“菱形继承”问题。如果一个派生类同时从两个基类派生,而这两个基类又共同继承自同一个虚基类,就会形成“菱形”继承结构,导致派生类中存在两份共同继承的虚基类的实例,从而引发问题。

虚继承会使得派生类中只存在一份共同继承的虚基类实例,避免多个实例之间的冲突。虚基类可以被实例化。

66. C++哪些运算符不能被重载?

  • 成员访问操作符 .

  • 域解析操作符 ::

  • 条件运算符 ?:

  • sizeof 运算符

  • typeid 运算符

不推荐重载的运算符:逗号运算符、逻辑或逻辑与运算符,容易造成歧义。

67. 动态链接和静态链接的区别,动态链接的原理是什么?

区别

  • 静态链接:在形成可执行程序前进行。将库中的代码包含到自己的程序中,每个程序链接静态库后都会包含一份独立的代码,运行时所有这些重复的代码占用独立的存储空间,浪费资源。

  • 动态链接:程序执行时进行,不将代码直接复制到自己程序中,只留下调用接口,运行时加载动态库,所有程序共享一份动态库,因此动态库也称为共享库。

动态链接原理: 将程序按照模块拆分成各个相对独立部分,程序运行时将它们链接在一起形成完整的程序,而不是像静态链接那样将所有模块都链接成一个单独的可执行文件。

68. C++中怎么编译C语言代码?

使用 extern "C" 让 C++ 代码按照 C 语言的方式编译。

69. 未初始化的全局变量和初始化的全局变量放在哪里?

  • 初始化的全局变量:存放在数据段,数据段数据静态分配。

  • 未初始化的全局变量:存放在 BSS 段(Block Started By Symbol),属于静态内存分配。

70. 说一下内联函数及其优缺点

内联函数:在编译期将函数体内嵌到程序中,以节省函数调用的开销。

优点:节省函数调用开销,使程序运行更快。

缺点:函数体过长时频繁使用内联函数会导致代码膨胀问题,不能递归执行。

71. C++11中的auto是怎么实现自动识别类型的?模板是怎样实现转化成不同类型的?

auto:在编译期间被真正的类型替代,编译器推导出变量的类型。C++ 中变量必须有明确类型,auto 是由编译器推导出来的。

模板:函数模板是一个蓝图,本身不是函数,是编译器用来生成具体类型函数的模具。模板将原本应该由程序员做的重复工作交给编译器。

72. map和set的区别和底层实现是什么?map取值的find,[],at方法的区别(at有越界检查功能)

map和set区别

  • map:键值对,允许通过键访问值。

  • set:只存储键,没有值。

底层实现:都是红黑树。

取值方法区别

  • find:返回迭代器,需判断返回结果才能确定是否找到。

  • []:如果键不存在则插入,如果存在则覆盖,返回值。

  • at:进行越界检查,如果键不存在则抛出异常。

73. 详细说一说fcntl的作用

作用:用于控制打开的文件描述符的属性和行为。

功能

  1. 复制一个现有的描述符(cmd=F_DUPFD)。

  2. 获得/设置文件描述符标记(cmd=FGETFD或FSETFD)。

  3. 获取/设置文件状态标记(cmd=FGETFL或FSETFL)。

  4. 获取/设置异步IO所有权(cmd=FGETOWN或FSETOWN)。

  5. 获取/设置记录锁(cmd=FGETLK或FSETLK)。

74. C++的面向对象主要体现在那些方面?

C++ 的面向对象体现在引入了封装、继承、多态的特性。

  • 封装:隐藏对象的实现细节,只暴露接口。

  • 继承:通过继承关系复用代码,构建类的层次结构。

  • 多态:通过虚函数实现不同对象对同一消息的不同响应,提高代码灵活性和可扩展性。

75. 介绍一下extern "C"关键字,为什么会有这个关键字?

extern "C" 用于在 C++ 代码中按照 C 语言的方式编译代码。C++ 为了兼容 C 语言而引入这个关键字,使得 C++ 编译器能够识别并正确处理 C 语言函数。

76. 讲一讲迭代器失效及其解决方法

迭代器失效:当容器的结构发生变化(如插入、删除元素)时,原有的迭代器可能会变得无效。

解决方法

  • 序列式容器(vector、deque、list)

  • vector:插入元素时,如果空间未重新分配,指向插入位置之前的迭代器有效,之后的迭代器失效。删除元素时,指向删除位置之后的迭代器失效。使用 erase 返回的迭代器继续操作。

  • deque:插入到首尾位置之外的任意位置都会导致迭代器失效。如果在首尾位置添加元素,迭代器失效但指针和引用有效。

  • list:插入或删除元素时,不影响其他迭代器。

  • 关联型容器(map、set)

  • map、set:删除当前元素的迭代器失效,其他迭代器仍然有效。

77. 编译器是如何实现重载的?

编译器遇到函数时,在符号表中命名一个符号存放函数地址。如果函数定义在使用之前编译,符号表直接存储函数地址。C 语言中符号表以函数名存储函数地址,函数名相同的重载函数地址会冲突。C++ 中符号表符号是函数名加参数等属性,避免了重载函数的查询歧义。

78. 什么是函数调用约定?

函数调用约定是对函数调用的约束和规定,描述函数参数的传递方式和栈指针的恢复方式。

常见约定

  • __stdcall:参数从右到左入

栈,被调用函数清理堆栈,返回值在 EAX 中。

  • __cdecl:参数从右到左入栈,调用者清理堆栈,返回值在 EAX 中,允许可变参数函数存在。

  • __fastcall:前两个双字参数通过寄存器传递,其余参数从右到左压栈,被调用函数清理堆栈,返回值在 EAX 中。

  • __thiscall:用于 C++ 类成员函数调用,this 指针通过 ECX 传递,参数从右到左入栈,调用者清理堆栈。

  • __pascal:与 __stdcall 类似,已废弃。

79. 使用条件变量的时候需要注意什么?

  • signal先于wait时信号会丢失:wait 和 signal 操作需要加锁,确保原子性。

  • 生产者在生产资源和 cond signal 时加锁:确保 cond wait 和 cond signal 的先后顺序正确,避免丢失信号。

80. 类内普通成员函数可以调用类内静态变量吗,类内静态成员函数可以访问类内普通变量吗?

  • 普通成员函数可以调用静态变量:静态变量在编译时已初始化和分配内存,普通成员函数可以访问。

  • 静态成员函数不能直接访问普通变量:静态函数没有 this 指针,不能直接访问非静态变量,但可以通过类实例化对象访问非静态成员变量。

81. 强制类型转换有哪几种类型,分别有什么特点?原理是什么?

1. static_cast

  • 特点

  • 用于数据类型的强制转换。

  • 在类层次结构中基类和派生类之间的指针或引用转换。

  • 用于基本类型之间的转换,如 intchar

  • 将空指针转换成目标类型的空指针。

  • 将任意类型的表达式转换成 void 类型。

  • 原理:在编译期间进行转换,没有运行时检查,不改变表达式的 constvolatile__unaligned 属性。

2. const_cast

  • 特点

  • 用于去除 constvolatile 属性。

  • 常量指针转换为非常量指针,指向原来的对象。

  • 常量引用转换为非常量引用,指向原来的对象。

  • 常量对象转换成非常量对象。

  • 原理:用于去除指针或引用的常量性,不用于基本数据类型的转换。

3. reinterpret_cast

  • 特点

  • 用于改变指针或引用的类型。

  • 将指针或引用类型转换为足够长的整数,或将整数转换为指针或引用。

  • 传入类型必须是指针、引用、算术类型、函数指针、成员函数或成员指针。

  • 原理:在内存中直接进行比特位拷贝,转换后指向相同的内存地址。

4. dynamic_cast

  • 特点

  • 运行时处理,进行类型检查。

  • 用于类层次结构中基类和派生类之间的指针或引用转换。

  • 不能用于内置基本数据类型。

  • 转换成功返回指向类的指针或引用,失败返回 NULL 或抛出 std::bad_cast 异常。

  • 基类中必须有虚函数。

  • 原理:通过运行时类型信息(RTTI)进行类型检查和转换。

82. 回调函数是什么,为什么要有回调函数?有什么优缺点?回调的本质是什么?

回调函数:使用者定义一个函数,由其他函数在特定事件发生时调用。即将一个函数作为参数传入另一个函数,由后者在运行时调用。

优点

  • 调用者和被调用者分离,提高模块化和可复用性。

  • 实现多种处理方式和操作。

  • 隐藏耗时操作,提高主程序效率。

  • 使代码逻辑集中,易于阅读和维护。

缺点

  • 回调函数过多会导致代码难以维护。

  • 资源竞争:共享资源访问容易出现争抢,导致程序出错。

  • 可读性差,可能破坏代码结构和可读性。

本质:将函数作为参数使用,使程序更加灵活和普适。

83. Linux中的信号有哪些?

  • SIGINT:终端中断符(Ctrl+C),默认动作:终止。

  • SIGQUIT:终端退出符(Ctrl+\),默认动作:终止并生成core文件。

  • SIGILL:非法硬件指令,默认动作:终止并生成core文件。

  • SIGABRT:进程中止,默认动作:终止并生成core文件。

  • SIGFPE:算术运算错误,默认动作:终止并生成core文件。

  • SIGKILL:强制终止,默认动作:终止,无法捕捉和忽略。

  • SIGSEGV:无效内存引用,默认动作:终止并生成core文件。

  • SIGALRM:定时器超时,默认动作:终止。

  • SIGTERM:终止,默认动作:终止,可以被捕捉和处理。

  • SIGCONT:使暂停进程继续,默认动作:忽略。

  • SIGURG:紧急情况,默认动作:忽略。

  • SIGPOLL/SIGIO:可轮询事件/异步IO,默认动作:终止。

84. 什么是尾递归?

尾递归:递归函数在尾部直接调用自身的递归函数。

原理:编译器检测到尾递归调用时,会覆盖当前的活动记录,而不是在栈中创建新的记录,减少栈空间使用,提高运行效率。

特点:在尾部调用自身,通过优化使计算仅占用常量栈空间。

85. 为什么会有栈溢出,为什么栈会设置容量?

栈溢出:函数嵌套调用过多或定义局部变量过大,超过了预设的栈空间大小。

原因

  • 栈的地址空间必须连续,任其无限增长会给内存管理带来困难。

  • 多线程程序每个线程需要分配栈,栈容量不能设置过大。

86. 二叉树和平衡二叉树的区别

  • 二叉树:没有平衡因子的限制,可能退化为链表。

  • 平衡二叉树:有平衡因子限制,保持平衡,不会退化为链表。

87. 平衡二叉树的优缺点

优点:避免二叉树退化为链表,平均查找时间复杂度为 O(log N)。

缺点:插入和删除操作需要维护平衡,旋转操作较多,性能较低。

88. 静态成员函数可以是虚函数吗?为什么?

静态成员函数不能是虚函数。静态成员函数属于类的共有函数,不依赖于对象调用,没有 this 指针,无法放进虚函数表。

89. 构造函数可以为虚函数吗?为什么?

构造函数不能为虚函数。虚表指针存储在对象的内存空间,调用虚函数时通过虚表指针查找虚函数表。而构造函数实例化对象,定义为虚函数时,对象还没有实例化,没有虚表指针,无法调用构造函数。

90. make_shared函数的优点,缺点?

优点

  • 减少内存分配次数,降低系统开销,提高效率。

  • 使用 new 构造至少需要两次内存分配,make_shared 只需一次。

缺点

  • 当构造函数是保护或私有时无法使用 make_shared

  • weakptr 保持控制块生命周期,只有最后一个 weakptr 离开作用域时内存才释放,对内存要求高的场景需注意。

91. 函数调用进行的操作:

  1. 将参数压栈:按照参数顺序的逆序进行,如果参数中有对象则先进行拷贝构造。

  2. 保存返回地址:即函数调用结束返回后接着执行的语句的地址。

  3. 保护维护函数栈帧信息的寄存器内容如,SP(堆栈指针),FP(栈帧指针)等。

  4. 保存一些通用寄存器的内容:因为有些通用寄存器会被所有函数用到,所以在函数调用之前,这些寄存器可能已经放置了对函数有用的信息。

  5. 调用函数,函数执行完毕。

  6. 恢复通用寄存器的值。

  7. 恢复保存函数栈帧信息的那些寄存器的值。

  8. 通过移动栈指针,销毁函数的栈帧。

  9. 将保存的返回地址出栈,并赋给寄存器。

  10. 通过移动栈指针,回收传给函数的参数所占用的空间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sagima_sdu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值