摘要:本文結合做者的工做經驗和學習心得,對C++語言的一些高級特性,作了簡單介紹;對一些常見的誤解,作了解釋澄清;對比較容易犯錯的地方,作了概括總結;但願藉此能增進你們對C++語言瞭解,減小編程出錯,提高工做效率。
C++是一門被普遍使用的系統級編程語言,更是高性能後端標準開發語言;C++雖功能強大,靈活巧妙,但卻屬於易學難精的專家型語言,不只新手難以駕馭,就是老司機也容易掉進各類陷阱。java
本文結合做者的工做經驗和學習心得,對C++語言的一些高級特性,作了簡單介紹;對一些常見的誤解,作了解釋澄清;對比較容易犯錯的地方,作了概括總結;但願藉此能增進你們對C++語言瞭解,減小編程出錯,提高工做效率。c++
Rule:C++在不一樣模塊(源文件)裏定義的全局變量,不保證構造順序;但保證在同一模塊(源文件)裏定義的全局變量,按定義的前後順序構造,按定義的相反次序析構。程序員
咱們程序在a.cpp裏定義了依次全局變量X和Y;算法
按照規則:X先構造,Y後構造;進程中止執行的時候,Y先析構,X後析構;但若是X的析構依賴於Y,那麼core的事情就有可能發生。數據庫
結論:若是全局變量有依賴關係,那麼就把它們放在同一個源文件定義,且按正確的順序定義,確保依賴關係正確,而不是定義在不一樣源文件;對於系統中的單件,單件依賴也要注意這個問題。編程
相信工做5年以上至少50%的C/C++程序員都被它坑過,我已經聽到過了無數個悲傷的故事,《聖鬥士星矢》,《仙劍》,還有別人家的項目《每天愛消除》,都有人掉坑,程序運行幾天莫名奇妙的Crash掉,一臉懵逼。segmentfault
若是要用,要本身提供比較函數或者函數對象,必定搞清楚什麼叫「嚴格弱排序」,必定要知足如下3個特性:後端
儘可能對索引或者指針sort,而不是針對對象自己,由於若是對象比較大,交換(複製)對象比交換指針或索引更耗費。設計模式
考慮遊戲玩家回血回藍(魔法)刷新給客戶端的邏輯。玩家每3秒回一點血,玩家每5秒回一點藍,回藍回血共用一個協議通知客戶端,也就是說只要有回血或者回藍就要把新的血量和魔法值通知客戶端。數組
玩家的心跳函數heartbeat()在主邏輯線程被循環調用
void GamePlayer::Heartbeat() { if (GenHP() || GenMP()) { NotifyClientHPMP(); } }
若是GenHP回血了,就返回true,不然false;不必定每次調用GenHP都會回血,取決因而否達到3秒間隔。
若是GenMP回藍了,就返回true,不然false;不必定每次調用GenMP都會回血,取決因而否達到5秒間隔。
實際運行發現回血回藍邏輯不對,Word麻,原來是操做符短路了,若是GenHP()返回true了,那GenMP()就不會被調用,就有可能失去回藍的機會。你須要修改程序以下:
void GamePlayer::Heartbeat() { bool hp = GenHP(); bool mp = GenMP(); if (hp || mp) { NotifyClientHPMP(); } }
邏輯與(&&)跟邏輯或(||)有一樣的問題, if (a && b) 若是a的表達式求值爲false,b表達式也不會被計算。
有時候,咱們會寫出 if (ptr != nullptr && ptr->Do())這樣的代碼,這正是利用了操做符短路的語法特徵。
for (unsigned int i = 5; i >=0; --i) { //... }
程序跑到這,WTF?根本停不下來啊?問題很簡單,unsigned永遠>=0,是否是心中一萬隻馬奔騰?
解決這個問題很簡單,可是有時候這一類的錯誤卻沒這麼明顯,你須要罩子放亮點。
memcpy,memset有很強的限制,僅能用於POD結構,不能做用於stl容器或者帶有虛函數的類。
帶虛函數的類對象會有一個虛函數表的指針,memcpy將破壞該指針指向。
對非POD執行memset/memcpy,免費送你四個字:自求多福
內存拷貝的時候,若是src和dst有重疊,須要用memmov替代memcpy。
不能在棧上定義過大的臨時對象。通常而言,用戶棧只有幾兆(典型大小是4M,8M),因此棧上建立的對象不能太大。
由於sprintf的函數實現裏是按格式化串從棧上取參數,任何不一致,都有可能引發不可預知的錯誤; /usr/include/inttypes.h裏定義了跨平臺的格式化符號,好比PRId64用於格式化int64_t
好比用strncpy替代strcpy,用snprintf替代sprintf,用strncat代替strcat,用strncmp代替strcmp,memcpy(dst, src, n)要確保[dst,dst+n]和[src, src+n]都有有效的虛擬內存地址空間。多線程環境下,要用系統調用或者庫函數的安全版本代替非安全版本(_r版本),謹記strtok,gmtime等標準c函數都不是線程安全的。
vector,list,map,set等各有不一樣的寫法:
int main(int argc, char *argv[]) { //vector遍歷刪除 std::vector v(8); std::generate(v.begin(), v.end(), std::rand); std::cout << "after vector generate...\n"; std::copy(v.begin(), v.end(), std::ostream_iterator(std::cout, "\n")); for (auto x = v.begin(); x != v.end(); ) { if (*x % 2) x = v.erase(x); else ++x; } std::cout << "after vector erase...\n"; std::copy(v.begin(), v.end(), std::ostream_iterator(std::cout, "\n")); //map遍歷刪除 std::map m = {{1,2}, {8,4}, {5,6}, {6,7}}; for (auto x = m.begin(); x != m.end(); ) { if (x->first % 2) m.erase(x++); else ++x; } return 0; }
有時候遍歷刪除的邏輯不是這麼明顯,可能循環裏調了另外一個函數,而該函數在某種特定的狀況下才會刪除當前元素,這樣的話,就是很長一段時間,程序都運行得好好的,而當你正跟別人談笑風生的時候,突然crash,這就尷尬了。
聖鬥士星矢項目曾經遭遇過這個問題,基本規律是一個禮拜game server crash一次,折磨團隊將近一個月。
比較low的處理方式能夠把待刪元素放到另外一個容器WaitEraseContainer裏保存下來,再走一趟單獨的循環,刪除待刪元素。
固然,咱們推薦在遍歷的同時刪除,由於這樣效率更高,也顯得行家裏手。
經過空間換取時間是提升性能的慣用法,bitmap,int map[]這些慣用法要了然於胸。
瞭解Copy On Write。
只要可能就應該減小拷貝,好比經過共享,好比經過引用指針的形式傳遞參數和返回值。
好比遊戲服務器端玩家的戰力,由屬性a,b決定,也就是說屬性a,b任何一個變化,都須要重算戰力;但若是ModifyPropertyA(),ModifyPropertyB()以後,都重算戰力卻並不是真正必要,由於修改屬性A以後有可能立刻修改B,兩次重算戰力,顯然第一次重算的結果會很快被第二次的重算覆蓋。
並且不少狀況下,咱們可能須要在心跳裏,把最新的戰力值推送給客戶端,這樣的話,ModifyPropertyA(),ModifyPropertyB()裏,咱們其實只須要把戰力置髒,延遲計算,這樣就能避免沒必要要的計算。
在GetFightValue()裏判斷FightValueDirtyFlag,若是髒,則重算,清髒標記;若是不髒,直接返回以前計算的結果。
預計算的思想相似。
分散計算是把任務分散,打碎,避免一次大計算量,卡住程序。
減小字符串比較,構建hash,可能會多費一點存儲空間,但收益可觀,信我。
日誌的開銷不容忽視,要分級,能夠把日誌做爲debug手段,但要release乾淨。
由於效率,C++被設計爲系統級的編程語言,效率是優先考慮的方向,c++秉持的一個設計哲學是「不爲沒必要要的操做付出任何額外的代價」。因此它有別於java,不給成員變量和局部變量作默認初始化,若是須要賦初值,那就由程序員本身去保證。
結論:從安全的角度出發,不該使用未初始化的變量,定義變量的時候賦初值是一個好的習慣,不少錯誤皆因未正確初始化而起,C++11支持成員變量定義的時候直接初始化,成員變量儘可能在成員初始化列表裏初始化,且要按定義的順序初始化。
X86_64體系結構由於通用寄存器數目增長到16個,因此64位系統下參數數目很少的函數調用,將會由寄存器傳遞代替壓棧方式傳遞參數,但棧幀創建、撤銷和控制轉移依然會對性能有所影響。
雖然遞歸函數能簡化程序編寫,但也經常帶來運行速度變慢的問題,因此須要預估好遞歸深度,優先考慮非遞歸實現版本。
遞歸函數要有退出條件且不能遞歸過深,否則有爆棧危險。
數組:內存連續,隨機訪問,性能高,局部性好,不支持動態擴展,最經常使用。
鏈表:動態伸縮,脫離插入極快,特別是帶先後驅指針,內存一般不連續(固然能夠經過從固定內存池分配規避),不支持隨機訪問。
查找:3種:bst,hashtable,基於有序數組的bsearch。二叉搜索樹(RBTree),這個從begin到end有序,最壞查找速度logN,壞處內存不連續,節點有額外空間浪費;hashtable,好的hash函數很差選,搜索最壞退化成鏈表,難以估計捅數量,開大了浪費內存,擴容會卡一下,無序;基於有序數組的bsearch,局部性好,insert/delete慢。
由於有序數組支持二分查找,效率跟map差很少。對於只須要在程序啓動的時候構建(排序)一次的查詢結構,有序數組相比map和hash可能有更好的內存命中性(局部命中性)。
運行過程當中,穩定的查詢結構(好比配置表,須要根據id查找配置表項,運行過程當中不增刪),有序數組是個不錯的選擇;若是不穩定,則有序數組的插入刪除效率比map,hashtable差,因此選用有序數組須要注意適用場合。
想清楚他們的利弊,map是用紅黑樹作的,unorder_map底層是hash表作的,hash表相對於紅黑樹有更高的查找性能。hash表的效率取決於hash算法和衝突解決方法(通常是拉鍊法,hash桶),以及數據分佈,若是負載因子高,就會下降命中率,爲了提升命中率,就須要擴容,從新hash,而從新hash是很慢的,至關於卡一下。
而紅黑樹有更好的平均複雜度,因此若是數據量不是特別大,map是勝任的。
理解const不只僅是一種語法層面的保護機制,也會影響程序的編譯和運行。
const常量會被編碼到機器指令。
避免用錯,儘可能少用向下轉型(能夠經過設計加以改進)
static_cast, dynamic_cast,const_cast,reinterpret_cast,傻傻分不清?
C++磚家說:一句話,儘可能少用轉型,強制類型轉換是C Style,若是你的C++代碼須要類型強轉,你須要去考慮是否設計有問題。
字節對齊能讓存儲器訪問速度更快。
字節對齊跟cpu架構相關,有些cpu訪問特定類型的數據必須在必定地址對齊的儲存器位置,不然會觸發異常。
字節對齊的另外一個影響是調整結構體成員變量的定義順序,有可能減小結構體大小,這在某些狀況下,能節省內存。
只在須要接管的時候才自定義operator=和copy constructor,若是編譯器提供的默認版本工做的很好,不要去自找麻煩,自定義的版本勿忘拷貝每個成分,若是要接管就要處理好。
典型的適配器模式有類適配器和對象適配器,通常而言,建議用對象適配的方式,而非用基於繼承的類適配方式。
打開的句柄要關閉,加鎖/解鎖,new/delete,new[]/delete[],malloc/free要配對,可使用RAII技術防止資源泄露,編寫符合規範的代碼
Valgrind對程序的內存使用方式有指望,須要乾淨的釋放,因此規範編程才能寫出valgrind乾淨的代碼,否則再好的工具碰到不按規劃寫的代碼也是武功盡廢啊。
多繼承會存在菱形繼承的問題,多個基類有相同成員變量會有問題,須要謹慎對待。
主要是爲了基類的析構函數能獲得正確的調用。
virtual dtor跟普通虛函數同樣,基類指針指向子類對象的時候,delete ptr,根據虛函數特徵,若是析構函數是普通函數,那麼就調用ptr顯式(基類)類型的析構函數;若是析構函數是virtual,則會調用子類的析構函數,而後再調用基類析構函數。
構造函數裏,對象並無徹底構建好,此時調用虛函數不必定能正確綁定,析構亦如此。
從輸入流獲取數據,要作好數據不夠的處理,要加try catch;沒有被吞嚥的exception,會被傳播
從網絡數據流讀取數據,從數據庫恢復數據都須要注意這個問題。
能夠考慮用整數替代浮點,好比萬分之五(5%%),就保存5。
要對每一個變量加括弧,有時候須要加do {} while(0)或者{},以便能將一條宏當成一個語句。要理解宏在預處理階段被替換,不用的時候要#undef,要防止污染別人的代碼。
理解基於引用計數法的智能指針實現方式,瞭解全部權轉移的概念,理解shared_ptr和unique_ptr的區別和適用場景
指針能帶來彈性,但不要誤用,它的彈性指一方面它能在運行時改變指向,能夠用來作多態,另外一方面對於不能固定大小的數組能夠動態伸縮,但不少時候,咱們對固定大小的array,也在init裏new/malloc出來,其實不必,並且會多佔用sizeof(void*)字節,並且增長一層間接訪問。
size_t類型是被設計來保存系統存儲器上能保存的對象的最大個數。
32位系統,一個對象最小的單位是一個字節,那2的32次方內存,最多能保存的對象數目就是4G/1字節,正好一個unsigned int能保存下來(typedef unsigned int size_t)。
一樣,64位系統,unsigned long是8字節,因此size_t就是unsigned long的類型別名。
對於像索引,位置這樣的變量,是用有符號仍是無符號呢?像money這樣的屬性呢?
一句話:要講道理,用最天然,最瓜熟蒂落的類型。好比索引不可能爲負用size_t,帳戶可能欠錢,則money用int。好比:
template <class T> class vector { T& operator(size_t index) {} };
標準庫給出了最好的示範,由於若是是有符號的話,你須要這樣判斷
if (index < 0 || index >= max_num) throw out_of_bound();
而若是是無符號整數,你只須要判斷 if (index >= max_num),你承認嗎?
整型包括int,short,long,long long和char,沒錯,char也是整型,float是實型。
絕大多數狀況下,用int,long就很好,long通常等於機器字長,能直接放到寄存器,硬件處理起來速度也一般更快。
不少時候,咱們但願用short,char達到減小結構體大小的目的。可是因爲字節對齊的緣由,可能並不能真正減小大小,並且1,2個字節的整型位數太少,一不當心就溢出了,須要特別注意。
因此,除非在db、網絡這些對存儲大小很是敏感的場合,咱們才須要考慮是否以short,char替代int,long。其餘狀況下,就至關於爲省電而不開樓道的燈,省不了多少錢卻冒着摔斷腿的危險。
局部變量更沒有必要用(unsigned) short,char等,棧是自動伸縮的,它既不節省空間,還危險,還慢。
模板和泛型編程,union,bitfield,指向成員的指針,placement new,顯式析構,異常機制,nested class,local class,namespace,多繼承、虛繼承,volatile,extern "C"等
有些高級特性只有在特定狀況下才會被用到,但技多不壓身,平時仍是須要積累和了解,這樣在需求出現時,才能從本身的知識庫裏拿出工具來對付它。
關注新技術,c++11/14/1七、lambda,右值引用,move語義,多線程庫等
c++98/03標準到c++11標準的推出歷經13年,13年來程序設計語言的思想獲得了很大的發展,c++11新標準吸取了不少其餘語言的新特性,雖然c++11新標準主要是靠引入新的庫來支持新特徵,核心語言的變化較少,但新標準仍是引入了move語義等核心語法層面的修改,每一個CPPer都應該瞭解新標準。
神化設計模式和反設計模式,都不是科學的態度,設計模式是軟件設計的經驗總結,有必定的價值;GOF書上對每個設計模式,都用專門的段落講它的應用場景和適用性,限制和缺陷,在正確評估得失的狀況下,是鼓勵使用的,但顯然,你首先須要準確get到她。