基礎知識
一、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
,用于線程同步。
- 結合
七、設計模式
-
單例模式實現?
class Singleton { private:Singleton() {} public:static Singleton& getInstance() {static Singleton instance; // C++11 保證線程安全return instance;} };
-
工廠模式、觀察者模式 —— 常考理論。
八、常見算法題型
-
二分查找模板
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; }
-
快速排序模板
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); }
-
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
,容器在擴容等操作時可能退回到拷貝(性能或語義影響)。
示例(拷貝-移動-釋放的正確實現):
class Buffer {size_t n_;int* data_;
public:Buffer(size_t n=0): n_(n), data_(n ? new int[n]() : nullptr) {}~Buffer(){ delete[] data_; }// copyBuffer(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;}// 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;}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)——必須會舉例并解釋
常見 UB:
- 訪問釋放后的內存(use-after-free)。
- 雙重釋放(double free)。
- 有符號整數溢出(
int
溢出是 UB)。 - 解引用空指針。
- 同時無同步的并發讀寫(data race)。
示例:
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 更簡單):
MySingleton& instance() {static MySingleton inst; // C++11 保證線程安全的局部靜態初始化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 異常安全賦值
(見 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、異常安全級別、并發安全假設與潛在改進。