C++ 面试题01
一、C++ 基础(1–10)
1. 指针和引用的区别?各自适用场景?
- 怎么说: “简单说,指针是个变量,存的是地址,可以为空也可以重新赋值;引用就是个别名,绑定后就不能变了,也不能为空。”
- 场景: “需要表示‘可能没有对象’或者要切换指向时用指针;函数传参想避免拷贝又保证不为空时,首选引用。”
2. const 修饰变量、指针、成员函数分别表示什么?
- 变量: 值不能改(常量)。
- 指针: 记住口诀“const在星号左,指向内容不能动;const在星号右,指针本身不能动”。
- 成员函数: 承诺不修改类的成员变量(this指针变成const),这样const对象也能调用它。
3. 值传递、指针传递、引用传递的区别?
- 值传递: 拷贝一份副本,函数里改了不影响外面,适合小对象或内置类型。
- 指针传递: 传地址,通过解引用能改外面的值,但可能传nullptr,得判空。
- 引用传递: 也是传地址但语法像值传递,安全且高效,C++推荐默认用这个。
4. 堆和栈的区别?各自存什么、谁分配释放?
- 栈: 编译器自动管,存局部变量、函数参数、返回地址。速度快但有大小限制(容易溢出)。
- 堆: 程序员手动管(new/delete),存动态分配的对象。空间大但速度慢,忘了释放就内存泄漏。
5. 左值和右值是什么?std::move 做了什么?
- 左值: 有名字、能取地址的东西(比如变量)。
- 右值: 临时的、没名字的、即将销毁的东西(比如字面量、函数返回值)。
- std::move: 它其实不移动任何东西!只是把左值“强转”成右值引用,告诉编译器:“这东西我不要了,你可以偷它的资源”,从而触发移动语义。
6. #define 和 const、inline 的区别?
- #define: 预处理阶段纯文本替换,没类型检查,调试困难,尽量别用。
- const: 有类型、有作用域,编译器能做安全检查。
- inline: 建议编译器内联展开(省函数调用开销),但有类型检查,比宏安全得多。
7. static 在全局变量、局部变量、类成员上的作用?
- 全局变量/函数: 限制链接属性,只在本文件可见(隐藏)。
- 局部变量: 生命周期延长到整个程序运行期,但作用域还是局部的(只初始化一次)。
- 类成员: 属于类而不是某个对象,所有实例共享一份。
8. 四种类型转换分别用在什么场景?
- static_cast: 基本类型转换、非多态的父子类转换(编译期检查)。最常用。
- dynamic_cast: 多态类的父子类转换,运行时检查RTTI,失败返回nullptr(指针)或抛异常(引用)。
- const_cast: 去掉const/volatile属性(慎用,除非你确定原对象本来就不是const)。
- reinterpret_cast: 纯粹的比特位重解释,比如int转指针,非常危险,底层开发才用。
9. 函数重载的条件?和重写(覆盖)有什么区别?
- 重载: 同一作用域,函数名相同,参数列表不同(跟返回值无关)。是横向关系。
- 重写: 子类重新定义父类的虚函数,签名必须完全一样。是纵向的继承关系,实现多态。
10. 野指针和悬空指针怎么产生?怎么避免?
- 野指针: 未初始化的指针,指向随机地址。→ 避免: 定义时初始化为nullptr。
- 悬空指针: 指向的内存已经free/delete了,但指针没置空。→ 避免: 释放后立即赋nullptr;更好的办法是用智能指针。
二、面向对象(11–20)
11. 类对象在内存里大致长什么样?
- 怎么说: “普通成员变量按声明顺序排列(考虑对齐);如果有虚函数,对象开头会有个vptr指向虚表;静态成员不在对象里,在全局数据区;成员函数也不在对象里,在代码段。”
12. 虚函数的实现原理?vptr 和 vtable 是什么?
- 怎么说: “每个含虚函数的类有个虚表(vtable),里面存着虚函数的地址。每个对象有个虚指针(vptr)指向所属类的虚表。调用虚函数时,先通过vptr找到vtable,再查表拿到真正的函数地址去调用,这就是动态绑定。”
13. 为什么基类析构函数通常要声明为虚函数?
- 怎么说: “如果用基类指针delete一个派生类对象,而基类析构不是虚的,那就只会调基类析构,派生类部分就泄漏了。声明为虚析构才能保证从派生类开始正确链式析构。”
14. 构造函数里调用虚函数为什么不安全?
- 怎么说: “构造子类时,基类构造函数先执行,此时子类的vptr还没设置好(还指向基类vtable),所以调虚函数只会调到基类版本,根本不会多态。而且子类成员还没初始化,访问它们就是未定义行为。”
15. 重载、重写、隐藏的区别?
- 重载: 同名不同参,同一作用域。
- 重写: 子类覆盖父类虚函数,签名一致。
- 隐藏: 子类定义了和父类同名但签名不同的函数(或同名非虚函数),父类那个名字在子类作用域就被“遮住”了,哪怕参数不同也不会构成重载。
16. 深拷贝和浅拷贝的区别?什么时候必须自定义?
- 浅拷贝: 只拷指针值,两个对象指向同一块内存。
- 深拷贝: 重新分配内存并把内容复制过来。
- 何时自定义: 类里有裸指针指向堆内存时,必须自定义拷贝构造和赋值运算符做深拷贝,否则double free。
17. 三法则、五法则、零法则是什么?
- 三法则(C++03): 如果你自定义了析构、拷贝构造、拷贝赋值中的任何一个,通常三个都要自定义。
- 五法则(C++11): 加上移动构造和移动赋值,五个一起考虑。
- 零法则: 最佳实践——尽量用RAII类型(如智能指针、容器)管理资源,让编译器自动生成这五个函数,自己啥都不用写。
18. RAII 是什么?在 C++ 里怎么体现?
- 怎么说: “Resource Acquisition Is Initialization,资源获取即初始化。核心思想是把资源和对象生命周期绑定:构造时获取资源,析构时释放资源。C++里的智能指针、锁(lock_guard)、文件流都是RAII的典型体现。”
19. 构造函数和析构函数的调用顺序(单继承)?
- 构造: 基类 → 成员变量(按声明顺序) → 派生类。
- 析构: 完全反过来,派生类 → 成员变量(逆序) → 基类。
20. 多继承的菱形问题是什么?虚继承怎么解决?
- 问题: D继承B和C,B和C都继承A,D里就有两份A的数据,歧义+浪费。
- 解决: B和C继承A时加virtual关键字(虚继承)。这样D里只保留一份A的数据,由最终派生类D负责构造A。代价是多了个vbptr,构造更复杂。
三、内存管理(21–28)
21. new/delete 和 malloc/free 的区别?
- new/delete: C++运算符,会调构造/析构,类型安全,失败抛异常。
- malloc/free: C库函数,只分配原始内存,不调构造析构,返回void*需强转,失败返回NULL。
22. 内存泄漏是什么?常见原因有哪些?
- 是什么: 堆上分配的内存不再被使用,但也没被释放,再也无法回收。
- 常见原因: new了忘delete;异常导致delete没执行;循环引用(shared_ptr);容器里存裸指针但没清理。
23. 什么是内存对齐?为什么需要对齐?
- 怎么说: “CPU访问内存是按字长来的,如果数据跨了两个内存块就要读两次。对齐就是让变量地址落在其自然边界上(比如4字节int地址是4的倍数),提高访问效率。struct填充padding也是为了这个。”
24. 栈溢出一般怎么发生?
- 无限递归或递归太深。
- 局部变量太大(比如在栈上开几MB数组)。
- 解决:改用堆分配、减小递归深度、加大栈空间(不推荐)。
25. 堆上对象和栈上对象的生命周期有何不同?
- 栈上: 出作用域自动销毁,确定性析构。
- 堆上: 直到显式delete或智能指针引用计数归零才销毁,生命周期由程序员控制。
26. new 失败了会怎样?
- 默认抛
std::bad_alloc异常。 - 如果用
new(std::nothrow)则不抛异常,返回nullptr。 - 实际项目中一般靠异常处理或用智能指针避免直接new。
27. 什么是内存碎片?怎么缓解?
- 外部碎片: 空闲内存总量够但不连续,分配不了大块。
- 内部碎片: 对齐导致的已分配内存浪费。
- 缓解: 用内存池、固定大小分配器、jemalloc/tcmalloc等优秀分配器;减少小块频繁分配释放。
28. 如何检测和定位内存泄漏?
- 工具: Valgrind(Linux)、ASAN(AddressSanitizer,编译选项-fsanitize=address)、Visual Studio诊断工具。
- 代码层面: 重载new/delete打日志、用智能指针从根本上避免。
四、智能指针(29–34)
29. 为什么需要智能指针?
- 怎么说: “裸指针new了容易忘delete,异常安全也难保证。智能指针利用RAII,离开作用域自动释放,还能表达所有权语义,是现代C++内存管理的基石。”
30. unique_ptr 和 shared_ptr 的区别与使用场景?
- unique_ptr: 独占所有权,不能拷贝只能移动,零开销。默认首选。
- shared_ptr: 共享所有权,引用计数,可拷贝。用于确实需要多个所有者共享资源的场景。
31. shared_ptr 的引用计数是怎么工作的?
- 有一个控制块(control block)存引用计数和弱引用计数。
- 拷贝时计数+1,析构或reset时-1,归零时delete对象并销毁控制块。
- 计数操作是原子的,所以多线程安全(但对象本身的访问不安全)。
32. shared_ptr 循环引用怎么产生?weak_ptr 怎么解决?
- 产生: A持有B的shared_ptr,B又持有A的shared_ptr,计数永远≥1,谁也释放不了。
- 解决: 其中一方改用weak_ptr。weak_ptr不增加引用计数,只是观察,使用前lock()尝试提升为shared_ptr,对象已销毁则返回空。
33. make_shared 比直接 new 好在哪里?
- 性能: 一次内存分配同时放对象和控制块,减少分配次数,缓存友好。
- 安全: 避免
f(shared_ptr(new X), shared_ptr(new Y))这种参数求值顺序不确定导致的潜在泄漏。 - 简洁: 不用重复写类型名。
34. shared_ptr 线程安全吗?
- 引用计数: 原子操作,多线程增减计数安全。
- 所指对象: 不安全!多线程同时读写同一个shared_ptr指向的对象仍需加锁。
- shared_ptr实例本身: 多线程同时读写同一个shared_ptr变量也不安全。
五、STL(35–42)
35. vector 的底层实现和扩容机制?
- 底层: 连续内存数组,三个指针(start, finish, end_of_storage)。
- 扩容: 容量不够时,分配新内存(通常1.5倍或2倍),移动/拷贝旧元素过去,释放旧内存。所以push_back可能触发昂贵的重新分配。
36. vector 哪些操作会导致迭代器失效?
- 插入: 如果触发扩容,所有迭代器失效;没扩容,插入点之后的失效。
- 删除: 删除点之后的全部失效。
- resize/reserve: 可能触发扩容导致全部失效。
37. vector 和 list 怎么选?
- vector: 随机访问O(1),尾部插入O(1)均摊,缓存友好。绝大多数情况首选。
- list: 双向链表,任意位置插入删除O(1)(已有迭代器时),但不能随机访问,缓存不友好。只有频繁在中间插删且不需要随机访问时才选。
38. map 和 unordered_map 底层分别是什么?怎么选?
- map: 红黑树,有序,查找O(log n)。需要有序遍历或范围查询时用。
- unordered_map: 哈希表,无序,平均O(1)。只需要快速查找时用,大多数场景优先选它。
39. emplace_back 和 push_back 的区别?
- push_back: 先构造临时对象,再移动/拷贝进容器。
- emplace_back: 直接在容器内存里原地构造,省掉一次移动/拷贝。参数是构造函数参数而非对象本身。性能更好,推荐用。
40. 迭代器失效的常见场景有哪些?
- vector/deque: 插入删改都可能失效(见36题)。
- list/map/set: 只有被删除的那个元素的迭代器失效,其他不受影响。
- unordered_map: rehash时全部失效,否则只有被删元素失效。
- 通用原则: 修改容器后不要假设旧迭代器还有效。
41. deque 和 vector 的区别?
- vector: 一块连续内存,尾部高效,中间插删慢。
- deque: 分段连续(中央控制器+多个固定大小缓冲区),头尾插入删除都O(1),随机访问稍慢(两级寻址),迭代器更复杂。适合队列场景。
42. 常见 STL 容器时间复杂度?
| 操作 | vector | list | map | unordered_map |
|---|---|---|---|---|
| 查找 | O(n) / O(1)下标 | O(n) | O(log n) | O(1)平均 |
| 插入(尾部) | O(1)均摊 | O(1) | O(log n) | O(1)平均 |
| 插入(中间) | O(n) | O(1)* | O(log n) | N/A |
| 删除 | O(n) | O(1)* | O(log n) | O(1)平均 |
*前提是已有目标位置的迭代器
六、现代 C++ 与并发(43–48)
43. C++11 有哪些你用过的重要新特性?
- 怎么说: “auto类型推导、右值引用和移动语义、智能指针、lambda、constexpr、线程库(thread/mutex/atomic)、统一初始化列表、nullptr、range-for、tuple。这些让C++更安全、更高效、更易读。”
44. 右值引用和移动语义是什么?解决什么问题?
- 右值引用(&&): 绑定到临时对象的引用。
- 移动语义: 把临时对象的资源“偷”过来而不是深拷贝。
- 解决问题: 避免不必要的深拷贝,大幅提升性能(比如vector扩容、函数返回大对象时)。
45. 移动构造和拷贝构造的区别?
- 拷贝构造: 深拷贝源对象,源对象不变。
- 移动构造: 窃取源对象的资源(比如接管指针),源对象被置为有效但未指定的状态(通常是空的)。参数是右值引用,通常标记noexcept。
46. lambda 的 [=]、[&]、[x] 捕获方式区别?
- [=]: 按值捕获所有外部变量(副本)。
- [&]: 按引用捕获所有外部变量(小心悬空引用)。
- [x]: 只按值捕获x。
- 建议: 明确列出需要的变量,避免无脑[=]或[&]带来的意外副作用。
47. mutex、lock_guard、unique_lock 的区别?
- mutex: 原始互斥锁,手动lock/unlock,容易忘解锁或异常不安全。
- lock_guard: RAII封装,构造加锁析构解锁,简单安全,但不能手动解锁。
- unique_lock: 更灵活,可以延迟加锁、手动解锁/再锁、配合条件变量。需要灵活性时用unique_lock,否则用lock_guard。
48. 死锁产生的条件?怎么避免?
- 四个必要条件: 互斥、占有并等待、不可抢占、循环等待。
- 避免方法:
- 固定加锁顺序(破坏循环等待)。
- 用
std::lock()同时锁多个mutex。 - 超时尝试(try_lock)。
- 减少锁粒度,能用原子操作就不用锁。
- 使用RAII锁管理器,避免忘记解锁。
💡 背诵小贴士
- 别死记硬背全文,记住每道题的2-3个关键词,面试时用自己的话串起来。
- 结合项目经验:比如问到RAII,可以说“我在项目里用unique_ptr管理XXX资源…”;问到内存泄漏,说“我用ASAN排查过…”。
- 不懂就说不懂,但可以补充相关知识点:“这个细节我不太确定,但我了解相关的XXX…”
- 画图辅助:虚表、内存布局、循环引用这类问题,面试时主动画个简图比干讲强十倍。