一、面向对象核心
1. 封装、继承、多态
| 特性 | 定义与核心作用 |
|---|
| 封装 | 将数据(成员变量)与操作(成员函数)封装为类,仅通过公共接口访问,隐藏实现细节,降低耦合,保护数据安全。 |
| 继承 | 子类复用父类的成员(数据 + 函数),但父类私有成员可继承不可访问;构造 / 析构 / 友元 / 静态成员不可继承。作用是提高代码复用率。 |
| 多态 | 不同子类对象对同一消息(父类接口)做出不同响应。核心价值是可扩展性(新增子类不影响原有逻辑)和可替换性。 |
2. 虚函数与多态实现
(1)虚函数底层原理
(2)哪些函数不能是虚函数?
- 构造函数:虚函数调用依赖 vptr,而 vptr 在构造函数执行时才初始化,若构造函数为虚函数,会因 vptr 未就绪导致无法调用;
- 静态成员函数:无 this 指针,无法访问 vptr(虚表依赖 this 指针定位);
- 内联函数:编译时展开,无函数地址,无法存入虚函数表;
- 友元函数:不属于类成员,无 this 指针,无法关联虚表。
(3)final 关键字作用
- 修饰类:该类无法被继承(阻止继承);
- 修饰虚函数:该虚函数无法被子类重写(阻止重写,注意:不是 “重载”,重载是同一作用域函数名相同参数不同)。
3. 类相关核心问题
(1)空类的大小与默认函数
- 空类大小:1 字节(编译器为区分空类的不同对象,分配 1 字节占位符);
- 空类默认生成的函数:默认构造函数、默认拷贝构造函数、默认析构函数、默认赋值运算符(
operator=)、const 版本 / 非 const 版本取值运算符(operator&)。
(2)构造函数与析构函数的特殊规则
- 父类构造函数不能为虚函数(原因见 “虚函数” 部分);
- 父类析构函数建议为虚函数:若父类指针指向子类对象,非虚析构仅调用父类析构,子类资源未释放,会导致内存泄漏;
explicit关键字:仅修饰「单参数构造函数」(或除第一个参数外其余有默认值的构造函数),禁止隐式类型转换(如A a = 1;若构造函数为explicit A(int x)则编译报错)。
(3)成员变量初始化顺序
- 优先级:静态成员(编译阶段初始化,存全局区)→ 基类成员 → 子类成员;
- 初始化列表:与「列表中顺序无关」,仅与「类中成员变量的定义顺序」一致;
- 特殊成员:
const成员必须在初始化列表初始化;静态成员必须在「类外初始化」。
(4)class 与 struct 的区别
- 默认继承权限:class 默认
private继承,struct 默认public继承;
- 模板参数:class 可用于定义模板参数(如
template <class T>),struct 不可;
- 兼容性:struct 保留是为了兼容 C 语言,C++ 中推荐用 class 定义对象模型。
二、内存管理
1. 指针与引用
(1)指针核心概念
- 本质:存储内存地址的变量(“指针变量” 的简称);
- 大小:与 CPU 位数一致 ——32 位系统 4 字节,64 位系统 8 字节;
- 野指针:指向已释放内存或非法地址的指针,避免方式:①指针初始化(如
int* p = nullptr;);②释放后赋值nullptr;③避免越界访问。
(2)左值引用与指针的区别
| 对比维度 | 左值引用 | 指针 |
|---|
| 初始化要求 | 必须初始化(绑定一个已存在的左值) | 可默认初始化(如int* p;) |
| 本质 | 左值的 “别名”,无独立内存 | 独立变量,存储地址,占内存 |
| 空值支持 | 不支持(无法绑定nullptr) | 支持(int* p = nullptr;) |
2. 智能指针
(1)本质与核心原理
- 本质:封装「原始指针」的类模板,通过「对象析构自动释放资源」,避免内存泄漏;
- 核心逻辑:智能指针对象生命周期结束时,析构函数自动调用
delete(或delete[])释放管理的原始指针。
(2)常见智能指针特性
| 类型 | 核心特点 | 线程安全? |
|---|
shared_ptr | 引用计数(控制块维护),多个指针共享资源;计数为 0 时释放资源。 | 引用计数线程安全,资源访问不安全(需额外加锁)。 |
weak_ptr | 弱引用,不增加shared_ptr的计数;解决shared_ptr循环引用问题;需通过lock()转为shared_ptr使用。 | 无独立线程安全保证,依赖shared_ptr。 |
unique_ptr | 独占资源,不支持拷贝(仅移动);效率高于shared_ptr(无计数开销)。 | 不涉及共享,单线程安全。 |
(3)weak_ptr的计数与内存
- 有计数:控制块中维护「强弱引用计数」(强计数:
shared_ptr数量;弱计数:weak_ptr数量);
- 内存分配:①若通过
make_shared初始化,控制块与shared_ptr管理的资源在同一块内存;②若通过new初始化(如shared_ptr<int> p(new int)),控制块与资源在不同内存。
3. malloc 与 new 的区别
| 对比维度 | malloc | new |
|---|
| 本质 | C 标准库函数 | C++ 运算符 |
| 返回类型 | 无类型指针(void*),需强转 | 有类型指针(如int*),无需强转 |
| 失败处理 | 返回nullptr | 抛bad_alloc异常 |
| 构造 / 析构 | 仅分配内存,不调用构造 / 析构 | 分配内存后调用构造,释放前调用析构 |
| 内存区域 | 堆(heap) | 自由存储区(取决于operator new实现,可是堆或静态区) |
补充:malloc 的内存分配策略
| 分配大小 | 系统调用方式 | 释放逻辑 |
|---|
| ≤128KB | brk():移动堆顶指针分配内存 | 释放后缓存到 malloc 内存池,不归还系统 |
| >128KB | mmap():文件映射区分配内存 | 释放后直接归还系统 |
为什么不全部用 mmap/brk?
- 不全部用 mmap:频繁
mmap会触发多次内核态 / 用户态切换,且每次分配的内存是「缺页状态」,首次访问会触发缺页中断,效率低;
- 不全部用 brk:频繁分配 / 释放小块内存会产生大量「内存碎片」,导致后续大内存无法分配。
4. 内存对齐
(1)定义
处理器要求「数据的起始地址必须是某个整数(如 4、8 字节)的倍数」,称为内存对齐。
(2)为什么需要对齐?
- 提高访问效率:CPU 以「字长」为单位访问内存(如 32 位 CPU 一次读 4 字节),未对齐数据需 2 次访问,对齐数据仅 1 次;
- 保证移植性:部分 CPU(如 ARM)不支持非对齐访问,强行访问会触发硬件异常。
5. 内存泄漏与避免
- 定义:动态分配的堆内存未释放(或无法释放),导致系统内存浪费;
- 避免方式:①用智能指针管理动态内存;②释放数组用
delete[](匹配new[]);③避免在堆上频繁分配小块内存;④用内存检测工具(如 Valgrind)。
三、容器与 STL
1. STL 核心组成
STL 是 C++ 标准库的核心,包含 6 大组件:
- 容器:存储数据(如
vector、list、map);
- 算法:操作数据(如
sort、find);
- 迭代器:连接容器与算法(如
vector<int>::iterator);
- 空间配置器:管理容器的内存分配;
- 仿函数:重载
operator()的类,模拟函数行为(如less<int>);
- 配接器:适配组件接口(如
stack基于deque实现)。
2. 常用容器底层与优缺点
| 容器 | 底层实现 | 优点 | 缺点 |
|---|
vector | 连续动态数组 | 支持下标随机访问(O (1));尾插 / 尾删高效(O (1)) | 非尾部插入 / 删除效率低(O (n));扩容有开销 |
list | 双向链表 | 任意位置插入 / 删除高效(O (1));无需扩容 | 不支持随机访问;遍历效率低(O (n)) |
map | 红黑树(有序) | 按键有序;插入 / 查找 / 删除效率稳定(O (logn)) | 空间占用高(红黑树维护平衡需额外节点) |
unordered_map | 哈希表(无序) | 平均插入 / 查找 / 删除效率高(O (1)) | 哈希碰撞时性能下降;无序 |
(1)vector 关键操作
(2)unordered_map 扩容
- 触发条件:元素数量达到「桶数 × 负载因子(默认 0.75)」;
- 扩容逻辑:桶数翻倍(可通过
max_load_factor调整负载因子),所有元素重新哈希到新桶。
3. 迭代器
(1)迭代器与指针的区别
| 对比维度 | 迭代器 | 指针 |
|---|
| 本质 | 模板类对象,重载*/->等运算符模拟指针 | 存储地址的变量,直接操作内存地址 |
| 适用范围 | 仅容器(如vector、list) | 可指向任意内存(变量、函数、数组) |
| 返回值 | 对象引用(如*it返回T&) | 地址对应的值(如*p返回T) |
(2)迭代器失效场景与解决
| 容器类型 | 失效场景 | 解决方式 |
|---|
vector | 1. erase后,删除位置及后续迭代器失效;2. push_back触发扩容后,所有迭代器失效 | 1. erase返回下一个有效迭代器(it = vec.erase(it));2. 提前用reserve预分配内存 |
list | 仅删除位置的迭代器失效,其他迭代器正常 | erase后更新迭代器(it = list.erase(it)) |
map/unordered_map | 仅删除位置的迭代器失效 | 同上,用erase返回值更新迭代器 |
四、函数与表达式
1. 函数三大核心概念(重载、重写、隐藏)
| 概念 | 作用域 | 函数名 / 参数 / 返回值要求 | 核心场景 |
|---|
| 重载(Overload) | 同一作用域 | 函数名相同,参数列表(个数 / 类型 / 顺序)不同,返回值无要求 | 同一类内的同名函数(如add(int, int)/add(double, double)) |
| 重写(Override) | 父子类 | 函数名 / 参数 / 返回值完全相同,父类函数为虚函数 | 多态(子类覆盖父类虚函数) |
| 隐藏(Hide) | 父子类 | 函数名相同,与参数 / 返回值无关(非虚函数) | 子类屏蔽父类同名非虚函数 |
2. 特殊函数与表达式
(1)内联函数
(2)lambda 表达式(匿名函数)
(3)回调函数
(4)四种强制类型转换
| 类型 | 用途 | 注意事项 |
|---|
static_cast | 1. 基本类型转换(如int→char);2. 父子类指针 / 引用上行转换(子类→父类,安全) | 下行转换(父类→子类)无类型检查,不安全;不能去除const |
const_cast | 去除指针 / 引用的const属性(如const int*→int*) | 仅作用于指针 / 引用,不能直接修改const变量的值 |
reinterpret_cast | 强制转换指针 / 引用类型(如int*→char*);整数与指针互转 | 仅拷贝比特位,风险极高(如访问越界),谨慎使用 |
dynamic_cast | 父子类指针 / 引用下行转换(父类→子类),运行时类型检查 | 1. 父类必须有虚函数(否则编译报错);2. 转换失败时,指针返回nullptr,引用抛bad_cast异常 |
五、线程与进程
1. 进程与线程创建
| 类型 | Linux 创建方式 | Windows 创建方式 | 核心区别 |
|---|
| 进程 | fork()(复制父进程地址空间) | CreateProcess() | 进程有独立地址空间,线程共享进程地址空间 |
| 线程 | pthread_create()(POSIX 线程库) | CreateThread() | 线程切换开销远小于进程 |
2. 进程 / 线程通信方式
| 通信主体 | 方式 | 特点 |
|---|
| 进程间 | 1. 管道(匿名 / 命名):半双工,仅父子进程 / 同血缘进程;2. 消息队列:有大小限制,需拷贝数据;3. 共享内存:无数据拷贝,效率最高(需配合信号量同步);4. 信号:用于异常通知(如SIGINT是 Ctrl+C);5. 套接字(Socket):跨网络通信 | 共享内存效率最高,管道 / 消息队列易用性高 |
| 线程间 | 1. 互斥量(mutex):保证同一时间仅一个线程访问共享资源;2. 条件变量(condition_variable):线程间同步(如 “生产者 - 消费者”);3. 信号量:计数器,控制并发访问数量 | 依赖进程共享内存,无需跨地址空间通信 |
3. 线程同步与死锁
(1)线程同步手段
- 原子操作(
atomic):不可分割的操作(如atomic<int> cnt),底层通过 CPU 指令(如 X86 的LOCK前缀)保证线程安全,无需加锁;
- 互斥量(
mutex):加锁(lock())后仅一个线程进入临界区,解锁(unlock())后释放;
- 条件变量:配合互斥量使用,实现线程间 “等待 - 唤醒”(如生产者唤醒消费者);
- 读写锁(
shared_mutex):读操作共享(多线程同时读),写操作互斥(仅一个线程写),适合 “读多写少” 场景。
(2)死锁
- 定义:多个线程互相等待对方持有的资源,无法推进(如线程 A 持锁 1 等锁 2,线程 B 持锁 2 等锁 1);
- 四个必要条件:①互斥(资源仅一个线程可用);②请求保持(持有资源时请求新资源);③不可剥夺(资源不可强制抢占);④环路等待(线程间形成等待环);
- 解决方式:破坏任意一个条件(如按固定顺序加锁、超时释放锁、一次性申请所有资源)。
4. 线程安全相关
shared_ptr线程安全:引用计数的增减是原子操作(线程安全),但资源的读写不安全(需额外加锁保护资源);
- 线程状态:创建 → 就绪 → 运行 → 阻塞(如等待锁 / IO) → 死亡(函数执行完或异常退出)。
五、编译与链接
1. 程序运行四步骤
| 阶段 | 输入文件 | 输出文件 | 核心操作 |
|---|
| 预编译 | .cpp/.h | .i | 1. 展开头文件(如#include);2. 替换宏(如#define);3. 删除注释 |
| 编译 | .i | .s(汇编) | 1. 词法 / 语义分析;2. 语法检查;3. 生成汇编代码 |
| 汇编 | .s | .o(目标文件) | 将汇编指令转为机器码,生成符号表(存储函数 / 变量地址) |
| 链接 | .o + 库文件 | 可执行文件(.exe/.out) | 1. 合并目标文件;2. 解析符号表(将未定义符号绑定到库函数地址);3. 分配内存地址 |
2. 静态链接与动态链接
| 对比维度 | 静态链接 | 动态链接(共享库) |
|---|
| 链接时机 | 编译阶段(生成可执行文件前) | 程序运行时 |
| 库代码存储 | 库代码拷贝到可执行文件中 | 仅存储库的调用接口,运行时加载库 |
| 文件大小 | 可执行文件大 | 可执行文件小 |
| 资源占用 | 多个程序复用库时,内存中有多份拷贝 | 所有程序共享一份库内存,节省资源 |
| 兼容性 | 库更新后需重新编译程序 | 库更新后无需重新编译(接口不变即可) |
3. extern "C"
- 作用:让 C++ 编译器按 C 语言规则编译指定代码(如函数名不做 “名字修饰”);
- 场景:C++ 代码调用 C 语言库(如
#include "c_lib.h"时,用extern "C" { #include "c_lib.h" }避免 C++ 对 C 函数名的修饰导致链接失败)。
六、其他核心概念
1. 左值、右值与移动语义
(1)左值与右值
- 左值:可取地址、有名字的变量(如
int a = 5;中的a);
- 右值:不可取地址、临时存在的变量(如
5、a+b、i++的返回值);
- 关键判断:
++i是左值(返回i本身,可修改),i++是右值(返回临时变量,不可修改);++i效率更高(无临时变量拷贝)。
(2)移动语义与完美转发
- 右值引用(
T&&):绑定右值,用于实现移动语义(窃取右值的资源,避免拷贝);
move():本质是强制类型转换,将左值转为右值引用(如string s1 = "abc"; string s2 = move(s1);,s1资源被窃取,变为空字符串);
- 完美转发(
std::forward):通过 “引用折叠”,保持参数的左 / 右值属性,传递给内部函数(如模板函数中func(std::forward<T>(args)))。
2. 哈希碰撞处理
- 开放定址法:碰撞后按一定规则(如线性探测、二次探测)寻找下一个空闲桶;
- 链地址法:每个桶存储一个链表,碰撞的元素加入链表(
unordered_map采用此方式);
- 再哈希法:碰撞时用备用哈希函数重新计算地址;
- 公共溢出区:将碰撞元素统一存入 “溢出表”,基本表存储无碰撞元素。
3. Linux 信号(高频信号)
| 信号名 | 触发场景 | 默认动作 |
|---|
SIGINT | 用户按 Ctrl+C | 终止进程 |
SIGKILL | kill -9命令发送 | 强制终止进程(不可捕获 / 忽略) |
SIGSEGV | 非法内存访问(如空指针解引用) | 终止进程 + 生成 core 文件 |
SIGTERM | kill命令默认发送(如kill 进程号) | 终止进程(可捕获,用于清理资源) |
SIGCONT | 恢复暂停的进程(如fg命令) | 继续进程运行 |