基础知识
一、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::thread
std::async
std::packaged_task
-
互斥锁和自旋锁区别?
- 互斥锁:阻塞等待,适合长任务。
- 自旋锁:忙等待,适合短任务。
-
条件变量 (condition_variable) 用法?
- 结合
unique_lock
,用于线程同步。
- 结合
七、设计模式
-
单例模式实现?
cppclass Singleton { private: Singleton() {} public: static Singleton& getInstance() { static Singleton instance; // C++11 保证线程安全 return instance; } };
-
工厂模式、观察者模式 ------ 常考理论。
八、常见算法题型
-
二分查找模板
cppint 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; }
-
快速排序模板
cppvoid 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); }
-
LRU 缓存(哈希表 + 双链表)
- 高频考点,需熟练掌握。
九、综合类问题
- 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
,容器在扩容等操作时可能退回到拷贝(性能或语义影响)。
示例(拷贝-移动-释放的正确实现):
cpp
class Buffer {
size_t n_;
int* data_;
public:
Buffer(size_t n=0): n_(n), data_(n ? new int[n]() : nullptr) {}
~Buffer(){ delete[] data_; }
// copy
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){ // copy-and-swap 提供强异常安全
swap(*this, o);
return *this;
}
// move
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 风格):
cpp
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)------必须会举例并解释
常见 UB:
- 访问释放后的内存(use-after-free)。
- 双重释放(double free)。
- 有符号整数溢出(
int
溢出是 UB)。 - 解引用空指针。
- 同时无同步的并发读写(data race)。
示例:
cpp
int a = INT_MAX;
int b = a + 1; // 有符号溢出 ------ UB(不要假设会 wrap-around)
面试点:说明 UB 会让编译器基于假设做优化,从而产生难以预期的行为。
五、STL 深入(常被问到的细节)
-
push_back
vsemplace_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
vscompare_exchange_strong
:weak 可能虚假失败(适用于循环),strong 不会。- ABA 问题:CAS 仅比较值,若中间值先改为 B 再改回 A 会误判。常用解决:加版本号(tagged pointer)、使用 hazard pointers 或垃圾回收策略。
- 线程同步经典题:
std::condition_variable
的使用(生产者-消费者),std::call_once
和std::once_flag
做线程安全单例。
示例(线程安全单例,C++11 更简单):
cpp
MySingleton& instance() {
static MySingleton inst; // C++11 保证线程安全的局部静态初始化
return inst;
}
示例(简单生产者-消费者):
cpp
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)
cpp
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
)
cpp
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 异常安全赋值
(见 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、异常安全级别、并发安全假设与潜在改进。