嵌入式C/C++语言相关知识——C++八股
嵌入式C/C++语言相关知识——C++八股
- 一、博客声明
- 二、C++八股
- 1、解释封装、继承和多态?
- 2、解释多态的实现原理以及有点?
- 3、final标识符的作用是什么?
- 4、虚函数是怎么实现的?它被存放在内存的哪个区?是什么时候产生。
- 5、智能指针的本质是什么?实现原理是什么?
- 6、匿名函数的本质是什么?优点是什么?
- 7、解释一下右值引用,什么时候要使用它?
- 8、左值引用和指针的区别?
- 9、指针是什么?
- 10、weak_ptr真的不计数?是否有计数方式,在哪分配的空间。
- 11、malloc的内存分配的方式,有什么缺点?
- 11.1、为什么不全部使用mmap来分配内存?
- 11.2、为什么不全用brk来分配内存?
- 12、传入一个指针,它如何确认具体要清理多少空间?
- 13、define和const的区别是什么?
- 14、程序运行的步骤是什么?
- 15、锁是什么,核心本质是什么?
- 16、锁的类型有哪些?
- 17、锁的工作原理?
- 18、锁的底层原理是什么?
- 19、原子操作是什么?
- 20、class和struct的区别?
- 21、内存对齐是什么?为什么要进行内存对齐?内存对齐有什么好处?
- 22、什么是进程(Process)?
- 23、什么是线程(Thread)?
- 24、进程和线程的区别是什么?
- 25、讲一讲进程间的通信?
- 26、讲一讲进程间的通信有哪些方式?
- 27、讲一讲各进程间的通信的优缺点?
- 28、讲一讲线程间的通信?
- 29、线程间的通信的方式有哪些?
- 30、类的生命周期
- 31、父类的构造函数和析构函数是否能为虚函数?这样的操作将导致什么后果?
- 32、死锁是什么?什么会出现死锁?死锁产生的条件?如何解决死锁?
- 33、描述一下面向过程和面向对象.
- 34、介绍一下vector、list的底层实现原理和优缺点。
- 35、静态变量在哪里初始化?在哪个阶段初始化?
- 36、空对象指针为什么能调用函数?
- 37、shared_ptr线程安全吗?
- 38、push_back()左值和右值的区别是什么?
- 39、std::move()底层是怎么实现的?
- 40、完美转发的原理是什么?
- 41、空类中有什么函数?
- 42、explicit用在哪里?有什么用?
- 43、成员变量初始化的顺序是什么?
- 44、指针占用的大小是多少?
- 45、内存泄漏和野指针是什么?如何避免?
- 46、malloc和new的区别?
- 47、什么是STL?
- 48、迭代器和指针有什么区别?
- 49、线程有哪些状态,线程锁有哪些?
- 50、解释一下map和unordered_map
- 51、 vector中的push_back()和emplace_back()的区别、以及使用场景
- 52、如何实现线程安全,除了加锁还也没有其他方式?
- 53、vector扩容,resize和reserve的区别?
- 54、vector扩容为了避免重复扩容做了哪些机制?
- 55、C++的空类大小是多少?
- 56、weak_ptr是怎么实现
- 57、移动构造和拷贝构造的区别?
- 58、哈希碰撞的处理方法是什么?
- 59、unordered_map的扩容过程
- 60、类中的static函数是否能声明为虚函数?
- 61、哪些函数不能被声明为虚函数?
- 62、如何保证类的对象只能被开辟在堆上?(将构造函数声明为私有、单例)
- 63、讲讲你理解的虚基类
- 64、C++哪些运算符不能被重载?
- 65、动态链接和静态链接的区别,动态链接的原理是什么?
- 66、C++中怎么编译C语言代码?
- 67、说一下内联函数及其优点?
- 68、C++11中的auto是怎么实现自动识别类型的?模板是怎样实现转化成不同类型的?
- 69、map和set的区别和底层实现是什么?map取值的 find,[],at方法的区别(at有越界检查功能)
- 70、详细说一说fcntl的作用
- 71、讲一讲迭代器失效及其解决方法
- 72、编译器是如何实现重载的?
- 73、使用条件变量的时候需要注意什么?
- 74、类内普通成员函数可以调用类内静态变量吗,类内静态成员函数可以访问类内普通变量吗?
- 75、回调函数是什么,为什么要有回调函数?有什么优缺点?回调的本质是什么?
- 76、什么是尾递归?
- 77、为什么会有栈溢出,为什么栈会设置容量?
- 78、二叉树和平衡二叉树的区别?
- 79、什么是this指针,为什么会存在this指针?
- 80、什么是重载、重写、隐藏?
- 81、静态成员函数可以是虚函数吗?为什么?
- 82、构造函数可以为虚函数吗?为什么?
- 83、make_shared函数的优点,缺点?
- 84、函数调用进行的操作:
- 三、参考链接
一、博客声明
又是一年一度的秋招,怎么能只刷笔试题目呢,面试题目也得看,想当好厂的牛马其实也不容易呀O(∩_∩)O。注意:这篇博客大部分是来自网上的资源,我只是为了加深印象,总结和抄一遍,没错,就是抄一遍。原博客地址:芒果甜汤圆:C++八股
二、C++八股
1、解释封装、继承和多态?
封装: 将代码具体实现的过程以及数据变量封装成一个类,只通过接口的形式访问,降低代码的耦合性,使类成为一个具有内部数据自我隐藏能力、功能独立的软件模块。其意义在于保护和防止代码无意中被破坏,保护类中的数据成员,不让类中以外的程序直接访问或者修改,只能通过提供的公共接口进行访问。
继承: 子类继承父类的特征和行为,复用基类的全体数据和成员函数,具有从基类复制而来的数据成员和成员函数(其中基类的私有成员可以被继承,但是无法访问)。其中构造函数、析构函数、友元函数、静态数据成员、静态成员函数都不能被继承。基类中成员的访问方式只能决定派生类能否访问它们。增强代码耦合性,当父类中的成员变量或者类本身被final关键字修饰时,修饰的类不能被继承,修饰的成员函数不能重写活修改。其意义在于,基类的程序代码可以被派生类复用,提高软件复用的效率,缩短开发周期。
多态:不同继承类的对象对同一消息的不同响应,基类的指针指向或者绑定到派生类的对象,使得基类指针呈现不同的表现形式。其意义在于对已存在的代码具有可替代性,对代码具有可扩充性,新增子类不会影响已存在类的各种性质,在程序中体现了灵活多样的操作,提高了使用效率,简化了对应用代码的编写和修改过程。
2、解释多态的实现原理以及有点?
实现原理:
1、 动态多态: 动态多态是利用虚函数实现运行时的多态,即在系统编译的时候并不知道将要调用哪一个函数,只有在运行到这里的时候才能确认接下来会跳转到哪一个函数。原理是通过虚函数实现,虚函数是类的成员函数,存在储存虚函数指针的表称为虚函数表,虚函数表是一个储存类成员函数的指针,每个指针都指向调用它的地方,当子类调用虚函数时,就会去虚函数表中找到自己对应的虚函数指针,从而实现“谁调用,谁实现”从而实现多态。
2、 静态多态: 又称为编译期多态,即在系统编译期间就可以确认程序将要执行哪个函数。原理上是通过函数重载(函数名相同,参数不同,两个函数在同一作用域),运算符重载和重定义来实现。
优点: 加强代码的可扩展性,可替换性,增强程序的灵活性,提高使用效率,简化对应代码的编写和修改过程。
3、final标识符的作用是什么?
放在类的后面表示该类无法继承,也就是阻止了从类的继承,放在虚函数后面该函数无法被重写,表示阻止虚函数的重载。
4、虚函数是怎么实现的?它被存放在内存的哪个区?是什么时候产生。
在C++中,虚函数的实现原理基于两个关键概念:虚函数表和虚函数指针。
虚函数表: 每个包含虚函数的类都会生成一个虚函数表,其中储存着该类所有虚函数的地址。虚函数表是一个由指针构成的数组,每个指针指向一个虚函数的实现代码。
虚函数指针: 在对象的内存布局中,编译器会添加一个额外的指针,称为虚函数指针或者虚表指针。这个指针指向该对象对应的虚函数表,从而让程序能够动态的调用虚函数。
当一个基类指针或者引用调用虚函数时,编译器会使用虚表指针来查找该对象对应的虚函数表,并根据函数在虚函数表中的位置来调用正确的虚函数。
虚函数在编译阶段生成,虚函数和普通函数一样存放在代码段,只是它的指针有存放在虚表中。
5、智能指针的本质是什么?实现原理是什么?
智能指针的本质上是封装了一个原始C++指针的类模板,为了确保动态内存的安全性而产生的。实现原理是通过一个对象储存需要被自动释放的资源,然后依靠对象的析构函数来释放资源。
6、匿名函数的本质是什么?优点是什么?
匿名函数的本质上是一个对象,在其定义的过程中会创建出一个栈对象,内部通过重载()符号实现函数调用外部。
优点: 使用匿名函数,可以免去函数的声明和定义。这样匿名函数仅仅在调用函数的时候才会创建函数对象,而调用结束后立即释放,所以匿名函数比非匿名函数更节省空间。
7、解释一下右值引用,什么时候要使用它?
-
右值引用: 是为临时变量取别名,它只绑定到一个临时变量或表达式上。实际开发中我们可能需要对右值进行修改(实现语义移动时需要),而右值引用可以对右值进行修改。
-
为什么:
- 为了支持移动语义,右值引用可以绑定到临时对象、表达式等右值上,这些右值在生命周期结束后就会被销毁,因此可以在右值引用中窃取其资源,从而避免昂贵的复制操作,实现高效的移动语义。
- 完美转发:右值引用可以绑定到任何类型的右值上,可以将其作为参数传递给函数,并在函数内部将其“转化”到其他函数中,从而实现完美转发。
- 拓展可变参数模板,实现更加灵活的模板编程。
8、左值引用和指针的区别?
- 初始化的区别: 指针可以不用初始化,引用必须初始化。
- 性质上的区别: 指针是一个变量,引用是对被引用的对象取一个别名。
- 占用内存单元的区别: 指针有自己的空间地址,引用和被引用对象暂用同一个空间。
9、指针是什么?
指针的全名称为指针变量,计算机在存储数据是有序存放的,为了能够使用存放的地址,就需要一个地址来区别每个数据的位置,指针变量就是用来存放这些地址的变量。
10、weak_ptr真的不计数?是否有计数方式,在哪分配的空间。
计数,控制块中有强弱引用计数,如果是使用make_shared初始化的函数,则它所在的控制块空间是在所引用的shared_ptr中同一块空间,若是new,则控制器所分配的内存与share_ptr本身所在的空间不在同一块内存。
11、malloc的内存分配的方式,有什么缺点?
malloc并不是系统调用,而是C库中的函数,用于动态内存分配,在使用malloc分配内存的时候会有两种方式向操作系统申请 堆内存。
- 方式1: 当用户分配的内存小于 128KB 时通过
brk()
系统调用从堆分配内存。- 实现方式:将堆顶指针指向高地址移动,获取内存空间,如果使用free释放空间,并不会将内存归还给操作系统,而是会缓冲在malloc的内存池中,待下次使用。
- 方式2: 当用户分配的内存大于 128KB 时通过
mmap()
系统调用在文件映射区域飞陪内存。- 实现方式:使用私有匿名映射的方式,在文件映射区分配一块内存,也就是从文件映射区拿了一块内存,free释放内存的时候,会把内存归还给操作系统。
- 缺点: 容易造成内存泄漏和过多的内存碎片,影响系统正常运行,还得主义判断内存是否分配成功,而且内存释放后(使用free函数之后指针变量p本身保存的地址没有改变),需要将p的赋值为NULL拴住野指针。
11.1、为什么不全部使用mmap来分配内存?
原因: 因为向操作系统申请内存的时候,是要通过系统调用的,执行系统调用要进入内核态,然后再回到用户态,状态的切换会耗费不少时间,所以申请内存的操作应该避免频繁的系统调用,如果都使用mmap
来分配内存,等于每次都要执行系统调用。另外,因为mmap
分配的内存每次释放都要回归到操作系统,于是每次mmap
分配的虚拟地址都是缺页状态,然后在第一次访问该虚拟地址的时候就会触发缺页中断。
11.2、为什么不全用brk来分配内存?
如果全用brk
来申请内存,那么随着程序频繁调用malloc和free,尤其是小块内存,堆内将产生越来越多不可用的内存碎片。
12、传入一个指针,它如何确认具体要清理多少空间?
我们在申请内存的时候,会多分配16字节的内存,里面保存了内存块的详细信息,free
会对传入的内存地址向左偏移16字节,然后分析出当前内存块的大小,就知道要释放多大的内存空间了。
13、define和const的区别是什么?
- 编译阶段的区别:
define
在编译预处理阶段进行简单的文本替换,const
在编译阶段确定其值。 - 安全性的区别:
define
定义的宏变量没有数据类型,只是进行简单的文本替换,不会进行类的安全性检查;const
定义常量时是有类型的,是要进行类型的判断的。 - 内存占用的区别:
define
定义的宏变量,在程序中使用多少次就会进行多少次替换,内存中有多个备份,占用的是代码段的内存;const
定义常量储存在只读数据段中。 - 调试上的区别:
define
定义的宏常量不能调试;const
定义的常量可以进行调试。
14、程序运行的步骤是什么?
详细请看这篇博客:编译过程
-
预编译: 将头文件编译,进行宏替换,输出.i文件
-
编译: 将其转化为汇编语言文件,主要做词法分析,语义分析以及检查错误,检查无误后将代码翻译成汇编语言,生成.s文件
-
汇编: 汇编器将汇编语言文件翻译成机器语言,生成.o文件
-
链接: 将目标文件和库链接到一起,生成可执行文件.exe
15、锁是什么,核心本质是什么?
-
概念: 在计算机科学中,锁 是用于控制多个线程或进程并发访问资源的一种机制。主要目的是 避免竞态条件 ,确保同一时间只有一个线程能够访问某个共享资源,从而防止数据不一致或损坏的情况。
-
核心功能(本质):
- 互斥: 锁通常用于保证多个线程或进程对共享资源的 互斥访问 。当一个线程持有锁时,其他线程必须等待,知道该线程释放锁。
- 同步: 通过使用锁,程序可以协调多个线程的执行顺序,确保他们按照正确的步骤操作。
16、锁的类型有哪些?
- 互斥锁(Mutex): 确保在某一时刻只有一个线程嫩访问共享资源,其他线程被阻塞。
- 自旋锁(Spinlock): 与互斥锁类似,但自旋锁不会阻塞线程,而是反复尝试获取锁,适合锁持有时间很短的场景。
- 读写锁(RWLock): 允许多个线程同时读取资源,但是在写入时,必须保证没有其他线程在读取或者写入。
- 递归锁(Reentrant Lock): 允许同一个线程多次获得同一个锁,避免因为同一个线程重复加锁导致死锁。
17、锁的工作原理?
锁的基本工作原理归结为 互斥 和 同步,也是锁的核心功能和本质。
多的操作通常包括:获取锁、释放锁、阻塞和等待。
- 获取锁: 线程尝试获取锁,如果锁未被持有,线程将成功获取锁并进入临界区执行任务。
- 释放锁: 线程在任务执行完毕后释放锁,其他等待的线程才有机会获取锁。
- 阻塞和等待: 如果锁已经被其他线程持有,当前线程可能会被阻塞,直到锁被释放。
18、锁的底层原理是什么?
锁的底层原理是通过CAS、atomic机制实现。
- CAS机制: 全称为Compare And Swap(比较相同再交换)可以将比较和交换操作转换为原子操作,CAS操作依赖于三个值:内存中的值V、旧的预估值X、要修改的新值B,如果旧的预估值X等于内存中的值V,旧将新的值B保存在内存之中。(就是每一个线程从主内存复制一个变量副本后,进行操作,然后对其进行修改,修改完后,再刷新会主内存前。再去一次主内存的值,看拿到的主内存的新值与当初保存的快照值是否一样,若不一样,说明有其他线程修改,本次修改放弃,重试。)
- atomic机制: 如19问。
19、原子操作是什么?
原子操作是指不会被线程调度机制打断的操作,这种操作一旦开始,就一直允许到结束,中间不会有任何步骤切换到另外一个线程。
原理: 在X86的平台下,CPU提供了在指令执行期间对总线加锁的手段,CPU中有一根引线 #LOCK pin
连接到北桥,如果汇编语言的程序在程序中的一条指令前面加上了LOCK
前缀,经过汇编之后的机器码就使CPU在执行这条指令的时候把#LOCK pin
的电平持续拉低到这条指令结束的时候放开,从而把总线锁住,这样别的CPU就暂时不能够通过总线访问内存了,保证了多处理器环境中的原子性。
20、class和struct的区别?
-
默认访问缺陷区别:
- struct: 默认情况下,
struct
的所有成员(包括变量和函数)都是公共的(public)。
struct Mystruct { int a; //默认public void func(); //默认public }
- class: 默认情况下,
class
的所有成员都是私有的(private)。
class MyClass { int a; // 默认是 private void func(); // 默认是 private };
- struct: 默认情况下,
-
使用场景惯例上的区别:
- struct:
- 通常表示数据结构,即它们主要是用来保存数据,通常不会有复杂的成员函数。
- 更常用于简单的数据聚合,比如表示几何形状的点,颜色等
- class: 通常用于实现更复杂的面向对象编程(OOP),它更强调行为(方法)和数据(成员变量)的结合,允许更复杂的封装、继承、多态等。
- struct:
-
继承的默认访问权限的区别:
- struct: 默认继承方式是公共继承(public)。
- class: 默认继承方式是私有继承(private)。
21、内存对齐是什么?为什么要进行内存对齐?内存对齐有什么好处?
-
内存对齐: 是计算机系统中为了优化数据存取效率而采取的一种技术。它指的是将数据存储在内存中时,根据数据类型的大小,将其存储在特定的内存边界上,以便处理器能够更高效地访问和处理这些数据。本质是:要求数据的存储地址是特定对齐边界的倍数。
-
内存对齐的原因:
- 为了便于不同设备的移植: 有些CPU可以访问任意地址上的任意数据,而有些CPU只能在特定的地址访问数据,因此不同硬件平台具有差异性,这样的代码就不具有移植性,如果在编译时将进行对齐,这就具有平台的移植性。
- 性能上的优化: CPU每次寻址有时需要消耗时间的,并且CPU访问内存的时候并不是逐个字节访问,而是以字长为单位访问,所以数据结构应该尽可能地在自然边界上对齐,如果访问未对齐内存,处理器需要做多次内存访问,而对齐的内存访问可以减少访问次数,提升性能。
- 减少空间的浪费: 内存对齐虽然会在某些情况下引入少量的“空隙”,但是总体上能够让系统更加高效,降低内存管理和访问的复杂性。
-
内存对齐的好处:
- 提高访问速度: 对齐内存时,CPU 可以通过一次内存访问读取完整数据,而无需多次访问。例如,读取未对齐的数据可能需要访问两次甚至更多次内存。
- 硬件友好,便于移植: 某些硬件平台对内存对齐有严格要求,未对齐的内存访问会导致异常或错误,内存对齐使得程序可以在更多平台上更稳定地运行。
- 简化地址计算: 内存对齐使得硬件和编译器可以更容易进行地址的计算和数据的访问,降低了复杂度。
22、什么是进程(Process)?
进程是操作系统分配资源和调度的基本单位,它是一个正在执行的程序的示例,它拥有自己的内存空间和系统资源。进程与进程之间相互独立,不能直接访问对方的资源。
23、什么是线程(Thread)?
线程是进程中的一个执行路径,它是程序执行的最小单位,一个进程可以包含多个线程,这些线程共享进程的内存空间、文件描述符和其他资源,但每个线程有自己的栈、寄存器和程序计数器。
24、进程和线程的区别是什么?
特性 | 进程(Process) | 线程(Thread) |
---|---|---|
基本定义 | 进程是操作系统资源分配的基本单位 | 线程是程序执行的最小单位,一个进程可以包含多个线程 |
资源共享 | 进程之间相互独立,资源不共享 | 同一个进程中的线程共享内存、全局变量等资源 |
通信 | 进程之间的通信复杂,需要使用进程间(IPC)机制,如管道、消息队列等 | 线程之间的通信简单,可以直接访问共享内存 |
地址空间 | 每个进程有自己独立的地址空间 | 同一个进程内的线程共享地址空间 |
崩溃影响 | 一个进程的崩溃不会影响其他进程 | 一个线程崩溃可能导致整个进程崩溃 |
并发性 | 多进程可以并发执行,但是开销较大 | 多线程可以并发执行,且更高效 |
25、讲一讲进程间的通信?
进程拥有自己独有的资源和数据,在某些任务要求中,可能需要不同进程的数据,而直接访问进程彼此的数据是不行的。因此需要进程间进行通信,在不同进程之间进行数据共享或者协调操作。优点: 同步和协作、模块化设计、提高并发性、提高容错性等
26、讲一讲进程间的通信有哪些方式?
- 管道(Pipe): 管道是一种最简单的进程间通信方式。它是 单向 的,数据只能在管道的一端写入,另一端读取。并且分为了 匿名管道 和 命名管道。
- 匿名管道: 只有在父子进程之间进行通信。
- 命名管道: 可以在没有亲缘关系的进程之间通信,管道存在于文件系统中。
- 消息队列(Message Queue): 消息队列是一种链式结构,进程可以通过队列发送和接收消息。它允许进程间以消息为单位进行通信,消息可以携带不同类型的数据。消息队列是有操作系统维护的,消息通过系统调用传递。
- 共享内存(Shared Memory): 共享内存允许多个进程直接访问同一块内存区域。它是最有效的进程通信方式,因为它不需要数据复制。然而,共享内存的同步机制需要借助其他方式(如信号量)来避免并发读写冲突。
- 信号量(Semaphore): 信号量是一种同步机制,常用于进程之间的资源管理。通过信号量,进程可以实现对共享资源的互斥访问。信号量本质上是一个计数器,进程通过它来等待或通知其他进程。
- 信号(Signal): 信号是一种进程间通信的软中断机制,进程可以通过信号通知另一个进程发生了某种事件。常见的有
SIGINT(中断)
、SIGKILL(杀死进程)
等。信号传递的内容有限,通常用于控制或者终止进程。 - 套接字(Socket): 套接字主要用于网络通信,但也可以用于本地进程间通信。通过使用套接字,进程可以在不同的计算机之间通信,也可以在同一台计算机上通过套接字通信。
27、讲一讲各进程间的通信的优缺点?
-
管道(Pipe):
- 优点:
- 简单易用:特别是匿名管道,常用于父子进程之间通信。
- 自动同步:管道是先进先出的,因此读写过程中的同步性较好。
- 无需显式同步机制:管道自身解决了数据写入和读取的同步问题,避免了竞争条件。
- 缺点:
- 单向通信:数据只能从写入端向读取端,双向通信就需要两个管道。
- 仅限本地通信:管道仅用于同一台计算机上的进程,无法跨网络通信。
- 性能较低:涉及内核缓冲区,传输效率较共享内存低。
- 优点:
-
消息队列(Message Queue):
- 优点:
- 支持多进程通信:可以在无亲缘关系的多个进程间进行通信。
- 消息有类型:消息队列允许消息带有类型,接收进程可以根据类型有选择的读取消息。
- 内核管理:有操作系统内核管理,数据结构相对稳定。
- 缺点:
- 复杂性较高:相对于管道,消息队列需要管理队列的大小和数据类型等。
- 消息大小有限:消息队列对每条消息的大小有严格限制,无法传递大块数据。
- 性能较低:消息需要在内核和用户之间进行复制,增加系统的开销。
- 优点:
-
共享内存(Shared Memory):
- 优点:
- 高效:共享内存是最快的进程间通信方式,因为数据无需在内核和用户空间之间复制,所有进程直接访问同一内存区域。
- 大数据传输:共享内存适合大块数据的传输,因为不涉及消息大小的限制。
- 缺点:
- 需要显式同步:多个进程并发访问共享内存时需要借助其他同步机制(如信号量)来避免竞争条件。
- 安全性较低:如果同步机制处理不当,容易导致数据竞争,死锁等问题。
- 管理复杂:需要显式分配和释放共享内存段,以及手动处理同步和互斥。
- 优点:
-
信号量(Semaphore):
-
优点:
- 提供同步机制:信号量非常适合用于进程之间同步和互斥访问共享资源。
- 灵活性高:可以用于解决各种同步问题,如生产者-消费者模型。
-
缺点:
- 复杂性较高:对于复杂系统,使用信号量可能导致“信号量饥饿”、“死锁”等问题,管理起来较为复杂。
- 不用于数据传递:信号量仅仅用于同步,不实际传递数据。
-
-
信号(Signal):
-
优点:
- 轻量级:信号是非常轻量级的通信方式,进程可以快速发送和处理信号。
- 适合通知类事件:进程可以通过信号进行简单的通知或控制操作,例如终止进程,暂停进程等。
-
缺点:
- 信号量有限:信号只能传递极少的信息,通常只是表示一个事件的发生,而无法传递复杂的数据。
- 不可靠:信号在某些情况下可能被忽略或丢失,特别是多个信号到来时处理较为复杂。
-
-
套接字(Socket):
-
优点:
- 支持网络通信:套接字不仅可以在本地进程间通信,也可以用于跨网络的进程间通信,是网络通信的核心机制。
- 灵活性:可以进行TCP或UDP通信,满足不同的需求。
-
缺点:
- 性能较低:涉及较多协议栈的操作(特别是TCP通信),在本地进程间通信中效率较低。
- 实现复杂:编程相对复杂,需要处理通信连接,错误恢复等问题。
-
28、讲一讲线程间的通信?
是指在同一进程内不同线程之间传递信息的机制。由于线程共享进程的地址空间,因此可以通过共享内存直接访问数据。线程之间的通信可以帮助多线程程序更高效地协调操作,避免竞争条件和死锁问题。
29、线程间的通信的方式有哪些?
- 共享变量: 线程可以通过共享全局变量、静态变量或者其他共享数据结构来交换信息。这种方式简单高效,但需要使用同步机制(如互斥锁、读写锁)来防止数据竞争问题。
- 锁机制:
- 互斥锁(Mutex): 确保在某一时刻只有一个线程嫩访问共享资源,其他线程被阻塞。
- 自旋锁(Spinlock): 与互斥锁类似,但自旋锁不会阻塞线程,而是反复尝试获取锁,适合锁持有时间很短的场景。
- 读写锁(RWLock): 允许多个线程同时读取资源,但是在写入时,必须保证没有其他线程在读取或者写入。
- 递归锁(Reentrant Lock): 允许同一个线程多次获得同一个锁,避免因为同一个线程重复加锁导致死锁。
- 信号量: 用于控制对共享资源的访问量,可以允许多个线程同时访问资源。常用于限制资源的访问数量,比如线程池、数据库连接池等。
- 条件变量: 用于线程等待某个条件成立才进行执行。
- 消息队列: 是线程间通过队列传递数据的一种方式,尤其适用于需要异步传递数据的场景。
- 事件: 允许一个线程等待另一个线程触发某个事件,常用于线程的同步。
30、类的生命周期
类从被加载到内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。
- 全局对象在
main()
开始前被创建,main()
退出后被销毁。 - 静态对象在第一次进行作用域时被创建,
main()
退出后被销毁。 - 局部对象在进入作用域时被创建,在退出作用域时被销毁。
New
创建的对象知道内存被释放的时候都存在。
31、父类的构造函数和析构函数是否能为虚函数?这样的操作将导致什么后果?
-
构造函数不能为虚函数, 虚函数的调用是通过虚函数表来查找的,而虚函数表由类的实例化对象的
vptr
指针指向,该指针存放在对象的内部空间之中,需要调用构造函数完成初始化,如果构造函数为虚函数,那么调用构造函数就需要寻找vptr
,但此时的vptr
没有完成初始化,导致无法构造对象。 -
析构函数可以且经常作为虚函数,当我们使用父类指针指向子类时,只会调用父类的析构函数,子类的析构函数不会被调用,容易造成内存泄漏。
32、死锁是什么?什么会出现死锁?死锁产生的条件?如何解决死锁?
- 死锁: 死锁是在多线程编程中发生的一种状态,当两个或者多个线程相互等待对方释放资源时,陷入永久阻塞状态。这种情况下,所有被锁住的线程都无法继续执行,因为它们都在等待一个永远释放的资源。
- 产生死锁的原因: 死锁通常出现是因为线程在获取资源时的相互依赖关系没有得到妥善管理,导致一个线程在持有某些资源的同时,等待其他线程的资源,而其他线程也在等待此线程释放它所持有的资源,这样就形成了“相互等待”的死循环,导致死锁。
- 死锁产生的条件: 一共四个必要条件:
- 互斥:每个资源要么分配个一个资源,要么不可用。
- 持有并等待:一共线程持有至少一个资源,同时等待获取其他线程占用资源。
- 不可剥夺:已经分配给线程的资源在任务完成前不能被强制剥夺。
- 循环等待:存在一个线程等待链,形成循环依赖关系。
- 解决办法:
- 破坏四个条件中的任意一个;
- 死锁检测和恢复;
- 超时机制。
33、描述一下面向过程和面向对象.
- 面向过程: 是一种通过步骤和流程来解决问题的编程方法,主要关注的是过程(即一系列指令),从头到尾按顺序执行。它将程序划分为 函数 或 过程 ,每个函数实现特定的更。
- 面向对象: 是一种将现实世界抽象为对象的编程思想,通过“对象” 和“类” 来封装数据和行为。程序设计围绕对象来进行,数据和操作数据的行为被绑定在一起,彼此之间相对独立。
34、介绍一下vector、list的底层实现原理和优缺点。
- vector:
- 底层原理: 有一块连续的内存空间组成,由三个指针实现的分别是头指针(表示目前使用的空间头),尾指针(表示目前使用的空间尾)和可用空间尾指针实现。
- 优点: 可使用下标随机访问,尾插尾删效率高。
- 缺点: 前面部分的插入删除效率低,扩容有消耗,可能存在一定的空间浪费。
- List:
- 底层原理: 双向链表。
- 优点: 按需申请内存,不需要扩容,不会造成内存空间浪费。在任意位置插入和删除效率高。
- 缺点: 不支持下标随机访问。
35、静态变量在哪里初始化?在哪个阶段初始化?
静态变量、全局变量、常量都在编译阶段完成初始化和内存分配。其他变量都在编译阶段进行初始化,运行阶段进行内存分配。
36、空对象指针为什么能调用函数?
在类的初始化的时候,编译器会将它的函数分配到类的外部,这也包括静态成员函数,这样做主要是为了节省内存,如果我们在调用类中的的成员函数时没有使用类中的任何成员变量,它不会使用到this指针所以可以正常调用这个函数。
37、shared_ptr线程安全吗?
本身是线程安全的,这意味着多个线程可以安全地共享和拷贝同一个 shared_ptr
实例,但线程安全的范围有限,其内部指向的对象并没有任何线程保护。也就是说,如果多个线程同时访问或修g改shared_ptr
所指向的对象,可能会导致数据竞争或未定义行为。
38、push_back()左值和右值的区别是什么?
如果push_back()的参数是左值,则使用它拷贝构造新对象,如果是右值,则使用它移动构造新对象.。
39、std::move()底层是怎么实现的?
其功能是将一个左值引用强制转换为右值引用,继而可以通过右值引用使用该值,以用于移动语义,从实现原理上将基本等同一个强制类型转换。
优点: 可以将左值变为右值而避免拷贝构造,将对象的状态所有权从一个对象转移到另一个对象,只是转移,没有内存搬迁或者内存拷贝。
40、完美转发的原理是什么?
完美转发是指函数可以将自己的参数完美的转发给内部调用的其他函数,完美是指不仅能够准确的转发参数的值,还能保证被转发参数的左、右值属性不变,使用引用折叠的规则,将传递进来的左值以左值传递出来,将传递进来的右值以右值方式传出。
41、空类中有什么函数?
默认构造函数、默认拷贝构造函数、默认析构函数、默认赋值运算符、取值运算符、const取值运算符。
42、explicit用在哪里?有什么用?
只能用于修饰只有一个参数的类构造函数(有一个例外就是,当除了第一个参数以外的其他参数都有默认值的时候此关键字依然有效),它的作用是表明该构造函数是显示的,而非隐式的,跟它对应的另一个关键字是implicit,意思是隐藏的,类构造函数默认情况下声明为implicit。作用是防止类构造函数的隐式自动转换。
43、成员变量初始化的顺序是什么?
成员变量在使用初始化列表初始化时,与构造函数中初始化成员列表的顺序无关,由它们在类中声明的顺序决定。
如果不使用初始化列表初始化,在构造函数内初始化时,此时与成员变量在构造函数中的位置有关。类中const
成员常量必须在构造函数初始化列表中初始化。类中的static
成员变量,只能在类外初始化。
顺序: 基类的静态变量或全局变量、派生类的静态变量或全局变量、基类的成员变量、派生类的成员变量。
44、指针占用的大小是多少?
64位电脑占8个字节,32位电脑占4个字节。
45、内存泄漏和野指针是什么?如何避免?
- 内存泄漏:
- 概念:是指程序中以动态分配的堆内存由于某种原因程序未释放或者无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
- 如何避免:使用智能指针进行资源管理,在释放对象数组的时候使用
delete[]
,尽量避免在堆上分配内存。
- 野指针:
- 概念:指向一个已经删除的对象或者未申请访问权限内存区域的指针。
- 如何避免:对指针进行初始化,用已合法的可访问内存地址对指针进行初始化,指针用完释放内存,将指针赋值为
nullptr
。
46、malloc和new的区别?
- 本质上的区别:
malloc/free
是标准库函数,new/delete
是C++运算符;
- 返回值上的区别:
malloc
分配内存失败会返回空,new
会抛异常;new
返回有类型的指针,malloc
返回无类型的指针;
- 类作用上的区别:
new/delete
会调用析构函数,malloc/free
不会;
- 分配内存上的区别:
malloc
从堆上动态分配内存,new
是从自由存储区为对象动态分配内存;
47、什么是STL?
是C++标准库的重要组成部分,不仅是一个可复用的组件库,也是一个包含了数据结构与算法的软件架构,它拥有六大组件:仿函数、算法、迭代器、空间配置器、容器、配机器。
48、迭代器和指针有什么区别?
- 本质上的区别:
- 指针:是内存地址的直接表示,指向内存中的具体位置,通过解引用来访问该位置的内容。
- 迭代器:是一个抽象化的概念,提供一种通用的方式来遍历容器,不仅仅是地址,更是一个对象,封装了访问容器内部元素的逻辑。不同容器的迭代器可以实现不同的行为。
- 适用范围的区别:
- 指针:主要用于操作数组或内存块,也适合管理单一的连续内存结构。
- 迭代器:专为容器设计,能够适配各种数据结构。不同容器类型的迭代器会根据容器特点实现不同的遍历范式。
- 类型安全性和通用性上的区别:
- 指针:类型比较严格,直接与内存位置关联。
- 迭代器:具备更高的类型安全性,支持泛型编程。
注意: 指针能够指向函数而迭代器不行迭代器只能指向容器。
49、线程有哪些状态,线程锁有哪些?
- 五种状态: 创建、就绪、运行、阻塞、死亡。
- 线程锁的状态: 互斥锁、条件锁、自旋锁、读写锁、递归锁。
50、解释一下map和unordered_map
Map 内部实现是一个 红黑树,内部所有的元素都是有序的。unorderd_map 内部实现了一个哈希表,内部存储元素都是无序的。
- Map优点: 有序,其次内部实现是一个红黑树,使得很多操作都可以在Olog(n)的复杂度下实现。
- Map缺点: 空间占用率高。
- unordered_map优点: 查找效率非常高。
- unordered_map缺点: 哈希表的建立比较耗费时间。
51、 vector中的push_back()和emplace_back()的区别、以及使用场景
当使用Push_back时会先调用类的有参构造函数创建一个临时变量,再将这个元素拷贝或者移动到容器之中,而emplace_back则是直接在容器尾部进行构造比push_back少进行一次构造函数调用。在大部分场景中emplace_back可以替换push_back,但是push_back会比emplace_back更加安全,emplace_back只能用于直接在容器中构造新元素的情况,如果要将现有的对象添加到容器中则需要使用push_back。
52、如何实现线程安全,除了加锁还也没有其他方式?
除了锁之外,还可以使用以下方式:
- 互斥量(防止多个线程同时访问共享资源,从而避免数据竞争的问题),
- 原子操作(原子操作是不可分割的,使用原子操作可以确保在多线程环境中操作是安全的),
- 条件变量(协调线程之间的写作,用来在线程之间传递信号,从而控制线程的执行流程)等方法。
53、vector扩容,resize和reserve的区别?
- resize 改变的是vector的大小,可能会添加或删除元素。
- reserve 改变的是vector的容量,不会改变当前元素的数量,仅仅是为了优化内存使用和性能。
54、vector扩容为了避免重复扩容做了哪些机制?
当vector内存不够时,本身内存会按照1.5~2倍的增长,以减少扩容次数。引入reserve,自定义vector最大容量。
55、C++的空类大小是多少?
1字节,尽管空类没有任何数据成员或虚函数,但编译器仍然会为它分配至少1字节的空间。这是因为每个对象必须占据一个唯一的地址,以便可以正确处理对象的地址运算和类型识别。
56、weak_ptr是怎么实现
实现依赖于计数器和寄存器实现的,计数器用来记录弱引用的数量,寄存器用来储存shared_ptr。
57、移动构造和拷贝构造的区别?
- 移动构造: 本质上是基于指针的拷贝,实现对堆区内存所有权的移交,在一些特定场景下,可以减少不必要的拷贝。比如用一个临时对象或者右值对象初始化类实例时。我们可以使用move()函数,将一个左值对象转变为右值对象。
- 拷贝构造: 将传入的对象复制一份然后放进新的内存中。
58、哈希碰撞的处理方法是什么?
- 开放地址法: 当遇到哈希冲突时,去寻找一个新的空闲的哈希地址。
- 再哈希法: 同时构造多个哈希函数,等发生哈希冲突时就使用其他哈希函数直到不发生冲突为止,虽然不容易发生聚集,但是增加了计算时间。
- 链地址法: 将所有的哈希表地址相同的记录都链接在同一个链表中。
- 建立公共溢出区: 将哈希表分为基本表和溢出表,将发生冲突都存放在溢出表中。
59、unordered_map的扩容过程
unordered_map 中的元素数量达到桶的负载因子(0.75)时,会重新分配桶的数量(通常会按照原有桶的数量 *2 的方式进行扩容,但是具体的增长策略也可以通过修改容器中的max_load_factor成员变量来进行调整),并将所有的元素重新哈希到新的桶中。
60、类中的static函数是否能声明为虚函数?
不能,因为类中的static函数是所有类实例化对象所共有的,没有this指针,而虚函数依靠vptr和vtable来处理,vptr是一个指针,在类中的构造函数中生成,并且只能通过this指针访问,对于静态成员函数来说,他没有this指针,无法访问vptr,因此static函数无法声明为虚函数。
61、哪些函数不能被声明为虚函数?
- 构造函数
- 内联函数
- 静态成员函数
- 友元函数
- 非类成员函数
62、如何保证类的对象只能被开辟在堆上?(将构造函数声明为私有、单例)
将构造函数设置为私有,这样只能使用new运算符来建立对象,但是我们必须准备一个destory函数来进行内存的释放,然后将析构函数设置为protected,提供一个public的static函数来完成构造,类似于单例模式
如果在栈上分配呢?则是重载new操作符,使得new操作符的功能为空,这样就使得外层程序无法在堆上分配对象,只可以在栈上分配
63、讲讲你理解的虚基类
虚基类是 C++ 中一种特殊的类,用于解决多继承所带来的“菱形继承”问题。如果一个派生类同时从两个基类派生,而这两个基类又共同继承自同一个虚基类,就会形成一个“菱形”继承结构,导致派生类中存在两份共同继承的虚基类的实例,从而引发一系列的问题。
为了解决这个问题,我们可以将虚基类作为共同基类,并在派生类中采用虚继承的方式。
虚继承会使得派生类中只存在一份共同继承的虚基类的实例,从而避免了多个实例之间的冲突。
虚基类是可以被实例化的。
64、C++哪些运算符不能被重载?
- 成员访问操作符
- 域解析操作符
- 条件运算符之类
65、动态链接和静态链接的区别,动态链接的原理是什么?
-
区别: 他们的最大区别就是在于链接的时机不同,静态链接是在形成可执行程序前,而动态链接的进行则是程序执行时。
-
静态库: 就是将库中的代码包含到自己的程序之中,每个程序链接静态库后,都会包含一份独立的代码,当程序运行起来时,所有这些重复的代码都需要占用独立的存储空间,显然很浪费计算机资源。
-
动态库: 不会将代码直接复制到自己程序中,只会留下调用接口,程序运行时再去将动态库加载到内存中,所有程序只会共享这一份动态库,因此动态库也被称为共享库。
-
动态链接原理: 是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件
66、C++中怎么编译C语言代码?
使用extern "C"让C++按照C语言的方式去编译。
67、说一下内联函数及其优点?
内联函数是在编译期将函数体内嵌到程序中去,以此来节省函数调用的开销。
- 优点: 是节省了函数调用的开销,让程序运行的更加快速。
- 缺点: 如果函数体太长,频繁使用内联函数会导致代码编译膨胀问题,不能递归执行。
68、C++11中的auto是怎么实现自动识别类型的?模板是怎样实现转化成不同类型的?
auto仅仅只是一个占位符,在编译期间它会被真正的类型替代,或者说C++中变量必须要有明确类型的,只是这个类型是由编译器自己推导出来的。函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式具体类型函数的模具,所以模板其实就是将原本应该我们做重复的事情交给了编译器。
69、map和set的区别和底层实现是什么?map取值的 find,[],at方法的区别(at有越界检查功能)
都是红黑树,find查找需要判断返回的结果才知道有没有查询成功。[]不管有没有就是0,如果原先不存在该key,则插入,如果存在则覆盖插入,at方法则会进行越界检查,这会损失性能,如果存在则返回它的值,如果不存在则抛出异常。
70、详细说一说fcntl的作用
作用:用于控制打开的文件描述符的一些属性和行为。有五个功能:
- 复制一个现有的描述符(cmd=F_DUPFD)
- 获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD)
- 获取/设置文件状态标记(cmd=F_GETFL或F_SETFL)
- 获取设置异步IO所有权(cmd=F_GETOWN或F_SETFL)
- 获取设置记录锁(cmd=F_GETLK或F_SET)
71、讲一讲迭代器失效及其解决方法
序列式容器迭代器失效:当当前元素的迭代器被删除后,后面所有元素的迭代器都会失效,他们都是一块连续存储的空间,所以当使用erase函数操作时,其后的每一个元素都会向前移动一个位置,此时可以使用erase函数操作可以返回下一个有效的迭代器。
Vector迭代器失效问题总结:
- 当执行了erase方法时,指向删除节点的迭代器全部失效,指向删除节点之后的全部迭代器也失效。
- 当进行push_back方法时,end操作返回的迭代器肯定失效。
- 当插入一个元素后,capacity返回值与没有插入元素之前相比有改变,则需要重新加载整个容器,此时first和end操作返回的迭代器失效。
- 当插入一个元素后,如果空间未重新分配,指向插入位置之前的元素的迭代器依然有效,但指向插入元素之后元素的迭代器全部失效。
Deque迭代器失效总结:
- 对于deque,插入到除首尾位置之外的任何位置都会导致迭代器、指针和引用都会失效,如果在首尾位置添加元素,迭代器会失效,但是指针和引用不会失效。
- 如果在首尾之外的任何位置删除元素,那么指向被删除元素外其他元素的迭代器都会失效。3.如果在其首部和尾部删除元素则只会使指向被删除元素的迭代器失效。
关联型容器迭代器失效:删除当前的迭代器,仅仅会使当前的迭代器失效,只要erase时,递增当前迭代器即可。
72、编译器是如何实现重载的?
在编译时,编译器如果遇到了函数,就会在符号表里面命名一个符号来存放函数的地址,如果函数的使用在定义之前编译,无法在符号表中找到对应函数地址,则先标记为“?”(暂时未知),在全部编译结束后的链接过程将“?”在符号表里找到并替代为相应的函数地址,如果函数的定义在使用之前编译,则可以直接在符号表里找到对应函数地址直接使用,而在C语言中的符号表是以函数名为符号来存储函数地址,函数名相同的重载函数的地址应该不同,于是符号表中存在两个同符号的函数地址,在查找使用时会存在歧义和冲突。而C++符号表中的符号不是以函数名命名的,称为函数名修饰规则,虽然函数名相同,但是函数参数等其他属性不同,取的符号也不同,所以不会产生查询歧义的问题,使得函数可以重载。
73、使用条件变量的时候需要注意什么?
当signal先于wait时,该信号会丢失,不会被后续的wait捕获
条件变量wait时,条件的判断和wait操作需要锁来保证原子性,要保证这一点,需要生产者在生产资源、cond signal时加和cond wait相同的锁,这样就会保证cond wait和cond signal先后顺序不会有问题,无论是谁先执行,都不会存在任何问题。
74、类内普通成员函数可以调用类内静态变量吗,类内静态成员函数可以访问类内普通变量吗?
类内普通成员函数可以调用类内静态变量,因为类内静态变量在编译时就已经完成了初始化和内存分配,类内普通函数调用类内静态变量说明类已经完成实例化,所以可以调用。静态函数可以直接访问静态变量,静态函数不能直接访问非静态变量,但是可以通过将类实例化对象后,静态函数去访问对象的非静态成员变量。
75、回调函数是什么,为什么要有回调函数?有什么优缺点?回调的本质是什么?
回调函数是指使用者自己定义一个函数,实现这个函数的程序内容,然后别人把这个函数(入口地址)作为参数传入别人的函数中,由别人的函数在运行时来调用的函数,简单说就是放发生某种事件时,系统或其他函数将会自动调用你定义的一段函数。
可以把调用者和被调用者分开。调用者不关心谁是被调用者,所以它只需要知道的,只是一个存在某种特定类型原型,某些限制条件的被调用函数。
优点:
- 可以让实现方根据回调方的多种形态进行不同的处理和操作
- 可以让实现方,根据自己的需要定制回调方的不同形态
- 可以将耗时的操作隐藏在回调方,不影响实现方其他信息的展示。
- 让代码的逻辑更加集中,更加易读。
缺点:
- 回调函数过多会导致代码难以维护
- 回调函数容易造成资源竞争:如果回调函数中有共享资源访问,容易出现资源争抢,导致程序出错
- 代码可读性差,可能会破坏代码的结构和可读性
本质: 是将函数当作参数使用,目的是为了使程序更加普适。
76、什么是尾递归?
尾递归是递归的一种特殊情形,尾递归是一种特殊的尾调用,即在尾部直接调用自身的递归函数。核心思想是边调用边产生结果。
原理: 当编译器检测到一个函数调用是尾递归的时候,它会覆盖当前的活动记录而不是在栈中创建一个新的。编译器可以做到这一点,因为递归调用是当前活跃期内最后一条待执行的语句,于是当这个调用返回时栈帧中并没有其他事情可以做,因此也就没有保存栈帧的必要了,通过覆盖当前的栈帧而不是在其之上重新添加一个,这样所使用的栈空间就大大缩减了,这使得实际的运行效率会变得更高。
特点: 在尾部调用的是函数自身,可通过优化使得计算仅占用常量栈空间。
77、为什么会有栈溢出,为什么栈会设置容量?
-
为什么会栈溢出: 栈空间是预设的,它通常用于存放临时变量,如果你在函数内部定义一个局部变量,空间超出了设置的栈空间大小,就会溢出。不仅如此,如果函数嵌套太多,也会发生栈溢出,因为函数没有结束前,函数占用的变量也不被释放,占用了栈空间。
-
栈设置容量的原因: 是栈的地址空间必须连续,如果任其任意成长,会给内存管理带来困难。对于多线程程序来说,每个线程都必须分配一个栈,因此没办法让默认值太大。
78、二叉树和平衡二叉树的区别?
- 二叉树没有平衡因子的限制,而平衡二叉树有
- 二叉树可能退化为链表,而平衡二叉树不会
79、什么是this指针,为什么会存在this指针?
-
概念: 是一个隐含在每个非静态成员函数中的指针,它指向调用该成员函数的当前对象的地址。类和对象中的成员函数存储在公共的代码段,不同的对象调用成员函数时编译器为了知道具体操作的是哪一个对象给每个“非静态的成员函数”增加了一个隐藏的指针参数,让该指针指向当前对象,在函数体中所有成员变量的操作,都是通过这个指针来完成的由编译器自动完成。
-
存在原因:
-
区分同名成员变量: 在成员函数中,this 指针可以用来区分成员变量和函数参数或局部变量。例如,如果函数参数和类的成员变量名称相同,可以通过 this->member 来明确表示使用成员变量。
Copy code class MyClass { int value; public: MyClass(int value) { this->value = value; } // 使用 this 指针区分成员变量和参数 };
-
支持链式调用: 通过返回 *this 或 this,可以实现链式调用,便于将多个操作连在一起使用。
Copy code class MyClass { public: MyClass& setValue(int v) { /* 设置值 */ return *this; } MyClass& display() { /* 显示值 */ return *this; } }; MyClass obj; obj.setValue(10).display(); // 链式调用
-
动态绑定和多态支持: 当一个基类指针指向派生类对象时,通过 this 指针可以确保调用正确的虚函数版本,从而实现动态绑定和多态性。
-
对象的唯一标识: this 提供了当前对象的地址,使得在函数中可以获取和使用对象的内存地址,这对调试、操作和优化有帮助。
-
80、什么是重载、重写、隐藏?
-
重载: 函数名相同,函数参数不同,两个函数在同一作用域
-
重写: 两个函数分别在子类和父类中,函数名,返回值,参数均相同,函数必须为虚函数
-
隐藏: 在继承关系中,子类实现了一个和父类名字名字一样的函数。这样子类的函数就把父类的同名函数隐藏了。隐藏只与函数名有关。
81、静态成员函数可以是虚函数吗?为什么?
它不属于类中的任何一个对象或示例,属于类共有的一个函数,不依赖于对象调用,静态成员函数没有this指针,无法放进虚函数表。
82、构造函数可以为虚函数吗?为什么?
虚表指针是存储在对象的内存空间,当调虚函数时,是通过虚表指针指向的虚表里的函数地址进行调用的。如果将构造函数定义为虚函数,就要通过虚表指针指向的虚表的构造函数地址来调用。而构造函数是实例化对象,定义为虚函数后,对象空间还没有实例化,那就没有虚表指针,自然无法调用构造函数,那构造函数就失去意义,所以不能将构造函数定义为虚函数。
83、make_shared函数的优点,缺点?
-
优点: 减少了内存分配的次数,降低了系统开销,提高了效率,使用new构造的话至少会进行两次内存分配,(一次为智能指针本身,一次为共享指针的控制块)
-
缺点: 当构造函数是保护或者私有的时候无法使用make_shared函数。
会导致weak_ptr保持控制块,的生命周期,连带着保持了对象分配的内存,只有当最后一个weakptr离开作用域时,内存才会被释放,对于内存要求高的场景来说,是一个需要注意的问题。
84、函数调用进行的操作:
- 将参数压栈: 按照参数顺序的逆序进行,如果参数中有对象则先进行拷贝构造;
- 保存返回地址:即函数调用结束返回后接着执行的语句的地址;
- 保护维护函数栈帧信息的寄存器内容如,SP(堆栈指针),FP(栈帧指针)等;
- 保存一些通用寄存器的内容:应为有些通用寄存器会被所有函数用到,所以在函数调用之前,这些寄存器就可能已经放置了对函数有用的信息;
- 调用函数,函数执行完毕;
- 恢复通用寄存器的值;
- 恢复保存函数栈帧信息的那些寄存器的值;
- 通过移动栈指针,销毁函数的栈帧;
- 将保存的返回地址出栈,并赋给寄存器;
- 通过移动栈指针,回收传给函数的参数所占用的空间
三、参考链接
史上最全C/C++面试、C++面经八股文,一文带你彻底搞懂C/C++面试、C++面经!