文章

C++ 面试题01

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 容器时间复杂度?

操作vectorlistmapunordered_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. 死锁产生的条件?怎么避免?

  • 四个必要条件: 互斥、占有并等待、不可抢占、循环等待。
  • 避免方法:
    1. 固定加锁顺序(破坏循环等待)。
    2. std::lock() 同时锁多个mutex。
    3. 超时尝试(try_lock)。
    4. 减少锁粒度,能用原子操作就不用锁。
    5. 使用RAII锁管理器,避免忘记解锁。

💡 背诵小贴士

  1. 别死记硬背全文,记住每道题的2-3个关键词,面试时用自己的话串起来。
  2. 结合项目经验:比如问到RAII,可以说“我在项目里用unique_ptr管理XXX资源…”;问到内存泄漏,说“我用ASAN排查过…”。
  3. 不懂就说不懂,但可以补充相关知识点:“这个细节我不太确定,但我了解相关的XXX…”
  4. 画图辅助:虚表、内存布局、循环引用这类问题,面试时主动画个简图比干讲强十倍。
本文由作者按照 CC BY 4.0 进行授权