C++ 常见面试题汇总
基础知识
一、C++ 基础语法
- C++ 和 C 的区别?
- C++ 支持面向对象(封装、继承、多态)。
- C++ 引入模板、STL、异常处理。
- 值传递、指针传递、引用传递的区别?
- 值传递:拷贝一份副本。
- 指针传递:传地址,可修改原数据。
- 引用传递:别名,语法更简洁。
- const 的用法?
- 修饰变量:常量。
- 修饰指针:
const int* p(指向常量),int* const p(常指针)。 - 修饰成员函数:
void f() const;表示函数内不能修改成员变量。
- static 的作用?
- 局部静态变量:函数调用间保持值。
- 修饰全局变量/函数:只在文件内可见。
- 修饰类成员:属于类而非对象。
- inline 内联函数的原理?
- 编译器用函数体替换调用点,减少函数调用开销。
- 适用于小函数,频繁调用。
二、面向对象
- C++ 四大特性?
- 封装、继承、多态、抽象。
- 多态的实现方式?
- 静态多态:函数重载、模板。
- 动态多态:虚函数(虚函数表实现)。
- 虚函数、纯虚函数、抽象类区别?
- 虚函数:子类可重写。
- 纯虚函数:
=0,子类必须实现。 - 抽象类:含有纯虚函数,不能实例化。
- 虚函数表 (vtable) 的工作原理?
- 类中有虚函数时,编译器生成 vtable,存储函数指针。
- 对象包含 vptr,指向 vtable,实现动态绑定。
- 构造函数和析构函数的调用顺序?
- 构造:先基类,再成员对象,最后派生类。
- 析构:先派生类,再成员对象,最后基类。
三、内存管理
- C++ 内存分区?
- 栈:局部变量、函数参数。
- 堆:
new/delete分配的内存。 - 全局/静态区:全局变量、静态变量。
- 常量区:字符串常量。
- 代码区:存放可执行代码。
- new/delete 与 malloc/free 的区别?
new调用构造函数,返回指定类型指针。malloc只分配内存,不调用构造函数。delete调用析构函数,释放内存。free只释放内存。
- 内存泄漏如何检测?
- 工具:Valgrind、ASan。
- 手动:智能指针(
shared_ptr,unique_ptr)。
四、C++11/14/17/20 新特性
- C++11 特性
- auto 类型推导、nullptr、lambda、智能指针、右值引用、move 语义。
- 右值引用 & move 语义?
T&&表示右值引用,用于接收临时对象。std::move转换为右值,避免拷贝,提高性能。
- 智能指针的区别?
unique_ptr:独占所有权。shared_ptr:引用计数共享所有权。weak_ptr:弱引用,不增加计数,解决循环引用。
五、STL
- vector 和 list 的区别?
- vector:连续存储,随机访问快,插入删除慢。
- list:链表存储,插入删除快,随机访问慢。
- map 和 unordered_map 的区别?
- map:红黑树实现,元素有序,O(log n)。
- unordered_map:哈希表实现,无序,O(1) 平均。
- 迭代器失效问题?
- vector 插入/删除时可能导致迭代器失效。
- list 插入/删除不会影响其他迭代器。
六、多线程与并发
- 线程创建方式?
std::threadstd::asyncstd::packaged_task
- 互斥锁和自旋锁区别?
- 互斥锁:阻塞等待,适合长任务。
- 自旋锁:忙等待,适合短任务。
- 条件变量 (condition_variable) 用法?
- 结合
unique_lock,用于线程同步。
- 结合
七、设计模式
- 工厂模式、观察者模式 —— 常考理论。
单例模式实现?
classSingleton{private:Singleton(){}public:static Singleton&getInstance(){static Singleton instance;// C++11 保证线程安全return instance;}};八、常见算法题型
- LRU 缓存(哈希表 + 双链表)
- 高频考点,需熟练掌握。
快速排序模板
voidquickSort(vector<int>& a,int l,int r){if(l >= r)return;int i = l, j = r, pivot = a[l];while(i < j){while(i < j && a[j]>= pivot) j--;while(i < j && a[i]<= pivot) i++;if(i < j)swap(a[i], a[j]);}swap(a[l], a[i]);quickSort(a, l, i-1);quickSort(a, i+1, r);}二分查找模板
intbinarySearch(vector<int>& nums,int target){int l =0, r = nums.size()-1;while(l <= r){int mid = l +(r - l)/2;if(nums[mid]== target)return mid;elseif(nums[mid]< target) l = mid +1;else r = mid -1;}return-1;}九、综合类问题
- C++ 内存对齐规则?
- 深拷贝 vs 浅拷贝区别?
- 智能指针的循环引用问题怎么解决?
- 多态中析构函数为什么要设为虚函数?
高级知识点
一、 对象生存期与资源管理(RAII / Rule of Five)
- RAII:资源由对象构造获取(constructor),析构释放(destructor)。推荐把资源封装在对象里,避免裸 new/delete。
- Rule of Five:如果定义了自定义析构、拷贝/赋值/移动中的任意一个,通常要考虑五个函数:
~T()、T(const T&)、T& operator=(const T&)、T(T&&) noexcept、T& operator=(T&&) noexcept。 noexcept:移动构造/移动赋值应尽量标注noexcept,因为很多 STL 容器在需要判断是否可用noexceptmove 时会选择拷贝或移动;若移动不是noexcept,容器在扩容等操作时可能退回到拷贝(性能或语义影响)。
示例(拷贝-移动-释放的正确实现):
classBuffer{ size_t n_;int* data_;public:Buffer(size_t n=0):n_(n),data_(n ?newint[n]():nullptr){}~Buffer(){delete[] data_;}// copyBuffer(const Buffer& o):n_(o.n_),data_(o.n_ ?newint[o.n_]:nullptr){ std::copy(o.data_, o.data_ + n_, data_);} Buffer&operator=(Buffer o){// copy-and-swap 提供强异常安全swap(*this, o);return*this;}// moveBuffer(Buffer&& o)noexcept:n_(o.n_),data_(o.data_){ o.n_ =0; o.data_ =nullptr;} Buffer&operator=(Buffer&& o)noexcept{if(this!=&o){delete[] data_; n_ = o.n_; data_ = o.data_; o.n_ =0; o.data_ =nullptr;}return*this;}friendvoidswap(Buffer& a, Buffer& b)noexcept{using std::swap;swap(a.n_, b.n_);swap(a.data_, b.data_);}};面试点:为什么 operator=(Buffer o)(按值)提供强异常安全?因为拷贝发生在进入函数时,随后 swap 保证不会抛异常;若拷贝失败,原对象不受影响。
二、拷贝 vs 移动 vs 完美转发
std::move:将左值转换为右值(允许“移动”语义)。它只是类型转换,不做实际移动。std::forward<T>:用于完美转发(保持值类别),常出现在模板转发场景(T&&是 forwarding reference)。- 完美转发示例(容器 emplace 风格):
template<typenameT>voidpush_back_emplace(std::vector<T>& v, T&& val){ v.emplace_back(std::forward<T>(val));// 保持传入值类别}面试点:区分 forwarding reference(模板 T&&)和纯右值引用。
三、 异常安全分级(面试必问)
- 无保证(No guarantee):函数失败后程序状态不确定。
- 基本保证(Basic):不泄露资源,对象处于有效但未定义的状态。
- 强保证(Strong):要么成功,要么回滚到原状态(事务式)。
- 不抛异常保证(No-throw):函数保证不抛异常(对析构函数很重要)。
实现强保证常用技术:copy-and-swap、先构造新对象再替换。
四、 Undefined Behavior(UB)——必须会举例并解释
常见 UB:
- 访问释放后的内存(use-after-free)。
- 双重释放(double free)。
- 有符号整数溢出(
int溢出是 UB)。 - 解引用空指针。
- 同时无同步的并发读写(data race)。
示例:
int a = INT_MAX;int b = a +1;// 有符号溢出 —— UB(不要假设会 wrap-around)面试点:说明 UB 会让编译器基于假设做优化,从而产生难以预期的行为。
五、STL 深入(常被问到的细节)
push_backvsemplace_back:emplace_back直接在容器末构造对象(避免一次临时拷贝/移动)。reserve:对vector预分配容量以避免多次 realloc(均摊复杂度)。- 容器复杂度与迭代器失效规则(面试常问)。举例:
vector:reallocation(如 push_back 导致容量增长)会使所有指针/引用/迭代器失效;在中间insert/erase会使其后的迭代器失效。list/forward_list:插入/删除不影响除被删除元素外的迭代器(稳定迭代器)。map(平衡树):插入/删除不会使其他元素的引用/迭代器失效(除了被删除的)。unordered_map:rehash会使迭代器失效;插入可能导致 rehash。
allocator基本概念:定制内存分配策略(进阶题)。
面试点:能解释为什么对 vector resize 可能触发移动还是拷贝(取决于元素是否可 noexcept move)。
六、 并发与内存模型(非常重要)
- 数据竞争(Data race):两个或多个线程无同步地访问同一内存位置,且至少一个为写,程序行为未定义。
std::mutex/std::lock_guard/std::unique_lock:RAII 锁封装;std::scoped_lock用于多锁防死锁。std::atomic<T>:提供原子操作和内存序(memory_order_relaxed/acquire/release/seq_cst)。compare_exchange_weakvscompare_exchange_strong:weak 可能虚假失败(适用于循环),strong 不会。- ABA 问题:CAS 仅比较值,若中间值先改为 B 再改回 A 会误判。常用解决:加版本号(tagged pointer)、使用 hazard pointers 或垃圾回收策略。
- 线程同步经典题:
std::condition_variable的使用(生产者-消费者),std::call_once和std::once_flag做线程安全单例。
示例(线程安全单例,C++11 更简单):
MySingleton&instance(){static MySingleton inst;// C++11 保证线程安全的局部静态初始化return inst;}示例(简单生产者-消费者):
std::mutex mu; std::condition_variable cv; std::queue<int> q;voidproducer(){{ std::lock_guard lk(mu); q.push(42);} cv.notify_one();}voidconsumer(){ std::unique_lock lk(mu); cv.wait(lk,[]{return!q.empty();});int v = q.front(); q.pop();}七、 性能与优化实践(面试考点)
- CPU 缓存友好(contiguous memory 优于链表),尽量让热点数据放在一起。
- 减少内存分配(使用内存池 /
reserve)。 - 避免不必要的拷贝(move semantics、emplace)。
- 关注分支预测、内联(
inline)与编译器优化,先用 profiling(perf/gprof)确认热点,再优化。 - 提前测量:microbenchmark(防止过早优化)。
八、 常见进阶题与样例实现(面试常问,附模板)
a) LRU Cache(O(1) get/put)
classLRUCache{int cap; list<int> keys; unordered_map<int, pair<int, list<int>::iterator>> mp;public:LRUCache(int capacity):cap(capacity){}intget(int k){auto it = mp.find(k);if(it == mp.end())return-1; keys.splice(keys.begin(), keys, it->second.second);return it->second.first;}voidput(int k,int v){auto it = mp.find(k);if(it != mp.end()){ it->second.first = v; keys.splice(keys.begin(), keys, it->second.second);return;}if((int)mp.size()== cap){int old = keys.back(); keys.pop_back(); mp.erase(old);} keys.push_front(k); mp[k]={v, keys.begin()};}};面试点:解释 splice 的常数复杂度和为什么使用 list + unordered_map。
b) 线程安全单例(call_once)
classS{public:static S&instance(){static std::once_flag f;static S* p =nullptr; std::call_once(f,[]{ p =newS();});return*p;}private:S()=default;};c) Copy-swap 异常安全赋值
(见 Buffer 示例)
九、 调试与检测工具(面试可能问会用哪些)
- AddressSanitizer (ASan):检测内存越界、use-after-free。
- UndefinedBehaviorSanitizer (UBSan):检测 UB(如有符号溢出)。
- ThreadSanitizer (TSan):检测 data race。
- Valgrind:检测内存泄漏(Linux)。
- gdb / lldb:调试断点、查看 backtrace。
- perf / Flamegraphs:性能分析。
十、 高频面试问题(附要点回答)
- 为什么要用
unique_ptr而不是裸指针?
→ 表达所有权,自动释放,防止泄漏;shared_ptr代价(引用计数)比unique_ptr高,且会引入循环引用风险。 std::move之后对象状态如何?
→ 留在“可析构但未指定状态”,只能赋值或析构;使用前须重新赋值或立即处理。volatile在 C++ 中的作用?
→ 不用于线程同步;仅抑制某些编译器优化,真正并发应使用std::atomic。- 如何避免死锁?
→ 统一锁顺序、使用std::scoped_lock、用try_lock超时退让、减少锁粒度。 - 如何写高性能 IO / 内存敏感代码?
→ 减少 system call、使用缓冲、减少分配、考虑内存对齐/预取/向量化。
小建议
- 练习方式:读题后写出 O(1)/O(n) 解法,然后讨论边界、异常安全、并发假设与性能瓶颈。
- 面试时不要只写能过的代码,还要能解释时间/空间复杂度、是否有 UB、异常安全级别、并发安全假设与潜在改进。