C++后端面试八股文
一、C++ 语言基础与底层原理
请解释 new
/ delete
和 malloc
/ free
的区别和联系,以及使用它们时需要注意什么
|
简述 C++ 的内存分区(栈、堆、全局/静态存储区、常量存储区、代码区)以及对象在这些区域的创建过程。
栈区:自动分配/释放(函数调用时创建,返回时销毁) 堆区:手动分配/释放 全局/静态存储区:程序启动时分配,程序结束时释放 常量存储区:只读内存,生命周期与程序相同 代码区:只读内存 |
移动语义 (std::move
) 和完美转发 (std::forward
) 解决了什么问题?它们的实现原理(引用折叠)是什么?在什么情况下使用?(RValue Reference, Universal Reference)
移动语义:允许资源所有权转移(而非复制),显著提升资源管理效率 完美转发:模板函数参数转发时丢失值类别(左值/右值)和 CV 属性 保持参数的原始值类别(左值/右值)和类型属性(const/volatile) |
面向对象:
详细解释 C++ 中的虚函数机制(vptr, vtable)。
虚函数表:编译器为每个包含虚函数的类生成的静态函数指针数组 vptr:编译器自动添加到每个对象实例中的隐藏指针 |
多重继承下菱形继承(钻石问题)是如何产生的?C++ 如何通过虚继承来解决它?(涉及 virtual base class pointer, vtordisp 等,至少讲清概念和原理)
A
A 使用虚继承 class A { /* ... */ }; // Base class class B : public virtual A { /* ... */ }; // Virtual inheritance from A class D : public B, public C { /* ... */ }; // Regular multiple inheritance 虚继承如何解决菱形继承问题:
|
面向对象的三大特性(封装、继承、多态)在 C++ 中是如何体现的?
核心机制:通过 类型支持:
实现基础:虚函数表(vtable)+ 动态绑定 |
并发与同步:
解 std::thread
, std::mutex
, std::lock_guard
, std::unique_lock
, std::condition_variable
的用法和区别。
thread:用于创建和管理新的执行线程,代表一个执行单元 包含以下成员函数 join():阻塞调用函数,通常为主线程,等待*this线程执行结束 detach():分离*this线程,与thread对象解耦,允许其在后台允许 get_id() 获取线程标识符 mutex:提供基本的独占互斥所有权语义,防止多个线程访问共享数据,避免数据竞争 lock():尝试获取互斥锁,如果互-互斥锁已经被其它线程持有,则阻塞 unlock():释放互斥锁 try_lock():尝试获取互斥锁,如果锁不可用立即返回 lock_guard:自动管理mutex的锁定和解锁:构造时自动lock关联的互斥量,析构时自动unlock unique_lock:相较于lock_guard更将灵活,且增加了更多的特性 支持延迟上锁 支持尝试锁定 支持多次锁定和解锁 支持条件变量:condition_variable 可移动但不可复制 下面给出unique_lock使用的一个示例
|
std::mutex mtx;
std::condition_variable cv;
bool data_ready = false;// 生产者线程
void producer() {{std::unique_lock<std::mutex> lock(mtx); // 上锁// ...生产数据...data_ready = true;} // unique_lock 这里可能解锁,更晚些也行cv.notify_one(); // 通知消费者
}// 消费者线程
void consumer() {std::unique_lock<std::mutex> lock(mtx); // 构造时上锁// 关键:wait 会在内部原子地解锁 mtx 并使线程阻塞等待通知// 收到通知后(可能虚假唤醒),wait 会重新尝试获取锁cv.wait(lock, []{ return data_ready; }); // 直到 data_ready 为真// 此时 lock 持有锁,数据安全可用// ...消费数据...data_ready = false;
} // 解锁
std::atomic
解决了什么问题?它相比直接使用锁的优势和局限在哪里?
解决了无锁或低锁并发编程中的核心问题,安全,高效的在多线程环境下访问和修改共享数据,而无需每次都使用重量级别的互斥锁 相较于直接使用锁:高频轻量操作,无阻塞操作,更少的缓存行竞争 局限于:使用场所有限,存在ABA问题,而且由于较弱的内存序可能会导致非原子变量的读写操作被CPU重新排序到错误的位置,破坏程序逻辑 |
简述无锁编程(Lock-Free)的思想以及 CAS 操作。无锁队列的基本实现思路?
无锁编程是一种并发编程范式,其核心目标是设计在多线程环境下无需使用传统互斥锁(如 无锁算法的关键特征是:即使某些线程被任意延迟(如被操作系统挂起、发生页错误),也至少有一个线程能够取得进展(完成操作)。 乐观并发控制 依赖硬件原子指令 提高并发性和可伸缩性 CAS思想伪代码描述如下 bool compare_and_swap(T* ptr, T expected, T new_value) { 无锁队列 struct Node { class LockFreeQueue { |
二、 操作系统与网络
操作系统原理:
Linux 进程间通信(IPC)有哪些主要方式?对比它们的优缺点和适用场景。
管道:最简单的IPC形式,在具有亲缘关系(通常是父子或兄弟) 的进程间创建单工(半双工)字节流通道。数据写入管道的写入端,从读取端顺序读取(FIFO)。 命名管道:突破管道必须具有亲缘关系的限制,允许任何进程(甚至无亲缘关系) 通过打开这个“文件”名进行通信。遵循FIFO原则。 消息队列:在内核中维护的消息链表。进程可以向队列添加结构化的消息(有类型和负载数据)或从队列中读取特定类型的消息。消息具有优先级(POSIX)或类型(SysV)。 共享内存:速度最快的IPC方式!内核将同一块物理内存映射到多个进程各自的用户空间地址范围。进程可以直接读写这块内存,就像访问自己的内存一样,无需内核介入拷贝。 信号量:它是一个用于同步多个进程(或线程)对共享资源(如共享内存区域、文件、硬件设备)访问的计数器。基本操作是PV操作(wait/P - 申请资源减小计数,signal/V - 释放资源增加计数)。 socket:最强大、最通用的IPC/RPC机制。支持不同主机(网络IPC)或同一主机(Unix Domain Socket)上进程间通信。 |
Linux 中文件描述符(File Descriptor)的本质是什么?select/poll/epoll I/O 多路复用的区别和各自的优势?epoll 的水平触发(LT)和边沿触发(ET)模式区别?
Linux中的文件描述符不是文件本身,也不是指向文件内容的指针,其为一个非负整数,代表一个进程级别打开文件表的条目索引,每个进程都存在一个打开文件表,当进程打开一个文件的时候,内核会在这个表中创建一个条目,这个表中存在指向系统级别打开文件表的指针,文件的访问模式等,同时存在一个系统打开文件表,每个条目代表一个真正被打开的文件实例,时间上,这个文件描述符就是已打开IO资源的句柄 select:存在数量限制:1024 || 这里叙述一下这个的流程,调用select,内核扫描用户传入fd_set中的所有fd,检查它们的状态,随后当有fd超时或者就绪,标记,全部标记完返回,用户再次扫描 poll:长度可变化,事件分离 epoll:内核使用红黑树组织列表,使用双向链表管理就绪列表,内核使用回调极值仅在fd状态发生变化的时候将其加入就绪列表,返回值只拷贝就绪的事件信息,使用一个单独的函数添加修改删除内核需要剪视的fd,使得每次不需要传入所有需要监视的fd,监视列表在内核中维护 水平触发:只要fd缓冲区处于就绪状态,就会持续报告这个事件 边缘触发:只在fd缓冲区状态发生改变的时候,才会进行报告 |
用户态和内核态切换的开销在哪里?
首先介绍一下用户态和内核态 用户态:应用程序运行的环境,CPU在这个状态指向的指令受限,不能访问特权硬件资源 内核态:操作系统内核运行的环境,CPU在这个状态下拥有最高权限 开销主要来源于以下几个方面 硬件上下文保存和恢复==(通用寄存器,指令指针,栈指针,状态寄存器) CPU流水线,缓存以及分支预测失效==流水线刷新,TLB失效,Cache污染(内核代码将热点数据挤出缓存),分支预测干扰 |
进程的虚拟地址空间布局是怎样的?
现代操作系统为每个进程提供一个私有的,连续的,独立的虚拟地址空间 一般从高地址到低地址部分为 内核空间 操作系统内核的代码、数据和每个进程的内核栈(处理系统调用/中断)、页表等关键数据结构。注意,所有进程共享同一份内核空间映射 用户栈 内存映射区 将文件内容映射到进程地址空间。这是 用户堆 未初始化数据段 .bss 存储全局/静态变量 已初始化数据段 .data 显式初始化的全局变量和静态变量 代码段 .text |
网络协议:
详细描述一次完整的 HTTP 请求过程(从输入 URL 到浏览器显示,涉及 DNS、TCP 握手、HTTP 请求/响应、TLS 握手等)。
阶段0: 用户输入URL 浏览器解析URL,提取关键信息,包括协议,主机名,端口号,未指定使用默认端口,路径等等 检查缓存,浏览器检查内置HSTS,没有,进入下一步 阶段1: 本地缓存未命中 操作系统进行DNS查询,操作系统查询本地的DNS缓存,没有 将DNS请求发送配置的DNS递归解析器,如果其本地还没有 迭代查询:顺次查询根域名服务器,顶级域名服务器,权威域名服务器,得到对应IP地址 返回给操作系统,缓存 阶段2:建立传输连接-TCP握手 阶段3:如果使用的时https协议:进行TLS握手,流程写在了下面 阶段4:客户端构造HTTP请求,发送给服务端,服务端收到,并返回 阶段5:浏览器渲染 |
TLS握手流程:
1.TLS 第一次握手:客户端向服务器发起加密通信请求,发送 ClientHello 消息。
客户端主要发送以下信息:
支持的 TLS 协议版本,如 TLS 1.2。
客户端生成的随机数(Client Random),用于生成「会话密钥」的一部分。
客户端支持的密码套件列表,如 RSA 加密算法。
2.TLS 第二次握手:服务器收到客户端请求后,向客户端发送 ServerHello 消息作为响应。
服务器回应的内容包括:
确认的 TLS 协议版本,如果浏览器不支持,则关闭加密通信。
服务器生成的随机数(Server Random),用于生成「会话密钥」的一部分。
确认的密码套件列表,如 RSA 加密算法。
服务器的数字证书(Certificate)。
3.TLS 第三次握手:客户端收到服务器的响应后,首先通过 CA 公钥确认服务器的数字证书的真实性。
如果证书验证通过,客户端从数字证书中取出服务器的公钥,并用该公钥加密报文,向服务器发送以下信息:
一个随机数(pre-master key),该随机数将被服务器的私钥解密。
加密通信算法改变通知,表示随后的信息都将使用「会话密钥」加密通信。
客户端握手结束通知,表示客户端的握手阶段已经结束。
4.TLS 第四次握手:服务器收到客户端的第三次握手消息后,通过协商的加密算法,计算出本次通信的「会话密钥」。
然后,服务器向客户端发送以下信息:
加密通信算法改变通知,表示随后的信息都将使用「会话密钥」加密通信。
服务器握手结束通知,表示服务器的握手阶段已经结束。
客户端和服务器的握手阶段全部结束,建立安全连接,可以开始进行加密通信。
TLS1.3改进了上述行为
首先是直接发送客户端公钥
然后服务端恢复证书签名和自己的公钥
这样双方就都持有各自的公钥可以进行加密了
TCP 和 UDP 的核心区别是什么?TCP 如何保证可靠传输(序列号、确认应答、超时重传、流量控制、拥塞控制)?
面向连接和无连接 可靠性和尽力而为 TCP传递字节流,UDP传递消息/数据报 TCP有流量控制,拥塞控制,超时重传,确认应答,序列号等复杂机制,UDP没有 |
解释 TCP 的三次握手和四次挥手过程,为什么是三次不是两次?为什么挥手需要四次?TIME_WAIT 状态的作用是什么?
对于为什么是三次握手: 两次握手情况下:我们假设这样一种场景,由于网络阻塞,客户端发送的一个请求连接没有到达,客户端发送了第二个连接请求,此时第一个请求到达,服务器返回,并且认为自己同客户端建立了连接,但是这个回复到达客户端的时候,由于客户端已经发送了新的连接就会丢弃,然后如果第二个请求到达,服务器以为这个是新的连接,就再次返回,此时客户端收到后建立连接,但是这样就造成服务端多了一个连接,所以我们采用三次握手,如果客户端丢弃了这个回复,那么服务端就会因为超时释放那个连接 同时第三次握手使得服务端知道客户端同步了自己的序号 对于为什么是四次挥手:这个理由就很多了 全双工需要双向独立关闭 被动关闭方需要处理遗留数据 主动关闭方需要确认对方的 FIN主动关闭方需要确认对方的 FIN
可靠地终止最后一个 ACK:确保被动关闭方能够成功收到第四次挥手的ACK 消除旧连接报文干扰:使得此时网络中属于这个连接的报文全部过期 |
三、数据库与存储
数据库:
数据库索引的原理(B+Tree 结构)?为什么索引能加快查询速度?聚簇索引和非聚簇索引的区别?
B+Tree是平衡多叉树 叶子结点都位于同一层,保证了任何查找操作从根结点到叶子的路径长度基本相等 内部结点仅存储索引键值,不存储真实数据,这样使得每个块可以存储更多的索引结点,从而减少了IO次数 叶子节点为双向链表,可以实现顺序查找 索引加速查询的核心在于可以减少需要扫描的数据量以及利用数据的有序性 1. 避免了全表扫描 2. 利用有序性高效的定位 3. 减少了磁盘IO次数 聚簇索引(InnoDB)数据和索引一起存放 非聚簇索引(MyISAM)数据和索引单独存放,需要进行回表,回表是随机IO,性能会急剧下降 |
数据库事务的 ACID 特性分别是什么含义?数据库是如何通过日志(redo log, undo log)和锁(共享锁、排他锁)来实现事务的?
A:原子性:事务中的所有操作要么全部成功,要么失败回滚 C:一致性:事务执行的结果必须使数据库从一个一致性状态到另一个一致性状态 I:隔离性:事务可以并发执行,但是一个事务的执行不能被其他事务干扰 D:持久性:一旦事务成功提交,其对数据库的修改就是永久了,不会因为断电崩溃而丢失 redo log:重做日志,保证事务的持久性,并在系统崩溃后用于恢复已经提交的事务,记录的内容是对数据页的物理修改 工作原理: 当事务修改数据的时候,首先发生在内存的Buffer Pool,同时数据库引擎会生成一条或者多条 redo log record,用来描述这个物理修改 在脏页(被修改但未写入磁盘的数据页)被刷新到磁盘数据文件之前,对于的redo log记录必须先被陷入并刷新到持久化的redo log文件中 事务提交的时候,数据库引擎会强制将所有与该事务相关的redo log刷新到磁盘中 提交操作本身通常也会记录一条特殊的redo log 崩溃恢复:系统崩溃重启后,数据库进入恢复阶段 1. 从redo log中读取文件记录 2. 从上次完成的Checkpoint开始进行提交 undo log:回滚日志,保证事务的原子性,用于事务回滚,支持隔离性与MVCC 记录的是事务修改数据之前的旧值和逻辑操作 工作原理 当修改内存的数据页,会先向undo log中写入一条记录,记录修改前的旧值以及如何撤销这个修改的信息,这条undo log也会被写到磁盘上的undo log文件 如果事务需要回滚,数据库引擎会根据该书屋对于的undo log记录,逆向执行操作 MVCC(多版本并发控制),当一个读操作开始的时候,数据库会记录当前系统版本号,数据库会利用undo log重建改行在事务开始时的旧版数据,提供给读操作,这样读操作就不会看到未提交修改 锁的区分有很多 1. 共享锁(读锁):允许事务读取数据项,其它事务也可以获取该数据项的共享锁 排他锁(写锁):允许事务读取和修改数据项,其余事务不允许获取任何类型的锁 2. 行级锁 页级锁 表级锁 3. 意向锁:表级锁,表示事务打算在表的更细粒度(页或行)上加锁。用于快速判断表级冲突 |
四、 数据结构与算法
编码能力:
逻辑题)海量数据相关:如何从100亿个URL中快速找出重复出现的URL?(分治、哈希、布隆过滤器)
方法1:哈希+分冶 设计一个哈希函数将每个URL映射到一个哈希值,根据哈希值将URL分配到多个小文件中,这样相同的URL一定会被分配到同一个文件中,使得每个小文件的大小不超过内存限制 然后进行逐个文件处理 方法2:布隆过滤器 我们可以用布隆过滤器作为第一层过滤,然后对候选的URL再进行精确统计。 |