C++ 編程技巧筆記記錄(持續更新)

来源:https://www.cnblogs.com/KillerAery/archive/2019/09/28/11601229.html
-Advertisement-
Play Games

前言:C++是博大精深的語言,特性複雜得跟北京二環一樣,繼承亂得跟亂倫似的。 不過它仍然是我最熟悉且必須用在游戲開發上的語言,這篇文章用於挑選出一些個人覺得重要的條款/經驗/技巧進行記錄總結。 文章最後列出一些我看過的C++書籍/博客等,方便參考。 其實以前也寫過相同的筆記博文,現在用markdow ...


目錄

前言:C++是博大精深的語言,特性複雜得跟北京二環一樣,繼承亂得跟亂倫似的。

不過它仍然是我最熟悉且必須用在游戲開發上的語言,這篇文章用於挑選出一些個人覺得重要的條款/經驗/技巧進行記錄總結。
文章最後列出一些我看過的C++書籍/博客等,方便參考。

其實以前也寫過相同的筆記博文,現在用markdown”重置“一下。

類/對象


1.多態基類的析構函數應總是public virtual,否則應為protected

當要釋放多態基類指針指向的對象時,為了按正確順序析構,必須得藉助virtual從而先執行析構派生類再析構基類。
當基類沒有多態性質時,可將基類析構函數聲明protected,並且也無需耗費使用virtual。

2.編譯器會隱式生成預設構造,複製構造,複製賦值,析構,(C++11)移動構造,(C++11)移動賦值的inline函數

當你在代碼中用到以上函數時且沒有聲明該函數時,就會預設生成相應的函數。
特殊的,當你聲明瞭構造函數(無論有無參數),都不會隱式生成預設構造函數。
不過隱式生成的函數比自己手寫的函數(即使行為一樣)效率要高,因為經過了編譯器特殊優化。

(c++11)當你需要顯式禁用生成以上某個函數時,可在聲明函數 = delete
例如:

Type(const Type& t) = delete;

(c++11)當你需要顯式預設生成以上某個函數時,可在聲明函數 = default
例如:

Type(Tpye && t) = default;

3.不要在析構函數拋出異常,也儘量避免在構造函數拋出異常

析構函數若拋出異常,可能會使析構函數過早結束,從而可能導致一些資源未能正確釋放。
構造函數若拋出異常,則無法調用析構函數,這可能導致異常發生前部分資源成功分配,卻沒能執行析構函數的正確釋放行為。

模板


1. 不要偏特化模板函數,而是選擇重載函數。

編譯器匹配函數時優先選擇非模板函數(重載函數),再選擇模板函數,最後再選擇偏特化模板函數。
當匹配到某個模板函數時,就不會再匹配選擇其他模板函數,即使另一個模板函數旗下有更適合的偏特化函數。
所以這很可能導致編譯器沒有選擇你想要的偏特化模板函數。

2.(C++11)不要重載轉發引用的函數,否則使用其它替代方案

轉發引用的函數是C++中最貪婪的函數,容易讓需要隱式轉換的實參匹配到不希望的轉發引用函數。(例如下麵)

template<class T>
  void f(T&& value);

void f(int a);

//當使用f(long類型的參數)或者f(short類型的參數),則不會匹配int版本而是匹配到轉發引用的版本

替代方案:

  1. 捨棄重載。換個函數名或者改成傳遞const T&形參。
  2. 使用更複雜的標簽分派或模板限制(不推薦)。

函數


1.(C++11)禁用某個函數時,使用 = delete而非private

原因有4個:

  • private函數仍需要寫定義(即使那是空的實現),
  • 派生類潛在覆蓋禁用函數名的可能性,
  • “=delete”語法比private語法更直觀體現函數被禁用的特點,
  • 在編寫非類函數的時候,無法提供private屬性。

一般 = delete的類函數應為public,因為編譯器先檢測可訪問性再檢驗禁用性

2.(C++11)lambda表達式一般是函數對象。特殊地,在無捕獲時是函數指針。

編譯器編譯lambda表達式時實際上都會對每個表達式生成一種函數對象類型,然後構造出函數對象出來。
特殊地,lambda表達式在無任何捕獲時,會被編譯成函數,其表達式值為該函數指針(畢竟函數比函數對象更效率)。
因此在一些老舊的C++API只接受函數指針而不接受std::function的時候,可以使用無捕獲的lamdba表達式。

3.(C++11)儘可能使用lamada表達式代替std::bind

直接舉例說明,假設有如下Func函數:

void Func(int a, float b);

現在我們讓Func綁定上2.0f作為參數b,轉化一個void(int a)的函數對象。

std::function<void(int)> f;
float b = 2.0f;

//std::bind寫法
f = std::bind(Func, std::placeholders::_1, b);
f(100);

//lambda表達式寫法
f = [b](int a) {Func(a, b); };
f(100);

可以看到使用std::bind會十分不美觀不直觀,還得註意占位符位置順序。
而使用lambda表達式可以讓代碼變得十分簡潔優雅。

4.(C++11)使用lambda表達式時,避免預設捕獲模式

按引用預設捕獲容易造成引用空懸,而顯示的引用捕獲更能容易提醒我們捕獲的是哪個變數的引用,從而更容易理清該引用的生命周期。
按值預設捕獲容易讓人誤解lambda式是自洽的(即不依賴外部)。下麵是一個典型例子:

void test() {
   static int a = 0;
   auto func = [=]() {
   return a + 2;
   };
   a++;
   int result = func();
}

由於預設捕獲,你以為a是以按值拷貝過去,所以期待result總會會是2。但是實際上你是調用了同一個作用域的靜態變數,沒有拷貝的行為。

所以,無論是按值還是引用,都儘量指定變數,而不是用預設捕獲。

記憶體相關


1.檢查new是否失敗通常是無意義的。

new幾乎總是成功的,現代大部分操作系統採取進程的惰式記憶體分配(即請求記憶體時不會立即分配記憶體,當使用時才慢慢吞吞分配)。
所以當使用new時,通常不會立即分配記憶體,從而無法真正檢測到是否記憶體將會耗盡。

2.儘量避免多次new同一種輕量級類型,而是先new一個大區域再分配多次。

每次new的時候,實際上還會額外分配出一個存放記憶體信息的區域,而多次分配記憶體給輕量級類型時,會造成臃腫的記憶體信息。
而且在刪除這些區域時,很容易造成很多塊記憶體碎片,導致記憶體利用率不高。
所以應當使用記憶體池的方式,先new一大塊區域,再從區域分配記憶體給輕量級類型。

STL標準庫


1.(C++11)使用emplace/emplace_back/emplace_front而不是insert/push_back/push_front

emplace 最大的作用是避免產生不必要的臨時變數,因為它可以直接在容器相應的位置根據參數來構造變數。
而 insert / push_back / push_front 操作是會先通過參數構造一個臨時變數,然後將臨時變數移動到容器相應的位置。

2.在遍歷容器時刪除迭代器需謹慎

順序式容器刪除迭代器會破壞本身和後面的迭代器,節點式容器刪除迭代器會破壞本身,導致迴圈遍歷崩潰(迴圈遍歷依賴於容器原有的迭代器)。

兩個值得借鑒的正確做法:

auto it = vec.begin();
while (it != vec.end()){
    if (...){
        // 順序式容器的erase()會返回緊隨被刪除元素的下一個元素的有效迭代器
        it = vec.erase(it);
    }
    else{
        it++;
    }
}
auto it = list.begin();
while (it != list.end()){
    if (...) {
        t.erase(it++);
    }
    else {
        it++;
    }
}

3.容器的at()會檢查邊界,[]則不檢查邊界

STL小細節。另外std::vector<bool>和std::bitset的[]提供的是值拷貝,而不是引用。

4.sort()的< 比較操作符,若兩者相等則必須返還失敗。

STL的sort演算法基本是快排,是不穩定的排序。
若比較的兩者相等時返還成功,則不穩定排序容易出現死迴圈,從而導致程式崩潰。

5.永遠記住,更低的時間複雜度並不意味著更高的效率

STL容器,特別是set,map,有著很多O(logN)的操作速度,但並不意味著是最佳選擇,因為這種複雜度表示往往隱藏了常數很大的事實。

例如說,集合的主流實現是基於紅黑樹,基於節點存儲的,而每次插入/刪除節點都意味著調用一次系統分配記憶體/釋放記憶體函數。這相比vector等矢量容器所有操作僅一次系統分配記憶體(理想情況來說),實際上就慢了不少。

此外,矢量容器對CPU緩存更加友好,遍歷該種容器容易命中緩存,而節點式容器則相對容易命中失敗。

綜合上述,如果要選擇一個最適合的容器,那麼不要過度信賴時間複雜度,除非你十分徹底的瞭解STL容器,或對各容器進行多次效率測試。

優化與效率


1.儘可能使用 ++i 而不是 i++

這個是老生常談的C++經典問題,對於int/unsigned等內置類型時,++i與i++似乎在效率上沒有區別。
然而在使用迭代器或其他自定義類型時,i++往往還得創建一個額外的副本來用於返還值,而++i則直接返還它本身。

2.在後期遇到性能瓶頸,萬不得已時才使用inline

現代編譯器已經十分智能,很多時候該寫成inline的函數編譯器會自動幫你inline,不該inline的時候即使你顯式寫了inline編譯器也有可能認為不該inline。
也就是說顯式的寫出inline只是給編譯器一個建議,它不一定會採納。
因此在開發時不用過早優化,過早考慮inline,而是遇到性能瓶頸時才考慮使用顯式寫出inline,不過大部分這時候你更應該考慮的是你寫的演算法效率。

3.儘量不使用dynamic_cast並且禁用RTTI

依靠dynamic_cast的代碼往往可以用多態虛函數解決,而且多態虛函數更加優雅。因此,儘可能避免編寫dynamic_cast。
另外可以隨之禁用與dynamic_cast相關的RTTI特性,禁用該特性可以提升程式效率(每個類少一些臃腫的RTTI信息)。

異常


1.(C++11)若保證異常不會拋出,應使用noexpect異常規格,否則不要聲明異常規格。

無聲明異常規格,意思是可能拋出任何異常。
相比無聲明異常規格的函數,noexpect函數能得到編譯器的優化(發生異常時不必解開棧),且能清晰表示自己的無異常保證。

雜項


1.(C++17)需要用到任意可變的類型時,使用std::any,std::variant而不是union

union是從c繼承來的特性,它的成員不可以是帶構造函數,析構函數,自定義複製構造函數的c++類。
因此最好不要使用union,而是用std::any或std::variant ,目前C++17已引入<any>庫和<variant>庫。

2.(C++11)auto只能推導出類型型別,而decltype能夠推導出聲明型別

int& value = 233;
auto a = value;//auto是int類型
decltype(auto) b = value; //decltype(auto)是int&類型

也就是說auto的推導類型會拋棄引用性質,而decltype能夠推導出完整的聲明類型。

3.(C++11)使用nullptr而不是NULL或0

NULL是C語言遺留的東西,是將巨集定義成0的,容易造成指針和整數的二義性。
而nullptr很好的避免了整數的性質。

4.(C++11)使用enum class語法為枚舉類型提供限定範圍

C帶來的enum語法是允許枚舉類進行隱式轉換的,潛在造成程式員不希望發生的轉換。
而C++11的enum class會阻止隱式轉換,需要程式員顯示轉換

enum class Color{Red,Blue,Green};
Color color = Color::Red;
int i = static<int>(color);

5.(C++11)只要潛在編譯期可計算的函數/變數,就使用constexpr

constexpr能讓一些函數/變數在編譯期就可計算,可減少運行期運算。(可視作模板元運算的美化語法)
此外,constexpr如果接受的是運行期變數/參數,則會變成運行期計算。
也就是說它既可用作編譯期運算,也可運行期運算,語境作用域比非constexpr更廣。

參考


  • 《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》:游戲編程綜合技術書,有部分章節講C++的經驗。
  • 《游戲編程精粹 3》:同上。

C++是非常非常複雜的語言,看的這方面書越多就越覺得自己的無知(例如C++ Boost)。
但是在學習C++的中途也必須認識到,C++是一門工具,不要過多鑽C++語言的牛角尖。
謹記:程式員是要成為工程師而不是語言學家。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 外觀模式(Facade): 外部通過一個統一的介面,訪問子系統中的一群介面。外觀模式定義了一個高層介面,為子系統中的一組介面提供一個一致的入口,使得子系統更容易使用。外觀模式相對比較簡單,可以理解為中介,原先租房需要自己一個個篩選,聯繫房東,談好價格,簽合同等等,現在不需要這些了,只要你說出要求中介 ...
  • 場景 不知道大家有沒有遇到這樣的情況,就是去自動取款機取錢的時候,比如說你去取1000塊錢,這個時候系統會先幫你把1000塊錢扣除,然後自動取款機再把錢吐出來。但是如果取款機出現問題,會發現錢被扣了,但是錢沒有取出來。我第一次遇到這個問題的時候很擔心,當時跨行取取了3000塊錢,簡訊提醒我錢已經被扣 ...
  • 此文前端框架使用 "rax" ,全篇代碼暫未開源(待開源) 原文鏈接地址: "Nealyang/PersonalBlog" 前言 貌似在面試中,你如果設計一個 react/vue 組件,貌似已經是司空見慣的問題了。本文不是理論片,更多的是自己的一步步思考和實踐。文中會有很多筆者的思考過程,歡迎評論區 ...
  • 前言 今天我們一起來看行為型設計模式中的命令模式、何為命令模式呢?先談命令——我現在需要對某一條信息進行刪除,我進行點擊刪除按鈕。後臺執行刪除的命令、對信息進行刪除。那麼我們要講的命令模式又是什麼呢?命令模式就是把一個操作或者行為抽象為一個對象。然後通過對命令的抽象化來使得發出命令的職責和執行命令的 ...
  • 概述 簡單介紹一下七大設計原則: 1. 開閉原則 :是所有面向對象設計的核心,對擴展開放,對修改關閉 2. 依賴倒置原則 :針對介面編程,依賴於抽象而不依賴於具體 3. 單一職責原則 :一個介面只負責一件事情,只能有一個原因導致類變化 4. 介面隔離原則 :使用多個專門的介面,而不是使用一個總介面 ...
  • 一 鎖 行級鎖 select_for_update(nowait=False, skip_locked=False) 註意必須用在事務裡面,至於如何開啟事務,我們看下麵的事務一節。 返回一個鎖住行直到事務結束的查詢集,如果資料庫支持,它將生成一個 SELECT ... FOR UPDATE 語句。 ...
  • 別看Spring現在玩的這麼花,其實它的“籌碼”就兩個,“容器”和“bean定義”。只有先把bean定義註冊到容器里,後續的一切可能才有可能成為可能。所以在進階的路上如果要想走的順暢些,徹底搞清楚bean定義註冊的所有細節至關重要。畢竟這是萬里長征的第一步。有句話怎麼說來著,“勿在浮沙築高臺”。Sp ...
  • 增加Security配置類 前面演示了一個簡單的登錄入門例子,使用springboot security預設的配置實現,雖然非常簡單,但是基本實現了登錄功能。不過在生產環境下,顯然不能僅僅使用如此簡單的登錄功能,我們還需要更多個性化的登錄配置,所以我們要使用配置類來代替預設配置。新建一個配置類 We ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...