【C++面试】常见面试题及相关解析

173 阅读16分钟

一、面向对象核心

1. 封装、继承、多态

特性定义与核心作用
封装将数据(成员变量)与操作(成员函数)封装为类,仅通过公共接口访问,隐藏实现细节,降低耦合,保护数据安全。
继承子类复用父类的成员(数据 + 函数),但父类私有成员可继承不可访问;构造 / 析构 / 友元 / 静态成员不可继承。作用是提高代码复用率。
多态不同子类对象对同一消息(父类接口)做出不同响应。核心价值是可扩展性(新增子类不影响原有逻辑)和可替换性

2. 虚函数与多态实现

(1)虚函数底层原理

  • 核心依赖「虚函数表(vtable)  」和「虚表指针(vptr)  」:

    1. 含虚函数的类编译时生成「虚函数表」:一个存储该类所有虚函数地址的指针数组;
    2. 类的每个对象会隐含一个「虚表指针(vptr)」,指向所属类的虚函数表(vptr 在对象构造时初始化);
    3. 当父类指针 / 引用指向子类对象时,通过 vptr 找到子类的虚函数表,调用对应虚函数,实现动态多态。

(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)成员变量初始化顺序

  1. 优先级:静态成员(编译阶段初始化,存全局区)→ 基类成员 → 子类成员
  2. 初始化列表:与「列表中顺序无关」,仅与「类中成员变量的定义顺序」一致;
  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 的区别

对比维度mallocnew
本质C 标准库函数C++ 运算符
返回类型无类型指针(void*),需强转有类型指针(如int*),无需强转
失败处理返回nullptrbad_alloc异常
构造 / 析构仅分配内存,不调用构造 / 析构分配内存后调用构造,释放前调用析构
内存区域堆(heap)自由存储区(取决于operator new实现,可是堆或静态区)

补充:malloc 的内存分配策略

分配大小系统调用方式释放逻辑
≤128KBbrk():移动堆顶指针分配内存释放后缓存到 malloc 内存池,不归还系统
>128KBmmap():文件映射区分配内存释放后直接归还系统

为什么不全部用 mmap/brk?

  • 不全部用 mmap:频繁mmap会触发多次内核态 / 用户态切换,且每次分配的内存是「缺页状态」,首次访问会触发缺页中断,效率低;
  • 不全部用 brk:频繁分配 / 释放小块内存会产生大量「内存碎片」,导致后续大内存无法分配。

4. 内存对齐

(1)定义

处理器要求「数据的起始地址必须是某个整数(如 4、8 字节)的倍数」,称为内存对齐。

(2)为什么需要对齐?

  1. 提高访问效率:CPU 以「字长」为单位访问内存(如 32 位 CPU 一次读 4 字节),未对齐数据需 2 次访问,对齐数据仅 1 次;
  2. 保证移植性:部分 CPU(如 ARM)不支持非对齐访问,强行访问会触发硬件异常。

5. 内存泄漏与避免

  • 定义:动态分配的堆内存未释放(或无法释放),导致系统内存浪费;
  • 避免方式:①用智能指针管理动态内存;②释放数组用delete[](匹配new[]);③避免在堆上频繁分配小块内存;④用内存检测工具(如 Valgrind)。

三、容器与 STL

1. STL 核心组成

STL 是 C++ 标准库的核心,包含 6 大组件:

  • 容器:存储数据(如vectorlistmap);
  • 算法:操作数据(如sortfind);
  • 迭代器:连接容器与算法(如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 关键操作

  • 扩容机制:当size == capacity时触发扩容,通常按1.5 倍或 2 倍扩容(减少扩容次数);

  • resizereserve区别:

    • resize(n):修改size(若n>size,新增元素默认初始化;若n<size,删除尾部元素);
    • reserve(n):仅修改capacity(预分配内存,不改变size,避免频繁扩容);
  • push_backemplace_back区别:

    • push_back:先构造临时对象,再拷贝 / 移动到容器;
    • emplace_back:直接在容器尾部构造对象,少一次临时对象的构造 / 析构,效率更高(优先使用,除非需复用现有对象)。

(2)unordered_map 扩容

  • 触发条件:元素数量达到「桶数 × 负载因子(默认 0.75)」;
  • 扩容逻辑:桶数翻倍(可通过max_load_factor调整负载因子),所有元素重新哈希到新桶。

3. 迭代器

(1)迭代器与指针的区别

对比维度迭代器指针
本质模板类对象,重载*/->等运算符模拟指针存储地址的变量,直接操作内存地址
适用范围仅容器(如vectorlist可指向任意内存(变量、函数、数组)
返回值对象引用(如*it返回T&地址对应的值(如*p返回T

(2)迭代器失效场景与解决

容器类型失效场景解决方式
vector1. 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)内联函数

  • 原理:编译时将函数体 “嵌入” 调用处,避免函数调用的栈开销(参数压栈、返回地址保存等);

  • 优缺点:

    • 优点:减少调用开销,提高运行效率;
    • 缺点:函数体过长会导致代码膨胀(建议内联函数体不超过 5 行);不支持递归(递归调用无法嵌入)。

(2)lambda 表达式(匿名函数)

  • 本质:编译时生成临时函数对象,重载operator()实现调用;

  • 捕获列表(核心):

    • 按值捕获:[=](拷贝外部变量,不可修改,除非加mutable);
    • 按引用捕获:[&](引用外部变量,需注意:外部变量生命周期若短于 lambda,会导致悬挂引用);
  • 优点:无需声明函数,减少代码冗余;仅在调用时创建对象,节省空间。

(3)回调函数

  • 定义:用户定义函数,将其地址作为参数传入其他函数,由其他函数在运行时调用;

  • 本质:将函数作为 “参数” 传递,解耦 “调用者” 与 “被调用者”;

  • 优缺点:

    • 优点:提高代码灵活性(如排序函数sort传入自定义比较回调);
    • 缺点:回调过多易导致 “回调地狱”,代码可读性 / 维护性下降;共享资源访问需加锁,避免竞争。

(4)四种强制类型转换

类型用途注意事项
static_cast1. 基本类型转换(如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.i1. 展开头文件(如#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);
  • 右值:不可取地址、临时存在的变量(如5a+bi++的返回值);
  • 关键判断:++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终止进程
SIGKILLkill -9命令发送强制终止进程(不可捕获 / 忽略)
SIGSEGV非法内存访问(如空指针解引用)终止进程 + 生成 core 文件
SIGTERMkill命令默认发送(如kill 进程号终止进程(可捕获,用于清理资源)
SIGCONT恢复暂停的进程(如fg命令)继续进程运行