這段內容講的是 Qt 容器(Qt Containers)和標準庫容器(STL Containers)之間的選擇和背景:
主要觀點:
- Qt 容器的歷史背景
- Qt 自身帶有一套容器類(如
QList
,QVector
,QMap
等),主要是因為歷史原因:- 早期Qt需要支持沒有標準庫(STL)支持的平臺。
- 避免將標準庫的符號暴露在Qt的ABI(應用二進制接口)中,保證二進制兼容性。
- Qt 自身帶有一套容器類(如
- 現在的情況(Qt 5及以后)
- Qt 5以后,已經假定目標平臺有“可用的、可用的STL實現”。
- 這意味著Qt本身內部開始依賴標準庫,標準庫的可用性已成為前提。
- Qt容器在API中的角色
- Qt依然在API中使用Qt容器,且這些容器對應用程序開發者開放。
- 但對于新項目和業務代碼,建議優先使用標準庫容器,只有在必要時才用Qt容器。
總結
- Qt容器是為了歷史兼容和API穩定性存在。
- 對于大多數現代C++項目,尤其是跨平臺和與第三方庫交互,推薦使用標準庫容器(如
std::vector
,std::map
等)。 - 僅在和Qt框架接口交互時,或者特殊性能需求時考慮Qt容器。
這部分內容詳細對比了 Qt 容器和 C++ 標準庫容器的設計哲學及對應關系,并介紹了一個特殊的 Qt 容器——QVarLengthArray。以下是總結:
Qt 容器設計哲學 vs 標準庫設計哲學
特點 | Qt容器 | 標準庫容器 |
---|---|---|
目標 | 足夠好,主要用于構建 GUI | 真正通用,適合各種場景 |
API 風格 | 使用 camelCase(例如 isEmpty() ) | 使用 snake_case(例如 empty() ) |
設計重點 | 易用性和API易發現性 | 高效和正確性 |
典型用法示例 | QVector<int> v; v << 1 << 2 << 3; | std::vector<int> v; v.push_back(1); |
拷貝行為 | 拷貝可能較“便宜”(淺拷貝或引用計數機制) | 拷貝通常是深拷貝 |
算法實現 | 作為成員函數(如 contains() ) | 通過標準算法(如 std::find ) |
Qt與標準庫對應容器對照表
Qt 容器 | 標準庫容器 |
---|---|
QVector | std::vector |
QList | — |
QLinkedList | std::list |
QVarLengthArray | — |
QMap | std::map |
QMultiMap | std::multimap |
QHash | std::unordered_map |
QMultiHash | std::unordered_multimap |
QSet | std::unordered_set |
QVarLengthArray 介紹
- QVarLengthArray 是 Qt 中一個特殊的容器,類似于
std::vector
,但它預先在棧上分配一定的空間,避免頻繁的堆分配。 - 類似于 Boost 的
small_vector
,支持所謂的“短小優化”(SSO,small string optimization思想),提高小規模數據的性能。 - 適用于大多數情況下容器元素數量不會超過某個固定值的場景。
- 示例聲明:
QVarLengthArray<Obj, 32> vector;
表示預分配32個對象的空間。
這段內容進一步強調了 QList 以及整體 Qt容器 的局限性,并建議更傾向使用標準庫容器。總結如下:
QList 的特性和問題
- 不是鏈表,而是基于數組實現的列表。
- 對于存儲大于指針大小的對象非常低效,因為它會為每個對象單獨分配堆內存。
- 建議避免使用 QList,除非別無選擇。
- 自己寫代碼時優先使用 QVector。
不建議使用 Qt 容器的理由
- Qt 容器維護和更新不活躍,缺乏新特性。
- STL 容器更快,生成的代碼更小且經過更多測試。
- Qt 容器功能遠不及 STL,例如:
- 存放的類型必須可默認構造和可復制。
- 沒有異常安全保證。
- 缺少許多 C++98/C++11/C++17 新增的API,如范圍構造、插入、就地構造(emplace)、基于節點的操作等。
- 不支持靈活的分配器、比較器、哈希函數等自定義操作。
- Qt 容器的API不一致,比如
QVector<T>
支持append(T&&)
,但QList<T>
不支持。 - 還有在 resize、capacity、shrink 等行為上的差異。
- Qt 容器API 與 STL 容器存在微妙差異,可能帶來使用上的困擾。
建議
- 優先使用 STL 容器(如
std::vector
,std::map
,std::unordered_map
等)。 - 只有在必須與 Qt API 交互時,才考慮使用 Qt 容器。
這段主要給出了在實際項目中選擇容器的建議,核心點總結如下:
選擇哪個容器?
- STL 容器大多數情況下性能和特性都優于 Qt 容器。
- Qt 自身實現內部也開始采用 STL 容器,說明它們的優勢。
- Qt 的 API 仍然暴露 Qt 容器,無法輕易替換,因為 Qt 有強 API 和 ABI 兼容性承諾。
應用程序的推薦策略:
- 優先使用 STL 容器。
- 僅在以下情況考慮使用 Qt 容器:
- 沒有對應的 STL 或 Boost 容器(這幾乎不存在)。
- 與 Qt 或基于 Qt 的庫接口交互時。
- 如果用到了 Qt 容器,盡量避免來回轉換 STL 和 Qt 容器。
保持使用 Qt 容器,減少性能開銷和復雜度。
簡單來說,除非為了兼容 Qt 接口,推薦用 STL 容器,既現代又高效。
關于 resize、capacity、shrink 這幾個行為,Qt 容器和 STL 容器確實存在一些細節差異:
1. resize()
- STL 容器
resize(n)
會調整容器大小到n
,如果變大,會用默認值或指定值填充新增元素。- 對于
std::vector
,新增元素構造且初始化。 - 可以保證元素連續且大小準確。
- Qt 容器(比如
QVector
)resize(n)
也會調整大小,但內部實現可能采用引用計數共享數據。- 新增元素初始化行為和 STL 類似,但某些情況下效率可能略差。
QList
的行為因內部結構不同,resize()
可能導致額外的內存分配,效率不佳。
2. capacity()
- STL 容器
capacity()
返回當前已分配但未使用的內存空間大小。std::vector
會預先分配一定空間,減少擴容次數。- 可以通過
reserve()
來預先分配容量,避免多次重新分配。
- Qt 容器
capacity()
也返回預分配空間大小。- 但 Qt 容器(尤其是老版本)對容量管理不如 STL 靈活,擴容策略可能不同。
- 不能像 STL 一樣明確調用
reserve()
保證容量。
3. shrink_to_fit()
- STL 容器
- C++11 引入的函數,
shrink_to_fit()
用于請求減少容量以匹配當前大小。 - 實現是非強制的,但多數現代庫會釋放多余內存。
- 提高內存利用率,避免浪費。
- C++11 引入的函數,
- Qt 容器
- 大多數 Qt 容器沒有
shrink_to_fit()
接口。 - 只能通過拷貝或交換技巧手動釋放多余容量,比如重新構造一個容器拷貝數據。
- 缺乏直接控制容量的函數,不夠靈活。
- 大多數 Qt 容器沒有
額外說明
- Qt 容器內部通常使用引用計數和共享數據的技術,這導致某些操作(比如 resize)會更復雜,可能出現延遲復制(copy-on-write)。
- STL 容器行為更加透明直接,便于性能優化和行為預測。
總結
操作 | STL 容器 | Qt 容器 |
---|---|---|
resize() | 直接調整大小,初始化新增元素 | 類似,但可能因共享數據延遲復制 |
capacity() | 返回預分配容量,可用 reserve 控制 | 返回預分配容量,容量管理不夠靈活 |
shrink_to_fit() | 標準接口,嘗試釋放多余內存 | 無對應接口,需手動技巧釋放多余容量 |
總結來說,針對 Qt 6:
- Qt 容器必須繼續保留,確保兼容性和穩定的 API/ABI,不會做大破壞性改動。
- QList 在 Qt 中使用非常廣泛,但它其實并不是一個理想的線性容器。
- 未來有可能讓 QList 直接成為 QVector 的別名(typedef),簡化內部實現。
- 同時,Qt 可能會推出一個新的容器類型(比如 QArrayList)來替代 QList 的部分功能,提供更好的性能和設計。
- Qt 容器通過類型特征(type traits)來優化性能,尤其是判斷一個類型是否支持relocatable(可重定位)。
- 如果類型是可重定位的,容器擴容時可以直接用
realloc
這樣高效的內存操作,而不需要一個個移動元素,性能大幅提升。 - 使用 Qt 容器時,建議用
Q_DECLARE_TYPEINFO
宏來告訴 Qt該類型是否可重定位,從而啟用優化。 - 一些典型例子:
- 簡單結構體(如
IntVector
)通常是可重定位的。 - 有指針指向自己或有內部聯系的結構(如
TreeNode
)通常不可重定位,因為移動內存會破壞指針。 - 有短字符串優化(SSO)的字符串類型,如果內部指針指向內部緩沖區,也不可重定位。
這個機制可以顯著提高 Qt 容器的性能,前提是正確聲明類型信息。
- 簡單結構體(如
編譯器不能自動判斷類型是否可重定位(relocatable),需要開發者手動標注。
- Qt 通過宏
Q_DECLARE_TYPEINFO(Type, Kind)
來告訴容器該類型的“性質”,Kind 可以是:- Q_PRIMITIVE_TYPE
- 類型非常簡單,比如
int
,任何位模式都是有效的 - 構造和析構可以跳過,直接內存拷貝即可
- 類型非常簡單,比如
- Q_MOVABLE_TYPE
- 類型可被內存移動(如用
memmove
或realloc
) - 但仍然調用構造和析構函數
- 類型可被內存移動(如用
- Q_COMPLEX_TYPE(默認)
- 普通復雜類型,需要正常調用構造、復制、析構
- Q_PRIMITIVE_TYPE
- EASTL 有類似機制(
EASTL_DECLARE_TRIVIAL_RELOCATE
),而 STL 標準庫本身沒有明確這個特性。
這讓 Qt 容器能根據類型特性選擇最優內存操作,提高性能。
每次定義可能會被放入 Qt 容器的自定義類型時,都應該用 Q_DECLARE_TYPEINFO
顯式聲明其類型信息。
- 例如:
struct IntVector {int size, capacity;int *data; }; Q_DECLARE_TYPEINFO(IntVector, Q_MOVABLE_TYPE);
- 如果之后再加這個 trait,有可能會導致 ABI 兼容性問題,影響程序穩定性和升級安全。
所以,建議一開始就定義好,避免后期修改帶來的麻煩。
Qt 的**隱式共享(Implicit Sharing)**核心思想是:
- 對象內部包含一個指向實際數據(pimpl)的指針,這個數據塊有一個引用計數器(refcount)。
- 創建對象時,refcount = 1。
- 拷貝對象時,只拷貝指針,refcount +1。
- 調用 const 方法不改數據,refcount 不變。
- 調用非 const 方法時,如果 refcount > 1,說明數據被共享,必須先detach(深拷貝數據),保證修改不會影響其他對象(寫時拷貝,Copy-On-Write)。
這樣設計的好處是: - 拷貝操作很輕量(只增引用計數),節省性能。
- 保證數據修改時不會影響到其他對象,實現值語義。
- 但需要注意調用非 const 方法會觸發隱式深拷貝,可能會有性能開銷。
這個機制常見于 Qt 的字符串(QString)、容器等類。
要小心“隱藏的 detach”,即你可能沒有意識到調用了非 const 方法,導致了拷貝開銷。
這里演示了隱式共享在 QVector 中的實際效果。
示意過程是這樣的:
QVector<int> v {10, 20, 30};
QVector<int> v2 = v; // 復制v,不會馬上拷貝數據,而是共享內部數據
v
和v2
共享同一塊內存(payload),里面存著 {10, 20, 30}。- 引用計數(refcount)為 2,表示兩個 QVector 對象共享同一數據。
- 這時,內存只保存了一份數據,拷貝成本很低。
只有當你對v
或v2
調用非 const 方法(修改操作)時,如果 refcount > 1,就會觸發 detach,深拷貝數據,分配獨立內存,避免數據沖突。
總結: - 復制 QVector 很輕量(共享數據 + refcount++)
- 修改共享數據前會觸發深拷貝(detach)
這個例子具體展示了隱式共享(copy-on-write)機制在 QVector 修改時的行為:
QVector<int> v {10, 20, 30};
QVector<int> v2 = v; // v2 共享 v 的數據,refcount = 2,payload = {10, 20, 30}
v2[0] = 99; // 修改 v2,第一個元素變成 99
過程分析:
- 初始時,
v
和v2
共享同一份數據(payload),內容是{10, 20, 30}
,引用計數是 2。 - 當執行
v2[0] = 99
這個寫操作時,v2
檢測到引用計數大于 1(表示數據被共享),觸發detach。 - detach 意味著
v2
會進行一次深拷貝,分配自己的內存來存儲數據。 - 修改只會影響
v2
,v
依舊保持原數據{10, 20, 30}
。 - 結果是:
v
仍然是{10, 20, 30}
,v2
變成了{99, 20, 30}
,- 兩者的數據不再共享,引用計數分別為 1。
這個機制保證了:
- 復制對象時開銷很小,都是共享數據。
- 只有寫操作時才真正做深拷貝,保證數據安全。
這是 Qt 容器里隱式共享的核心思想,也是性能優化的關鍵點。
Qt 的 Implicit Sharing(隱式共享) 的總結。以下是對這段內容的詳細理解解釋:
什么是 Implicit Sharing?
隱式共享是一種 “寫時拷貝”(Copy-On-Write, COW) 機制,結合了引用計數和延遲深拷貝的技術,核心目的是:
- 節省內存
- 提高性能
- 簡化代碼書寫
為什么有用?
“This mechanism makes writing code a lot simpler”
這句話的意思是:你可以像寫普通值一樣寫 Qt 的類(例如 QString
, QByteArray
, QVector
等),而不用擔心性能問題。
比如:
QString a = "hello";
QString b = a; // 不會拷貝數據,只是增加引用計數
b[0] = 'H'; // 觸發深拷貝(detach),a 仍然是 "hello",b 變成 "Hello"
你可以放心用:
- 值返回(
return QString
) - 拷貝賦值(
QString b = a
) - 不需要頻繁考慮性能陷阱
哪些 Qt 類支持 Implicit Sharing?
“The great majority of Qt value classes are implicitly shared”
常見支持隱式共享的類包括:
類型 | 是否支持隱式共享 |
---|---|
QString | 是 |
QByteArray | 是 |
QVariant | 是 |
QImage / QPixmap | 是 |
QVector | 是 |
QList | 是 |
QMap / QHash | 是 |
QVarLengthArray | 否 |
例外:
QVarLengthArray
是值語義容器,不走引用計數,它在棧上直接分配數據,所以不隱式共享。
工作原理簡述:
- 拷貝對象時,不復制數據,只是引用計數 +1。
- 寫入對象時,如果引用計數 > 1,就自動復制一份(detach),以避免影響其他對象。
- 析構對象時,引用計數 -1,當為 0 時釋放資源。
總結:
隱式共享是 Qt 的一大特色。它允許你:
- 寫出高效、簡潔的值語義代碼;
- 不擔心性能陷阱;
- 但也要注意:寫操作(即使看起來是 const)可能隱式觸發深拷貝。
“Implicit sharing and containers: where’s the catch?”指出了 Qt 隱式共享容器的一些隱藏陷阱和誤區。以下是逐句解析與理解:
隱式共享和容器:問題出在哪?
? Handing out references to data inside a container does not make the container unshareable
意思:
即使你將容器中的元素通過引用的方式返回或傳遞出去,比如:
QVector<int> v1 = {1, 2, 3};
QVector<int> v2 = v1; // 現在引用計數是 2
int &ref = v1[0]; // 獲取引用
此時 v1
仍然是可共享的,Qt 不會自動觸發 detach。
也就是說:只是訪問引用,不會破壞共享關系。
? It's easy to accidentally detach a container
意思:
一旦你對容器做了寫操作(哪怕是間接的),就會觸發 detach(深拷貝)。比如:
v2[0] = 100; // 一寫就會 detach,變成獨立的副本
這種操作很容易發生在你沒意識到的地方,從而悄悄改變了對象的共享狀態。
? Accidental detaching can hide bugs
意思:
這種悄悄發生的 detach 行為可能導致 bug 被隱藏,因為:
- 你以為兩個對象共享同一份數據(如
v1
,v2
),但其實不再共享; - 導致數據不同步、調試困難;
- 在多線程或資源受限環境中尤其危險。
例如:
if (v1 == v2) {doSomething(); // 你以為它們是同一份數據,但可能早就 detach 了
}
? IOW, it's not just about performance
IOW = In Other Words(換句話說)
不是只有性能問題,還是“正確性問題”!
- 深拷貝帶來的性能開銷固然重要;
- 代碼邏輯混亂、共享狀態錯亂、數據不一致更加危險;
- 這些 bug 可能非常隱蔽,特別是當代碼中混入了隱藏的 detach 操作。
? Code polluted by (out-of-line) detach/destructor calls
意思:
編譯后的代碼里會因為 Qt 的隱式共享機制,出現許多:
- 隱藏的拷貝構造函數調用
- 深拷貝(detach)操作
- 析構函數調用
這會讓代碼生成“變重”、函數調用棧變復雜,甚至會破壞 inlining,從而降低性能或調試可讀性。
總結:使用 Qt 隱式共享容器的注意事項
項目 | 建議 |
---|---|
寫入操作 | 明確知道何時觸發了 detach |
多對象共享容器時 | 小心副作用、不可預期的獨立副本 |
性能敏感代碼 | 盡量使用 std::vector 等無隱式共享的 STL 容器 |
傳引用/指針訪問內部數據 | 知道不會破壞共享狀態,但不要寫入! |
這段內容解釋了 Qt 隱式共享容器(如 QVector
)中一個容易被忽略的陷阱:引用(包括迭代器)不會阻止容器被拷貝(detach),可能導致代碼行為與你預期的不同。
下面是逐句解釋和深入理解:
Returning references to data inside a container(從容器中返回引用)
“Handing out references to data inside a container does not make the container unshareable”
意思是:
即使你取出了容器中某個元素的引用,這個容器仍然是“共享的”,Qt 不會因為你持有引用而主動拷貝(detach)數據。
也就是說,這不是 COW(Copy-On-Write)觸發的條件。
“E.g. of such references: iterators”
像下面這樣:
QVector<int> v = {10, 20, 30};
auto it = v.begin(); // 拿到迭代器
int &ref = v[0]; // 或直接取引用
這些引用/迭代器不會改變引用計數,也不會讓 Qt 自動 detach。
例子分析:
QVector<int> v {10, 20, 30};
auto &r = v[0]; // 取引用
QVector<int> v2 = v; // 現在 v 和 v2 是共享的,refcount = 2
r = 99; // 修改通過 v 的引用,會影響共享數據!
圖示如下(內存共享前):
v 和 v2 共用同一個 payload:
payload = [10, 20, 30]
refcount = 2
當執行 r = 99;
時,沒有觸發 detach,因為 r
是直接引用底層數據。
結果是:
v = [99, 20, 30]
v2 = [99, 20, 30] <-- 也被修改了!
你以為 v2[0]
還是 10,結果 斷言失敗:
assert(v2[0] == 10); // fails!
關鍵陷阱總結:
行為 | 是否觸發 detach? | 說明 |
---|---|---|
賦值一個容器 | 不觸發 | 引用計數增加 |
調用非 const 成員函數 | 可能觸發 | 如 v[0] = 99 ,會自動復制(detach) |
手動獲取元素引用然后修改 | 不自動 detach | 數據是共享的,兩個容器都會變 |
使用迭代器修改內容 | 不自動 detach | 同樣直接影響共享內存 |
正確做法建議:
- 避免持久使用引用或迭代器后再修改容器副本;
- 如果你想“安全修改一個副本”,請手動 detach:
QVector<int> v2 = v; v2.detach(); // 強制深拷貝 v2[0] = 99; // 不會影響原 v
- 使用 STL 容器(如
std::vector
)時,這種問題不會出現,因為沒有隱式共享。
Qt 隱式共享容器中「意外的深拷貝(accidental detach)」問題。這是 Qt 使用者常常忽略的一大坑。以下是詳細解釋:
你需要理解的核心要點:
例子:
QVector<int> calculateSomething();
const int firstResult = calculateSomething().first();
問題分析:
QVector<T>::first(); // 是非 const 的,返回 T&
這就意味著:
calculateSomething()
生成了一個臨時的QVector<int>
。.first()
是 非 const 成員函數,所以 Qt 會 觸發 detach(即 deep copy 臨時對象的數據)。
但其實你并不需要修改這個容器!你只是想拿第一個值!但 Qt 沒法知道你的意圖,調用了非 const 版本,就要執行 Copy-On-Write。
結果:
你只是想:
const int x = QVector<int>{10, 20, 30}.first();
但 Qt 背后悄悄:
- 創建 QVector 臨時對象
- 進行一次深拷貝(detach)
- 然后返回引用(其實沒用到)
這就引發了不必要的內存分配和復制——而你完全沒有意識到!
正確的寫法:
const int firstResult = calculateSomething().constFirst();
這樣:
.constFirst()
是const
成員函數- 不會觸發 detach
- 沒有不必要的深拷貝
- 返回值仍然是你需要的第一個元素(但是只讀的)
總結:Qt 中意外 detach 的教訓
錯誤寫法 | 原因 | 正確寫法 |
---|---|---|
v.first() | 非 const 成員函數,可能 deep copy | v.constFirst() |
v.last() | 同上 | v.constLast() |
v[i] | 返回引用,非 const,可能 detach | v.at(i) 或 const auto val = v[i]; |
怎么發現這些問題?
- 它們在編譯時不會報錯;
- 但會在 heap profiler(如
massif
,heaptrack
)中看到內存突增; - 一旦你分析出代碼里這些細節,會發現許多“不該拷貝的地方在偷偷拷貝”。
Qt 容器的隱式共享(implicit sharing)機制以及 accidental detach(意外拷貝) 帶來的陷阱。現在我們來逐條解析你說的這個更嚴重的問題:
1. Accidental Detach 導致的 Bug(不是性能問題,而是邏輯錯誤)
場景代碼:
QMap<int, int> map;
// ...
if (map.find(key) == map.cend()) {std::cout << "not found" << std::endl;
} else {std::cout << "found" << std::endl;
}
問題來了:
map.find(key)
是一個 非常量成員函數(non-const);- 它可能導致容器 detach,即做一次深拷貝(復制 pimpl)。
- 而你已經提前調用了
map.cend()
,它指向 原始容器的末尾; - 之后容器被 detach 成新副本,
map.find()
返回的是 新副本的迭代器; - 兩個 end 迭代器 來自不同的容器副本,它們 不相等!
結果:
哪怕 key 根本就不在原始 map 中,也可能打印:
found
這就不是性能問題了,而是一個 邏輯 Bug,非常隱蔽、危險!
正確做法:使用 const 方法
if (map.constFind(key) == map.cend()) {std::cout << "not found" << std::endl;
}
或者,等價更安全:
if (!map.contains(key)) {std::cout << "not found" << std::endl;
}
總結建議:Qt 容器 + 隱式共享 + 非 const 方法 = 潛在 Bug
場景 | 錯誤方法 | 原因 | 正確做法 |
---|---|---|---|
查找元素 | map.find(key) | 可能觸發 detach,破壞邏輯判斷 | map.constFind(key) |
訪問第一個元素 | v.first() | 非 const 方法可能導致 deep copy | v.constFirst() |
遍歷時比較 | 混用 iterator 和 const_iterator | 來自不同容器副本,比較結果錯誤 | 統一用 const_iterator |
🗣 標準庫的立場 vs Qt 的立場
C++ STL | Qt |
---|---|
避免隱式共享機制(copy-on-write) | 依賴 implicit sharing |
強調明確語義、值語義 | 更偏重方便與 API 一致性 |
更安全、更一致 | 更方便、更快捷,但埋雷多 |
如果你開發的是性能敏感或邏輯嚴謹的模塊(比如底層庫、工具鏈),建議: |
- 盡量使用 STL 容器;
- 只在與 Qt API 交互時使用 Qt 容器;
- 嚴格區分 const 和非 const 使用;
- 使用靜態分析工具(如 Clazy)來檢測 Qt-specific misuse。
千萬不要再用 foreach
或 Q_FOREACH
!
原因總結:
foreach (var, container)
等價于以下代碼:
{const auto _copy = container; // 拷貝了整個容器!auto it = _copy.begin(), end = _copy.end();for (; it != end; ++it) {var = *it;body;}
}
嚴重問題:
1. 容器整體拷貝一次:
即使你只想遍歷,但 Qt 容器采用隱式共享機制(copy-on-write),會把整個容器 復制一份!這完全是你意料之外的。
2. 邏輯錯誤隱患:
容器變了,你拿到的是副本,里面元素可能不對,還以為遍歷的是原始容器。
3. 性能問題非常嚴重:
在有大量數據或頻繁迭代的場景下,每次循環都在悄悄 deep copy。
正確做法:使用 C++11 的 range-based for
for (const auto& value : container) {// Safe, efficient, no copy
}
- 不會觸發隱式深拷貝
- 更現代,更清晰
- 完美支持 STL 和 Qt 容器(如
QVector
,QStringList
)
尤其小心 Qt 容器
由于 Qt 容器隱式共享(implicit sharing)+ Q_FOREACH 的復制行為,一起使用等于 踩雷必炸。
例如:
QStringList list = {"a", "b", "c"};
foreach (QString s, list) {// 修改 s 沒問題,但 list 是被拷貝的副本
}
你以為你在修改 list
,其實根本沒改到!
永久禁用建議(企業/團隊級別)
在 .pro
或 CMakeLists.txt
中添加:
DEFINES += QT_NO_FOREACH
這會在你代碼中使用 foreach
/ Q_FOREACH
時 編譯失敗。
總結
特性 | Q_FOREACH | C++11 range-for |
---|---|---|
是否復制容器 | 會復制 | 不會 |
支持 STL 容器 | 否 | 是 |
是否安全高效 | 否 | 是 |
是否推薦使用 | 完全不推薦 | 強烈推薦 |
foreach
/ Q_FOREACH
使用建議總結
“優點”(其實是個誤導):
- “可以安全修改原容器”
其實是誤導:因為循環中你操作的是拷貝副本,不是原容器。真正修改容器行為反而不可控、不清晰。
缺點(致命):
缺點 | 說明 |
---|---|
總是復制容器 | Q_FOREACH 會拷貝一份容器(無論你愿不愿意),即使你只讀。對 STL 容器非常昂貴 |
拷貝的是 const 容器 | 無法修改容器元素(因為元素是 const) |
可讀性差 | 隱式語義不清楚,容易出 bug |
與現代 C++ 不兼容 | 不支持 STL 容器,不支持迭代器,不支持 structured bindings 等 |
將在 Qt 6 中被移除 | 官方明確計劃廢棄此功能 |
最佳實踐
- 使用 C++11 的 range-based for:
for (const auto& item : container) {// 安全、高效、清晰
}
- 禁用 Q_FOREACH / foreach:
在項目中定義:
DEFINES += QT_NO_FOREACH // for qmake
# 或
add_definitions(-DQT_NO_FOREACH) // for CMake
這將使編譯器在你使用 foreach
時直接報錯,強制你使用現代 C++。
額外建議
替換建議 | 替代語法 |
---|---|
foreach (auto x, list) | for (const auto& x : list) |
foreach (QString s, strings) | for (const QString& s : strings) |
foreach (int i, QVector<int>) | for (int i : QVector<int>) |
如果你愿意,我可以: |
- 掃描一個項目中的所有
foreach
并一鍵轉換為 C++11 語法; - 或者寫一個
clang-tidy
規則 / Python 腳本幫助自動替換。
這一節講的是 range-based for 循環(基于范圍的 for 循環) 在 Qt 和 STL 容器上的行為細節,尤其是它可能引發的 隱式分離(implicit detach) 問題。
Range-based for 的真實展開形式:
for (var : container) body;
等價于:
{auto &&c = (container);auto i = begin(c);auto e = end(c);for (; i != e; ++i) {var = *i;body;}
}
STL 容器(如 std::vector<T>
)的行為
i
和e
是std::vector<T>::iterator
- 如果你不在
body
中修改元素或容器,沒有副作用。 - 這也是現代 C++ 推薦的方式。
Qt 容器(如 QVector<T>
)的行為
i
和e
是QVector<T>::iterator
,不是 const_iterator。- 即使你不在
body
中修改容器,也可能導致 隱式 detach:因為 Qt 容器在調用
non-const
成員函數(如begin()
和end()
)時,如果 refcount > 1,會觸發深拷貝(detach)。
例如:
QVector<QString> v = ...;
for (const auto& s : v) {qDebug() << s;
}
乍一看沒問題,但 v.begin()
和 v.end()
是 非 const 成員函數,可能導致:
- 性能開銷:觸發 deep copy
- 行為變化:影響共享數據的其它副本
正確做法(避免 detach)
推薦方式:明確使用 const&
或 const_iterator
:
// 使用 const 引用,避免 detach
for (const QString& s : v) {qDebug() << s;
}
或者,如果你寫模板代碼,優先使用 const QVector<T>&
參數,這樣 begin()
會是 const_iterator
,避免 detach。
小結:如何安全使用 range-based for?
情況 | 建議 |
---|---|
使用 STL 容器 | 直接使用 range-based for,安全高效 |
使用 Qt 容器 | 容器變量加 const& ,元素加 const& |
不確定是否 detach | 用 const_iterator 避免陷阱 |
這一部分是對 Qt 的 Q_FOREACH
和 C++11 的 range-based for loop 在使用 Qt 容器和 STL 容器時的行為差異總結。下面幫你歸納一下關鍵點:
range-based for
vs Q_FOREACH
對比總結
容器類型 | Q_FOREACH | range-based for (auto & : c) | range-based for (const auto & : c) |
---|---|---|---|
Qt 非 const | OK(cheap) | 可能會 detach(非 const 迭代器) | 可能會 detach(begin() 不是 const) |
Qt const | OK(cheap) | 不會 detach(const 迭代器) | 不會 detach(const 迭代器) |
STL 非 const | 會 deep copy(復制一份) | OK | OK |
STL const | 會 deep copy | OK | OK |
為什么有這些區別?
Q_FOREACH
的問題:
- 始終復制容器(哪怕是 const 容器),對于 STL 容器來說是災難性的(深拷貝)。
- 對 Qt 容器沒什么問題,因為 Qt 使用了 implicit sharing,所以復制是廉價的。
- 缺點是代碼難以推理,性能不透明,因此 Qt 6 已廢棄
Q_FOREACH
。
range-based for
的細節:
for (auto &item : container) {// ...
}
- 如果
container
是 Qt 非 const 容器:- 調用的是
begin()
和end()
,它們是 非常量成員函數。 - 如果容器被共享(refcount > 1),會發生 detach(深拷貝)。
- 調用的是
- 如果
container
是const
,就會使用const_iterator
,不會觸發 detach。
實踐建議(寫 Qt 代碼時):
- 避免使用
Q_FOREACH
,在項目中定義:#define QT_NO_FOREACH
- 優先使用 range-based for,但要注意:
const QVector<int> vec = ...; for (const auto &x : vec) { // 安全,不會 detach... } QVector<int> vec = ...; for (auto &x : vec) { // 可能 detach(如果被共享)... }
- 如果一定需要修改容器或元素,考慮:
- 保證容器未共享;
- 或使用
detach()
手動控制。
- 非 const Qt 容器使用 range-based for 循環時,要小心可能觸發隱式 detach。
- 如果不修改容器,盡量用 const 容器或者通過
qAsConst()
(Qt5.7起)或std::as_const()
(C++17起)將容器轉換為 const。 - 不能對臨時(rvalue)直接使用
qAsConst()
,這種情況下先用 const 引用綁定,再循環。
這樣可以避免不必要的深拷貝,提升性能,且代碼更安全。
Clazy,它是基于 Clang 的開源靜態分析工具,專門針對 Qt 代碼。總結一下:
- Clazy 類似于 clang-tidy,但聚焦于 Qt 風格和 Qt 特有的坑。
- 它自帶 50+ 規則檢查,比如:
- detaching-temporary(檢測隱式 detach 相關問題)
- strict-iterators(檢測迭代器使用)
- missing-typeinfo(缺少類型信息)
- foreach(檢測不建議用的 Qt foreach)
- 它還能自動提供 fix-it,幫你自動改代碼。
- 即使是 Qt 自己的代碼庫,也有不少問題被 Clazy 檢測出來。
- 建議定期用 Clazy 掃描你的代碼,修復警告,提升代碼質量和性能。
這工具對保持 Qt 代碼庫的健康和現代化很重要,尤其是避免隱式 detach 等細節導致的性能問題。
Qt 字符串類創建方式
常見的創建字符串的方式很多:
- 直接字符串字面量
"string"
QByteArray("string")
—— 字節數組,無編碼信息QByteArrayLiteral("string")
—— 編譯時常量,不分配內存QString("string")
—— UTF-16編碼字符串,分配內存QLatin1String("string")
—— 輕量視圖,適合拉丁1編碼QStringLiteral("string")
—— 編譯時UTF-16常量,不分配內存(Qt 5.9+)QString::fromLatin1("string")
—— 從Latin1編碼構造QString::fromUtf8("string")
—— 從UTF-8編碼構造tr("string")
—— 用于國際化的字符串QStringView(u"string")
—— 輕量視圖,不分配內存
QByteArray
- 表示字節序列,類似
std::string
,不包含編碼信息 - 隱式共享(copy-on-write)
- 構造函數會分配內存
QByteArray::fromRawData()
可以避免部分分配QByteArrayLiteral()
不分配內存,存儲于只讀段,適合靜態數據
QString
- 使用 UTF-16 編碼,支持 Unicode 操作(優于
std::u16string
) - 隱式共享
- 構造函數會分配內存
QString::fromRawData()
可以避免分配(只讀視圖)- 推薦使用
QStringView
作為輕量字符串視圖 QStringLiteral()
自 Qt 5.9 起不分配內存,數據存儲在只讀段,適合字符串常量
總結:- 用 Qt 來管理 Unicode 字符串,選
QString
;如果只讀且想避免拷貝,選QStringView
或QStringLiteral
。 - 用
QByteArray
處理原始字節流或二進制數據。
“Latin1” 是“ISO 8859-1”編碼的簡稱,全稱是 ISO/IEC 8859-1: Latin Alphabet No. 1。
簡單來說: - 它是一種單字節字符編碼,使用 1 個字節(8 位)表示一個字符;
- 能表示西歐主要語言的字符集,比如英語、法語、德語、西班牙語等;
- 范圍覆蓋了 0x00 到 0xFF 共 256 個字符,其中前 128 個字符和 ASCII 碼完全一樣;
- 它不支持像中文、日文、韓文等復雜字符,只適合基本拉丁字母和西歐符號;
- 在 Qt 里,
QLatin1String
是對 Latin1 編碼字符串的輕量包裝,用來高效處理這類字符串,避免轉碼成本。
總結:Latin1 是一種舊式的、西歐字符編碼,適合只包含拉丁字母的文本,不支持多語言 Unicode。
簡單總結一下 QLatin1String
的作用和特點:
- 它是一個輕量的字符串包裝類,只包含一個
const char*
指針和字符串長度,不做內存管理; - 主要用來表示 Latin1(ISO 8859-1)編碼的字符串字面量,比如代碼里的
"foo"
; - 用于
QString
相關函數的重載,避免不必要的臨時QString
分配和轉換,提高性能; - 例如:
QString::startsWith()
同時有兩個版本,一個接受QString
,一個接受QLatin1String
,后者性能更好,因為不產生臨時字符串。
你可以把它看成是 Qt 里對純 ASCII 或 Latin1 字符串字面量的一個“快捷通道”,用來減少字符串轉換和內存開銷。
總結一下這段內容的重點:
- Qt 的主要字符串類是
QString
和QByteArray
。 - 這幾年對它們的改進不多,保持了比較穩定的設計。
- 從 Qt 5.9 開始,
QStringLiteral
和QByteArrayLiteral
這兩個宏的實現優化了——它們不會再動態分配內存,而是直接使用編譯時生成的靜態內存,這樣可以顯著提升性能。
簡單總結一下 QStringView
:
- 從 Qt 5.10 引入的類型,是一個 非擁有(non-owning) 的字符串視圖,類似于 C++17 標準的
std::u16string_view
。 - 它直接指向一段 UTF-16 編碼的字符序列(比如
QString
內部存儲格式),但不負責管理這段內存。 - 這樣可以避免不必要的字符串拷貝,提升性能,特別適合只讀訪問場景。
- 它提供了和
QString
大部分相似的只讀接口,方便使用。 - Qt 5.11 之后還會有更多的 API 和對
QStringBuilder
的支持,使用體驗會更好。
這段講的是用 QStringView
作為函數參數類型的理由和好處,重點如下:
- 主要用途:
QStringView
適合作為函數參數,尤其是函數需要讀取字符串但不需要保留它時。 - 如果函數需要一個 Unicode 字符串參數,而且函數不會保存這個字符串,推薦用
QStringView
,避免無謂的拷貝和分配。 - 討論了一個例子:
Document::find(StringType substring)
,StringType
用哪個類型好?QString
會強制調用者提供一個完整的QString
,可能要動態分配內存,或者使用QStringLiteral
編譯期字符串。QByteArray
不是 Unicode 安全的,不適合處理 Unicode 文本。QLatin1String
雖然性能好(因為是 Latin-1 編碼),但不 Unicode 安全,不過可以做為額外重載實現快速路徑。
總結:QStringView
既支持 Unicode,也避免了字符串不必要的復制,非常適合作為 API 中接受字符串參數的接口類型。
這部分內容強調了 QStringView
作為接口類型的優勢:
QStringView
是 Unicode 安全的(支持 UTF-16 編碼)。- 它不會進行內存分配(alloc-free),性能好。
- 它可以從多種字符串源構造:
- 編譯時的字符串字面量(
u"compile time"
) - 動態分配的
QString
對象 - 甚至是一個大字符串的子串(通過
QStringView(bigString, 40)
)
舉例:
- 編譯時的字符串字面量(
class Document {iterator find(QStringView substring);
};
Document d;
d.find(u"compile time"); // 傳入編譯時字符串字面量
QString allocatedString = "...";
d.find(allocatedString); // 傳入動態分配的QString
d.find(QStringView(bigString, 40)); // 傳入大字符串的子串視圖
另外,QStringView
還能作為“零分配”切割字符串的工具,比如:
QString str = "...";
QRegularExpression re("reg(.*)ex");
QRegularExpressionMatch match = re.match(str);
if (match.hasMatch()) {QStringView cap = match.capturedView(1); // 直接獲取子串視圖,無需分配內存// ...
}
總結:QStringView
讓字符串處理既高效又安全,非常適合作為函數參數和字符串子串的視圖類型。
這一部分講的是 QStringView
對 Qt API 的巨大影響:
- 許多 Qt 函數現在接受
QString
參數,但實際上并不需要持有字符串數據,只是讀取它們。 - 在 Qt 6 中,應該將這些函數改為接受
QStringView
,這樣避免不必要的內存分配和拷貝,提高性能。 - Qt 5 里還有一個類似的非擁有字符串視圖類型叫
QStringRef
,但它設計有缺陷:- 它必須綁定到一個
QString
對象,而不能直接表示任何 UTF-16 字符序列。 - 因此靈活性較差。
- 它必須綁定到一個
QStringRef
只是權宜之計,建議如果迫切需要用字符串視圖,可以暫時用它,但隨著QStringView
在功能上達到 API 完整性,QStringRef
會被廢棄。
總結:QStringView
是更現代、更高效的字符串視圖接口,未來 Qt 版本將以它為標準,替代舊的字符串引用方式。
POD(Plain Old Data) 平凡數據類型 類型是否等同于“可搬移”(relocatable)?
- 答案是否定的:POD和可搬移是兩個獨立的概念。
- 一個類型是否可搬移(relocatable)與它是否是POD類型無關。
- 可搬移類型可能有非平凡的構造函數和析構函數,比如 Qt 里基于pimpl(指針實現)的值類。
- 反過來,即使是平凡(trivial)的類型,也未必可搬移——比如某些類型的對象地址本身代表身份(identity),搬移會破壞語義。
- 所有C數據類型都是trivial,但不一定是relocatable。
另外,關于Qt中廢棄的API(deprecated APIs): - Qt會標記舊的API為廢棄,雖然它們仍然可用且通過測試,但Qt 6版本會移除大部分廢棄API。
- Qt源碼中通過宏
QT_DEPRECATED_SINCE(major, minor)
來標記,并用QT_DEPRECATED_X("建議替代方法")
生成編譯警告。 - 你可以通過定義宏來控制廢棄API的使用:
- 定義
QT_DEPRECATED_WARNINGS
來開啟廢棄API的警告。 - 定義
QT_DISABLE_DEPRECATED_BEFORE=版本號
來將早于某版本的廢棄API使用視為錯誤。
例如:
- 定義
DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x050900
這樣就強制不允許使用 Qt 5.9.0 及更早版本中廢棄的API。
總結:
- POD ≠ relocatable,二者語義不同。
- Qt鼓勵逐步遷移,避免使用廢棄API,尤其是升級到Qt 6時。
QList 的核心問題是:
- 它是一個“基于數組”的容器,但內部實現是一個
void*
指針數組。 - 根據存儲的類型不同,QList 可能存放指向元素的指針(每個元素單獨堆分配),也可能直接存放元素本身。
- 這種設計導致:
- 每個元素單獨堆分配時性能和內存效率都很差。
- 元素存儲方式依賴于平臺(32位 vs 64位)和元素類型,行為不穩定難以預測。
- 對于小且可搬移的數據類型(如 int 在64位平臺)非常浪費空間。
- QList 優化了前置插入操作(prepend),但代價較大。
- 盡管有這些問題,QList仍然是Qt API中最常暴露的容器之一。
總結:
不要在自己的代碼中使用 QList,推薦使用 QVector 或 STL 容器,除非必須和 Qt API 交互。
總結下 QList 和 QVector 的區別和使用建議:
- 推薦使用 QVector,除非你必須調用需要 QList 的 Qt API。
- QVector 通常生成更少的代碼,性能也更好。
- QVector 在大多數操作上比 QList 快,唯一例外是:
- 經常在前面插入元素時,QList表現可能更好。
- 對非常大的對象進行重新分配時,QList可能更合適。
- QVector 重新分配時,不保證引用或指針的有效性(會失效)。
- 如果需要引用或指針保持有效,建議用指針的容器,比如
QVector<T*>
。
簡單說就是:
絕大多數情況下用 QVector,只有少數場景考慮 QList。