跳到主要内容
C++ 核心面试题与底层原理详解 | 极客日志
C++ 算法
C++ 核心面试题与底层原理详解 C++ 面试涵盖基础语法、内存管理、STL、并发及设计模式等核心领域。本文梳理了值传递、虚函数表、智能指针、RAII 原则及异常安全等关键知识点,并提供了 LRU 缓存、快速排序等经典算法模板。重点解析了 C++11 新特性、多线程同步机制及性能优化实践,帮助开发者系统掌握底层原理与工程落地细节。
基础知识
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::thread
std::async
std::packaged_task
互斥锁和自旋锁区别?
互斥锁:阻塞等待,适合长任务。
自旋锁:忙等待,适合短任务。
条件变量 (condition_variable) 用法?
设计模式 class Singleton {
private :
Singleton () {}
public :
static Singleton& getInstance () {
static Singleton instance;
return instance;
}
};
常见算法题型 void quickSort (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);
}
int binarySearch (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;
else if (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 容器在需要判断是否可用 noexcept move 时会选择拷贝或移动;若移动不是 noexcept,容器在扩容等操作时可能退回到拷贝(性能或语义影响)。
class Buffer {
size_t n_;
int * data_;
public :
Buffer (size_t n = 0 ) : n_ (n), data_ (n ? new int [n]() : nullptr ) {}
~Buffer () { delete [] data_; }
Buffer (const Buffer& o) : n_ (o.n_), data_ (o.n_ ? new int [o.n_] : nullptr ) {
std::copy (o.data_, o.data_ + n_, data_);
}
Buffer& operator =(Buffer o) {
swap (*this , o);
return *this ;
}
Buffer (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 ;
}
friend void swap (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 <typename T>
void push_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)——必须会举例并解释
访问释放后的内存(use-after-free)。
双重释放(double free)。
有符号整数溢出(int 溢出是 UB)。
解引用空指针。
同时无同步的并发读写(data race)。
int a = INT_MAX;
int b = a + 1 ;
面试点:说明 UB 会让编译器基于假设做优化,从而产生难以预期的行为。
STL 深入(常被问到的细节)
push_back vs emplace_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_weak vs compare_exchange_strong:weak 可能虚假失败(适用于循环),strong 不会。
ABA 问题:CAS 仅比较值,若中间值先改为 B 再改回 A 会误判。常用解决:加版本号(tagged pointer)、使用 hazard pointers 或垃圾回收策略。
线程同步经典题:std::condition_variable 的使用(生产者 - 消费者),std::call_once 和 std::once_flag 做线程安全单例。
MySingleton& instance () {
static MySingleton inst;
return inst;
}
std::mutex mu;
std::condition_variable cv;
std::queue<int > q;
void producer () {
{
std::lock_guard lk (mu) ;
q.push (42 );
}
cv.notify_one ();
}
void consumer () {
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) class LRUCache {
int cap;
list<int > keys;
unordered_map<int , pair<int , list<int >::iterator>> mp;
public :
LRUCache (int capacity) : cap (capacity) {}
int get (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;
}
void put (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) class S {
public :
static S& instance () {
static std::once_flag f;
static S* p = nullptr ;
std::call_once (f, []{ p = new S (); });
return *p;
}
private :
S () = default ;
};
c) Copy-swap 异常安全赋值
调试与检测工具(面试可能问会用哪些)
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、异常安全级别、并发安全假设与潜在改进。
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online