天天看點

學習現代的C++ | 資源管理 | SFINAE

  1. 資源管理

    C++标準裡自由存儲區,freestore,new 和 delete 操作的區域是 free store

    malloc 和 free 操作的區域是 heap

    兩者差別:new delete不一定使用malloc free,那麼自由存儲區!= 堆

    RAII:Resource Acquisition Is Initialization, 依托棧和析構函數,進行資源管理,編譯器會自動調用析構函數,包括在函數執行發生異常的情況(棧展開),(關閉檔案,釋放鎖,釋放資源)

    java 垃圾回收機制:可達性分析後,标記不可達記憶體區域,根據算法清除方法區和堆中垃圾

    釋放記憶體,delete前可能會有異常,記憶體洩漏

    棧:

    棧的增長方向是低位址,當函數調用另外一個函數時,會把參數也壓入棧裡(我們此處忽略使用寄存器傳遞參數的情況),然後把下一行彙編指令的位址壓入棧,并跳轉到新的函數。新的函數進入後,首先做一些必須的儲存工作,然後會調整棧指針,配置設定出本地變量所需的空間,随後執行函數中的代碼,并在執行完畢之後,根據調用者壓入棧的位址,傳回到調用者未執行的代碼中繼續執行。
    堆:

    不能存儲在棧上的情況:

    對象很大;對象的大小在編譯時不能确定;對象是函數的傳回值,但由于特殊的原因,不應使用對象的值傳回

    靜态存儲區:編譯時确定,和堆棧不同

    非靜态資料成員加上動态類型所需的空間。注意後者不一定是4,而一般是指針的大小,在64位系統上是8位元組。還有,要考慮位元組對齊的影響。靜态資料成員和成員函數都不占個别對象的空間

    工廠方法傳回基類指針,如果傳回對象,會産生對象切片

  2. 智能指針

    unique_ptr 一個對象隻能被單個指針擁有;shared_ptr 一個對象被多個指針擁有,共享引用計數。

    // 成員函數
    T& operator*() const noexcept { return *ptr_; }  
    T* operator->() const noexcept { return ptr_; }  
    operator bool() const noexcept { return ptr_; }
               
    if (this != &rhs),指派過程中發生異常,this内容可能被破壞掉。這樣寫,先複制再交換,複制過程中異常也不影響this:
    smart_ptr& operator=(smart_ptr rhs)
    {   
        rhs.swap(*this);
        return *this;
    }
               
    模闆的移動構造函數不被編譯期視為真的移動構造,仍然會幫助生成拷貝構造函數。
  3. 右值與移動

    類型是右值引用的變量是一個左值,有辨別符,可以取位址

    std::move(ptr1) 有名字的右值,xvalue

    如果一個 prvalue 被綁定到一個引用上,它的生命周期則會延長到跟這個引用變量一樣長。對 xvalue

    無效。如果由于某種原因,prvalue 在綁定到引用以前已經變成了 xvalue,那生命期就不會延長。

    右值的目的:實作移動,減少運作開銷。智能指針中,使用移動後,減少一次addCount計數,析構時減少reduceCount。

    使對象支援移動的技巧:

    ① 移動構造函數

    ② swap成員函數

    ③ 全局的swap函數,調用成員函數,更便捷

    ④ 實作通用的 operator=

    ⑤ 聲明為noexcept

    傳回值優化(NRVO),把對象直接構造到調用者的棧上。從 C++11 開始,傳回值優化仍可以發生,但在沒有傳回值優化的情況下,編譯器将試圖把本地對象移動出去,而不是拷貝出去。對傳回值臨時變量,std::move,會阻止NOVO,多此一舉。

    引用坍縮:

    對于 template foo(T&&) 這樣的代碼,如果傳遞過去的參數是左值,T 的推導結果是左值引用;如果傳遞過去的參數是右值,T 的推導結果是參數的類型本身。

    如果 T 是左值引用,那 T&& 的結果仍然是左值引用——即 type& && 坍縮成了 type&。

    如果 T 是一個實際類型,那 T&& 的結果自然就是一個右值引用

    完美轉發:保持參數的左值右值類别進行轉發。std::forward
  4. 容器

    string_view 是隻讀的輕量對象,傳參時不會拷貝。

    vector 連續記憶體,通路快,大小增長導緻移動,可以提前reserve空間。

    deque 雙端隊列,頭尾添加删除元素快速,空間部分連續,下标通路效率也比較高。

    list O(1) 複雜度的任意位置的插入和删除操作,每個元素記憶體空間單獨分布,周遊性能低,插入高效。

    forward_list 單向清單,記憶體占用少7

    擴充卡:queue FIFO ,預設deque

    stack LIFO,預設deque

    priority_queue 排序

    函數對象及其特化 less 、hash

    有序關聯容器:set(集合)、map(映射)、multiset(多重集)和 multimap(多重映射),存儲在關聯容器中的鍵一般應滿足嚴格弱序關系

    lower_bound(k) 找到第一個不小于查找鍵 k 的元素(!(x < k))

    upper_bound(k) 找到第一個大于查找鍵 k 的元素(k < x)

    無序關聯容器:unordered_set、unordered_map、unordered_multiset 、unordered_multimap,實作使用哈希表

    數組: C++17 array 棧上配置設定, std::size(arr)

  5. 異常

    異常安全是指當異常發生時,既不會發生資源洩漏,系統也不會處于一個不一緻的狀态。如果一個函數聲明了不會抛出異常、結果卻抛出了異常,C++ 運作時會調用 std::terminate 來終止應用程式。

    vector 會在元素類型沒有提供保證不抛異常的移動構造函數的情況下,在移動元素時會使用拷貝構造函數。

  6. 疊代器

    疊代器 對象可以被拷貝構造、拷貝指派和析構;對象支援 * 運算符;對象支援前置 ++ 運算符

    輸入疊代器 input iterator, 隻要求可以單次通路

    前向疊代器 forward iterator ,允許多次通路

    雙向疊代器 bidirectional iterator, forward 支援 –

    随機通路疊代器 random-access iterator ,雙向疊代器,如果額外支援在整數類型上的 +、-、+=、-=,跳躍式地移動疊代器

    (C++20)連續疊代器 contiguous iterator,疊代器指向的對象在記憶體中是連續存放的

    輸出疊代器 back_inserter_iterator

    vector<int> v1{1, 2, 3, 4, 5};
    vector<int> v2;
    copy(v1.begin(), v1.end(), back_inserter(v2));
               
  7. 易用性改進

    auto推斷

    decltype:

    decltype(a) 類型

    decltype((a)) 引用

    decltype(a + a) 類型

    C++14 decltype(auto) a = expr; 既可以是值類型,也可以是引用類型;用在通用的轉發函數模闆中

    清單初始化:vector v{1, 2, 3, 4, 5}; initializer_list

    統一初始化:幾乎可以在所有初始化對象的地方使用大括号而不是小括号,不可窄化,{1.0} 不可調用 obj(int)

    二進制字面量:unsigned mask = 0b111000000;

    靜态斷言:static_assert(編譯期條件表達式, 可選輸出資訊);

    default delete成員函數:

    沒有初始化的非靜态 const 資料成員和引用類型資料成員會導緻預設提供的預設構造函數被删除;

    非靜态的 const 資料成員和引用類型資料成員會導緻預設提供的拷貝構造函數、拷貝指派函數、移動

    構造函數和移動指派函數被删除;

    使用者如果沒有自己提供一個拷貝構造函數(必須形如 Obj(Obj&) 或 Obj(const Obj&);不是模闆),編譯器會隐式聲明一個;

    使用者如果沒有自己提供一個拷貝指派函數(必須形如 Obj& operator=(Obj&) 或 Obj& operator=(const Obj&);不是模闆),編譯器會隐式聲明一個;

    使用者如果自己聲明了一個移動構造函數或移動指派函數,則預設提供的拷貝構造函數和拷貝指派函數被删除;

    使用者如果沒有自己聲明拷貝構造函數、拷貝指派函數、移動指派函數和析構函數,編譯器會隐式聲明一個移動構造函數;

    使用者如果沒有自己聲明拷貝構造函數、拷貝指派函數、移動構造函數和析構函數,編譯器會隐式聲明一個移動指派函數。

    override:顯式聲明了成員函數是一個虛函數且覆寫了基類中的該函數

    final:聲明了成員函數是一個虛函數,且該虛函數不可在派生類中被覆寫

  8. 要不要傳回對象

    注意對于本地變量,永遠不要傳回引用或者指針,會導緻未定義行為,如果有析構函數釋放本地變量記憶體,通路到的記憶體是被破壞的。

    如果一個對象可拷貝可指派,也可以預設構造,成為半正則的。

    傳回值優化,拷貝消除。

    c++17對不可拷貝、不可移動的對象,仍然是可以被傳回的。

    F.20 在函數輸出數值時,盡量使用傳回值而非輸出參數。

  9. Unicode

    最早的中文字元集标準:GB2312

    Unicode 文本檔案通常有一個使用 BOM(byte order mark)字元的約定,區分各種編碼。

    Unix寬字元串wcout char32_t u32string,區域設定

    UTF-8:1 到 4 位元組的變長編碼。在一個合法的 UTF-8 的序列中,如果看到一個位元組的最高位是 0,那就是一個單位元組的

    Unicode 字元;如果一個位元組的最高兩比特是 10,那這是一個 Unicode 字元在編碼後的後續位元組;否則,這就是一個 Unicode

    字元在編碼後的首位元組。

  10. 編譯期多态

    容器類多共性,鴨子類型,不必繼承。

    類模闆和函數模闆,編譯器在定義時隻做基本的文法檢查,執行個體化時進行類型檢查。編譯過程中可能産生多個這樣的執行個體,連結時最後剩下一個執行個體。

    template class vector; 顯示執行個體化

    extern template class vector; 外部執行個體化, 告訴編譯器不需要執行個體化

    對函數模闆進行重載,對類模闆進行特化。

  11. 編譯期計算(模闆元程式設計)

    編譯期程式設計,需要把計算轉變成類型推導。e.m

    template <bool cond,
              typename Then,
              typename Else>
    struct If;
    
    template <typename Then,
              typename Else>
    struct If<true, Then, Else> {
      typedef Then type;
    };
    
    template <typename Then,
              typename Else>
    struct If<false, Then, Else> {
      typedef Else type;
    };
               

    用 :: 取一個成員類型、并且 :: 左邊有模闆參數的話,得額外加上 typename 關鍵字來标明結果是一個類型。

    <type_traits> ,提取某個類型在某方面的特點,特點既是類型,又是常值。

    值和類型轉換:integral_constant

  12. SFINAE 替換失敗非錯

    函數模闆執行個體化過程:

    1.調用函數與函數模闆名稱比對時,根據名稱找出所有适用的函數和函數模闆;

    2.适用的模闆形參替換,替換過程發生錯誤則丢棄;

    3.找到一個最佳比對産生該函數的調用;

    4.如果沒有找到最佳比對,或者有多個,編譯器報錯。

    SFINAE:執行個體化過程失敗時,會繼續尋找其他函數模闆重載,這個特性可用于檢測一個類是否有某個成員函數。

    enable_if 選擇性啟用某個函數的重載。比如,根據SFINAE特性,檢測container容器是否有reserve函數,如果有就事先調用reserve函數預留白間,避免不必要的移動和拷貝。

    enable_if_t<has_reserve::value, void> : 如果類型C有reserve函數,就啟用下面的成員函數,并且傳回類型為void。

    使用decltype傳回值

    declval :聲明某類型的參數,僅用于比對模闆,不實際使用,某類型沒有預設構造函數時,假想出一個該類的對象進行類型推導。

    declval<C&>().reserve(xxx) 測試C&類型的對象是否可用xxx調用reserve

    true_type false_type 标簽分發

  13. constexpr

    C++17 産生内斂變量概念 , static const int = 1; 預設不内斂 ,需要額外定義

    不使用動态記憶體配置設定的類型, 析構函數是平凡的(啥也不做)的類型,可以稱為字面類型,可以使用constexpr編譯期計算

  14. lambda表達式

    每一個lambda表達式都是一個全局唯一的類型,需要使用auto; 也可function模闆,但是消耗更多性能

    按引用捕獲的變量需要保證 變量和表達式的生命周期一樣長

    泛型lambda表達式,參數類型auto:auto func = [](auto x, auto y){}

    std::bind 的參數數量:函數對象的參數數量+1,比如bind(plus<>(), _1, _3) , 或者bind(plus<>(), _1, 100) 生成新的函數對象

    std::function:

    std::function<const int&()> F([]{ return 42; });

    int x = F(); // 未定義行為: F() 的結果是懸垂引用

  15. 函數式程式設計

    指令式程式設計:

    高階函數 accumulate 歸并

    partition 滿足條件放在前面

    remove_if 不滿足條件的放在前面

    說明式程式設計:sql

    自動的并行計算:std::reduce(std::execution::par, v.begin(), v.end());

  16. 工具

    Valgrind

    nvwa::debug_new http://wyw.dcweb.cn/leakage.htm

    Compiler Explorer

    https://cppinsights.io/

    Clang-Tidy

    Cppcheck

    Clang-Format

  17. 可變模闆

    轉發參數 std::forward,確定參數轉發時仍然保持正确的左值或右值引用類型

    遞歸用法

    tuple_size_v (在編譯期)取得多元組裡面的項數

    std::apply , 編譯時解析tuple pair array

  18. 異步

    程序:獨立位址空間,管理堆記憶體,套接字、檔案,程序退出時傳回給作業系統

    thread 要求在析構之前要麼 join(阻塞直到線程退出),要麼 detach(放棄對線程的管理),否則程式會異常退出

    mutex 多次加鎖,未定義行為

    lock_guard

    未來量:future 可以代替條件變量通知,future.get() 一個future隻能調用一次

    承諾量:promise 和future一起使用,一次性管道

    packed_task

    volatile 關鍵字可以達到記憶體同步作用,但是不能通用地達到記憶體同步的效果

    c++11 記憶體模型,store(2, memory_order_release); load(memory_order_acquire);acquire 和 release 同步出現

    原子操作; 記憶體序

    讀:在讀取的過程中,讀取位置的内容不會發生任何變動。

    寫:在寫入的過程中,其他執行線程不會看到部分寫入的結果。

    讀‐修改‐寫:讀取記憶體、修改數值、然後寫回記憶體,整個操作的過程中間不會有其他寫入操作插入,其他執行線程不會看到部分寫入的結果。

  19. C++20

    concepts 概念

    Ranges 可以使用管道;基于概念

    Coroutines 協程(備注:協程與子程式不同,可中斷先去執行其他,在一個線程中執行)

  20. Boost

    sudo apt-get install libboost-dev

    Boost.TypeIndex

    Boost.Core

    Boost.Conversion

    Boost.Hana 編譯期

  21. 單元測試

    Boost.Test

    Catch2

  22. 日志

    Easylogging++ 性能跟蹤

    spdlog

  23. 網絡

    RestSDK HTTP

參考: 極客時間 吳詠炜老師 現代 C++ 課程

c++

繼續閱讀