2025 C++ 面试高频考点精讲:史上最全八股文整理
2025 C++ 面试高频考点精讲:史上最全八股文整理
前言:本文基于该文章整理,并进行额外的优化与扩展史上最全C/C++面试、C++面经八股文,一文带你彻底搞懂C/C++面试、C++面经!_c++八股-ZEEKLOG博客
目录
2.多态的实现原理(实现方式)是什么?以及多态的优点(特点)?
4.虚函数是怎么实现的?它存放在哪里在内存的哪个区?什么时候生成的
10.weak_ptr真的不计数?是否有计数方式,在哪分配的空间。
18.内存对齐是什么?为什么要进行内存对齐?内存对齐有什么好处?
21.介绍一下socket中的多路复用,及其他们的优缺点,epoll的水平和边缘触发模式
25.父类的构造函数和析构函数是否能为虚函数?这样操作导致的结果?
26.多线程为什么会发生死锁,死锁是什么?死锁产生的条件,如何解决死锁?
28.C++中左值和右值是什么?++i是左值还是右值,++i和i++哪个效率更高?
30.静态变量在哪里初始化?在哪一个阶段初始化?(都存放在全局区域)
48.vector中的push_back()和emplace_back()的区别、以及使用场景
55.一个函数f(int a,int b),其中a和b的地址关系是什么?
57.lamda表达式捕获列表捕获的方式有哪些?如果是引用捕获要注意什么?
60.vector如何判断应该扩容?(size和capacity)
61.构造函数是否能声明为虚函数?为什么?什么情况下为错误?
64.如何保证类的对象只能被开辟在堆上?(将构造函数声明为私有、单例)
71.C++11中的auto是怎么实现自动识别类型的?模板是怎样实现转化成不同类型的?
72.map和set的区别和底层实现是什么?map取值的 find,[],at方法的区别(at有越界检查功能)
75.介绍一下extern C关键字,为什么会有这个关键字?
80.类内普通成员函数可以调用类内静态变量吗,类内静态成员函数可以访问类内普通变量吗?
81.强制类型转换有哪几种类型,分别有什么特点?原理是什么?
Static_cast:用于数据类型的强制转换,强制将一种数据类型转化为另一种数据类型。
Const_cast:用于强制去除类似于const这种不能被修改的常数特性。
Reinterpret_cast:用于改变指针或引用的类型,将指针或引用类型转换成一个足够长的整形,将整形转换为指针或引用。
Dynamic_cast:其他三种都是在编译时完成的,它是在运行时处理的,运行时要进行类型检查。
82.回调函数是什么,为什么要有回调函数?有什么优缺点?回调的本质是什么?
1. 面向对象的三大特性:封装、继承、多态
封装
- 定义:把数据和实现细节隐藏在类内部,通过对外公开的接口访问。
- 特点:
- 降低耦合度,模块化;
- 保护内部数据安全,防止被外部随意破坏;
- 对外提供清晰的接口,保证类的独立性。
- 意义:增强代码安全性、可维护性和复用性。
*继承 - 定义:子类继承父类的属性和方法,从而复用基类功能。
- 特点:
- 私有成员虽然被继承,但不能直接访问;
- 构造函数、析构函数、友元函数、静态成员(变量/函数)不能被继承;
- 访问权限由基类修饰符决定。
- 意义:复用已有代码,提高开发效率;通过层次结构组织类,增强代码可扩展性。
多态 - 定义:相同的操作或接口,作用于不同对象时,表现出不同的行为。
- 实现方式:
- 静态多态:函数重载、运算符重载(编译期决定)
- 动态多态:虚函数 + 虚函数表(运行时决定)。
- 意义:增强程序的可替换性和灵活性,减少条件分支,支持代码扩展与维护。
2. 多态的实现原理与优点
1. 多态的实现方式
**``多态分为 静态多态 和 动态多态:**
- 静态多态(编译期多态)
- 编译时即可确定调用哪个函数。
- 主要方式:
- 函数重载(同名函数,参数不同)
- 运算符重载
- 函数隐藏(重定义):子类定义与父类同名函数,会隐藏父类的版本(只看名字,参数/返回值无关)。
- 特点:性能高,没有额外开销,但灵活性不足。
- 动态多态(运行时多态)
- 运行时根据对象的实际类型决定调用哪个函数。
- 通过 虚函数机制 实现:
- 类中若含虚函数,编译器会为该类生成 虚函数表(vtable)。
- 对象中保存一个 虚表指针(vptr),指向所属类的虚表。
- 当调用虚函数时,通过 vptr → vtable → 找到实际函数指针并执行。
- 特点:灵活性强,但有轻微开销(一次间接寻址)。
2. 多态的优点(特点)
- 可扩展性:新增子类,只需实现虚函数即可,不影响原有代码。
- 可替换性:父类指针/引用可指向不同子类对象,实现“调用接口,执行子类逻辑”。
- 灵活性:运行时决定具体行为,提升程序适应变化的能力。
- 简化代码:避免写大量
if-else/switch判断,不同子类逻辑由多态自动分发。
面试速答版
- 静态多态:编译期确定调用,靠函数重载、运算符重载、函数隐藏。
- 动态多态:运行时确定调用,靠虚函数 + 虚表实现。
- 优点:增强可扩展性、可替换性和灵活性,减少分支判断,简化维护。
3.final标识符的作用是什么?
放在类的后面表示该类无法被继承,也就是阻止了从类的继承,放在虚函数后面该虚函数无法被重写,表示阻止虚函数的重载
4.虚函数是怎么实现的?它存放在哪里在内存的哪个区?什么时候生成的
在C++中,虚函数的实现原理基于两个关键概念:虚函数表和虚函数指针
虚函数表:每个包含虚函数的类都会生成一个虚函数表,其中存储着该类中所有虚函数的地址。虚函数表是一个由指针构成的数组,每个指针指向一个虚函数的实现代码。
虚函数指针:在对象的内存布局中,编译器会添加一个额外的指针,称为虚函数指针或虚表指针。这个指针指向该对象对应的虚函数表,从而让程序能够动态的调用虚函数。
当一个基类指针或引用调用虚函数时,编译器会使用虚表指针来查找该对象对应的虚函数表,并根据函数在虚函数表中的位置来调用正确的虚函数。
在编译阶段生成,虚函数和普通函数一样存放在代码段,只是它的指针又存放在了虚表之中。
5.智能指针的本质是什么,它们的实现原理是什么?
智能指针本质是一个封装了一个原始C++指针的类模板,为了确保动态内存的安全性而产生的。实现原理是通过一个对象存储需要被自动释放的资源,然后依靠对象的析构函数来释放资源。
6.匿名函数的本质是什么?他的优点是什么?
匿名函数本质上是一个对象,在其定义的过程中会创建出一个栈对象,内部通过重载()符号实现函数调用的外表。
优点:使用匿名函数,可以免去函数的声明和定义。这样匿名函数仅在调用函数的时候才会创建函数对象,而调用结束后立即释放,所以匿名函数比非匿名函数更节省空间。
7.右值引用是什么,为什么要引入右值引用?
右值引用是为一个临时变量取别名,它只能绑定到一个临时变量或表达式(将亡值)上。实际开发中我们可能需要对右值进行修改(实现移动语义时就需要)而右值引用可以对右值进行修改。
为什么:
1.为了支持移动语义,右值引用可以绑定到临时对象、表达式等右值上,这些右值在生命周期结束后就会被销毁,因此可以在右值引用中窃取其资源,从而避免昂贵的复制操作,实现高效的移动语义。
2.完美转发:右值引用可以绑定到任何类型的右值上,可以将其作为参数传递给函数,并在函数内部将其“转发”到其他函数中,从而实现完美转发。
3.拓展可变参数模板,实现更加灵活的模板编程。
8.左值引用和指针的区别?
是否初始化:指针可以不用初始化,引用必须初始化
性质不同:指针是一个变量,引用是对被引用的对象取一个别名
占用内存单元不同:指针有自己的空间地址,引用和被引用对象占同一个空间。
9.指针是什么?
指针全名为指针变量,计算机在存储数据是有序存放的,为了能够使用存放的地址,就需要一个地址来区别每个数据的位置,指针变量就是用来存放这些地址的变量。
10.weak_ptr真的不计数?是否有计数方式,在哪分配的空间。
计数,控制块中有强弱引用计数,如果是使用make_shared初始化的函数则它所在的控制块空间是在所引用的shared_ptr中同一块的空间,若是new则控制器所分配的内存与shared_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字节, 内存块详细信息, 内存快大小)
我们在申请内存的时候,会多分配16字节的内存,里面保存了内存块的详细信息,free会对传入的内存地址向左偏移16字节,然后分析出当前内存块的大小,就知道要释放多大的内存空间了。
13.define和const的区别是什么?
(define: 预编译, 纯替换, 无类型检查, 不安全, 多备份, 不能调试) (const:编译, 类型检查, (静态)唯一份, 可调试)`
编译阶段:define是在编译预处理阶段进行简单的文本替换,const是在编译阶段确定其值
安全性:define定义的宏常量没有数据类型,只是进行简单的替换,不会进行类型安全检查;const定义的常量是有类型的,是要进行类型判断的
内存占用:define定义的宏常量,在程序中使用多少次就会进行多少次替换,内存中有多个备份,占用的是代码段的内存;const定义常量占用静态存储区域的空间,程序运行过程中只有一份
调试:define定义的宏常量不能调试,因为在预编译阶段就已经进行替换了;const定义的常量是可以进行调试的。
14.程序运行的步骤是什么
(预编译, 编译, 汇编,链接)
预编译:将头文件编译,进行宏替换,输出.i文件
编译:将其转化为汇编语言文件,主要做词法分析,语义分析以及检查错误,检查无误后将代码翻译成汇编语言,生成.s文件
汇编:汇编器将汇编语言文件翻译成机器语言,生成.o文件
链接:将目标文件和库链接到一起,生成可执行文件.exe
15.锁的底层原理是什么?
允许并发修改, 操作时先尝试修改,最后再检查冲突 缺点:反复重试,性能差, 引起ABA问题,值变过又变回去`
锁的底层是通过CAS,atomic 机制实现。
CAS机制:全称为Compare And Swap(比较相同再交换)可以将比较和交换操作转换为原子操作,CAS操作依赖于三个值:内存中的值V,旧的预估值X,要修改的新值B,如果旧的预估值X等于内存中的值V,就将新的值B保存在内存之中。(就是每一个线程从主内存复制一个变量副本后,进行操作,然后对其进行修改,修改完后,再刷新回主内存前。再取一次主内存的值,看拿到的主内存的新值与当初保存的快照值,是否一样,如果不一样,说明有其他线程修改,本次修改放弃,重试。)
atomic****机制:如16问。
16.原子操作是什么?
(不被打断) 原理:硬件加锁, LOCK前缀, 拦住总线执行指令
原子操作是指不会被线程调度机制打断的操作,这种操作一旦开始,就一直运行到结束,中间不会有任何切换到另一个线程。
原理是:在X86的平台下,CPU提供了在指令执行期间对总线加锁的手段,CPU中有一根引线#HLOCK pin连接到北桥,如果汇编语言的程序在程序中的一条指令前面加上了前缀“LOCK”,经过汇编之后的机器码就使CPU在执行这条指令的时候把#HLOCKpin的电平拉低持续到这条指令结束的时候放开,从而把总线锁住,这样别的CPU就暂时不能够通过总线访问内存了,保证了多处理器环境中的原子性。
17.class与struct的区别
(public和private, struct不能模板参数,class能)
默认继承权限不同:class默认继承的是private继承,struct默认是public继承。
Class还可用于定义模板参数,但是关键字struct不能同于定义模板参数,C++保留struct关键字,原因是保证与C语言的向下兼容性,为了保证百分百的与C语言中的struct向下兼容,,C++把最基本的对象单元规定为class而不是struct,就是为了避免各种兼容性的限制。
18.内存对齐是什么?为什么要进行内存对齐?内存对齐有什么好处?
是啥?为提高性能, 要求存取数据的的起始地址 为啥?1.保证不同硬件平台的代码可移植性2.访问内存非逐字节,对齐数据地址,减少跨字节访问
优点:提高运行效率,增强可移植性
内存对齐是处理器为了提高处理性能而对存取数据的起始地址所提出的一种要求。
1.有些CPU可以访问任意地址上的任意数据,而有些CPU只能在特定的地址访问数据,因此不同硬件平台具有差异性,这样的代码就不具有移植性,如果在编译时将进行对齐,这就具有平台的移植性。2.CPU每次寻址有时需要消耗时间的,并且CPU访问内存的时候并不是逐个字节访问,而是以字长为单位访问,所以数据结构应该尽可能地在自然边界上对齐,如果访问未对齐内存,处理器需要做多次内存访问,而对齐的内存访问可以减少访问次数,提升性能。
优:提高程序的运行效率,增强程序的可移植性。
19.进程之间的通信方式有哪些?
管道:半双工, 内核缓存,一写一读 消息队列:长度限制, 内核态/用户态拷贝问题共享内存:效率最高, 直接映射到内存空间 信号量:计数器,配合共享内存实现互斥/同步信号:异步通知机制
`套接字:网络通信
管道:管道分为匿名管道和命名管道,管道本质上是一个内核中的一个缓存,当进程创建管道后会返回两个文件描述符,一个写入端一个输出端。缺点:半双工通信,一个管道只能一个进程写,一个进程读。不适合进程间频繁的交换数据
消息队列:可以边发边收,但是每个消息体都有最大长度限制,队列所包含的消息体的总数量也有上限并且在通信过程中存在用户态和内核态之间的数据拷贝问题
共享内存:解决了消息队列存在的内核态和用户态之间的数据拷贝问题。
信号量:本质上是一个计数器,当使用共享内存的通信方式时,如果有多个进程同时往共享内存中写入数据,有可能先写的进程的内容被其他进程覆盖了,信号量就用于实现进程间的互斥和同步PV操作不限于信号量±1,而且可以任意加减正整数
信号:异步通信机制
套接字:网络通信
20.线程之间的通信方式有哪些?
信号量
条件变量
互斥量
21. 介绍一下 socket 中的多路复用,及它们的优缺点,epoll 的水平和边缘触发模式
总结:
多路复用就是让一个线程(或进程)同时监听多个 IO 通道(文件描述符),一旦其中某个通道有事件发生,就能立即处理,而不需要为每个通道单独创建线程或进程。
select:最古老,跨平台好,支持微秒级超时;缺点 fd 数有限(1024),频繁拷贝,效率低poll:无 fd 数限制,接口更方便;缺点 还是要频繁拷贝,效率不高epoll:Linux 专属,高效,无限 fd,注册一次就行,事件驱动LT 模式:状态满足就一直通知,安全但啰嗦ET 模式:状态变化才通知,高效,但要非阻塞+一次读完
- select
- 优点:可移植性最好,在老的 Unix 系统上通用;支持微秒级超时。
- 缺点:
- 监听的 fd 数有限(默认 1024,可改内核参数,但麻烦)。
- 每次调用都要把 fd 集合在 用户态和内核态之间来回拷贝,fd 多时开销大。
- poll
- 优点:没有 fd 上限限制,接口更简洁,调用时不需要像 select 那样重置参数。
- 缺点:和 select 一样,每次调用都要复制整个 fd 集合,fd 多时依旧效率低。
- epoll
- 优点:
- 没有 fd 数量限制。
- fd 注册只需一次(
epoll_ctl),监听时无需重复拷贝。 - 内核用 红黑树管理 fd,链表管理就绪事件,性能更高。
- 事件触发机制:只把“真的发生事件的 fd”告诉你,不像 select/poll 每次都要遍历一遍。
- 缺点:只支持 Linux,BSD 系统要用 kqueue。
- 优点:
- 水平触发(LT, Level Trigger)
- 行为:只要缓冲区不空/没满,就会一直告诉你“可读/可写”。
- 特点:类似 select/poll,用起来简单,不容易漏事件,但通知次数多。
- 边缘触发(ET, Edge Trigger)
- 行为:只有状态变化时才通知一次(比如缓冲区从空 → 有数据)。
- 特点:更高效,不会被重复通知淹没;但需要 非阻塞 IO,并且要一次把数据读完,否则可能漏事件。
通俗解释:
- 想象有一个保安(内核)帮你盯着一堆快递柜(fd)。
- select 就像你每天要带着一张“全员名单”去问保安:“这些柜子有没有快递?”
- 保安会一一核对,再告诉你。名单人数多时,很慢。
- poll 好一点:名单格式更清晰,人数不限,但还是得天天全名单过一遍,还是累。
- epoll 就聪明了:
- 你把要盯的柜子登记一次,保安会自己建个表。
- 有快递时,保安直接通知你是哪几个柜子,不用全名单检查。
- 效率大大提高。
- LT 模式:只要柜子里有快递,保安就一直提醒你:“快去拿!”
- ET 模式:保安只在快递刚放进去时提醒一次,“剩下的是你的事了”。
24.类的生命周期
类从被加载到内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。其中验证,准备,解析三个部分统称为连接
全局对象在main开始前被创建,main退出后被销毁。
静态对象在第一次进行作用域时被创建,在main退出后被销毁。
局部对象在进入作用域时被创建,在退出作用域时被销毁。
New创建的对象直到内存被释放的时候都存在。
25.父类的构造函数和析构函数是否能为虚函数?这样操作导致的结果?
虚函数调用依赖虚函数表,虚函数表又是依赖虚指针,而虚指针又是由实例化对象衍生,如果构造函数为虚函数,那么就要去找虚指针,但是虚指针还没有初始化
构造函数不能为虚函数,虚函数的调用是通过虚函数表来查找的,而虚函数表由类的实例化对象的vptr指针指向,该指针存放在对象的内部空间之中,需要调用构造函数完成初始化,如果构造函数为虚函数,那么调用构造函数就需要去寻找vptr,但此时vptr还没有完成初始化,导致无法构造对象。
析构函数可以且经常为虚函数:当我们使用父类指针指向子类时,只会调用父类的析构函数,子类的析构函数不会被调用,容易造成内存泄漏。
好,我们来把 死锁 这一题整理成你喜欢的形式:
26. 多线程为什么会发生死锁?死锁是什么?死锁产生的条件?如何解决死锁?
总结:
是啥?多个线程互相等待资源,大家都卡住 → 程序停滞为什么?多线程竞争资源,彼此等待,形成循环依赖四个条件:互斥,不可剥夺,请求和保持,环路等待
详细答案:
死锁是指两个或多个进程/线程在执行过程中,因为竞争资源而造成的一种相互等待的情况。
一旦进入死锁状态,如果没有外部干预,这些进程都无法继续执行。
死锁产生的四个必要条件:
- 互斥条件:一个资源只能被一个线程占用。
- 请求与保持条件:一个线程持有资源的同时又提出新的资源请求。
- 不可剥夺条件:已经分配给线程的资源不能被强行剥夺。
- 环路等待条件:线程之间形成首尾相接的环形等待资源关系。
解决死锁的思路:
- 预防死锁:打破条件(如规定加锁顺序,禁止请求保持)。
- 避免死锁:比如银行家算法,根据资源分配是否安全来决定是否满足请求。
- 检测死锁:运行时检测是否存在环路,如果有则终止或抢占。
- 解除死锁:杀死部分进程或回收部分资源
通俗解释:
死锁就像:
- 你手里有叉子,想拿勺子;我手里有勺子,想拿叉子。
- 谁也不肯放手,结果谁都吃不上饭 → 卡死。
解决方法就是规定大家“先拿勺子再拿叉子”,或者“东西可以强制抢回来”,这样就不会卡死。
27.描述一下面向过程和面向对象
面向对象:就是将问题分解为各个对象,建立对象的目的不是为了完成一个步骤,而是为了描述某个事物在整个解决问题的步骤中的行为,相比面向过程,代码更易维护和复用。但是代码效率相对较低。
面向过程:就是将问题分析出解决问题的步骤,然后将这些步骤一步一步的实现,使用的时候一个一个调用就好。代码效率更高但是代码复用率低,不易维护。
28.C++中左值和右值是什么?++i是左值还是右值,++i和i++哪个效率更高?
第一小问结合本文第七和第八问,++i是左值,因为++i返回的是一个左值没有发生拷贝,所以效率更高。(++i是左值,i++是右值。因为++i返回i本身,而i++返回i的值。)
29. 介绍一下 vector、list 的底层实现原理和优缺点
- vector
- 底层是 连续内存,靠三个指针维护:
begin:指向已使用空间的开头end:指向已使用空间的末尾capacity_end:指向分配空间的末尾
- 优点:
- 支持下标随机访问(O(1))。
- 尾部插入/删除效率高(均摊 O(1))。
- 缺点:
- 在中间插入/删除需要移动大量元素,效率低(O(n))。
- 扩容时会重新分配更大内存并拷贝元素,代价较大。
- 容量通常大于元素个数,可能造成空间浪费。
- 底层是 连续内存,靠三个指针维护:
- list
- 底层是 双向链表,每个节点保存:前驱指针、后继指针和数据。
- 优点:
- 插入和删除操作只需改变指针,效率高(O(1))。
- 不需要扩容,按需分配,不浪费空间。
- 缺点:
- 不支持下标随机访问,只能顺序遍历。
- 每个节点额外存两个指针,内存开销大。
- 节点分散在堆上,缓存不友好,访问速度慢。
30.静态变量在哪里初始化?在哪一个阶段初始化?(都存放在全局区域)
静态变量,全局变量,常量都在编译阶段完成初始化和内存分配。其他变量都是在编译阶段进行初始化,运行阶段内存分配.。
31.如何实现多进程?
在Linux中C++使用fork函数来创建进程
而windows中C++使用createprocess来创建进程
32.空对象指针为什么能调用函数?
在类的初始化的时候,编译器会将它的函数分配到类的外部,这也包括静态成员函数,这样做主要是为了节省内存,如果我们在调用类中的的成员函数时没有使用类中的任何成员变量,它不会使用到this指针所以可以正常调用这个函数。
好 👍 我重新帮你完整梳理一遍 第 33 题:shared_ptr 线程安全吗?,这次结合你刚才的困惑,一起解释清楚。
33. shared_ptr 线程安全吗?
(资源释放安全, 资源访问不安全)
- 引用计数安全
shared_ptr内部用std::atomic<size_t>来维护引用计数。- 所以多个线程同时构造/拷贝/销毁
shared_ptr时,计数加减不会出错。 - 这意味着 对象何时销毁 是安全的,不会出现悬空指针或二次释放。
- 对象访问不安全
shared_ptr管理的对象本身(也就是*ptr指向的资源),并没有加锁。- 如果多个线程同时写,或一个线程写另一个线程读,就会发生数据竞争。
- 因此,
shared_ptr不能保证对象的数据访问线程安全,需要用户手动加锁(如std::mutex)。
- 结论一句话
shared_ptr的线程安全仅限于资源释放,不包括资源访问。
通俗解释:shared_ptr 就像一份共享合同:
- 保证“这套房子什么时候退租”是有序的(计数安全)。
- 但里面的家具(对象内容)怎么用,它不管。你和室友要是一起乱抢电视遥控器,就会打架(数据竞争)。
34.push_back()左值和右值的区别是什么?
(左值:拷贝构造。 右值:移动)
如果push_back()的参数是左值,则使用它拷贝构造新对象,
如果是右值,则使用它移动构造新对象.。
好的 👍 给你一个简洁版,面试时直接说就够了:
35. move 底层是怎么实现的?
(非移动,而是转换,强制转换为右值引用,调用移动构造,避免拷贝)
std::move并不会真正“移动”数据,本质就是一个static_cast<T&&>,把左值强制转换成右值引用。- 这样编译器就会优先调用 移动构造/赋值,避免拷贝,直接转移资源所有权,更高效。
一句话记住:move 只是类型转换,不做搬运;移动语义的效率来自“资源转移”而不是拷贝。
36.完美转发的原理是什么?
(转发参数时,保持值与性质不变)
简要回答:
完美转发就是:函数模板在转发参数时,既保持值不变,也保持参数是左值还是右值的属性。
原理是:万能引用 (T&&) + 引用折叠 + std::forward。
编译器根据传参,利用 引用折叠规则 推导出正确类型:
- 左值 → T& → 最终是左值引用
- 右值 → T → 最终是右值引用
37.空类中有什么函数?
默认构造函数、默认拷贝构造函数、默认析构函数、默认赋值运算符
取值运算符、const取值运算符
38.explicit用在哪里?有什么作用?
(防止隐式转换)
- 用在哪里:只能修饰类的构造函数(单参构造,或其他参数有默认值时)。
- 作用:禁止隐式类型转换,必须显式调用构造函数。
- 意义:避免编译器自动转换带来的歧义或 bug。
39. 成员变量初始化的顺序是什么?
(成员变量初始化顺序 = 声明顺序,与初始化列表或构造函数体里的写法无关) 先父类后子类,先静态/const后成员
- 初始化顺序
- 成员变量的初始化顺序 严格按照它们在类中声明的顺序,而不是构造函数初始化列表里写的顺序。
- 即使你在初始化列表里写成
: b(2), a(1),也会先初始化a再初始化b,因为a在类中先声明。
- 构造函数体里的赋值
- 在进入构造函数体之前,成员变量已经按照声明顺序被“默认初始化”了。
- 构造函数体里的代码只是普通 赋值操作,不会改变初始化顺序。
- 特殊情况
const成员和引用成员必须在初始化列表里初始化,不能在构造函数体内赋值。static成员不属于对象,不在构造函数里初始化,而是在类外定义时初始化(C++17 起可以用inline static)。
示例:
如果你没有用初始化列表,而是在构造函数体里写:
Test(){ b =2; a =1;}#include<iostream>usingnamespace std;structDemo{int a;int b;Demo():b(2),a(1){// 写成 b 在前 cout <<"a="<< a <<" b="<< b << endl;}};输出:
a=1 b=2 👉 即使你写成 : b(2), a(1),仍然先初始化 a 再初始化 b。
一句话记忆:
👉 成员变量初始化顺序 = 声明顺序,与初始化列表或构造函数体里的写法无关。
40.指针占用的大小是多少?
(64:8B. 32:4B)
64位电脑上占8字节,32位的占4字节,我们平时所说的计算机多少位是指计算机CPU中通用寄存器一次性处理、传输、暂时保存的信息的最大长度。即CPU在单位时间内能一次处理的二进制的位数,因此CPU所能访问的内存所有地址由多少位组成,而8比特位表示1字节,就可以得出在不同位数的机器中指针的大小。
41.野指针和内存泄漏是什么?如何避免?
内存泄露:没释放堆内存 野指针:指向已删除,受限区域
内存泄漏:是指程序中以动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果
避免:使用智能指针管理资源,在释放对象数组时使用delete[],尽量避免在堆上分配内存
野指针:指向一个已删除的对象或未申请访问受限内存区域的指针。
避免:对指针进行初始化,用已合法的可访问内存地址对指针初始化,指针用完释放内存,将指针赋值nullptr。
42.malloc和new的区别是什么?
Malloc/free是标准库函数,new/delete是C++运算符
Malloc分配内存失败返回空,new失败抛异常
New/delete会调用构造析构函数,malloc/free不会,所以他们无法满足动态对象的要求。
New返回有类型的指针,malloc返回无类型的指针
分配内存的位置:malloc从堆上动态分配内存,new是从自由存储区为对象动态分配内存(取决于operator new的实现,可以为堆还可以是静态存储区)
New申请内存的步骤:调用operator new函数,分配一块足够大,且原始的,未命名的内存空间来存储特定类型的对象。运行相应的构造函数来构造对象,并为其传入初值,返回一个指向该对象的指针。
Delete:先调用对象的析构函数,再调用operator delete函数释放内存空间
43.多线程会发生什么问题?线程同步有哪些手段?
多线程问题主要是 :竞争和死锁,常用同步手段有 :原子变量、锁、条件变量、信号量、读写锁。
会引发资源竞争的问题,频繁上锁会导致程序运行效率低下,甚至会导致发生死锁。
线程同步手段:使用atomic原子变量,使用互斥量也就是上锁,使用条件变量或信号量制约对共享资源的并发访问。
44.什么是STL?
STL = 容器 + 算法 + 迭代器 为核心,辅以 仿函数 + 适配器 + 空间配置器
它是C++标准库的重要组成部分,不仅是一个可复用的组件库也是一个包含了数据结构与算法的软件架构,它拥有六大组件分别是:仿函数,算法,迭代器,空间配置器,容器,配接器
45.对比迭代器和指针的区别
指针是地址,迭代器是模拟指针的类
迭代器不是指针,是一个模板类,通过重载了指针的一些操作符模拟了指针的一些功能,迭代器返回的是对象引用而不是对象的值。
指针能够指向函数而迭代器不行迭代器只能指向容器
46.线程有哪些状态,线程锁有哪些?
五种状态:创建,就绪,运行,阻塞,死亡
线程锁的种类:互斥锁,条件锁,自旋锁,读写锁,递归锁
47.解释说明一下map和unordered_map
map: 红黑树,有序,O(logn) unordered_map: 哈希表, 无序,O(1)
实现方式
map:红黑树,有序。unordered_map:哈希表,无序。
复杂度map:查找/插入/删除 →O(log n)。unordered_map:平均O(1),最坏O(n)。
*优缺点map- ✅ 有序,支持区间遍历
- ❌ 较慢,占用空间大
unordered_map- ✅ 查找快,平均 O(1)
- ❌ 无序,冲突时效率下降,rehash 开销大
场景选择
- 要 有序 + 范围操作 → 用
map - 要 快速查找 → 用
unordered_map
48.vector中的push_back()和emplace_back()的区别、以及使用场景
push_back: 已有对象,再拷贝/移动 emplace_back: 直接在容器内构造,更高效
区别:
- push_back
- 需要一个已经构造好的对象。
- 过程:先创建临时对象 → 再拷贝/移动到容器尾部。
- emplace_back
- 直接在容器尾部原地构造对象。
- 少了一次临时对象的构造和拷贝/移动,更高效。
49.如何实现线程安全,除了加锁还有没有其他的方式?
互斥量,原子操作,信号量,条件变量,读写锁
除了锁之外还可以使用互斥量(防止多个线程来同时访问共享资源,从而避免数据竞争的问题),原子操作(原子操作是不可分割的,使用原子操作可以确保在多线程环境中操作是安全的),条件变量(协调线程之间的协作,用来在线程之间传递信号,从而控制线程的执行流程)等方式
50. 1. Vector中的resize vs reserve 的区别
- resize(n):改变 size(元素个数)。
- n > size → 增加新元素。
- n < size → 删除尾部元素。
- 可能触发扩容。
- reserve(n):改变 capacity(预分配空间),不改 size。
- 只分配内存,不新增元素。
- 一句话记忆:
resize改 元素数,reserve改 容量。
50. 2. size vs capacity
- size:当前元素个数,每插入/删除元素都会变化。
- capacity:预分配的空间,至少能容纳多少元素。
- 当 size > capacity 时触发扩容。
- 扩容时会 重新开辟大空间 + 拷贝/移动旧数据,不是在后面“拼接”。
- 扩容策略:一般按 2 倍增长(不同实现可能略有差别)。
50. 3. 扩容逻辑
- 不是“在尾部接空间”。
- 而是“重新申请更大连续内存 → 拷贝/移动旧数据 → 释放旧空间”。
- 这是因为 vector 必须保证 内存连续性。
一句话串起来:
vector 的关键点就是 连续存储。
- 插入时
size++,不足时capacity倍增扩容。 push_back拷贝已有对象,emplace_back原地构造。resize改元素个数,reserve预分配空间。
51.vector扩容为了避免重复扩容做了哪些机制?
当vector内存不够时本身内存会以1.5或者2倍的增长,以减少扩容次数
引入了reserve,自定义vector最大容量
底层流程(扩容时):
- 申请一块更大的连续内存(通常是 2 倍)。
- 遍历旧空间里的每个元素:
- 如果支持移动构造 → 调用 移动构造函数。
- 否则 → 调用 拷贝构造函数。
- 析构旧空间里的对象。
- 释放旧内存。
52.C++中空类的大小是多少?
1字节
53. weak_ptr 是怎么实现的?
weak_ptr和shared_ptr共用一个 控制块。- 控制块里有:
- use_count:记录
shared_ptr的数量。 - weak_count:记录
weak_ptr的数量。 - 对象指针:指向实际资源。
- use_count:记录
- 当
shared_ptr数量为 0 时,对象销毁;但只要还有weak_ptr,控制块会保留。 weak_ptr.lock()会检查use_count:- =0 → 返回空
shared_ptr
✅ 一句话记忆:
👉weak_ptr只记录弱引用,不延长对象生命周期,靠控制块里的 weak_count 实现。
- =0 → 返回空
0 → 返回可用的 shared_ptr54.虚函数的底层原理是什么?
虚函数表和虚表指针,详细看本文第四问。
55.一个函数f(int a,int b),其中a和b的地址关系是什么?
a和b的地址是相邻的。
56. 移动构造和拷贝构造的区别是什么?
拷贝:左值,复制,开销大 移动:右值,转移,效率高
拷贝构造:
- 传入左值对象。
- 行为是 完整复制资源(如重新分配内存,把数据一份份拷贝过去)。
- 开销较大。
移动构造: - 传入右值对象(临时对象或用
std::move转换的左值)。 - 行为是 转移资源所有权(如把指针指向的内存直接交给新对象,并将旧对象指针置空)。
- 避免了内存的复制,效率更高。
✅ 一句话记忆:
👉 拷贝构造 = 复制数据,移动构造 = “偷走”数据(转移所有权)。
好的 👍 我帮你把回答梳理得更清晰简洁,既能背又能答面试官:
57. lambda 表达式捕获方式有哪些?引用捕获要注意什么?
按值捕获,按引用捕获(注意悬挂引用)
捕获方式:
- 按值捕获(
[=]或[x]):在闭包里保存外部变量的副本。 - 按引用捕获(
[&]或[&x]):在闭包里保存外部变量的引用/别名。 - 其他:
[this]捕获当前对象指针[*this](C++17)拷贝当前对象- 初始化捕获(C++14):
[y = std::move(x)]
引用捕获注意点:
- 闭包里保存的是外部变量的引用,如果该变量在闭包执行前就被销毁(如局部变量作用域结束),会导致悬挂引用 → 未定义行为。
- 解决办法:
- 对生命周期可能短的变量,使用值捕获(存副本更安全);
- 或用 初始化捕获 + move 把资源搬进闭包;
- 捕获
this时要注意对象的生命周期,必要时改用[*this]或智能指针副本。
✅ 一句话总结:
按值捕获安全但有拷贝,按引用捕获高效但要保证外部变量活得够久;生命周期不确定时选值捕获或初始化捕获。
58.哈希碰撞的处理方法
最常见:链地址法(unordered_map)。常见:开放定址,再哈希,链地址,建立公共溢出区
开放定址法:当遇到哈希冲突时,去寻找一个新的空闲的哈希地址。一般采用线性探测,平方探测
再哈希法:同时构造多个哈希函数,等发生哈希冲突时就使用其他哈希函数知道不发生冲突为止,虽然不易发生聚集,但是增加了计算时间
链地址法:将所有的哈希地址相同的记录都链接在同一链表中
建立公共溢出区:将哈希表分为基本表和溢出表,将发生冲突的都存放在溢出表中
59.unordered_map的扩容过程
当unordered_map中的元素数量达到桶的负载因子(0.75)时,会重新分配桶的数量(通常会按照原有桶的数量*2的方式进行扩容,但是具体的增长策略也可以通过修改容器中的max_load_factor成员变量来进行调整),并将所有的元素重新哈希到新的桶中。
60.vector如何判断应该扩容?(size和capacity)
(元素数 >= 容量 -> 扩容)
由当前容器内元素数量的大小和容器最大大小进行比较如果二者相等就会进行扩容,一般是1.5倍,部分的有两倍
61.构造函数是否能声明为虚函数?为什么?什么情况下为错误?
构造函数不能为虚函数,虚函数的调用是通过虚函数表来查找的,而虚函数表由类的实例化对象的vptr指针指向,该指针存放在对象的内部空间之中,需要调用构造函数完成初始化,如果构造函数为虚函数,那么调用构造函数就需要去寻找vptr,但此时vptr还没有完成初始化,导致无法构造对象。
62. 类中 static 函数是否能声明为虚函数?
static属于类,无this,虚函数需要通过虚指针,而虚指针由this调用,因此不能
答案:不能。
原因:
- static 函数没有 this 指针
- 静态成员函数属于类本身,不依赖具体对象。
- 因此它没有
this指针,而虚函数机制依赖于对象中的vptr(虚函数指针),通过this->vptr找虚表。
- 虚函数的本质
- 虚函数需要对象的上下文(即
this),通过对象的vptr去查虚函数表 (vtable),再找到真正要调用的函数 - static 函数没有对象上下文,自然也没有 vptr,无法参与虚函数机制。
- 虚函数需要对象的上下文(即
一句话总结:
虚函数依赖 this 指针(对象上下文),而 static 函数没有 this 指针,所以 static 函数不能是虚函数。
63.哪些函数不能被声明为虚函数?
不能是虚函数的有:**构造函数、静态成员函数、友元函数、非成员函数**。
构造函数,/内联函数(内联函数有实体,在编译时展开,没有this指针)错的/,静态成员函数,友元函数(C++不支持友元函数的继承),非类成员函数
64. 如何保证类对象只能在堆上或栈上创建?
私有构造+静态函数接口
- 只能堆上:构造函数设为私有/保护,提供
static工厂方法(内部用new),外部只能拿到指针。 - 只能栈上:删除
operator new/delete,阻止堆分配,只能栈上声明对象。
示例代码
// 只能堆上classHeapOnly{private:HeapOnly(){}~HeapOnly(){}public:static HeapOnly*create(){returnnewHeapOnly();}voiddestroy(){deletethis;}};// 只能栈上classStackOnly{public:StackOnly(){}~StackOnly(){}void*operatornew(size_t)=delete;// 禁用堆上分配voidoperatordelete(void*)=delete;};intmain(){// HeapOnly h; // ❌ 错误:构造函数私有 HeapOnly* p =HeapOnly::create();// ✅ 堆上 p->destroy(); StackOnly s;// ✅ 栈上// StackOnly* ps = new StackOnly(); // ❌ 禁止堆上}记忆口诀:
- 堆上:私有构造 + 工厂方法。
- 栈上:禁用
new/delete。
65.讲讲你理解的虚基类
虚基类:采用虚继承的基类。解决基类多个子对象的问题(菱形继承)
虚基类是 C++ 中一种特殊的类,用于解决多继承所带来的“菱形继承”问题。如果一个派生类同时从两个基类派生,而这两个基类又共同继承自同一个虚基类,就会形成一个“菱形”继承结构,导致派生类中存在两份共同继承的虚基类的实例,从而引发一系列的问题。
为了解决这个问题,我们可以将虚基类作为共同基类,并在派生类中采用虚继承的方式。
虚继承会使得派生类中只存在一份共同继承的虚基类的实例,从而避免了多个实例之间的冲突。
虚基类是可以被实例化的。
66.C++哪些运算符不能被重载?
完全不能重载的运算符:
.(成员访问).*(成员指针访问)::(作用域解析)?:(三目条件运算符sizeoftypeidalignof(C++11)decltype(C++11)
虽然能重载但不推荐:
&&、||(逻辑与/或,可能破坏短路特性),(逗号运算符,易产生歧义)
67.动态链接和静态链接的区别,动态链接的原理是什么?
静态:形成可执行程序前,库中代码包含到程序中,占用资源 动态:程序运行时,调用接口
区别:他们的最大区别就是在于链接的时机不同,静态链接是在形成可执行程序前,而动态链接的进行则是程序执行时。
静态库:就是将库中的代码包含到自己的程序之中,每个程序链接静态库后,都会包含一份独立的代码,当程序运行起来时,所有这些重复的代码都需要占用独立的存储空间,显然很浪费计算机资源。
动态库:不会将代码直接复制到自己程序中,只会留下调用接口,程序运行时再去将动态库加载到内存中,所有程序只会共享这一份动态库,因此动态库也被称为共享库。
动态链接原理:程序运行时,系统中的动态链接器把库加载到进程里面,找到函数的真实地址,将地址记录到跳转表中,程序通过跳转表去调用库函数
68.C++中怎么编译C语言代码?
使用extern“C”让C++代码按照C语言的方式去编译
69. 未初始化的全局变量和初始化的全局变量放在哪里?
- 已初始化的全局变量:放在 数据段(.data 段),在程序加载时就分配好空间并初始化。
- 未初始化的全局变量:放在 BSS 段,同样是静态分配,但在程序加载时自动置零。
✅ 一句话记忆:
全局变量都在静态区 → 有初值放 .data,无初值放 .bss。
69. 1. 静态区包含哪些部分?
静态区 = **.data(已初始化) + .bss(未初始化) + 常量区**。
静态区(有时也叫全局区)主要包含:
- .data 段
- 存放 已初始化的全局变量 和 已初始化的 static 变量。
- 程序加载时分配,保存初始化值。
- .bss 段(Block Started by Symbol)
- 存放 未初始化的全局变量 和 未初始化的 static 变量。
- 程序加载时分配,自动清零。
- 常量区(有时单独列出)
- 存放 常量数据,比如
const全局常量、字符串常量。 - 一般是只读的,可能和代码段合并在一起。
- 存放 常量数据,比如
✅ 一句话总结:
静态区 = .data(已初始化) + .bss(未初始化) + 常量区。
70.说一下内联函数及其优缺点
空间换时间, 在调用处展开,只能简单函数(10行左右)
内联函数是在编译期将函数体内嵌到程序之中,以此来节省函数调用的开销。
**优点:是节省了函数调用的开销,让程序运行更加快速。直接在调用处展开
**缺点:是如果函数体过长,频繁使用内联函数会导致代码编译膨胀问题。不能递归执行
好的 👍 我帮你把这段话重新整理一下,更加精炼、逻辑清晰:
71. C++11 中 auto 的类型推导原理?模板是怎样实现转化成不同类型的?
auto:占位符,编译期,自动推导 模板:蓝图,根据传入类型生成对应的具体函数
- auto
auto是一个 占位符,在编译期由编译器根据初始化表达式 推导出真实类型。- 变量依然有确定类型,只是由编译器自动替换,不需要程序员手写。
- 模板
- 模板是一个 蓝图,本身不是函数/类。
- 当用不同类型实例化时,编译器会根据传入的类型 生成对应的具体函数或类代码(模板实例化)。
- 相当于把原本要写的重复代码交给编译器自动完成。
✅ 一句话记忆:
auto= 编译器帮你 推导变量类型。- 模板 = 编译器帮你 生成不同类型的函数/类。
72. map 和 set 的区别及底层实现?map 的 find / [] / at 方法区别?
键值对/key
区别
- map:键值对(key-value),key 唯一,底层 红黑树(有序)。
- set:只有 key,没有 value,key 唯一,底层同样是 红黑树(有序)。
map 的三种取值方式
- find(key)
- 返回一个迭代器,如果没找到返回
end()。 - 不会修改容器内容。
- 返回一个迭代器,如果没找到返回
- operator[]
- 如果 key 存在:返回对应 value 的引用。
- 如果 key 不存在:会 自动插入一个默认值(比如 int 类型默认是 0)。
- at(key)
- 如果 key 存在:返回对应 value 的引用。
- 如果 key 不存在:抛出
std::out_of_range异常。 - 多了一层边界检查,所以比
[]慢一些。
✅ 一句话记忆:
- map / set:都是红黑树。
- find:查有没有。
- []:查并插,没找到就插入默认值。
- at:查并检,没找到就抛异常。
73. 详细说一说 fcntl 的作用
作用:fcntl 是一个 文件控制函数,用来对已打开的文件描述符进行各种控制和管理。
主要功能(常见 cmd 参数):
- 复制文件描述符
F_DUPFD:复制一个已有的 fd,返回新的 fd,最小值 ≥ 指定值。
- 文件描述符标志
F_GETFD:获取当前 fd 的标志(比如FD_CLOEXEC)。F_SETFD:设置 fd 的标志。
- 文件状态标志
F_GETFL:获取文件状态(如O_NONBLOCK、O_APPEND)。F_SETFL:修改文件状态(比如设置非阻塞 I/O)。
- 异步 I/O 所有权
F_GETOWN/F_SETOWN:获取/设置接收异步 I/O 信号的进程 ID 或进程组。
- 记录锁
F_GETLK:查询文件锁。F_SETLK/F_SETLKW:设置/释放文件锁。
74.C++的面向对象主要体现在那些方面?
体现在C++引入了面向对象的一些特征,例如加入了封装继承多态的特点。(然后介绍一下封装继承多态)
75.介绍一下extern C关键字,为什么会有这个关键字?
是用来实现在C++代码段中用C语言的方式来编译代码,是C++为了兼容C语言所加入的关键字
76.讲一讲迭代器失效及其解决方法
序列式容器迭代器失效:当当前元素的迭代器被删除后,后面所有元素的迭代器都会失效,他们都是一块连续存储的空间,所以当使用erase函数操作时,其后的每一个元素都会向前移动一个位置,此时可以使用erase函数操作可以返回下一个有效的迭代器。
Vector迭代器失效问题总结:1.当执行了erase方法时,指向删除节点的迭代器全部失效,指向删除节点之后的全部迭代器也失效。
2.当进行push_back方法时,end操作返回的迭代器肯定失效。
3.当插入一个元素后,capacity返回值与没有插入元素之前相比有改变,则需要重新加载整个容器,此时first和end操作返回的迭代器失效。
4.当插入一个元素后,如果空间未重新分配,指向插入位置之前的元素的迭代器依然有效,但指向插入元素之后元素的迭代器全部失效。
Deque迭代器失效总结:1.对于deque,插入到除首尾位置之外的任何位置都会导致迭代器、指针和引用都会失效,如果在首尾位置添加元素,迭代器会失效,但是指针和引用不会失效。
2.如果在首尾之外的任何位置删除元素,那么指向被删除元素外其他元素的迭代器都会失效。3.如果在其首部和尾部删除元素则只会使指向被删除元素的迭代器失效。
关联型容器迭代器失效:删除当前的迭代器,仅仅会使当前的迭代器失效,只要erase时,递增当前迭代器即可。
77. 编译器是如何实现函数重载的?
函数名+参数类型+返回值 作唯一符号,调用对应函数时会加上对应符号修饰
核心原理:
C++ 通过 “名字修饰(Name Mangling)” 来支持函数重载。
- C语言:
- 符号表里函数只按名字区分。
- 所以 C 语言不支持函数重载(同名就冲突)。
- C++:
- 编译器会把函数名 + 参数类型 + 返回值等信息编码成一个唯一的符号(名字修饰)。
- 比如:
void func(int); // 可能修饰成 _Z4funci void func(double); // 可能修饰成 _Z4funcd - 这样即使名字相同,也能在符号表中区分。
结论:
- C++ 编译器用 名字修饰 技术解决了函数重载的歧义问题。
- 链接时不同的重载函数就是不同的符号,不会冲突。
👌 你的答案已经很全面了,我帮你优化精简一下,更适合面试时作答:
78. 什么是函数调用约定?
定义:
函数调用约定规定了 函数参数的传递方式(顺序、寄存器/栈)、堆栈的清理责任(调用方还是被调用方)、以及 符号修饰规则。
常见调用约定:
- __cdecl(默认)
- 参数:右到左入栈
- 清栈:调用者清理(可变参数函数能用)
- 修饰名:函数名前加下划线
_func
- __stdcall
- 参数:右到左入栈
- 清栈:被调用函数清理
- 修饰名:
_func@参数字节数 - Windows API 常用
- __fastcall
- 参数:前两个用寄存器(ECX、EDX),其余右到左入栈
- 清栈:被调用函数清理
- 修饰名:
@func@参数字节数
- __thiscall(C++ 成员函数缺省)
- 参数:右到左入栈
- this 指针:通过寄存器 ECX 传递
- 清栈:参数个数确定 → 被调函数清理;不确定 → 调用者清理
- __pascal(已废弃)
- 类似 __stdcall,参数从左到右入栈
一句话总结:
调用约定就是规定 参数怎么传、栈谁清理。
__cdecl:调用者清理(可变参数)。__stdcall:被调者清理(WinAPI)。__fastcall:寄存器优先。__thiscall:类成员函数专用。
79. 使用条件变量时需要注意什么?
条件变量必须配合互斥锁使用,并在循环里判断条件,避免信号丢失和虚假唤醒
- 信号丢失问题
- 如果
signal()先于wait(),后续的wait()可能永远阻塞。 - ✅ 解决:
wait必须和“条件检查”一起使用(通常用while循环判断条件)。
- 如果
- 条件检查必须受锁保护
wait()内部会先解锁,再阻塞,唤醒后再加锁。- 因此条件判断和
wait()要放在 同一把互斥锁 的保护下,避免竞态。
- 使用 while 而不是 if
- 被唤醒后要重新检查条件,防止虚假唤醒(spurious wakeup)。
✅ 一句话记忆:
条件变量必须配合互斥锁使用,并在循环里判断条件,避免信号丢失和虚假唤醒。
简单代码示例(生产者-消费者模型)
#include<iostream>#include<thread>#include<mutex>#include<condition_variable>#include<queue> std::queue<int> q; std::mutex mtx; std::condition_variable cv;voidproducer(){for(int i =1; i <=5; i++){{ std::lock_guard<std::mutex>lock(mtx); q.push(i); std::cout <<"生产: "<< i << std::endl;} cv.notify_one();// 通知消费者 std::this_thread::sleep_for(std::chrono::milliseconds(300));}}voidconsumer(){for(int i =1; i <=5; i++){ std::unique_lock<std::mutex>lock(mtx);//在循环里面判断条件 cv.wait(lock,[]{return!q.empty();});// 等待直到队列非空int val = q.front(); q.pop(); std::cout <<"消费: "<< val << std::endl;}}intmain(){ std::thread t1(producer); std::thread t2(consumer); t1.join(); t2.join();}这段代码演示了条件变量的正确用法:
- 消费者用
cv.wait(lock, condition),避免信号丢失/虚假唤醒。 - 生产者每次生产数据后
notify_one(),消费者立刻被唤醒去消费。
80. 类内普通成员函数可以调用类内静态变量吗?类内静态成员函数可以访问类内普通变量吗?
普通函数能访问静态变量,静态函数不能直接访问普通变量,但能通过对象间接访问。
回答
- ✅ 普通成员函数可以访问类内静态变量:因为静态变量在类加载时就已经分配好内存,与具体对象无关。
- ❌ 静态成员函数不能直接访问普通成员变量:因为它没有
this指针,不知道该访问哪个对象的普通成员。但可以通过传入对象引用或指针来间接访问。
好👌,我在保持简单代码示例的基础上,给每个例子加一两句文字解释,面试时说起来更自然。
81. C++ 强制类型转换有哪些?
static_cast, const_cast, reinterpret_cast, dynamic_cast
1) static_cast
double d =3.14;int i =static_cast<int>(d);// 编译期转换,结果 i=3structB{virtual~B()=default;};structD:B{}; D dobj; B* pb =static_cast<B*>(&dobj);// 派生→基类安全👉 编译期完成的常规转换,常用于基本类型转换、类层次的上行转换。
2) const_cast
constint a =10;constint* pa =&a;int* pb =const_cast<int*>(pa);// 去掉 const/volate//*pb = 20; // ⚠️ 对真正的常量修改=未定义行为👉 用来去掉指针或引用的常量性,但对象本身必须是可写的,否则修改会出错。
3) reinterpret_cast
int x =100;int* p =&x; uintptr_t addr =reinterpret_cast<uintptr_t>(p);// 指针→整数int* q =reinterpret_cast<int*>(addr);// 整数→指针👉 只是重解释比特位,不改变内容。常见于底层编程(比如指针转整数),但风险很大。
4) dynamic_cast
structBase{virtual~Base()=default;};structDer:Base{voidhi(){}}; Base* pb =new Der;if(Der* pd =dynamic_cast<Der*>(pb)){// 成功:pd 非空 pd->hi();// 安全调用}👉 运行时检查,基类必须有虚函数。上行安全,下行转换时如果类型不对会返回 nullptr(或抛异常)。
✅ 总结一句话
static_cast:编译期常规转换。const_cast:去掉 const/volatile。reinterpret_cast:底层重解释,风险大。dynamic_cast:运行时检查,类层次安全转换。
82. 回调函数是什么?为什么要有回调函数?优缺点?本质是什么?
函数作参数传给别人,提高解耦性,调用方不用关心被调用方的情况,过多会导致逻辑复杂
定义:
回调函数就是把一个函数的地址(或函数对象)作为参数传给另一个函数或类,当特定事件发生时,由对方“反过来调用”你写的函数。
👉 本质就是 “把函数当作参数”。
为什么要有回调函数?
- 解耦:调用方不需要关心事件发生后该干什么,只负责“触发”;逻辑由用户通过回调决定。
- 灵活性:用户可以定制不同的逻辑,框架/库只需调用即可。
- 异步/事件驱动:常见于 GUI、网络编程、信号处理。
优点:
- 解耦调用方和实现方,让代码更灵活。
- 可复用:不同回调实现对应不同业务逻辑。
- 常用于异步场景,隐藏耗时操作,提升并发能力。
缺点: - 回调函数过多会让逻辑分散,代码难以维护。
- 如果涉及共享资源,容易产生竞争问题。
- 滥用可能导致“回调地狱”,代码可读性差。
本质:
回调就是 函数指针或函数对象作为参数,把“做什么”的决定权交给调用者,而触发时机交给被调用方。
简单代码示例
#include<iostream>#include<functional>usingnamespace std;classButton{public:voidsetOnClick(function<void()> cb){ callback = cb;}voidclick(){ cout <<"Button clicked!\n";if(callback)callback();// 回调用户逻辑}private: function<void()> callback;};voidmyHandler(){ cout <<"Handle button click!\n";}intmain(){ Button btn; btn.setOnClick(myHandler);// 设置回调 btn.click();// 触发事件,自动调用回调return0;}输出:
Button clicked! Handle button click! ✅ 一句话总结:
回调就是“我把函数交给你保管,你在合适的时机帮我调”,常用于事件驱动和异步编程。
83.Linux中的信号有哪些?
SIGINT:终端中断符,默认动作:终止。当用户按中断键(Ctrl+C)时,终端驱动程序产生此信号并发送至前台进程组中的每一个进程,当一个进程在运行时失控,特别是在终端输出大量信息时,常用此信号终止它。
SIGQUIT:终端退出符,默认动作:终止+core。当用户在终端按退出键(Ctrl+\)时,终端驱动程序产生此信号,并发送给前台进程中所有进程,此信号不仅终止前台进程组,同时产生一个core文件。
SIGILL:非法硬件指令,默认动作:终止+core。此信号表示进程已执行一条非法硬件指令
SIGRAP:硬件故障,默认动作:终止+core。指示一个实现定义的硬件故障
SIGBUG:硬件故障,默认动作:终止+core。指示一个实现定义的硬件故障,当出现某些类型的内存故障时,常产生此信号。
SIGKILL:终止,默认动作:终止。这是两个不能被捕捉或忽略的信号之一,它向系统管理员提供一个可以杀死任一进程的可靠方法
SIGSEGV:无效的内存引用,默认动作:终止+core。指示进程进行了一次无效的内存引用,通常说明程序有错,比如 访问了一个未经初始化的指针。
SIGALRM:定时器超时,默认动作:终止。如果在管道的读进程终止时写管道,则产生此信号,当类型为SOCK_STREAM的套接字已不再连接时,进程写该套接字也产生此信号。
SIGTERM:终止,默认动作:终止。这是由kill命令发出的系统默认终止信号,由于该信号是由应用程序捕获的,所以使用SIGTERM也让程序有机会在退出之前做好清理工作,与SIGKILL不同的是,SIGKILL不能捕捉。
SIGCONT:使暂停进程继续,默认动作:忽略。此进程发送给需要运行但是目前状态是暂停的进程,如果接收到此信号的进程处于暂停状态则继续运行,否则忽略。
SIGURG:紧急情况,默认动作:忽略。通知进程发生一个紧急情况,在网络上街到带外的数据时,可以选择产生此信号
SIGPOLL:可轮询事件,默认动作:终止。产生条件当一个可轮询设备上发生一个特定事件时产生
SIGIO:异步IO,默认动作:终止。产生异步IO时产生
还有很多就不全部放进来了,全部链接:Linux常见的31种信号量_linux 信号量编号大全-ZEEKLOG博客
84. 什么是尾递归?
尾部调用递归函数,返回的事自身,编译期检测出来后,并不会新开栈帧而服用当前栈帧
尾递归是一种特殊的递归形式,函数在最后一步直接调用自身,并且递归调用的返回值就是整个函数的返回值。
原理:编译器发现这是尾递归时,会做尾调用优化(TCO),直接复用当前栈帧,而不是新开栈帧,因此不会造成栈溢出,空间复杂度可以降到 O(1)。
特点:
- 递归调用出现在函数的最后一步(尾部)。
- 没有额外的计算需要在递归调用返回后继续进行。
- 可优化为迭代,避免栈空间消耗。
✅ 一句话记忆:
尾递归就是在函数最后一步调用自身,编译器可优化为循环,从而避免栈溢出。
85. 为什么会有栈溢出?为什么栈要设置容量?
栈是连续的有限内存,局部变量过大/递归过深导致栈溢出。 为了内存管理效率,多线程支持
原因:
- 栈溢出
- 栈是用来存放局部变量、函数参数、返回地址等的,空间有限。
- 如果函数递归过深(函数栈帧过多未释放),或局部变量太大(如定义超大数组),都会超过栈的容量,导致栈溢出(stack overflow)。
- 为什么栈要设置容量
- 栈空间需要连续分配,不能无限增长,否则会破坏内存布局。
- 多线程环境下,每个线程都要分配独立的栈空间,如果栈无限大,会导致线程数受限,内存管理困难。
- 因此操作系统会为每个线程设置一个合理的默认栈大小(如 Linux 默认 8MB)。
✅ 一句话总结:
栈是连续的有限内存,如果局部变量太大或递归过深就会溢出;限制栈大小是为了内存管理效率和多线程支持。
86.二叉树和平衡二叉树的区别
二叉树没有平衡因子的限制,而平衡二叉树有。
二叉树可能退化为链表,而平衡二叉树不会。
87.平衡二叉树的优缺点
优点:避免了二叉排序树可能出现最极端情况(退化为链表),其平均查找的时间复杂度为logN
缺点:对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。
88. 什么是 this 指针?它存在哪里?
非静态成员函数会有一个指向调用对象的隐式指针this。 函数调用时存放在函数栈帧中
1. 定义
this是编译器在 非静态成员函数 中隐式添加的一个隐藏参数,指向当前调用该函数的对象。- 静态成员函数没有
this,因为它不依赖具体对象。
2. 存放位置 this不存储在对象内部,对象的内存只包含成员变量。this存在于 函数调用栈的参数区,由编译器在调用成员函数时自动传入。
3. 工作原理
编译器会把:
p.setAge(20);转化为:
Person::setAge(&p,20);// 把对象地址作为 this 传入代码示例
#include<iostream>usingnamespace std;classPerson{public:int age;voidsetAge(int a){this->age = a;// 编译器实际会写成 this->age}voidshowThis(){ cout <<"this = "<<this<< endl;}};intmain(){ Person p1, p2; p1.setAge(18); p2.setAge(25); cout <<"sizeof(Person) = "<<sizeof(Person)<< endl; p1.showThis();// 打印 p1 的地址 p2.showThis();// 打印 p2 的地址}输出示例:
sizeof(Person) = 4 this = 0x7ffeea42e9f8 // &p1 this = 0x7ffeea42e9f4 // &p2 ✅ 一句话总结this 指针不在对象里,而是函数调用时由编译器传入,存放在调用栈,作用是让成员函数知道当前操作的是哪个对象。
89.什么是重载、重写、隐藏?
重载:函数名相同,函数参数不同,两个函数在同一作用域
重写:两个函数分别在子类和父类中,函数名,返回值,参数均相同,函数必须为虚函数
隐藏:在继承关系中,子类实现了一个和父类名字名字一样的函数。这样子类的函数就把父类的同名函数隐藏了。隐藏只与函数名有关。
90.静态成员函数可以是虚函数吗?为什么?
它不属于类中的任何一个对象或示例,属于类共有的一个函数,不依赖于对象调用,静态成员函数没有this指针,无法放进虚函数表。
91.构造函数可以为虚函数吗?为什么?
虚表指针是存储在对象的内存空间,当调虚函数时,是通过虚表指针指向的虚表里的函数地址进行调用的。如果将构造函数定义为虚函数,就要通过虚表指针指向的虚表的构造函数地址来调用。而构造函数是实例化对象,定义为虚函数后,对象空间还没有实例化,那就没有虚表指针,自然无法调用构造函数,那构造函数就失去意义,所以不能将构造函数定义为虚函数。
好的,我帮你把这题整理得更简洁、专业一些(适合面试时回答):
92. make_shared 的优缺点
优点:高效一次性分配好内存 缺点:可能会延长生命周期高效:
优点
- 一次分配内存:对象和控制块一起分配,减少了内存分配次数(效率高于
shared_ptr<T>(new T(...)))。 - 异常安全:避免
new后在构造过程中抛异常导致内存泄漏。 - 代码简洁:不用显式写
new,可读性更好。
缺点 - 构造限制:不能用于构造函数是
private/protected的类(因为外部无法调用)。 - 延长生命周期:即使所有
shared_ptr都释放了,只要还有weak_ptr存在,控制块不会释放,而make_shared把对象和控制块绑在一起,就导致对象内存也无法及时释放。
✅ 一句话总结:make_shared 高效又安全,但在需要精细控制对象释放时(比如内存敏感场景),要小心 weak_ptr 延长生命周期的问题。
这个回答已经很全面了 👍,我帮你梳理优化一下,让它更精炼、更符合面试口吻:
93. 函数调用过程(大致步骤)
- 参数入栈
- 按照从右到左的顺序压栈(取决于调用约定)。
- 如果参数是对象,先进行拷贝构造。
- 保存返回地址
- 编译器会把调用点的下一条指令地址压栈,函数结束后能跳回。
- 建立新栈帧
- 保存调用者的栈帧指针(FP/EBP)。
- 更新栈指针(SP/ESP/RSP)。
- 保存必要寄存器
- 某些通用寄存器(如 EAX, EBX, ECX…)需要保护,避免覆盖调用者的数据。
- 执行函数体
- 函数返回准备
- 恢复被保存的寄存器值。
- 恢复调用者的栈帧指针。
- 弹出返回地址,跳回调用点
- 回收参数空间
- 调用约定决定由调用者还是被调用者清理参数栈空间。
✅ 一句话记忆:
函数调用就是压参数 → 保存现场 → 建栈帧 → 执行 → 恢复现场 → 回收参数。
94.红黑树定义和性质
红黑树是一种自平衡二叉查找树,满足5个性质:
- 节点非黑即红
- 根节点为黑色
- 叶子节点是黑色
- 如果一个节点为红色,子节点必须为黑色(红红不相邻)
- 从任意节点到其所有的叶子节点的路径上,黑色节点的数量相同
95. 什么是 RVO 和 NRVO?它们的区别是什么?
答题思路:
- 区别:
- RVO 返回匿名临时对象,NRVO 返回具名对象。
- 两者效果一样:避免拷贝/移动构造。
- C++17 之后:RVO/NRVO 优化几乎强制,不依赖编译器开关。
NRVO:Named Return Value Optimization,返回具名局部变量时的优化。当NRVO失效的时候,会调用移动构造转移资源,没有的话调用拷贝构造
MyClass create(){ MyClass obj;return obj;}// 具名对象 → NRVORVO:Return Value Optimization,编译器优化返回临时对象时,直接在调用者作用域构造,避免拷贝/移动。
MyClass create(){returnMyClass();}// 匿名对象 → RVO96. 在什么情况下 RVO/NRVO 会失效?
答题思路:
- 如果返回的不是函数内的局部对象,优化可能失效。比如:
MyClass global; MyClass&create(){return global;}// 返回引用 → 没有 RVO MyClass create(bool flag){ MyClass a, b;return flag ? a : b;// 不能保证是哪一个对象,NRVO 失效}- 在这些情况下,编译器不能确定返回值构造的位置,可能会退化为调用 拷贝构造或移动构造。
97. 类中的默认函数(特殊成员函数)有哪些?
- *默认构造函数
- 形式:
Class(); - 如果类里没有定义任何构造函数,编译器会自动生成一个 空参数的构造函数。
- 形式:
- 析构函数
- 形式:
~Class(); - 如果用户没有定义,编译器会生成一个默认的析构函数(负责清理成员对象)。
- 形式:
- 拷贝构造函数
- 形式:
Class(const Class&); - 如果用户没写,编译器会生成一个,把每个成员逐一拷贝(浅拷贝)。
- 形式:
- 拷贝赋值运算符
- 形式:
Class& operator=(const Class&); - 编译器生成的版本同样是浅拷贝。
- 形式:
- 移动构造函数(C++11 起)
- 形式:
Class(Class&&); - 条件:当类里 没有用户定义的拷贝构造/拷贝赋值/析构函数 时,编译器才会生成。
- 形式:
- 移动赋值运算符(C++11 起)
- 形式:
Class& operator=(Class&&); - 条件和移动构造类似。
- 形式:
98. 什么是段错误?常见原因有哪些?
访问受限区域;空/野,UAF,栈溢出,数组/指针越界,访问只读
定义:段错误(Segmentation Fault)是程序访问了不该访问的内存区域导致操作系统发出 SIGSEGV 信号.
常见原因有:
- 访问空指针或野指针;
- 数组/指针越界
- 释放后继续使用内存(Use After Free);
- 栈溢出,比如递归过深;
- 访问只读内存,比如写字符串常量。
99. 什么是内存泄漏?常见场景有哪些?
(堆,不释放;new/malloc, shared_ptr, delete,return)
定义:内存泄漏就是程序申请了堆内存但没有释放,导致内存长期被占用。
常见场景:
new/malloc后忘记delete/free;delete掉对象,但忘记释放里面的动态成员;- 循环引用,比如
shared_ptr相互持有; - 提前
return或异常抛出,跳过了释放逻辑。
100. 如何检测和定位内存泄漏?
- Windows 下可用 CRT 的
_CrtDumpMemoryLeaks() - 加日志统计
malloc/new和free/delete次数是否匹配;
101. 如何避免内存泄漏?
RAII; STL, weak_ptr
核心思想是 RAII:资源获取即初始化。
- 用智能指针替代裸指针;
- 用容器(
vector、string)代替手工分配数组; - 避免循环引用,必要时用
weak_ptr; - 异常安全:构造函数里申请的资源用成员对象包裹,确保析构时自动释放。