本文整理了 200+ 道常见面试题。大部分的面试题对于 C++ 后端、桌面开发、嵌入式、音视频开发都是通用的。
目录
- 一、C/C++ 常见问题
- 二、数据结构算法常见问题
- 三、gdb / gcc / g++
- 四、设计模式
- 五、操作系统原理
- 六、系统编程
- 七、定时器
- 八、网络原理
- 九、网络编程
- 十、数据库 MySQL
- 十一、Redis
- 十二、服务器开发
- 十三、RPC
- 十四、开放性问题 / 项目相关
- 十五、音视频相关
一、C/C++ 常见问题
1. 智能指针实现原理
智能指针是一个类模板,封装了原始指针,基于 RAII(资源获取即初始化)思想管理动态内存:
- 构造函数获取资源(持有裸指针),析构函数自动释放资源;
- 重载 operator* 和 operator-> 模拟指针行为;
- 通过引用计数(shared_ptr)或独占所有权(unique_ptr)管理生命周期;
- 栈上对象出作用域自动析构,从而保证异常安全(即使中途 return 或抛异常也会释放)。
template <typename T> class MySharedPtr { T* ptr; int* count; // 引用计数 public: explicit MySharedPtr(T* p = nullptr) : ptr(p), count(new int(1)) {} ~MySharedPtr() { if (--(*count) == 0) { delete ptr; delete count; } } // 拷贝构造:共享计数 +1 MySharedPtr(const MySharedPtr& o) : ptr(o.ptr), count(o.count) { ++(*count); } };2. 智能指针里面的计数器何时会改变
以 shared_ptr 为例,引用计数变化时机:
- +1:拷贝构造、拷贝赋值(赋值前先处理旧引用);
- -1:析构、调用 reset()、被新的 shared_ptr 覆盖赋值;
- 不变:移动构造 / 移动赋值(所有权转移,不增减计数);
- weak_ptr 不增加引用计数(只增加弱引用计数),unique_ptr 无引用计数。
3. 智能指针和管理的对象分别在哪个区
- 智能指针对象本身在栈区(作为局部对象/成员存在);
- 它托管的资源(new 出来的对象)在堆区;
- 核心原理:利用栈对象超出生命周期后自动析构的特征,析构时释放堆资源,无需手动 delete。
4. 面向对象的特性:多态原理
面向对象三大特性:封装、继承、多态。
- 静态多态:函数重载、运算符重载、模板(编译期确定);
- 动态多态:虚函数(运行期确定)。
- 动态多态原理:类含虚函数时编译器生成虚函数表(vtable),对象头部存虚指针(vptr)。通过基类指针/引用调用虚函数时,运行时按对象实际类型查虚表取得函数地址,实现"一个接口,多种实现"。
5. 介绍一下虚函数,虚函数怎么实现的
- 虚函数用 virtual 声明,实现动态绑定(晚绑定);
- 实现机制:编译器为含虚函数的类生成虚函数表(数组,存放各虚函数地址),对象内存开头放 vptr 指向该表;
- 调用 p->func() 时编译成间接调用:(*(p->vptr)[index])(p);
- 构造对象时 vptr 被初始化为当前类的虚表(构造函数中设置),因此基类构造函数中调用虚函数不会发生多态。
6. 多态和继承在什么情况下使用
- 继承:存在 "is-a" 关系(派生类是基类的一种)、需要复用基类接口与公共代码时使用;组合优先于继承,仅在真正语义相关时用继承;
- 多态:需要以统一接口操作不同子类对象、后续可能扩展新类型时使用(如策略模式、工厂模式、插件化架构);
- 设计原则:面向接口编程、开闭原则——增加新类型不修改已有代码。
7. 除了多态和继承还有什么面向对象方法
- 封装:隐藏内部实现,通过接口访问;
- 抽象:抽象基类/纯虚函数定义契约;
- 组合/聚合:对象包含对象,优于继承(低耦合);
- 泛型编程(模板):类型参数化,编译期多态;
- 接口(纯虚类):定义行为规范;
- RAII:用对象生命周期管理资源。
8. C++ 内存分布。什么样的数据在栈区,什么样的在堆区
- 栈区:局部变量、函数参数、返回地址,编译器自动分配释放,容量小(默认几 MB);
- 堆区:new/malloc 动态分配的内存,需手动释放,容量大;
- 全局/静态区:全局变量、static 变量;
- 常量区:字符串常量等只读数据;
- 代码区:存放函数指令。
9. C++ 内存管理(RAII 啥的)
- RAII(Resource Acquisition Is Initialization):在构造函数中获取资源(内存、文件、锁、连接),析构函数中释放,资源生命周期与对象绑定,天然异常安全;
- 结合智能指针管理堆内存;栈上对象自动释放;
- 注意 new[] 配 delete[]、防内存泄漏、防悬挂指针。
10. C++ 从源程序到可执行程序的过程
四个阶段:
- 预处理:展开宏、处理 #include、删除注释、条件编译,生成 .i 文件;
- 编译:词法/语法/语义分析、优化,生成汇编代码 .s;
- 汇编:汇编代码转机器指令,生成目标文件 .o(含符号表、重定位信息);
- 链接:合并目标文件与库(静态/动态),符号解析与重定位,生成可执行文件
g++ -E main.cpp -o main.i # 预处理 g++ -S main.i -o main.s # 编译 g++ -c main.s -o main.o # 汇编 g++ main.o -o main # 链接11. 一个对象 = 另一个对象会发生什么(赋值构造函数)
- 触发拷贝赋值运算符 operator=;
- 默认(编译器生成)执行浅拷贝(逐位拷贝):指针成员会复制地址,两个对象指向同一块内存,析构时双重释放,还可能内存泄漏(原资源未释放);
- 解决:自定义深拷贝(重新分配内存并复制内容)或使用智能指针/copy-and-swap。
12. 如果 new 了之后出了问题直接 return,会导致内存泄漏。怎么办
- 使用智能指针(unique_ptr/shared_ptr)自动管理,出作用域自动释放;
- 使用 RAII 封装资源;
- 使用异常安全写法:分配与使用都在对象生命周期内完成;
- 避免裸 new/delete 配对管理,遵循"谁分配谁释放"。
void f() { std::unique_ptr<int> p(new int(42)); // return/异常也不泄漏 if (cond) return; *p = 100; }13. C++11 的智能指针有哪些?weak_ptr 的使用场景?什么情况下会产生循环引用
- unique_ptr:独占所有权,不可拷贝,可移动,无额外开销;
- shared_ptr:共享所有权,引用计数,可拷贝,线程安全的计数操作;
- weak_ptr:弱引用,不增加引用计数,用于打破循环引用、观察 shared_ptr 管理的对象是否存活(expired()/lock())。
循环引用:两个对象各自持有对方的 shared_ptr,形成环,计数永远不为 0,内存无法释放。
struct A { std::shared_ptr<B> b; }; struct B { std::shared_ptr<A> a; }; // 循环引用 // 解决:将其中一方改为 weak_ptr struct B { std::weak_ptr<A> a; };14. 多进程 fork 后不同进程会共享哪些资源
- 共享:代码段(只读)、打开的文件描述符表(及文件偏移)、信号处理器、环境变量、共享内存(mmap MAP_SHARED)、全局的只读数据(写时拷贝,物理页共享);
- 不共享:独立的虚拟地址空间、栈、堆、进程 PID、互斥锁状态(锁不是共享的);
- fork 后采用写时拷贝(COW):只有写入时才复制物理页,节省内存。
15. 多线程里线程的同步方式有哪些
- 互斥锁(mutex):保护临界区;
- 读写锁(rwlock):读共享、写独占;
- 条件变量(condition_variable):线程间等待/通知;
- 信号量(semaphore):控制并发数量;
- 原子操作(atomic):无锁同步;
- 自旋锁(spinlock):忙等,适合短临界区;
- 屏障(barrier):多线程到达同步点。
16. sizeof 是在编译期还是在运行期确定
- 编译期确定(常量表达式),不执行表达式;
- 例外:C 语言中的变长数组(VLA) 在运行期确定(C++ 不支持 VLA);
- sizeof 不计算表达式值,例如 sizeof(*p) 即使 p 为空指针也安全(编译期计算类型大小)。
17. 函数重载的机制。重载是在编译期还是在运行期确定
- 重载依据:函数名相同 + 参数列表不同(个数/类型/顺序),与返回类型无关;
- 机制:编译器进行名字修饰(name mangling),将参数类型编码进符号名,使链接器可区分;
- 在编译期确定调用哪个版本(静态多态)。
18. 指针常量和常量指针
- 常量指针(const 修饰指针本身):int* const p; —— 指针不能再指向别处,但指向的内容可变;
- 指针常量(const 修饰指向的内容):const int* p; 或 int const* p; —— 指向的内容不可通过该指针修改,但指针本身可指向别处;
- 记忆技巧:const 在 * 左边修饰指向对象,在 * 右边修饰指针本身。
19. vector 的原理,怎么扩容
- 底层是动态数组(连续内存),维护三个指针:start、finish(size)、end_of_storage(capacity);
- 插入时若 size == capacity 触发扩容:申请新内存(通常 1.5 或 2 倍),将旧元素移动/拷贝过去,释放旧内存;
- 扩容是高代价操作(拷贝/移动所有元素),因此预先 reserve() 可避免频繁扩容;
- 扩容后迭代器、指针、引用失效。
20. 介绍一下 const
- 修饰变量:只读,不可修改;
- 修饰指针:见第 18 题(顶层/底层 const);
- 修饰成员函数:void f() const; 承诺不修改成员变量(mutable 成员除外),const 对象只能调用 const 成员函数;
- 修饰引用参数:const T& 避免拷贝且不可修改;
- 编译期约束,可与宏区分(有类型检查、作用域)。
21. 引用和指针的区别
| 区别 | 引用 | 指针 |
| 本质 | 变量的别名 | 存地址的变量 |
| 初始化 | 必须初始化 | 可以不初始化 |
| 重新绑定 | 不可改绑 | 可重新赋值 |
| 空值 | 无空引用 | 可为 nullptr |
| 运算 | 无指针运算 | 支持 ++/-- 等 |
| sizeof | 原对象大小 | 地址大小(8 字节 64 位) |
引用更安全、语义清晰;指针更灵活但易出错。
22. C++ 新特性知道哪些
- C++11:auto、nullptr、范围 for、智能指针、右值引用/移动语义、lambda、std::function/std::bind、可变参数模板、constexpr、thread、chrono、tuple;
- C++14:泛型 lambda、make_unique、返回类型推导;
- C++17:if constexpr、结构化绑定、std::optional/variant/any、filesystem、折叠表达式;
- C++20:协程(Coroutine)、concept、模块(Module)、ranges、span;
- C++23:std::expected、print 等。
23. 类型转换
- static_cast:编译期相关类型的转换(int→double、void*→T*、下行转换不检查);
- dynamic_cast:多态类型安全下行转换(运行时检查,失败返回 nullptr/抛 bad_cast),需有虚函数;
- const_cast:去除/添加 const;
- reinterpret_cast:底层二进制重新解释(如指针↔整数),最危险。
double d = static_cast<double>(1) / 3; Base* b = dynamic_cast<Base*>(derived_ptr); // 安全下行 const_cast<char*>(str). ...24. RAII 基于什么实现的
基于对象生命周期与作用域:对象在进入作用域时构造(获取资源),离开作用域时析构(释放资源),配合构造/析构函数的确定性调用,保证资源被自动、及时释放,异常安全。
25. 手撕:unique_ptr 控制权转移(移动语义);类继承、堆栈上分别代码实现多态
#include <memory> std::unique_ptr<int> p1(new int(10)); std::unique_ptr<int> p2 = std::move(p1); // 控制权转移,p1 变为空多态实现:
struct Base { virtual ~Base() {} virtual void show() { std::cout << "Base\n"; } }; struct Derived : Base { void show() override { std::cout << "Derived\n"; } }; // 堆上:基类指针 + 虚函数 std::unique_ptr<Base> p = std::make_unique<Derived>(); p->show(); // Derived // 栈上:基类引用指向派生类对象 Derived d; Base& ref = d; ref.show(); // Derived26. unique_ptr 和 shared_ptr 区别
| 特性 | unique_ptr | shared_ptr |
|---|---|---|
| 所有权 | 独占 | 共享(引用计数) |
| 拷贝 | 禁止(可移动) | 允许(计数+1) |
| 开销 | 与裸指针相当 | 计数维护(原子操作) |
| 循环引用 | 不存在 | 可能产生,需 weak_ptr |
| 适用 | 独占资源 | 多处共享资源 |
27. 右值引用
- 语法:T&&,只能绑定右值(临时对象、字面量、std::move 的结果);
- 作用:实现移动语义(移动构造/移动赋值),将即将消亡对象的资源"偷走",避免深拷贝;
- 延长临时对象生命周期(绑定到右值引用);
- 配合 std::move/std::forward 使用。
std::vector<int> v1{1,2,3}; std::vector<int> v2 = std::move(v1); // 移动构造,v1 为空28. 函数参数可不可以传右值
可以,有以下方式:
- 参数声明为右值引用 void f(T&& x):只能传右值;
- 按值传递 void f(T x):实参为右值时调用移动构造(传左值则拷贝);
- const T& 也能绑定右值,但不可修改且不会移动;
- 模板中 T&& 为万能引用,配合完美转发可保留左/右值属性。
29. 参考 c/c++ 堆栈实现自己的堆栈(不能用 STL 容器)
用动态数组 + 栈顶指针实现:
class MyStack { int* data; int cap; int top; public: MyStack(int n = 16) : data(new int[n]), cap(n), top(0) {} ~MyStack() { delete[] data; } void push(int x) { if (top == cap) resize(); data[top++] = x; } int pop() { return data[--top]; } int peek() const { return data[top-1]; } bool empty() const { return top == 0; } private: void resize() { /* 扩容拷贝 */ } };30. STL 容器了解吗?底层如何实现
| 容器 | 底层结构 | 特点 |
| vector | 动态数组 | 随机访问 O(1),尾部操作高效 |
| list | 双向链表 | 任意位置插入删除 O(1) |
| deque | 分段连续空间(map+缓冲区) | 两端操作 O(1) |
| map/set/multimap/multiset | 红黑树 | 有序,O(logN) |
| unordered_map/unordered_set | 哈希表(拉链法) | 无序,平均 O(1) |
| priority_queue | vector 上的堆(默认大顶堆) | 取最大/最小 O(1) |
红黑树实现要点:节点红/黑着色、根黑、红节点子必黑、任意路径黑节点数相同、最长路径不超过最短的 2 倍。
31. 完美转发介绍一下,去掉 std::forward 会怎样?
- 完美转发:在模板中将实参的左/右值属性原样传递给被调函数,保持值类别不丢失;
- 实现:万能引用 T&& + std::forward<T>(arg);
- 去掉 std::forward:参数在函数内部是具名左值,一律按左值传递,右值实参无法触发移动语义,导致不必要的深拷贝,性能下降。
template<typename T> void wrapper(T&& arg) { foo(std::forward<T>(arg)); // 保留实参左右值属性 }32. 介绍一下 unique_lock 和 lock_guard 区别
- lock_guard:构造时加锁、析构时解锁,不可手动控制,简单高效,适合常规作用域;
- unique_lock:更灵活——可手动 lock/unlock、延迟加锁(defer_lock)、尝试加锁(try_lock)、可移动,常与条件变量配合(wait 需要 unlock 能力);
- 开销上 unique_lock 略大,默认优先用 lock_guard。
33. C 代码中引用 C++ 代码有时候会报错为什么?
- C++ 支持函数重载,编译时对函数名进行名字修饰(name mangling)(如 func → _Z4funcv);C 编译器不做修饰;
- C++ 代码编译出的符号与 C 侧期望的符号不匹配,链接失败(undefined reference);
- 解决:C++ 头文件用 extern "C" { ... } 包裹,使函数按 C 方式命名和链接。
#ifdef __cplusplus extern "C" { #endif void c_api_func(int); #ifdef __cplusplus } #endif34. 静态多态有什么?虚函数原理、虚表是什么时候建立的、为什么析构函数要设置成虚函数
- 静态多态:函数重载、模板、运算符重载(编译期绑定);
- 虚函数原理:虚函数表(vtable)+ 虚指针(vptr),运行时根据对象实际类型查表间接调用;
- 虚表建立时机:虚表在编译期生成(存放在只读数据段);对象构造时(构造函数体内)vptr 被设置为指向当前类的虚表;
- 析构函数必须虚:若基类析构非虚,通过基类指针 delete 派生类对象时只调用基类析构,派生类资源泄漏;虚析构保证正确调用派生类析构。
35. map 为啥用红黑树不用 AVL 树?(map 和 unordered_map 区别)
- AVL 严格平衡(左右子树高度差 ≤1),查找稍快,但插入/删除需要频繁旋转,维护成本高;
- 红黑树只保证最长路径 ≤ 2 倍最短路径(近似平衡),插入删除最多 3 次旋转,综合性能更优;两者查找复杂度都是 O(logN)。
map vs unordered_map:
| 对比 | map | unordered_map |
| 底层 | 红黑树 | 哈希表 |
| 有序性 | 有序 | 无序 |
| 查找 | O(logN) | 平均 O(1) |
| 内存 | 节点连续紧凑 | 桶+节点,开销大 |
| 适用 | 需要有序/范围查询 | 追求查找速度 |
36. inline 失效场景
- 函数体过大(编译器权衡后放弃内联);
- 函数包含递归;
- 函数地址被取(需要实体存在);
- 虚函数(动态绑定无法内联);
- 通过函数指针/回调调用;
- 编译优化选项关闭;
- 构造函数/析构函数复杂(隐式调用多);
- inline 只是建议,编译器可忽略。
37. C++ 中 struct 和 class 区别
- 默认访问权限:struct 默认为 public,class 默认为 private;
- 默认继承方式:struct 默认 public 继承,class 默认 private 继承;
- 其他(成员函数、模板、继承等)完全相同。
38. 如何防止一个头文件 include 多次
- include guard(可移植):
#ifndef MY_HEADER_H #define MY_HEADER_H // 内容 #endif- #pragma once(主流编译器支持,简单高效):
#pragma once39. lambda 表达式的理解,它可以捕获哪些类型
- lambda 本质是编译器生成的匿名仿函数对象,格式:[捕获](参数) -> 返回类型 { 函数体 };
- 捕获类型:
- [=] 值捕获所有用到的局部变量;
- [&] 引用捕获所有用到的局部变量;
- [x, &y] 混合捕获;
- [this] 捕获 this 指针(访问成员);C++17 支持 [*this] 值捕获对象;
- C++14 初始化捕获 [x = expr];
- 默