目錄
類/對象
1.多态基類的析構函數應總是public virtual,否則應為protected
2.編譯器會隐式生成預設構造,複制構造,複制指派,析構,(C++11)移動構造,(C++11)移動指派的inline函數
3.不要在析構函數抛出異常,也盡量避免在構造函數抛出異常
模闆
1. 不要偏特化模闆函數,而是選擇重載函數。
2.(C++11)不要重載萬能引用的函數,否則使用其它替代方案
函數
1.(C++11)禁用某個函數時,使用 = delete而非private
2.(C++11)lambda表達式一般是函數對象。特殊地,在無捕獲時是函數指針。
3.(C++11)盡可能使用lambda表達式代替std::bind
4.(C++11)使用lambda表達式時,避免預設捕獲模式
記憶體相關
1.檢查new是否失敗通常是無意義的。
2.盡量避免多次new同一種輕量級類型,而是先new一個大區域再配置設定多次。
STL标準庫
1.(C++11)使用emplace/emplace_back/emplace_front而不是insert/push_back/push_front
2.在周遊容器時删除疊代器需謹慎
3.容器的at()會檢查邊界,[]則不檢查邊界
4.sort()的< 比較操作符,若兩者相等則必須返還失敗。
5.永遠記住,更低的時間複雜度并不意味着更高的效率
6.需要深度優化時,使用自定義STL配置設定器
優化與效率
1.盡可能使用 ++i 而不是 i++
2.在後期遇到性能瓶頸才考慮使用inline
3.盡量不使用dynamic_cast并且禁用RTTI
4.(C++11)隻要潛在編譯期可計算的函數/變量,就使用constexpr
異常1.(C++11)若保證異常不會抛出,應使用noexpect異正常格,否則不要聲明異正常格。
頭檔案
1.做好頭檔案include gurad
2.注意頭檔案包含順序
3.能使用前向聲明(Forward Declarations)就使用,減少對頭檔案的依賴
雜項
1.(C++11)使用nullptr而不是NULL或0
2.(C++11)使用enum class文法為枚舉類型提供限定範圍
3.(C++11)auto隻能推導出類型型别,而decltype能夠推導出聲明型别
4.(C++17)需要用到任意可變的類型時,使用std::any,std::variant而不是union
參考
前言:C++是博大精深的語言,特性複雜得跟北京二環一樣,繼承亂得跟亂倫似的。
不過它仍然是必須用在遊戲開發上的程式設計語言,這篇文章用于挑選出一些個人覺得重要的條款/經驗/技巧進行記錄總結。
文章最後列出一些我看過的C++書籍/部落格等,友善參考。
其實以前也寫過相同的筆記博文,現在用markdown”重置“一下。
當要釋放多态基類指針指向的對象時,為了按正确順序析構,必須得借助virtual進而先執行析構派生類再析構基類。
當基類沒有多态性質時,可将基類析構函數聲明protected,并且也無需耗費使用virtual。
當你在代碼中用到以上函數時且沒有聲明該函數時,就會預設生成相應的函數。
特殊的,當你聲明了構造函數(無論有無參數),都不會隐式生成預設構造函數。
不過隐式生成的函數比自己手寫的函數(即使行為一樣)效率要高,因為經過了編譯器特殊優化。
(c++11)當你需要顯式禁用生成以上某個函數時,可在函數聲明尾部加上 = delete ,例如:
(c++11)當你需要顯式預設生成以上某個函數時,可在函數聲明尾部加上 = default ,例如:
析構函數若抛出異常,可能會使析構函數過早結束,進而可能導緻一些資源未能正确釋放。
構造函數若抛出異常,則無法調用析構函數,這可能導緻異常發生前部分資源成功配置設定,卻沒能執行析構函數的正确釋放行為。
編譯器比對函數時優先選擇非模闆函數(重載函數),再選擇模闆函數,最後再選擇偏特化模闆函數。
當比對到某個模闆函數時,就不會再比對選擇其他模闆函數,即使另一個模闆函數旗下有更适合的偏特化函數。
是以這很可能導緻編譯器沒有選擇你想要的偏特化模闆函數。
萬能引用的函數是C++中最貪婪的函數,容易讓需要隐式轉換的實參比對到不希望的轉發引用函數。(例如下面)
替代方案:
舍棄重載。換個函數名或者改成傳遞const T&形參。
使用更複雜的标簽分派或模闆限制(不推薦)。
原因有4個:
private函數仍需要寫定義(即使那是空的實作),
派生類潛在覆寫禁用函數名的可能性,
“=delete”文法比private文法更直覺展現函數被禁用的特點,
在編寫非類函數的時候,無法提供private屬性。
一般 = delete的類函數應為public,因為編譯器先檢測可通路性再檢驗禁用性
編譯器編譯lambda表達式時實際上都會對每個表達式生成一種函數對象類型,然後構造出函數對象出來。
特殊地,lambda表達式在無任何捕獲時,會被編譯成函數,其表達式值為該函數指針(畢竟函數比函數對象更效率)。
是以在一些老舊的C++API隻接受函數指針而不接受std::function的時候,可以使用無捕獲的lambda表達式。
直接舉例說明,假設有如下Func函數:
現在我們讓Func綁定上2.0f作為參數b,轉化一個void(int a)的函數對象。
可以看到使用std::bind會十分不美觀不直覺,還得注意占位符位置順序。
而使用lambda表達式可以讓代碼變得十分簡潔優雅。
按引用預設捕獲容易造成引用空懸,而顯示的引用捕獲更能容易提醒我們捕獲的是哪個變量的引用,進而更容易理清該引用的生命周期。
按值預設捕獲容易讓人誤解lambda式是自洽的(即不依賴外部)。下面是一個典型例子:
由于預設捕獲,你以為a是以按值拷貝過去,是以期待result總會會是2。但是實際上你是調用了同一個作用域的靜态變量,沒有拷貝的行為。
是以,無論是按值還是引用,都盡量指定變量,而不是用預設捕獲。
new幾乎總是成功的,現代大部分作業系統采取程序的惰式記憶體配置設定(即請求記憶體時不會立即配置設定記憶體,當使用時才慢慢吞吞配置設定)。
是以當使用new時,通常不會立即配置設定記憶體,進而無法真正檢測到是否記憶體将會耗盡。
每次new的時候,實際上還會額外配置設定出一個存放記憶體資訊的區域,而多次配置設定記憶體給輕量級類型時,會造成臃腫的記憶體資訊。
而且在删除這些區域時,很容易造成很多塊記憶體碎片,導緻記憶體使用率不高。
是以應當使用記憶體池的方式,先new一大塊區域,再從區域配置設定記憶體給輕量級類型。
emplace 最大的作用是避免産生不必要的臨時變量,因為它可以直接在容器相應的位置根據參數來構造變量。
而 insert / push_back / push_front 操作是會先通過參數構造一個臨時變量,然後将臨時變量移動到容器相應的位置。
順序式容器删除疊代器會破壞本身和後面的疊代器,節點式容器删除疊代器會破壞本身,導緻循環周遊崩潰(循環周遊依賴于容器原有的疊代器)。
兩個值得借鑒的正确做法:
STL小細節。另外std::vector<bool>和std::bitset的[]提供的是值拷貝,而不是引用。
STL的sort算法基本是快排,是不穩定的排序。
若比較的兩者相等時返還成功,則不穩定排序容易出現死循環,進而導緻程式崩潰。
STL容器,特别是set,map,有着很多O(logN)的操作速度,但并不意味着是最佳選擇,因為這種複雜度表示往往隐藏了常數很大的事實。
例如說,集合的主流實作是基于紅黑樹,基于節點存儲的,而每次插入/删除節點都意味着調用一次系統配置設定記憶體/釋放記憶體函數。這相比vector等矢量容器所有操作僅一次系統配置設定記憶體(理想情況來說),實際上就慢了不少。
此外,矢量容器對CPU緩存更加友好,周遊該種容器容易命中緩存,而節點式容器則相對容易命中失敗。
綜合上述,如果要選擇一個最适合的容器,那麼不要過度信賴時間複雜度,除非你十分徹底的了解STL容器,或對各容器進行多次效率測試。
每個STL容器都會要求提供一個Allocator類型作為該容器的節點配置設定器,不提供時使用STL預設的預設配置設定器。
預設預設配置設定器的行為往往是簡單粗暴的new delete,這可能帶來一些效率問題和記憶體碎片問題。
而通過自己定制配置設定器,我們可以把STL容器的記憶體配置設定達到如下政策:
類型
政策描述
固定大小的緩沖池
所有記憶體配置設定都是一樣大小,減少每次配置設定記憶體浪費。
共享記憶體
配置設定使用共享記憶體。
多個堆
配置設定使用不同的堆,試配置設定大小和類型而定。
單線程的
配置設定和釋放均不保證線程安全。
垃圾回收
調用釋放的時候并不立即釋放,調用垃圾回收函數時才釋放。
基于棧的政策
所有記憶體都是在棧上,适用于短生命期的容器對象。
靜态記憶體
配置設定的記憶體位于程式的靜态記憶體區裡。
從不删除
調用釋放的時候不釋放記憶體,程式結束時才回收記憶體。
一次性删除
調用釋放的時候不釋放記憶體,通過定制函數來釋放記憶體。
邊界對齊政策
為了滿足某些條件,記憶體邊界總是對齊配置設定。例如在SSE中使用指令對齊記憶體的時候。
調試
配置設定記錄、檢查記憶體洩漏、檢查記憶體覆寫情況、峰值配置設定大小等等。
這個是老生常談的C++經典問題,對于int/unsigned等内置類型時,++i與i++似乎在效率上沒有差別。
然而在使用疊代器或其他自定義類型時,i++往往還得建立一個額外的副本來用于返還值,而++i則直接返還它本身。
現代編譯器已經十分智能,很多時候該寫成inline的函數編譯器會自動幫你inline,不該inline的時候即使你顯式寫了inline編譯器也有可能認為不該inline。
也就是說顯式的寫出inline隻是給編譯器一個建議,它不一定會采納。
而且inline實作往往寫在頭檔案,開發前期頻繁的更改也會導緻包含該頭檔案的編譯單元必須得重新編譯。
是以在開發時不用過早考慮inline優化,而是遇到性能瓶頸時才考慮使用顯式寫出inline,不過大部分這時候你更應該考慮的是算法的效率。
依靠dynamic_cast的代碼往往可以用多态虛函數解決,而且多态虛函數更加優雅。是以,盡可能避免編寫dynamic_cast。
另外可以随之禁用與dynamic_cast相關的RTTI特性,禁用該特性可以提升程式效率(每個類少一些臃腫的RTTI資訊)。
constexpr能讓一些函數/變量在編譯期就可計算,可減少運作期運算。(可視作模闆元運算的美化文法)
此外,constexpr如果接受的是運作期變量/參數,則會變成運作期計算。
也就是說它既可用作編譯期運算,也可運作期運算,語境作用域比非constexpr更廣。
無聲明異正常格,意思是可能抛出任何異常。
相比無聲明異正常格的函數,noexpect函數能得到編譯器的優化(發生異常時不必解開棧),且能清晰表示自己的無異常保證。
使用宏#pragma once或者#ifndef #define ... #endif ,確定本類隻會聲明一次,做好include guard,避免重複定義。
首先最好遵守的基本原則是:
XXX.cpp檔案最好首先包含對應的頭檔案XXX.h,這可以避免隐含依賴。
然後下面是兩種主流的包含順序,可根據自己需要選擇(實際上随便一種都可以,問題不大,這裡就當了解一下):
《Google C++ Style Guide》推薦順序,先包含cpp對應頭檔案,再從最一般到最特殊的頭檔案包含順序:XXX.h、C标準庫、C++标準庫、第三方庫頭檔案、你自己工程的頭檔案。因為這更加直覺,增加可讀性。
《C++程式設計思想》推薦順序,先包含cpp對應頭檔案,再從最特殊到最一般的頭檔案包含順序:
XXX.h、你自己工程的頭檔案、第三方庫頭檔案、C++标準庫、C标準庫。這可以檢測出你的庫的頭檔案是不是包含了所有必需的頭檔案:如果某個你的庫檔案沒有包含它必需的系統檔案的話,那麼這個順序就會導緻編譯錯誤。
一般而言,代碼檔案之間的耦合越小越好,如果可以使用前向聲明來代替包含頭檔案,那就使用前向聲明。
包含頭檔案時盡量具體,如:不要包含一個大而全的頭檔案,而是包含其中具體需要用到的頭檔案。
NULL是C語言遺留的東西,是将宏定義成0的,容易造成指針和整數的二義性。
而nullptr很好的避免了整數的性質。
C帶來的enum文法是允許枚舉類進行隐式轉換的,潛在造成程式員不希望發生的轉換。
而C++11的enum class會阻止隐式轉換,需要程式員顯示轉換
也就是說auto推導出的類型會抛棄引用性質,而decltype能夠推導出完整的聲明類型。
此外一提,auto是聲明類型的文法,而decltype()是一個表達式(類似于sizeof()),表達式的值是類型。
union是從c繼承來的特性,它的成員不可以是帶構造函數/析構函數/自定義複制構造函數的c++類。
是以在需要萬能變量的時候最好不要使用union,而是用std::any或std::variant ,目前C++17已引入<any>庫和<variant>庫。
萬能變量是指可以轉換任意類型(可擴充,如metadata)的變量,如果隻固定在幾個類型之間轉換的使用union是個效率更優的選擇。
《C++ Primer Plus》:當初入門C++語言的書籍。
《C++程式設計語言(特别版)》:C++之父編寫的入門教材,但實際上更應該算為介于入門與進階之間的工具書(用于查詢文法)。
《Effective C++》:C++ 進階書,深入了解與經驗
《More Effective C++》:C++ 進階書,深入了解與經驗
《深度探索C++對象模型》:C++ 進階書,深入了解
《Expectional C++》:C++ 進階書,深入了解與經驗
《高速上手 C++11/14/17》:C++11/14/17 入門書,介紹C++11/14/17各項新特性的基礎用法,它目前隻有電子版本: https://github.com/changkun/modern-cpp-tutorial/blob/master/book/zh-cn/toc.md
《Effective Modern C++》:C++11/14 進階書,介紹C++11/14部分新特性的深入了解與經驗。
《遊戲程式設計精粹》2/3/4/5/6/7:遊戲程式設計綜合技術書,有部分章節講C++的經驗。
UnrealEngine 4 官方文檔 C++編碼标準:https://docs.unrealengine.com/zh-CN/Programming/Development/CodingStandard/index.html
C++是非常非常複雜的語言,了解得越多就越發覺得自己的無知(例如C++ Boost)。
但是在學習C++的中途也必須認識到,C++是一門工具,不要過多鑽C++語言的牛角尖。
謹記:程式員是要成為工程師而不是語言學家。
作者:KillerAery
出處:http://www.cnblogs.com/KillerAery/
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利。