1. C++ 基础
1.1 指针和引用的区别
- 区别:
| 特性 | 指针(T*) |
引用(T&) |
|---|---|---|
| 本质 | 变量,存储地址 | 别名(原变量的另一个名字) |
| 初始化 | 可不初始化(但危险) | 必须初始化,且绑定后不可变 |
| 可空性 | 可为 nullptr |
不能为 null,必须绑定有效对象 |
| 重新赋值 | 可指向不同对象 | 不能重新绑定(始终指向初始化时的对象) |
| 取地址 | &ptr 是指针变量的地址 |
&ref 是被引用对象的地址 |
sizeof |
通常是 8 字节(64 位系统) | 等于被引用对象的大小 |
| 多级 | 支持(int**, int***) |
不支持(无"引用的引用") |
| 算术运算 | 支持(ptr++, ptr + n) |
不支持 |
- 使用建议:
| 场景 | 推荐 |
|---|---|
| 需要可选/可空语义 | 指针(如 std::optional 出现前) |
| 函数参数修改原值 | 引用(更安全、简洁) |
| 实现数据结构(链表、树) | 指针(需动态指向) |
| 避免拷贝大对象 | const 引用(const T&) |
| 与 C API 交互 | 指针(C 无引用) |
选择原则:优先用引用,必要时用指针。
1.2 函数重载和运算符重载
| 特性 | 函数重载 | 运算符重载 |
|---|---|---|
| 目的 | 提供同名但参数不同的多个函数 | 为自定义类型赋予运算符语义(如 a + b) |
| 名称 | 任意合法函数名(如 print, add) |
固定为 operator@(如 operator+, operator[]) |
| 参数数量 | 任意(包括 0) | 受运算符本身限制: • 一元运算符:1 个参数 • 二元运算符:2 个参数(成员函数时隐含 this) |
| 可重载的符号 | 无限制(自定义名) | 仅限 C++ 内置运算符(不能创建新运算符,如 **) |
| 返回类型 | 可不同(但不能仅靠返回类型区分重载) | 通常需符合直觉(如 + 返回新对象,= 返回引用) |
| 调用方式 | 显式函数调用:func(a, b) |
自然表达式:a + b 或显式:a.operator+(b) |
| 必须为成员函数? | 否(可全局或成员) | 部分运算符必须为成员: • 赋值 = • 下标 [] • 函数调用 () • 成员访问 -> |
1.3 虚函数和纯虚函数
| 特性 | 虚函数(Virtual Function) | 纯虚函数(Pure Virtual Function) |
|---|---|---|
| 定义方式 | virtual 返回类型 func(参数); |
virtual 返回类型 func(参数) = 0; |
| 是否必须实现 | ✅ 必须提供实现(可在基类或派生类) | ❌ 基类中无实现(= 0 表示"纯") |
| 能否实例化基类 | ✅ 可以(基类是具体类) | ❌ 不能(基类成为抽象类) |
| 目的 | 允许派生类可选重写,提供默认行为 | 强制派生类必须重写,定义接口契约 |
| 类的性质 | 普通类(concrete class) | 抽象类(abstract class) |
| 典型用途 | 提供可扩展的默认实现 | 定义接口(Interface) |
1.4 拷贝构造函数与移动构造函数
| 特性 | 拷贝构造函数(Copy Constructor) | 移动构造函数(Move Constructor) |
|---|---|---|
| 定义形式 | T(const T& other) |
T(T&& other) |
| 参数类型 | 左值引用(const T&) |
右值引用(T&&) |
| 语义 | 深拷贝:创建新对象,原对象不变 | 资源转移:窃取原对象资源,原对象变为空/有效但未指定状态 |
| 触发时机 | 用已命名对象初始化新对象 (如 T a = b;) |
用临时对象/右值初始化新对象 (如 T a = func();) |
| 性能 | 可能昂贵(如复制大数组、文件) | 通常廉价(仅指针交换) |
| 是否修改源对象 | ❌ 不修改 | ✅ 修改(置为"空"状态) |
| 默认生成条件 | 若未定义,编译器自动生成(成员逐个拷贝) | 若未定义且满足条件,C++11+ 编译器自动生成 |
2. 内存管理
2.1 C++ 内存分区
| 分区 | 别名 | 存储内容 | 生命周期 | 分配方式 | 特点 |
|---|---|---|---|---|---|
| 栈(Stack) | 自动存储区 | 局部变量、函数参数、返回地址 | 函数作用域内 | 编译器自动分配/释放 | 快速、连续、大小有限(通常几 MB) |
| 堆(Heap) | 自由存储区 | 动态分配的对象(new/malloc) |
手动控制(直到 delete/free) |
程序员显式分配/释放 | 大小受限于虚拟内存,速度较慢,易内存泄漏 |
| 全局/静态区(Global/Static) | 静态存储区 | 全局变量、静态变量(static)、常量字符串 |
整个程序运行期 | 编译时分配,启动时初始化 | 初始化一次,程序结束时释放 |
| 常量区(Constant / Literal) | 字符串常量区 | 字符串字面量(如 "hello")、const 全局变量 |
程序运行期 | 编译时分配 | 只读,修改会导致未定义行为(如段错误) |
| 代码区(Text / Code Segment) | 程序区 | 可执行机器指令(函数体代码) | 程序运行期 | 编译时生成 | 只读,通常与常量区合并或相邻 |
2.2 智能指针的种类及区别
| 特性 | std::unique_ptr |
std::shared_ptr |
std::weak_ptr |
|---|---|---|---|
| 所有权模型 | 独占所有权(唯一拥有者) | 共享所有权(多个拥有者) | 无所有权(观察者) |
| 是否可复制 | ❌ 不可复制(可移动) | ✅ 可复制(引用计数 +1) | ✅ 可复制 |
| 线程安全 | 本身非线程安全(但单线程下安全) | 控制块线程安全(引用计数原子操作) | 同 shared_ptr |
| 内存开销 | 无额外开销(≈ 原生指针) | 额外控制块(引用计数 + deleter 等) | 与 shared_ptr 共享控制块 |
| 典型用途 | 替代 new/delete,RAII 资源管理 |
多个对象共享同一资源 | 解决 shared_ptr 的循环引用问题 |
| 删除器支持 | ✅ 支持自定义删除器(模板参数) | ✅ 支持自定义删除器(运行时存储) | 依赖关联的 shared_ptr |
| 转换关系 | 可转为 shared_ptr |
可构造 weak_ptr |
只能从 shared_ptr 构造 |
3. Linux 系统知识
3.1 进程与线程的区别
| 对比维度 | 进程(Process) | 线程(Thread) |
|---|---|---|
| 资源占用 | 拥有独立的地址空间(代码段、数据段、堆、栈)、文件描述符表、信号处理表等,资源占用多。 | 共享所属进程的地址空间、文件描述符表等资源,仅拥有独立的栈、程序计数器(PC)、寄存器集合,资源占用少。 |
| 调度开销 | 进程切换时需保存和恢复整个地址空间、寄存器等上下文,开销大(毫秒级)。 | 线程切换仅需保存和恢复栈、PC、寄存器,开销小(微秒级),调度更高效。 |
| 通信方式 | 需通过进程间通信(IPC)机制,如管道(pipe)、消息队列(message queue)、共享内存(shared memory)、信号量(semaphore)等,实现复杂,效率较低。 | 可直接访问共享内存(进程地址空间中的全局变量、静态变量),通信简单高效;也可使用线程同步机制(互斥锁、条件变量)避免竞争。 |
| 独立性 | 进程间相互独立,一个进程崩溃通常不会影响其他进程(除非通过共享资源间接影响)。 | 线程属于同一进程,一个线程崩溃可能导致整个进程崩溃(如内存越界、野指针访问)。 |
| PID 与 TID | 每个进程有唯一的 PID(进程 ID),由内核分配。 | 每个线程有唯一的 TID(线程 ID),同一进程内的线程 PID 相同,TID 不同(Linux 下线程本质是轻量级进程,LWP)。 |
3.2 进程通信
| IPC 方式 | 是否支持跨主机 | 数据流向 | 同步/异步 | 持久性 | 典型用途 | 优点 | 缺点 |
|---|---|---|---|---|---|---|---|
| 管道(Pipe) | ❌ 仅限父子/相关进程 | 单向(匿名)或双向(命名) | 阻塞(默认) | 匿名:临时 命名:文件系统存在 | 简单父子进程通信 | 简单、内核支持 | 匿名 pipe 仅限亲缘进程;缓冲区有限 |
| FIFO(命名管道) | ❌ 本地 | 单向 | 阻塞 | 是(文件系统路径) | 无关进程通信 | 可用于无亲缘关系进程 | 仍为单向,需额外同步 |
| 消息队列(Message Queue) | ❌ 本地 | 双向(多对多) | 可配置 | 是(内核持久) | 结构化数据传递 | 消息边界清晰、支持优先级 | 内核限制消息大小/数量 |
| 共享内存(Shared Memory) | ❌ 本地 | 双向(直接读写) | 异步 | 是(需显式删除) | 高性能大数据交换 | 速度最快(无需拷贝) | 需手动同步(如配合信号量) |
| 信号量(Semaphore) | ❌ 本地 | ------(用于同步) | ------ | 是 | 控制资源访问、进程同步 | 计数灵活,可实现互斥/同步 | 易死锁,不传数据 |
| 信号(Signal) | ❌ 本地 | 通知(无数据或少量) | 异步 | ------ | 异常通知、简单控制 | 开销极小 | 不可靠(可能丢失),不能传复杂数据 |
| 套接字(Socket) | ✅ 支持(TCP/UDP) | 双向 | 可配置 | 否 | 网络通信、本地高性能 IPC(Unix Domain Socket) | 通用、跨平台、跨主机 | 相对复杂,有协议开销 |
3.3 线程同步
| 同步机制 | 作用 | 是否阻塞 | 典型用途 | C++ 标准库支持 | 特点 |
|---|---|---|---|---|---|
| 互斥锁(Mutex) | 保证同一时间只有一个线程访问临界区 | ✅ 阻塞 | 保护共享数据 | std::mutex, std::lock_guard, std::unique_lock |
最基础、最常用;防止并发写/读写冲突 |
| 递归锁(Recursive Mutex) | 允许同一线程多次加锁 | ✅ 阻塞 | 可重入函数 | std::recursive_mutex |
避免死锁(但应尽量避免设计依赖递归锁) |
| 读写锁(Read-Write Lock) | 多个读线程可并发,写线程独占 | ✅ 阻塞 | 读多写少场景 | std::shared_mutex (C++17) |
提高读密集型性能 |
| 条件变量(Condition Variable) | 线程等待某个条件成立 | ✅ 阻塞 | 生产者-消费者、线程间通知 | std::condition_variable |
需配合 mutex 使用;避免忙等待 |
| 原子操作(Atomic) | 对单个变量的无锁操作 | ❌ 非阻塞 | 计数器、标志位 | std::atomic<T> |
高性能;仅适用于简单操作(如 ++, compare_exchange) |
| 信号量(Semaphore) | 控制同时访问资源的线程数量 | ✅ 阻塞 | 资源池、限流 | C++20 起:std::counting_semaphore |
可实现更灵活的资源控制(非 C++11~17 原生支持) |
| 屏障(Barrier) | 多个线程到达某点后才继续 | ✅ 阻塞 | 并行计算同步 | C++20 起:std::barrier |
用于阶段式并行任务 |
3.4 死锁及避免方法
3.4.1 死锁条件
| 条件 | 说明 |
|---|---|
| 1. 互斥条件(Mutual Exclusion) | 资源一次只能被一个线程占用(如 mutex、文件锁)。 |
| 2. 占有并等待(Hold and Wait) | 线程已持有至少一个资源,同时还在等待其他被占用的资源。 |
| 3. 不可抢占(No Preemption) | 已分配的资源不能被强制剥夺,只能由持有者主动释放。 |
| 4. 循环等待(Circular Wait) | 存在一个线程等待环:T₁ → T₂ → ... → Tₙ → T₁。 |
3.4.2 死锁预防
| 方法 | 原理 | 适用性 |
|---|---|---|
| 固定加锁顺序 | 破坏循环等待 | ✅ 最常用、最有效 |
std::lock() |
标准库自动处理顺序 | ✅ 推荐用于多 mutex 场景 |
| 超时重试 | 避免永久等待 | ⚠️ 可能活锁,需指数退避 |
| 减少锁粒度 | 降低并发冲突概率 | ✅ 提升性能 + 降低死锁风险 |
无锁编程(atomic) |
完全避免互斥 | ✅ 适用于简单共享状态 |
4. 网络编程
4.1 IO 多路复用:select/poll/epoll 对比
| 对比维度 | select |
poll |
epoll(Linux 2.6+) |
|---|---|---|---|
| 监控方式 | 基于位图(fd_set),监控文件描述符集合 |
基于数组(struct pollfd []),存储 fd 和事件类型 |
基于红黑树(内核维护)和就绪链表,高效存储和查询 |
| 最大监控数 | 受内核参数 FD_SETSIZE 限制(默认 1024) |
无理论限制(仅受系统资源限制) | 无理论限制(仅受系统资源限制) |
| IO 事件类型 | 支持读、写、异常事件 | 支持读、写、异常事件(可扩展更多事件类型) | 支持读、写、异常、边缘触发(ET)等多种事件 |
| 效率 | 每次调用需遍历所有监控 fd,O(n) 复杂度 | 每次调用需遍历所有监控 fd,O(n) 复杂度 | 仅遍历就绪 fd,O(1) 复杂度(基于就绪链表) |
| 内存拷贝 | 每次调用需将 fd_set 从用户态拷贝到内核态 |
每次调用需将 pollfd 数组从用户态拷贝到内核态 |
仅初始化时拷贝 fd 到内核态,之后无需拷贝 |
| 触发模式 | 仅水平触发(LT):只要 fd 就绪,每次调用都通知 | 仅水平触发(LT) | 支持水平触发(LT)和边缘触发(ET) |
| 适用场景 | 监控 fd 数量少(≤1024)的简单场景 | 监控 fd 数量中等的场景 | 高并发场景(如百万级连接,Nginx、Redis 使用) |