C++ Tips
讀了 《C++ 的門門道道 | 技術頭條》 這篇文章之後有很多同感,可以說是近期看過的最好的技術小 tips 文章了。按照這篇文章裡面講到的幾點,我也來說一下我的感受。
成員變數初始化
成員變數忘了初始化是一個相當經典的錯誤,甚至《Effective C++》中還專門列了一條來講這個事情。在工作中,我也看到過這種錯誤,同時對一個新增的功能加上了開關的控制邏輯,但是忘了對這個開關的標識進行初始化,導致了。而且因為成員變數不初始化,那它的初始值是隨機的,所以導致線上的表現是時靈時不靈,增加了 debug 的難度。當然增加 Coverity 掃描提早發現這個問題,當時專案上線比較急就直接跳過了這一步。
從 C++11 開始支援在宣告成員變數的時候直接初始化,有了這個特性之後,我已經養成了所有成員變數都直接在宣告的時候初始化。
class Ad { private: unsinged int lifetime = 10000; };
sort() 裡的坑
這個還真出過特別經典的坑,有一次線上事故就是一個穩定跑了很久的邏輯,突然出現了 core,而且是持續地 core 在 sort 上。花了很長時間排查,最後才意識到實現 sort 比較函式的時候沒有保證 嚴格弱序(strict weak order) ,比較兩個物件的屬性時用了 <=
。這裡就涉及到 C++ 中 sort 的實現。細節之後會寫一篇文章來講,簡單說來就是 STL sort 核心排序演算法是快排,在依據 pivot 調整元素位置時採用的實現方式如下:
while (true) { while (__comp(*__first, __pivot)) ++__first; --__last; while (__comp(__pivot, *__last)) --__last; if (!(__first < __last)) return __first; std::iter_swap(__first, __last); ++__first; }
重點就在於 while (__comp(*__first, __pivot)) ++__first;
,當整個容器裡的元素都相等時,就會導致 __first 這個迭代器越界,程式就 core 了。
操作符短路
對我更常用的場景: if (!stack.empty() && stack.top() == 0)
,這恰恰是利用短路來合併判斷。
別讓迴圈停不下來
這個有個經典場景,一個亂序的 vector 裡面,我要找到第一個遞增序列的最後一個元素,很容易寫成這樣的程式碼:
while (i < ve.size() - 1 && ve[i] <= ve[i + 1]) i++;
這裡如何傳入的 ve 是個空 vector,那麼就會成為超大迴圈,因為 vector::size() 返回的是 unsigned int,根據數值型別傳遞,ve.size() - 1 的型別也是 unsigned int,那麼就會返回一個很大的數,導致 while 陷入超大迴圈。
理解 vector 的實現
vector 可以說是在日常開發中使用頻率最高的容器了,支援下標訪問,動態擴容,二分查詢的效率,C++11 之後支援移動構造,這些優點都讓它非常好用。vector 的坑都集中在它的動態擴容上,理解它動態擴容的機制可以在開發中避開這些坑。
vector 動態擴容的兩個特點:
-
vector 擴容是按照 2 的指數倍往上翻的,也就是 2, 4, 8, 64, 128, ……。
-
動態擴容時是會全量複製一遍現有的所有元素到新分配的記憶體中。
根據這兩個特點,結合 vector 的其他特性得到的 tips:
-
儘量預先分配好 vector 的空間,使用 reserve() 預分配空間,避免多次擴容。
-
不要存在大物件,擴容的時候會全量複製,額外的效能開銷很大。
-
不要儲存指向 vector 內部物件的指標,擴容時物件地址會發生變化。
-
reserve() 是提前分配空間,此時不能直接用下標索引訪問(如果用基本型別倒是能訪問,但是這種行為仍然是未定義的)。
有時候真的不必用 std::unorder_map
組裡有一個專案升級到 C++11 之後,一窩蜂地使用 unordered_map,但是其實對於小資料量,比如本次請求命中的一些配置,其實資料量基本都在 10 項以內,那其實用 map 就完全夠用了,unorder_map 查詢的效率當然是高的,但是也要認識到它維護一個紅黑樹額外付出的效能代價。
慎用用short,char
有些人寫程式碼的時候有一種傾向,就是能省則省,能用 int 的絕不用 long,能用 short 的絕不用 int。但是其實有些情況下 short 並不能節省空間(位元組對齊),還導致過度「優化」,導致之後要重寫,或者實際的取值不符合設計導致溢位。
避免箭頭型程式碼
什麼是「箭頭型程式碼」?見下圖:
這種程式碼其實在業務複雜的場景下並不少見,酷殼上有一篇檔案專門講過如何重構這種程式碼: 《如何重構“箭頭型”程式碼》 。
我在實際專案中應用比較多是利用 while (0) 來規避這種程式碼。
在專案經常遇到的場景是對一連串條件進行判斷,不符合條件的分支需要列印日誌,示例程式碼如下:
if (conditionA()) { if (conditionB()) { if (conditionC()) { if (conditionD()) { // do something } else { // log } } else { // log } } else { // log } } else { // log }
這種情況下用 do-while(0)
可以進行非常好的重構,重構之後的程式碼如下:
do { if (!conditionA()) { // log break; } if (!conditionB()) { // log break; } if (!conditionC()) { // log break; } if (!conditionD()) { // log break; } } while (0);