C++中std::vector Vs std::deque VS std::list的對比分析
1) 核心差異速覽
std::vector:連續(xù)內(nèi)存、隨機訪問 O(1)、尾部push_back攤還 O(1)、中間插入/刪除 O(n),非常緩存友好。std::deque:分段(block)存儲,不是整體連續(xù);隨機訪問 O(1)(但常數(shù)比 vector 大);兩端插入/刪除攤還 O(1);中間插入/刪除 O(n)。適合雙端隊列。std::list:雙向鏈表,節(jié)點分散、隨機訪問 O(n)、在已知位置插入/刪除 O(1)、splice(跨 list 移動)O(1) 并且不使其他迭代器失效(被移動元素的迭代器繼續(xù)有效但指向新容器)。適合需要穩(wěn)定指針/節(jié)點移動而不復制元素的場景。
2) 內(nèi)存布局與緩存局部性(關(guān)鍵影響性能)
std::vector:元素緊密連續(xù)排列(C-style 數(shù)組),單次分配大塊內(nèi)存 → 最佳緩存局部性 / CPU 預取效果好,遍歷與數(shù)值運算非???。適合數(shù)值、圖像、點云等大量數(shù)據(jù)的順序處理。std::deque:實現(xiàn)為若干固定大小塊(segments)+ 一個“map”(指針數(shù)組)指向塊。元素在邏輯上是連續(xù)的,但物理上分段存放;隨機訪問需要兩級尋址(map + block index),因此本地性和常數(shù)開銷不如vector。std::list:每個元素封裝在節(jié)點里(元素 + 前向/后向指針),每個節(jié)點通常是獨立分配 → 極差的緩存局部性 & 大量小分配開銷。只有在需要節(jié)點穩(wěn)定性和 O(1) 插入/刪除時才有價值。
3) 算法復雜度(常見操作 Big-O)
(常見且直觀版本)
| 操作 | vector | deque | list |
|---|---|---|---|
| 隨機訪問 operator[] | O(1)(連續(xù)) | O(1)(分段) | O(n) |
| push_back | 攤還 O(1)(可能重分配) | 攤還 O(1) | O(1) |
| push_front | O(n)(要移動元素) | 攤還 O(1) | O(1) |
| 插入/刪除 中間位置 | O(n)(移動元素) | O(n)(移動/拷貝) | O(1)(若已知迭代器) |
| splice / 跨容器移動 | — | — | O(1)(不復制元素) |
| 遍歷(線性訪問成本) | 最快(緩存) | 中等 | 最慢(指針跳轉(zhuǎn)) |
注:
vector 的尾部 push_back 是攤還 O(1)(因為擴容策略),但發(fā)生重分配時會拷貝/移動全部元素。
deque 在兩端插入通常是 O(1),但插入可能導致 map 調(diào)整。list 的插入/刪除在已定位節(jié)點時是真正 O(1)。(表中結(jié)論與 cppreference 的復雜度說明一致)
4) 迭代器 / 指針 / 引用失效規(guī)則(非常重要)
這是容易出 bug 的地方,下面列出常見操作如何影響已有的迭代器/指針/引用(以當前標準行為為準)。
std::vector
- 重分配(capacity 變化):會使所有迭代器、指針和引用失效(因為底層緩沖區(qū)搬移)。
push_back/emplace_back:若觸發(fā)重分配 → 所有失效;若不觸發(fā)重分配 → 僅影響end()(過去的 end 不再有效)。insert(非尾部):若不重分配 → 使插入點之后的迭代器/引用失效(元素被移動);若重分配 → 全部失效。erase:擦除后從被擦除位置到末尾的迭代器/引用失效。
std::deque
規(guī)則稍復雜,總結(jié)常見點:
- 在中間
insert/erase:通常會使所有迭代器失效(實現(xiàn)細節(jié)有所不同,但應當假設(shè)會全失效)。 - 在兩端(
push_front/push_back/pop_front/pop_back):通常是 O(1),但有可能使迭代器失效(特別是當內(nèi)部 map/blocks 需要擴展時)。擦除兩端通常只使被擦除元素的迭代器失效,但end()在某些情況也會失效。簡而言之:不要對 deque 假設(shè)嚴格的迭代器穩(wěn)定性。
std::list
- 插入/移動(
insert,splice等):不會使其他迭代器/引用失效(節(jié)點只是調(diào)整指針)。 - 擦除:只有指向被擦除元素的迭代器/引用失效;其他迭代器保持有效。
splice:把一段節(jié)點從一個 list 移到另一個 list,不復制元素、也不失效迭代器(但指向被移動元素的迭代器現(xiàn)在屬于新容器)。這點非常有用(實現(xiàn) LRU、鏈表合并等)。
5) 典型使用場景與實戰(zhàn)建議(什么時候選哪個)
優(yōu)先選擇 std::vector:
- 默認容器:絕大多數(shù)場景(順序訪問、數(shù)值計算、排序、與 C API 交互、內(nèi)存緊湊很重要)都用
vector。 - 優(yōu)化項:對頻繁
push_back的場景先reserve(),用emplace_back()來避免多余拷貝/移動。reserve可避免重分配從而保持指針/引用穩(wěn)定。
選擇 std::deque:
- 需要在兩端高效插入/刪除(如雙端隊列、實現(xiàn)
std::stack默認底層)。 - 不需要與 C 風格連續(xù)內(nèi)存交互,且能接受略差的緩存局部性。不要以為 deque 保證插入不失效 — 對迭代器失效要有防范。
選擇 std::list:
- 必須在容器內(nèi)部頻繁在已知迭代器處做 O(1) 插入/刪除,且必須要迭代器/引用穩(wěn)定(例如實現(xiàn)復雜鏈式結(jié)構(gòu)、需要頻繁 splice 的場景)。
- 否則通常不推薦:鏈表遍歷慢、內(nèi)存開銷大(節(jié)點 + 分配器元數(shù)據(jù)),并且常常有更好的替代(
vector+ index、deque、或 intrusive containers)。
6) 常見陷阱 & 優(yōu)化技巧(實戰(zhàn)干貨)
- 默認用
vector:現(xiàn)代 C++ 社區(qū)共識是“首選vector,除非有明確理由”。 - 避免頻繁小分配:
std::list每個節(jié)點通常單獨分配,帶來 malloc/allocator 開銷;如果必須,考慮自定義內(nèi)存池或boost::intrusive_list。 - 用
reserve()避免重分配:對于vector,若能預知元素數(shù)量,v.reserve(n)會大幅降低重分配/拷貝成本并保持指針穩(wěn)定。
刪除元素時的正確寫法(在遍歷中安全刪元素):
vector / deque:
for (auto it = c.begin(); it != c.end(); ) {
if (should_erase(*it)) it = c.erase(it); // erase 返回下一個迭代器(C++11 起)
else ++it;
}
list:
for (auto it = l.begin(); it != l.end(); ) {
if (cond) it = l.erase(it); // O(1),其他迭代器不受影響
else ++it;
}
erase-remove 慣用法:對 vector 批量刪除滿足條件的元素應該使用:
v.erase(std::remove_if(v.begin(), v.end(), pred), v.end());
這比在循環(huán)中逐個 erase 快很多。
- 避免在
deque中假設(shè)迭代器穩(wěn)定性:如果需要穩(wěn)定引用并且要頻繁在兩端操作,重新驗證你的需求;有時vector+ index 更簡單且更快。 splice是鏈表的王牌:當需要移動大量元素而不做復制/構(gòu)造/析構(gòu)時,用list::splice(O(1))能大幅提高性能。
7) 代碼示例 — 常見用法與陷阱演示
預分配避免重分配(vector)
std::vector<MyType> v; v.reserve(1000); // 預分配,避免擴容導致的整體移動 for (...) v.emplace_back(...); // in-place 構(gòu)造,避免拷貝
遍歷時刪除(安全寫法)
// vector / deque
for (auto it = v.begin(); it != v.end(); ) {
if (need_erase(*it)) it = v.erase(it);
else ++it;
}
// list
for (auto it = l.begin(); it != l.end(); ) {
if (need_erase(*it)) it = l.erase(it); // 只使被刪節(jié)點的迭代器失效
else ++it;
}
list::splice(O(1) 移動節(jié)點,不復制)
std::list<int> a{1,2,3,4}, b{10,11};
// 把 a 中的 [2,4) 移到 b 的開頭
auto first = std::next(a.begin()); // 指向 2
auto last = std::next(first, 2); // 指向 4 (不含)
b.splice(b.begin(), a, first, last); // O(1)
8) 快速參考表(便于記憶)
- 需要最快的遍歷/數(shù)值處理/與 C 互操作 →
std::vector - 需要兩端高效插入/彈出(deque/queue/stack) →
std::deque - 需要在已知位置 O(1) 插入/刪除 & 穩(wěn)定迭代器/指針/引用;或需要 O(1) 跨容器移動節(jié)點 →
std::list(并考慮內(nèi)存/緩存開銷)
9)std::queue應用示例
下面是一個 C++11/17 標準庫實現(xiàn)的多線程生產(chǎn)者-消費者模型完整示例。
采用 std::thread、std::mutex、std::condition_variable,隊列用 std::queue 封裝,支持多生產(chǎn)者、多消費者。
示例代碼
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <chrono>
// 線程安全隊列
template <typename T>
class ThreadSafeQueue {
public:
void push(T value) {
{
std::lock_guard<std::mutex> lock(mtx_);
queue_.push(std::move(value));
}
cv_.notify_one(); // 喚醒一個等待的消費者
}
T pop() {
std::unique_lock<std::mutex> lock(mtx_);
cv_.wait(lock, [this]{ return !queue_.empty() || done_; });
if (queue_.empty()) {
return T(); // 若結(jié)束且隊列空,返回默認值
}
T value = std::move(queue_.front());
queue_.pop();
return value;
}
void shutdown() {
{
std::lock_guard<std::mutex> lock(mtx_);
done_ = true;
}
cv_.notify_all(); // 喚醒所有等待線程
}
private:
std::queue<T> queue_;
std::mutex mtx_;
std::condition_variable cv_;
bool done_ = false;
};
// ------------------- 生產(chǎn)者/消費者測試 -------------------
void producer(ThreadSafeQueue<int>& q, int id, int count) {
for (int i = 0; i < count; i++) {
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模擬耗時
int value = id * 100 + i;
q.push(value);
std::cout << "[Producer " << id << "] produced: " << value << "\n";
}
}
void consumer(ThreadSafeQueue<int>& q, int id) {
while (true) {
int value = q.pop();
if (value == 0 && q.pop() == 0) break; // 簡單退出條件(可換為特殊結(jié)束標記)
if (value != 0) {
std::cout << " [Consumer " << id << "] consumed: " << value << "\n";
}
}
}
int main() {
ThreadSafeQueue<int> q;
// 啟動多個生產(chǎn)者和消費者
std::thread p1(producer, std::ref(q), 1, 5);
std::thread p2(producer, std::ref(q), 2, 5);
std::thread c1(consumer, std::ref(q), 1);
std::thread c2(consumer, std::ref(q), 2);
// 等生產(chǎn)者結(jié)束
p1.join();
p2.join();
// 通知消費者結(jié)束
q.shutdown();
c1.join();
c2.join();
std::cout << "All threads finished.\n";
return 0;
}
運行邏輯
- 生產(chǎn)者線程 持續(xù)往隊列里放任務 (
push)。 - 消費者線程 調(diào)用
pop,若隊列為空則阻塞等待。 shutdown()用于通知消費者退出。std::condition_variable保證高效等待,而不是忙輪詢。
輸出示例(順序可能不同,因為多線程)
[Producer 1] produced: 100
[Producer 2] produced: 200
[Consumer 1] consumed: 100
[Consumer 2] consumed: 200
[Producer 1] produced: 101
[Producer 2] produced: 201
[Consumer 1] consumed: 101
[Consumer 2] consumed: 201
...
All threads finished.
10)總結(jié)
- 首選
std::vector(最快、最省內(nèi)存、最常用); - 如果必須在兩端頻繁操作,考慮
std::deque(犧牲一些局部性和常數(shù)); - 只有在確實需要節(jié)點級別穩(wěn)定性或 O(1) splice/插入/刪除時才用
std::list(并準備承擔內(nèi)存與緩存的代價)。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
C++判斷主機是否處于聯(lián)網(wǎng)狀態(tài)
這篇文章主要為大家詳細介紹了C++判斷主機是否處于聯(lián)網(wǎng)狀態(tài),具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-06-06
C++中sort()函數(shù)和priority_queue容器中比較函數(shù)的區(qū)別詳析
C++中sort()和priority_queue都能自定義比較函數(shù),其中sort()自定義的比較函數(shù)比較好理解,priority_queue中自定義的比較函數(shù)的效果和sort()是相反的,這篇文章主要給大家介紹了關(guān)于C++中sort()函數(shù)和priority_queue容器中比較函數(shù)的區(qū)別的相關(guān)資料,需要的朋友可以參考下2023-03-03
新舊MFC版本實現(xiàn)CEdit透明的2種方法的實例代碼
新舊MFC版本實現(xiàn)CEdit透明的2種方法的實例代碼,需要的朋友可以參考一下2013-03-03

