0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

C++陷阱與套路

C語言與CPP編程 ? 來源:碼磚雜役 ? 作者:碼磚雜役 ? 2022-12-12 10:26 ? 次閱讀
【導語】 C++是一門被廣泛使用的系統(tǒng)級編程語言,更是高性能后端標準開發(fā)語言;C++雖功能強大,靈活巧妙,但卻屬于易學難精的專家型語言,不僅新手難以駕馭,就是老司機也容易掉進各種陷阱。本文結合作者的工作經驗和學習心得,對C++語言的一些高級特性,做了簡單介紹;對一些常見的誤解,做了解釋澄清;對比較容易犯錯的地方,做了歸納總結;希望借此能增進大家對C++語言了解,減少編程出錯,提升工作效率。

【1】我的程序里用了全局變量,但為什么進程正常停止的時候會莫名其妙的core掉

Rule:C++在不同模塊(源文件)里定義的全局變量,不保證構造順序;但保證在同一模塊(源文件)里定義的全局變量,按定義的先后順序構造,按定義的相反次序析構。

我們程序在a.cpp里定義了依次全局變量X和Y;

按照規(guī)則:X先構造,Y后構造;進程停止執(zhí)行的時候,Y先析構,X后析構;但如果X的析構依賴于Y,那么core的事情就有可能發(fā)生。

結論:如果全局變量有依賴關系,那么就把它們放在同一個源文件定義,且按正確的順序定義,確保依賴關系正確,而不是定義在不同源文件;對于系統(tǒng)中的單件,單件依賴也要注意這個問題。

【2】編譯器為什么不給局部變量和成員變量做默認初始化

因為效率,C++被設計為系統(tǒng)級的編程語言,效率是優(yōu)先考慮的方向,c++秉持的一個設計哲學是不為不必要的操作付出任何額外的代價,所以它有別于java,不給成員變量和局部變量做默認初始化,如果需要賦初值,那就由程序員自己去保證。

結論:從安全的角度出發(fā),定義變量的時候賦初值是一個好的習慣,很多錯誤皆因未正確初始化而起,C++11支持成員變量定義的時候直接初始化,成員變量盡量在成員初始化列表里初始化,且要按定義的順序初始化。

【3】std::sort()的比較函數有很強的約束,不能亂來啊

相信工作5年以上至少50%的C/C++程序員都被它坑過,我已經聽到過了無數個悲傷的故事,《圣斗士星矢》,《仙劍》,還有別人家的項目《天天愛消除》,都有人掉坑,程序運行幾天莫名奇妙的Crash掉,這鍋好沉。

如果要用,要自己提供比較函數或者函數對象,一定搞清楚什么叫“嚴格弱排序”,一定要滿足以下3個特性:

  • 非自反性

  • 非對稱性

  • 傳遞性

盡量對索引或者指針sort,而不是針對對象本身,因為如果對象比較大,交換(復制)對象比交換指針或索引更耗費。

【4】注意操作符短路

考慮游戲玩家回血回藍(魔法)刷新給客戶端的邏輯。玩家每3秒回一點血,玩家每5秒回一點藍,回藍回血共用一個協(xié)議通知客戶端,也就是說只要有回血或者回藍就要把新的血量和魔法值通知客戶端。

玩家的心跳函數heartbeat()在主邏輯線程被循環(huán)調用

void GamePlayer::Heartbeat()
{
  if (GenHP() || GenMP())
  {
    NotifyClientHPMP();
  }
}

如果GenHP回血了,就返回true,否則false;不一定每次調用GenHP都會回血,取決于是否達到3秒間隔。

如果GenMP回藍了,就返回true,否則false;不一定每次調用GenMP都會回血,取決于是否達到5秒間隔。

實際運行發(fā)現(xiàn)回血回藍邏輯不對,Word麻,原來是操作符短路了,如果GenHP()返回true了,那GenMP()就不會被調用,就有可能失去回藍的機會。OMG,你需要修改程序如下:

void GamePlayer::Heartbeat()
{
  bool hp = GenHP();
  bool mp = GenMP();
  if (hp || mp) 
  {  
    NotifyClientHPMP();
  }  
}

邏輯與(&&)跟邏輯或(||)有同樣的問題, if (a && b)如果a的表達式求值為false,b表達式也不會被計算。

有時候,我們會寫出 if (ptr != nullptr && ptr->Do())這樣的代碼,這正是利用了操作符短路的語法特征。

【5】理解std::vector的底層實現(xiàn)

(a) vector是動態(tài)擴容的,2的次方往上翻,為了確保數據保存在連續(xù)空間,每次擴充,會將原member悉數拷貝到新的內存塊;不要保存vector內對象的指針,擴容會導致其失效 ;可以通過保存其下標index替代。

(b) 運行過程中需要動態(tài)增刪的vector,不宜存放大的對象本身 ,因為擴容會導致所有成員拷貝構造,消耗較大,可以通過保存對象指針替代。

(c)resize()是重置大?。籸eserve()是預留空間,并未改變size(),可避免多次擴容;clear()并不會導致空間收縮 ,如果需要釋放空間,可以跟空的vector交換,std::vector .swap(v),c++11里shrink_to_fit()也能收縮內存。

(d) 理解at()和operator[]的區(qū)別 :at()會做下標越界檢查,operator[]提供數組索引級的訪問,在release版本下不會檢查下標,VC會在Debug版本會檢查;c++標準規(guī)定:operator[]不提供下標安全性檢查。

(e)C++標準規(guī)定了std::vector的底層用數組實現(xiàn),認清這一點并利用這一點。

【6】用c標準庫的安全版本(帶n標識)替換非安全版本,比如用strncpy替代strcpy,用snprintf替代sprintf,用strncat代替strcat,用strncmp代替strcmp,memcpy(dst, src, n)要確保[dst,dst+n]和[src, src+n]都有有效的虛擬內存地址空間。;多線程環(huán)境下,要用系統(tǒng)調用或者庫函數的安全版本代替非安全版本(_r版本),謹記strtok,gmtime等標準c函數都不是線程安全的

【7】理解函數調用的性能開銷(棧幀建立和銷毀,參數傳遞,控制轉移),性能敏感函數考慮inline

X86_64體系結構因為通用寄存器數目增加到16個,所以64位系統(tǒng)下參數數目不多的函數調用,將會由寄存器傳遞代替壓棧方式傳遞參數,但棧幀建立、撤銷和控制轉移依然會對性能有所影響。

【8】理解user stack空間很有限,不能在棧上定義過大的臨時對象,遞歸函數要有退出條件且不能遞歸過深

一般而言,用戶棧只有幾兆(典型大小是4M,8M),所以棧上創(chuàng)建的對象不能太大;雖然遞歸函數能簡化程序編寫,但也常常帶來運行速度變慢的問題,所以需要預估好遞歸深度,優(yōu)先考慮非遞歸實現(xiàn)版本。

【9】內存拷貝小心內存越界;memcpy,memset有很強的限制,僅能用于POD結構,不能作用于stl容器或者帶有虛函數的類

帶虛函數的類對象會有一個虛函數表的指針,memcpy將破壞該指針指向。

對非POD執(zhí)行memset/memcpy,免費送你四個字:自求多福。

【10】用sprintf格式化字符串的時候,類型和格式化符號要嚴格匹配,因為sprintf的函數實現(xiàn)里是按格式化串從棧上取參數,任何不一致,都有可能引起不可預知的錯誤;/usr/include/inttypes.h里定義了跨平臺的格式化符號,比如PRId64用于格式化int64_t

【11】stl容器的遍歷刪除要小心迭代器失效,vector,list,map,set等各有不同的寫法

#include 
#include 
#include 
#include 
#include 

int main(int argc, char *argv[])
{
  //vector遍歷刪除
  std::vector<int> v(8);
  std::generate(v.begin(), v.end(), std::rand);
  std::cout << "after vector generate...
";
  std::copy(v.begin(), v.end(), std::ostream_iterator<int>(std::cout, "
"));

  for (auto x = v.begin(); x != v.end(); )
  {
    if (*x % 2)
      x = v.erase(x);
    else
      ++x;
  }

  std::cout << "after vector erase...
";
  std::copy(v.begin(), v.end(), std::ostream_iterator<int>(std::cout, "
"));

  //map遍歷刪除
  std::map<int, int> 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;
}

有時候遍歷刪除的邏輯不是這么明顯,可能循環(huán)里調了另一個函數,而該函數在某種特定的情況下才會刪除當前元素,這樣的話,就是很長一段時間,程序都運行得好好的,而當你正跟別人談笑風生的時候,忽然crash,這就尷尬了。

圣斗士星矢項目曾經遭遇過這個問題,基本規(guī)律是一個禮拜game server crash一次,折磨團隊將近一個月。

比較low的處理方式可以把待刪元素放到另一個容器WaitEraseContainer里保存下來,再走一趟單獨的循環(huán),刪除待刪元素。

當然,我們推薦在遍歷的同時刪除,因為這樣效率更高,也顯得行家里手。

【12】積極的使用const,理解const不僅僅是一種語法層面的保護機制,也會影響程序的編譯和運行

const常量會被編碼到機器指令。

【13】理解四種轉型的含義和區(qū)別,避免用錯,盡量少用向下轉型(可以通過設計加以改進)

static_cast, dynamic_cast,const_cast,reinterpret_cast,傻傻分不清?

C++磚家說:一句話,盡量少用轉型,強制類型轉換是C Style,如果你的C++代碼需要類型強轉,你需要去考慮是否設計有問題。不管您信不信,我反正是信了。

【14】打開的句柄要關閉,加鎖/解鎖,new/delete,new[]/delete[],malloc/free要配對,可以使用RAII技術防止資源泄露,編寫符合規(guī)范的代碼

Valgrind對程序的內存使用方式有期望,需要干凈的釋放,所以規(guī)范編程才能寫出valgrind干凈的代碼,不然再好的工具碰到不按規(guī)劃寫的代碼也是武功盡廢啊。

【15】理解多繼承潛在的問題,慎用多繼承

多繼承會存在菱形繼承的問題,多個基類有相同成員變量會有問題,需要謹慎對待。

【16】有多態(tài)用法抽象基類的析構函數要加virtual關鍵字

主要是為了基類的析構函數能得到正確的調用。

virtual dtor跟普通虛函數一樣,基類指針指向子類對象的時候,delete ptr,根據虛函數特征,如果析構函數是普通函數,那么就調用ptr顯式(基類)類型的析構函數;如果析構函數是virtual,則會調用子類的析構函數,然后再調用基類析構函數。

【17】避免在構造函數和析構函數里調用虛函數

構造函數里,對象并沒有完全構建好,此時調用虛函數不一定能正確綁定,析構亦如此。

【18】從輸入流獲取數據,要做好數據不夠的處理,要加try catch;沒有被吞咽的exception,會被傳播

網絡數據流讀取數據,從數據庫恢復數據都需要注意這個問題。

【19】協(xié)議盡量不要傳float,如果傳float要了解NaN的概念,要做好檢查,避免惡意傳播

【20】定義宏要遵循常規(guī),要對每個變量加括弧,有時候需要加do {} while(0)或者{},以便能將一條宏當成一個語句。要理解宏在預處理階段被替換,不用的時候要#undef,要防止污染別人的代碼

【21】了解智能指針,理解基于引用計數法的智能指針實現(xiàn)方式,了解所有權轉移的概念,理解shared_ptr和unique_ptr的區(qū)別和適用場景

考慮用std::shared_ptr管理動態(tài)分配的對象。

【22】了解c++高階特性:模板和泛型編程,union,bitfield,指向成員的指針,placement new,顯式析構,異常機制,nested class,local class,namespace,多繼承、虛繼承,volatile,extern "C"等

有些高級特性只有在特定情況下才會被用到,但技多不壓身,平時還是需要積累和了解,這樣在需求出現(xiàn)時,才能從自己的知識庫里拿出工具來對付它。

【23】了解C++新標準,關注新技術,c++11/14/17、lambda,右值引用,move語義,多線程庫等

c++98/03標準到c++11標準的推出歷經13年,13年來程序設計語言的思想得到了很大的發(fā)展,c++11新標準吸收了很多其他語言的新特性,雖然c++11新標準主要是靠引入新的庫來支持新特征,核心語言的變化較少,但新標準還是引入了move語義等核心語法層面的修改,每個CPPer都應該了解新標準。

【24】OOP設計原則并不是胡扯

設計模式六大原則(1):單一職責原則

設計模式六大原則(2):里氏替換原則

設計模式六大原則(3):依賴倒置原則

設計模式六大原則(4):接口隔離原則

設計模式六大原則(5):迪米特法則

設計模式六大原則(6):開閉原則

【25】熟悉常用設計模式,活學活用,不生搬硬套

神化設計模式和反設計模式,都不是科學的態(tài)度,設計模式是軟件設計的經驗總結,有一定的價值;GOF書上對每一個設計模式,都用專門的段落講它的應用場景和適用性,限制和缺陷,在正確評估得失的情況下,是鼓勵使用的,但顯然,你首先需要準確get到她。

【26】了解延遲計算、COW和分散計算

比如游戲服務器端玩家的戰(zhàn)力,由屬性a,b決定,也就是說屬性a,b任何一個變化,都需要重算戰(zhàn)力;但如果ModifyPropertyA(),ModifyPropertyB()之后,都重算戰(zhàn)力卻并非真正必要,因為修改屬性A之后有可能馬上修改B,兩次重算戰(zhàn)力,顯然第一次重算的結果會很快被第二次的重算覆蓋。

而且很多情況下,我們可能需要在心跳里,把最新的戰(zhàn)力值推送給客戶端,這樣的話,ModifyPropertyA(),ModifyPropertyB()里,我們其實只需要把戰(zhàn)力置臟,延遲計算,這樣就能避免不必要的計算。

在GetFightValue()里判斷FightValueDirtyFlag,如果臟,則重算,清臟標記;如果不臟,直接返回之前計算的結果。

分散計算是把任務分散,打碎,避免一次大計算量,卡住程序。

延遲計算和分散計算都是常見的套路。

【27】理解字節(jié)對齊

自己對齊能讓存儲器訪問速度更快。
自己對齊跟cpu架構相關,有些cpu訪問特定類型的數據必須在一定地址對齊的儲存器位置,否則會觸發(fā)異常。

字節(jié)對齊的另一個影響是調整結構體成員變量的定義順序,有可能減少結構體大小,這在某些情況下,能節(jié)省內存。

【28】牢記3 rules和5 rules,當然C++11又多了&&的copy ctor和op=版本

只在需要接管的時候才自定義operator=和copy constructor,如果編譯器提供的默認版本工作的很好,不要去自找麻煩,自定義的版本勿忘拷貝每一個成分,如果要接管就要處理好。

【29】組合優(yōu)先于繼承,繼承是一種最強的類間關系

【30】減少依賴,注意隔離

最大限度的減少文件間的依賴關系,用前向聲明拆解相互依賴。

了解pimpl技術。

頭文件要自給自足,不要包含不必要的頭文件,也不要把該包含的頭文件推給user去包含,一句話,頭文件包含要不多不少剛剛好。

【31】別讓循環(huán)停不下來

for (unsigned int i = 5; i >=0; --i)
{
  ...
}

程序跑到這,納尼?根本停不下來???問題很簡單,unsigned永遠>=0,是不是心中一萬只馬奔騰?

解決這個問題很簡單,但是有時候這一類的錯誤卻沒這么明顯,你需要罩子放亮點,多個心眼。

【32】size_t到底是個什么鬼?我該用有符號還是無符號整數?

size_t類型是被設計來保存系統(tǒng)存儲器上能保存的對象的最大個數。

32位系統(tǒng),一個對象最小的單位是一個字節(jié),那2的32次方內存,最多能保存的對象數目就是4G/1字節(jié),正好一個unsigned int能保存下來(typedef unsigned int size_t)。

同樣,64位系統(tǒng),unsigned long是8字節(jié),所以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),你認可嗎?

【33】對于在啟動時加載好,運行中不變化的查詢結構,可以考慮用sorted array替代map,hash表等

因為有序數組支持二分查找,效率跟map差不多。對于只需要在程序啟動的時候構建(排序)一次的查詢結構,有序數組相比map和hash可能有更好的內存命中性(局部命中性)。

運行過程中,穩(wěn)定的查詢結構(比如配置表,需要根據id查找配置表項,運行過程中不增刪),有序數組是個不錯的選擇;如果不穩(wěn)定,則有序數組的插入刪除效率比map,hashtable差,所以選用有序數組需要注意適用場合。

【34】std::map還是std::unorder_map,我真的很糾結

想清楚他們的利弊,map是用紅黑樹做的,unorder_map底層是hash表做的,hash表相對于紅黑樹有更高的查找性能。hash表的效率取決于hash算法和沖突解決方法(一般是拉鏈法,hash桶),以及數據分布,如果負載因子高,就會降低命中率,為了提高命中率,就需要擴容,重新hash,而重新hash是很慢的,相當于卡一下。

而紅黑樹有更好的平均復雜度,所以如果數據量不是特別大,map是勝任的。

【35】整型一般用int,long就很好,用short,char需要很仔細,要防止溢出

大多數情況下,用int,long就很好,long一般等于機器字長,long能直接放到寄存器,硬件處理起來速度也更快。

很多時候,我們希望用short,char達到減少結構體大小的目的。但是由于字節(jié)對齊,可能并不能真正減少,而且1,2個字節(jié)的整型位數太少,一不小心就溢出了,需要特別注意。

所以,除非在db、網絡這些對存儲大小非常敏感的場合,我們才需要考慮是否以short,char替代int,long。

審核編輯 :李倩

聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • C++
    C++
    +關注

    關注

    21

    文章

    2102

    瀏覽量

    73457
  • 編譯器
    +關注

    關注

    1

    文章

    1617

    瀏覽量

    49019

原文標題:C++陷阱與套路

文章出處:【微信號:C語言與CPP編程,微信公眾號:C語言與CPP編程】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏

    評論

    相關推薦

    C語言和C++中結構體的區(qū)別

    同樣是結構體,看看在C語言和C++中有什么區(qū)別?
    的頭像 發(fā)表于 10-30 15:11 ?105次閱讀

    C7000優(yōu)化C/C++編譯器

    電子發(fā)燒友網站提供《C7000優(yōu)化C/C++編譯器.pdf》資料免費下載
    發(fā)表于 10-30 09:45 ?0次下載
    <b class='flag-5'>C</b>7000優(yōu)化<b class='flag-5'>C</b>/<b class='flag-5'>C++</b>編譯器

    C++語言基礎知識

    電子發(fā)燒友網站提供《C++語言基礎知識.pdf》資料免費下載
    發(fā)表于 07-19 10:58 ?7次下載

    C++中實現(xiàn)類似instanceof的方法

    C++有多態(tài)與繼承,但是很多人開始學習C++,有時候會面臨一個常見問題,就是如何向下轉型,特別是不知道具體類型的時候,這個時候就希望C++ 可以向Java或者Python中有instanceof這個
    的頭像 發(fā)表于 07-18 10:16 ?510次閱讀
    <b class='flag-5'>C++</b>中實現(xiàn)類似instanceof的方法

    鴻蒙OS開發(fā)實例:【Native C++

    使用DevEco Studio創(chuàng)建一個Native C++應用。應用采用Native C++模板,實現(xiàn)使用NAPI調用C標準庫的功能。使用C標準庫hypot接口計算兩個給定數平方和的平
    的頭像 發(fā)表于 04-14 11:43 ?2454次閱讀
    鴻蒙OS開發(fā)實例:【Native <b class='flag-5'>C++</b>】

    使用 MISRA C++:2023? 避免基于范圍的 for 循環(huán)中的錯誤

    在前兩篇博客中,我們?向您介紹了新的 MISRA C++ 標準?和?C++ 的歷史?。在這篇博客中,我們將仔細研究以 C++ 中?for?循環(huán)為中心的特定規(guī)則。
    的頭像 發(fā)表于 03-28 13:53 ?716次閱讀
    使用 MISRA <b class='flag-5'>C++</b>:2023? 避免基于范圍的 for 循環(huán)中的錯誤

    c語言,c++,java,python區(qū)別

    C語言、C++、Java和Python是四種常見的編程語言,各有優(yōu)點和特點。 C語言: C語言是一種面向過程的編程語言。它具有底層的特性,能夠對計算機硬件進行直接操作。
    的頭像 發(fā)表于 02-05 14:11 ?2153次閱讀

    C++簡史:C++是如何開始的

    MISRA C++:2023,MISRA? C++ 標準的下一個版本,來了!為了幫助您做好準備,我們介紹了 Perforce 首席技術支持工程師 Frank van den Beuken 博士撰寫
    的頭像 發(fā)表于 01-11 09:00 ?516次閱讀
    <b class='flag-5'>C++</b>簡史:<b class='flag-5'>C++</b>是如何開始的

    C語言和C++中那些不同的地方

    C語言雖說經常和C++在一起被大家提起,但可千萬不要以為它們是一個東西?,F(xiàn)在我們常用的C語言是C89標準,C++
    的頭像 發(fā)表于 12-07 14:29 ?894次閱讀
    <b class='flag-5'>C</b>語言和<b class='flag-5'>C++</b>中那些不同的地方

    開箱即用!教你如何正確使用華為云CodeArts IDE for C/C++

    華為云CodeArts IDE 定位華為云開發(fā)者桌面 ,是華為云面向開發(fā)者提供的一款智能化桌面集成開發(fā)環(huán)境。CodeArts IDE for C/C++集成了華為自研的C/C++語言服
    的頭像 發(fā)表于 11-29 17:40 ?743次閱讀
    開箱即用!教你如何正確使用華為云CodeArts IDE for <b class='flag-5'>C</b>/<b class='flag-5'>C++</b>!

    如何選擇創(chuàng)建c語言和c++

    選擇創(chuàng)建 C 語言和 C++ 都需要綜合考慮多個因素。在決定使用哪種語言之前,我們需要對這兩種語言的特點、優(yōu)缺點、適用場景、學習成本等進行全面的了解和對比。下面是關于選擇創(chuàng)建 C 語言和 C+
    的頭像 發(fā)表于 11-27 15:58 ?560次閱讀

    c++怎么開始編程

    C++是一種高級的、通用的編程語言,用于開發(fā)各種類型的應用程序。它是從C語言演變而來,也是一種靜態(tài)類型語言,可以在不同的平臺上進行開發(fā)。C++具有高度的靈活性和性能,并且廣泛應用于游戲開發(fā)、桌面
    的頭像 發(fā)表于 11-27 15:56 ?879次閱讀

    c++多行注釋快捷鍵

    C++中,多行注釋(也稱為塊注釋)是一種用于注釋大段代碼或多個語句的方法。當你希望暫時禁用一些代碼或者解釋特定部分代碼的作用時,多行注釋是非常有用的。 在C++中,多行注釋以 /* 開始,以
    的頭像 發(fā)表于 11-22 10:24 ?7973次閱讀

    使用Visual C++進行串口通信編程

    電子發(fā)燒友網站提供《使用Visual C++進行串口通信編程.doc》資料免費下載
    發(fā)表于 11-21 09:39 ?3次下載
    使用Visual <b class='flag-5'>C++</b>進行串口通信編程

    C/C++語言學習大全套

    電子發(fā)燒友網站提供《C/C++語言學習大全套.rar》資料免費下載
    發(fā)表于 11-18 14:33 ?4次下載
    <b class='flag-5'>C</b>/<b class='flag-5'>C++</b>語言學習大全套