C++基礎知識整理

EricLing0529發表於2024-04-06
  • 1. CPP編譯連結過程
  • 2. new和malloc區別,delete和free區別
  • 3. 指標和引用
  • 4. 左值引用和右值引用
  • 5. const
  • 6. 函式過載
  • 7. 函式呼叫棧幀開闢過程
  • 8. inline 行內函數
  • 9. static關鍵字
  • 10. 定義指向類的成員的指標
  • 11. this指標
  • 12. 常成員方法
  • 13. 函式模板與類别範本
    • 函式模板
    • 類别範本
      • 用類别範本實現一個順序棧
      • 用類别範本實現vector
  • 14. 容器空間配置器
  • 15. 運算子過載
  • 16. 實現一個string類
  • 17. 容器迭代器實現原理
    • 給自定義string類新增迭代器
    • 給自定義vector類新增迭代器
  • 18. 迭代器失效問題
  • 19. new和delete的過載
  • 20. 過載new和delete實現物件池
  • 21. 繼承結構中的訪問限定
  • 22. 過載、隱藏、覆蓋(重寫)
  • 23. 多型、虛擬函式、虛擬函式指標、虛擬函式表
    • 一些練習題
  • 24. 虛解構函式
  • 25. 虛基類、虛繼承、菱形繼承
  • 26. C++中的四種型別轉換
  • 27. STL 六大元件
    • 容器(container)
    • 演算法(algorithm)
    • 迭代器(iterator)
    • 仿函式(functor)
    • 介面卡(adapter)
    • 空間配置器(allocator)

1. CPP編譯連結過程

img

預處理
處理以#開頭的命令,純文字替換,型別不安全

#pragma lib#pragma link除外,#pragma lib用於指定要連結的庫,
#pragma link用於指定程式入口(預設入口是main函式,但可以透過該命令修改)
都是在連結階段進行處理

編譯
詞法分析,語法分析,程式碼最佳化,編譯生成相應平臺的彙編程式碼

彙編
將彙編程式碼轉成特定平臺的機器碼,生成二進位制可重定位的目標檔案(*.obj),

連結
為了構造可執行檔案,連結器必須完成兩個主要任務:

  1. 符號解析(symbol resolution) 目標檔案定義和引用符號,每個符號對應於一個函式、一個全域性變數或一個靜態變數。符號解析的目的是將每個符號引用正好和一個符號定義關聯起來。
  2. 重定位(relocation) 編譯器和彙編器生成從地址 0 開始的程式碼和資料節。連結器透過把每個符號定義與一個記憶體位置關聯起來,從而重定位這些節,然後修改所有對這些符號的引用,使得它們指向這個記憶體位置。

可以參考書籍《深入理解計算機系統》第7章 連結的相關內容

相關的命令:

g++ 編譯的相關命令,可以使用-E-S-c分別對原始碼進行預處理,編譯,彙編,分別生成對應的檔案

命令 描述
g++ -E source_filename.cpp -o output_filename.i 生成預處理後的 CPP 原始檔(.i 檔案)
g++ -S source_filename.cpp -o output_filename.s 生成彙編程式碼檔案(.s 檔案)
g++ -c source_filename.cpp -o output_filename.o 生成目標檔案(.o 檔案)

objdump 可用於檢視目標檔案或可執行檔案的一些資訊

命令 描述
objdump -d 可執行檔案 反彙編可執行檔案,顯示其彙編程式碼
objdump -t 可執行檔案 顯示可執行檔案的符號表
objdump -r 可執行檔案 顯示可執行檔案的重定位表
objdump -s 可執行檔案 顯示可執行檔案的完整節(section)內容
objdump -h 可執行檔案 顯示可執行檔案的節頭表資訊
objdump -x 可執行檔案 顯示可執行檔案的全部資訊

以下給出一段示例程式碼,我們透過objdump檢視透過編譯器生成的目標檔案的符號資訊
img

其中,每個的具體含義可以參考以下內容,擷取自書籍《深入理解計算機系統》第7章 連結
img

我們還可以檢視這個elf檔案的檔案頭資訊,透過以下命令

objdump -h <executable>

2. new和malloc區別,delete和free區別

newmalloc

特性 new(C++) malloc(C語言)
記憶體分配 為特定型別的物件分配記憶體 分配指定大小的記憶體塊 需計算記憶體大小
物件構造 呼叫物件的建構函式進行初始化 不呼叫建構函式
物件銷燬 使用delete時呼叫物件的解構函式 不呼叫解構函式
返回型別 返回指向分配物件的指標 返回void指標(void*),需要型別強轉
錯誤處理 在分配失敗時丟擲異常 在分配失敗時返回空指標
記憶體釋放 使用delete釋放透過new分配的記憶體 使用free()釋放透過malloc分配的記憶體
陣列分配 支援使用new[]進行陣列分配 支援使用malloccalloc進行陣列分配

deletefree

特性 delete (C++) free (C)
記憶體釋放 釋放透過 new 分配的記憶體 釋放透過 malloccalloc 分配的記憶體
物件銷燬 呼叫物件的解構函式 不呼叫物件的解構函式
陣列釋放 使用 delete[] 釋放透過 new[] 分配的陣列記憶體 使用free釋放
空指標處理 可以安全地刪除空指標 可以安全地釋放空指標

Note: newdelete都是運算子,支援運算子過載

3. 指標和引用

引用是更安全的指標

int a = 42;

// 指標可以初始化為nullptr
int *p = nullptr;
p = &a;

// 引用必須初始化,且無法重新繫結
int &r = a;

底層彙編程式碼一樣
img

引用不佔用記憶體空間,不是物件,只是別名而已,必須初始化,且無法重新繫結,重新對引用賦值,其實是對引用所繫結的物件賦值而非重新繫結
img

總結如下:

區別 引用 指標
語法 使用&宣告,用物件初始化 使用*宣告,用物件地址初始化
可空性 必須初始化,不可為null 可以用null值(nullptr)初始化
物件間接訪問 透明,可以直接使用物件語法 需要使用*進行顯式解引用
指標運算 不支援,引用不是真正的記憶體地址 支援加減等運算,用於在記憶體中導航
重新賦值 初始化後無法重新指向其他物件 可以隨時重新指向不同的物件
函式引數 用於透過引用傳遞引數 既可以用於透過引用傳遞引數,也可以作為空指標或可為空型別的引數

4. 左值引用和右值引用

int main(){
    int a = 10;  // a是左值,有名字,有記憶體,值可以修改
    int &b = a;  // b是左值引用

    // int &c = 20; // 20是右值,沒記憶體,沒名字
    const int &c = 20;

    // C++11提供了右值引用
    int &&d = 20;  // d是右值引用
    d = 30;
    int &e = d;  // d是右值引用,右值引用本身是一個左值
}

Note: 重點理解,一個右值引用變數本身是一個左值,可以被修改,可以被引用

TODO

SEE ALSO

  • [] 完美轉發
  • [] 引用摺疊

5. const

const 修飾的變數不能再作為左值,初始化完成後,值不能被修改

const和非const型別之間(無論哪一層的const)在不破壞型別安全的前提下,是可以進行自動型別轉換的,所以才能相互賦值(或初始化)。
比如int*const int*,把const int*賦值給int*是錯誤的,因為這可能導致int*指向一個const int,從而破壞型別安全。
但反過來把int*賦值給const int*則是安全的。

img

雖然,把int*賦值給const int*是正確的,但是把int**賦值給const int**卻是錯誤的。乍一看這兩種轉換非常相似,都是加一個底層const,但實際上前者確實是安全的,後者卻會引入致命的漏洞。

int* p = nullptr;
const int** cpp = &p; // 假如這行程式碼是正確的...

const int ci = 0;
*cpp = &ci;           // ???

關注最後這個賦值,cpp是const int**,所以等號左邊*cpp的型別是const int*,ci是const int,所以等號右邊&ci的型別是const int*,型別完全匹配,看起來毫無問題。然而實際上,*cpp返回的這個引用是繫結在p上的,也就是說這個賦值的語義是把p指向了ci,而實際上p是個int*而非const int*

也就是說,把int**賦值給const int**,這個多出的const可以把一個原本錯誤的賦值偽裝得人畜無害。所以這種轉換是錯誤的。到這裡已經很容易理解為什麼int**賦值給const int* const*是正確的——在上面這個例子中,假如cpp的型別改成const int* const*,那麼*cpp返回的就是一個常量,根本不能賦值,自然就沒有這個問題了。所以int**const int* const*的轉換是安全的,因此被允許。

Note: 多級指標T可以隱式轉換到有更多const限定符的型別,但是如果某一層加上了const,那麼右邊每層都必須有const

內容節選自C++ 底層const二級指標為何能被非const指標初始化? - 張三的回答 - 知乎

而在分析引用時,我們總是可以把引用轉化為指標來分析

int i = 10;
int &r = i;
// 我們總可以把上面的程式碼轉化為
int *r = &i;

在處理更復雜的引用時,也總是可以這樣進行轉化來分析。

#define, const, constexpr, consteval 比較

關鍵字 描述
#define #define 是C++中的預處理指令。它用於定義宏,宏是表示一系列記號的符號名稱。宏在編譯階段之前由前處理器處理。它們可用於文字替換、定義常量或建立類似函式的宏。
const const 是一個關鍵字,用於將變數宣告為常量。當變數宣告為const時,其值變為只讀,在初始化後無法修改。它在執行時解析,不提供編譯時評估(即不保證編譯時只讀,僅保證執行時只讀)。
constexpr constexpr 是C++11引入的關鍵字。它用於宣告一個值或表示式可以在編譯時評估。它允許在編譯期間進行計算,從而可能提高效能。constexpr 變數和函式使得編譯時評估成為可能
consteval consteval 是C++20引入的關鍵字。它用於指定函式必須在編譯時評估。它限制了函式只能在常量表示式上下文中呼叫。consteval 函式保證在編譯時完全評估,從而實現了最佳化和編譯時錯誤檢查。

6. 函式過載

函式過載:一組函式,在同一個作用域下,函式名相同,引數列表的個數或者型別不同

Note:

  • 注意必須處於同一個作用域下
  • 靜態(編譯時期)多型包括函式過載和模板
  • 僅僅返回值型別不同不叫函式過載
  • 還需要注意const和volatile是如何影響形參型別的(進而影響函式過載)

C++為什麼支援函式過載(C語言不支援)
C程式碼產生函式符號時,符號僅由函式名決定
C++程式碼產生函式符號時,符號由函式名和引數列表決定

C++程式碼和C程式碼之間如何互相呼叫

C++程式碼中呼叫C程式碼
add.c檔案

int add(int a, int b){
    return a + b;
}

main.cpp檔案

int add(int a, int b);

int main(){
    add(1, 2);
}

上述程式碼會出現連結錯誤,main.cpp檔案中使用到了add函式,這個函式的宣告一般是c庫的作者對外提供一份標頭檔案,而c庫的使用者將透過#include來使用這個c庫,那麼這個函式的宣告在編譯時,將透過c++語法規則生成對應的函式符號,這個函式符號生成規則與c編譯器是不同的(這也是c不支援函式過載的原因,因為c的函式符號僅由函式名生成),所以在連結時,是無法找到這個函式的。
正確的處理辦法如下

#ifdef __cplusplus
extern "C"
{
#endif
    int add(int a, int b);
#ifdef __cplusplus
}
#endif

__cplusplus是c++編譯器內建的宏,其值一般是C++標準的年份,用於區分不同的C++標準。C編譯器中沒有這個宏,且C編譯器不認識extern "C"這個語法。
上述程式碼在C++編譯器看來就是

extern "C"
{
    int add(int a, int b);
}

那麼C++編譯器就會按照C標準生成函式符號,從而成功連結

C程式碼中呼叫C++程式碼 (比較少見)
add.cpp檔案

#ifdef __cplusplus
extern "C"
{
#endif
    int add(int a, int b){
        return a + b;
    }
#ifdef __cplusplus
}
#endif

C++編譯器會將上面的程式碼按照C標準生成函式符號,提供給C程式使用

main.c檔案

int add(int a, int b);

int main(){
    add(1, 2);
}

7. 函式呼叫棧幀開闢過程

一些前置知識:

  • 程式計數器 (Program Counter,PC)保持跟蹤下一條要執行的指令的地址。在每個指令執行完成後,程式計數器自動遞增,將控制轉移到下一條指令
  • rbp存放當前棧幀的基地址
  • rsp存放當前棧幀的棧頂地址,且始終指向棧頂
  • 棧幀是由高地址向低地址方向開闢的,當函式被呼叫時,新的棧幀會被分配在棧的頂部,棧指標向低地址移動以留出空間。棧幀包含了函式的區域性變數、引數、返回地址和其他臨時資料(堆是由低地址向高地址方向增長的)。
  • 32位模式下,指標佔4位元組,64位模式下,佔8位元組
  • CALL指令會將當前指令的下一條指令的地址(即CALL指令後面的指令)壓入棧中。這是為了在子程式執行完畢後能夠返回到CALL指令後面的指令。
  • 子程式執行完畢後,透過RET(返回)指令返回到CALL指令後面的指令。CALL指令之後的指令地址被彈出棧,恢復為當前指令的下一條指令地址。
  • eax(累加器暫存器)是一個32位的通用暫存器,用於儲存算術和邏輯運算的結果,以及函式返回值
  • edx(資料暫存器)也是一個32位的通用暫存器,用於儲存除法和乘法運算的結果
  • esi(源索引暫存器)是用於資料傳輸和字串操作的32位通用暫存器。它通常用作源資料或源地址的索引
  • edi(目的索引暫存器)是另一個32位的通用暫存器,也用於資料傳輸和字串操作。它通常用作目標資料或目標地址的索引

Note:

  • rbp是64位模式下的基址指標暫存器,ebp是32位模式下的基址指標暫存器
  • rsp是64位模式下的棧指標暫存器,esp是32位模式下的棧指標暫存器

這裡對以下程式碼中的函式呼叫的棧幀進行詳細分析:

int add(int a, int b)
{
    int result = a + b;
    return result;
}
int main()
{
    int answer;
    answer = add(40, 2);
}

透過vimtermdebug模式,我們可以方便的檢視這段程式的反彙編指令和檢視記憶體,這將幫助我們理解棧幀開闢的具體過程。這裡先給出四張截圖,

img

img

圖一圖二是透過gdb對程式進行除錯,並使用命令檢視變數和暫存器以及反彙編指令

img

圖三是這段程式的棧幀,最左邊有記錄著變數和 rbprsp的位置,中間是記憶體地址,下面是高地址,上面是低地址,即棧幀是從下往上開闢的,最右邊是對應的記憶體中儲存的值。

Note: rbp,rsp表示main函式棧幀,rbp(2),rsp(2)表示add函式的棧幀

我們先分析main函式的彙編程式碼,

img

為了節約篇幅,這裡擷取部分指令地址,

147 <+0>:     endbr64 
14b <+4>:     push   %rbp		//rbp入棧,main函式執行完後,回退到之前的棧幀
14c <+5>:     mov    %rsp,%rbp		//把rsp賦值給rbp
14f <+8>:     sub    $0x10,%rsp		//rsp向低地址移動16位元組,即main函式的棧幀,就是16bytes
153 <+12>:    mov    $0x2,%esi		//把立即數2賦值給esi暫存器
158 <+17>:    mov    $0x28,%edi		//把立即數40賦值給edi暫存器
15d <+22>:    call   0x555555555129 <_Z3addii> //呼叫add函式
162 <+27>:    mov    %eax,-0x4(%rbp)	//把eax暫存器中存放的值賦值給rbp-4(也就是answer)
165 <+30>:    mov    $0x0,%eax		//把0賦給eax暫存器
16a <+35>:    leave  
16b <+36>:    ret    

main函式中的棧幀在0x7fffffffdb100x7fffffffdb00之間,如下所示

rsp->	0x7fffffffdb00
	    0x7fffffffdb04
	    0x7fffffffdb08
answer	0x7fffffffdb0c	21845
rbp->	0x7fffffffdb10
	    0x7fffffffdb14

彙編程式碼執行到0x000055555555515d <+22>: call 0x555555555129 <_Z3addii>時,會把call指令的下一條指令地址壓棧,即把指令地址0x0000555555555162壓棧,然後將控制轉移到指定的目標地址,即子程式的入口點。控制轉移後,程式開始執行子程式中的程式碼。

此時,我們再看圖一右半部分,

img

129 <+0>:     endbr64 
12d <+4>:     push   %rbp		//rbp壓棧
12e <+5>:     mov    %rsp,%rbp		//rsp賦給rbp
131 <+8>:     mov    %edi,-0x14(%rbp)	//把edi暫存器中的值賦給rbp-0x14
134 <+11>:    mov    %esi,-0x18(%rbp)	//把esi暫存器中的值賦給rbp-0x18
137 <+14>:    mov    -0x14(%rbp),%edx	//把rbp-0x14中的值賦給edx
13a <+17>:    mov    -0x18(%rbp),%eax	//把rbp-0x18中的值賦給eax
13d <+20>:    add    %edx,%eax		//把edx中的值加到eax中
13f <+22>:    mov    %eax,-0x4(%rbp)	//把eax的值賦給rbp-0x4
142 <+25>:    mov    -0x4(%rbp),%eax	//把rbp-0x4的值賦給eax
145 <+28>:    pop    %rbp		//彈出棧頂的值,賦給rbp(即退回到main的棧底)
146 <+29>:    ret			//CALL指令之後的指令地址被彈出棧,恢復為當前指令的下一條指令地址

其對應的棧幀為

	        b	0x7fffffffdad8	2
	        a	0x7fffffffdadc	40
		        0x7fffffffdae0
		        0x7fffffffdae4
		        0x7fffffffdae8
	    result	0x7fffffffdaec	42
rbp(2) rsp(2)->	0x7fffffffdaf0	0x00007fffffffdb10
		        0x7fffffffdaf4
		        0x7fffffffdaf8	0x0000555555555162
		        0x7fffffffdafc
	    rsp->	0x7fffffffdb00
		        0x7fffffffdb04
		        0x7fffffffdb08
	    answer	0x7fffffffdb0c	21845
	    rbp->	0x7fffffffdb10
		        0x7fffffffdb14
		        0x7fffffffdb18

圖二給出了

  • add函式執行完之後,引數a,b和result的地址和值(左半部分)
  • 返回到main函式中,成功恢復到之前的棧幀(右半部分)

重點關注以下問題:

  1. add函式呼叫,引數是如何傳遞的
  2. add函式執行完,返回值是如何傳遞的
  3. add執行完後,怎麼知道回到哪個函式中
  4. 回到main以後,如何知道從哪一行指令繼續執行的

以下給出解答:

add函式呼叫,引數是如何傳遞的

img

main函式中,在執行call指令之前,已經把實參傳遞到esi和edi通用暫存器中了,

img

add函式中,從esi和edi通用暫存器中,把實參取出,分別放到了rbp-0x18和rbp-0x14中

add函式執行完,返回值是如何傳遞的

add函式中,把rbp-0x18和rbp-0x14中存放的實參,分別賦給了eax和edx暫存器,執行加法操作後,最終結果存放在eax中,然後賦給rbp-0x4,即result區域性變數,然後把這個結果賦給eax暫存器,而main函式中,在call指令之後,mov %eax,-0x4(%rbp) 指令從eax暫存器中取出了返回值,放到了rbp-4即answer區域性變數中

add執行完後,怎麼知道回到哪個函式中,回到main以後,如何知道從哪一行指令繼續執行的

  1. 在main函式中,呼叫call指令時,這個指令會把call指令後的下一條指令地址壓棧 即0x0000555555555162壓入棧中;
  2. 隨後在add函式中,會把main函式棧幀的rbp壓入棧中,即0x00007fffffffdb10壓入棧中;
  3. add函式執行完畢,會執行pop rbp指令,把0x00007fffffffdb10彈出棧,賦給rbp,此時就是回到了main函式的棧幀了;
  4. 最後,ret指令會把CALL指令之後的指令地址彈出棧,即把0x0000555555555162彈出棧賦給pc,恢復為當前指令的下一條指令地址。

這裡給出一個截圖,可以看到call指令後的下一條指令地址和rbp確實先後壓入棧中了

img

再補充一個例子,這個例子節選自函式呼叫過程中棧到底是怎麼壓入和彈出的? - 一八七四的回答 - 知乎

void func_B(arg_B1, arg_B2){
    var_B1;
    var_B2;
}
void func_A(arg_A1, arg_A2){
    var_A;
    func_B(arg_B1, arg_B2);
}
int main(int argc, char *argv[], char **envp){
	func_A(arg_A1, arg_A2);
}

img

8. inline 行內函數

行內函數

  • 沒有函式呼叫開銷,直接在呼叫點處進行程式碼展開
  • 不再產生函式符號
  • inline只是建議編譯器處理為內聯,但是否採用取決於程式碼的複雜程度
  • 在debug模式下,內聯不起作用
  • 類內部定義的函式 預設內聯

9. static關鍵字

static關鍵字

  1. 靜態成員變數:類的所有物件之間共享的變數,而不是每個物件擁有自己的副本。靜態成員變數必須在類的定義外部進行初始化
class MyClass {
public:
    static int count;
};

// 必須在類的定義外部進行初始化
int MyClass::count = 0;

int main() {
    MyClass obj1;
    MyClass obj2;
    
    obj1.count = 5;
    cout << obj2.count; // 輸出 5,因為 count 是靜態成員變數,obj1 和 obj2 共享同一個變數
    return 0;
}
  1. 靜態成員函式:不依賴於類的任何物件,因此無需透過物件進行呼叫,可以直接使用類名加作用域解析運算子來呼叫。靜態成員函式不能訪問非靜態成員變數,只能訪問靜態成員變數。
class MyClass {
public:
    static void printMessage() {
        cout << "Hello, World!" << endl;
    }
};

int main() {
    MyClass::printMessage(); // 輸出 Hello, World!
    return 0;
}
  1. 靜態區域性變數: 靜態區域性變數在第一次執行到其宣告語句時初始化,並在函式呼叫之間保持其值。
void foo() {
    static int counter = 0;
    counter++;
    cout << "Counter: " << counter << endl;
}

int main() {
    foo(); // 輸出 Counter: 1
    foo(); // 輸出 Counter: 2
    foo(); // 輸出 Counter: 3
    return 0;
}
  1. 靜態全域性變數:在全域性作用域內宣告的靜態變數,其作用範圍僅限於當前原始檔。它與普通的全域性變數類似,但只能在宣告它的原始檔中訪問,其他原始檔無法直接訪問該變數。

Note: 靜態全域性變數的主要特點是它們具有檔案作用域(File Scope)和內部連結(Internal Linkage)。檔案作用域意味著該變數在宣告它的原始檔中的任何位置都是可見的。內部連結意味著該變數只能在宣告它的原始檔中訪問,其他原始檔無法直接訪問。

  1. 靜態全域性函式:全域性作用域內宣告的靜態函式,其作用範圍僅限於當前原始檔。靜態全域性函式只能在宣告它的原始檔中訪問,其他原始檔無法直接呼叫該函式。

10. 定義指向類的成員的指標

類的普通成員變數和普通成員

#include <iostream>
using namespace std;

class MyClass {
public:
    static int staticValue;
    int value;

    static void staticFunction() {
        cout << "Static Function" << endl;
    }

    void printValue() {
        cout << "Value: " << value << endl;
    }
};

// 靜態成員變數必須在類外定義
int MyClass::staticValue = 42;

int main() {
    MyClass obj;
    obj.value = 10;

    // 定義指向成員變數的指標
    int MyClass::*ptr = &MyClass::value;

    // 定義指向靜態成員變數的指標
    int* staticPtr = &MyClass::staticValue;

    // 透過指標訪問和修改成員變數
    obj.*ptr = 100;
    *staticPtr = 200;

    cout << "Modified Value: " << obj.value << endl;
    cout << "Modified Static Value: " << MyClass::staticValue << endl;

    // 定義指向成員函式的指標
    void (MyClass::*funcPtr)() = &MyClass::printValue;

    // 定義指向靜態成員函式的指標
    void (*staticFuncPtr)() = &MyClass::staticFunction;

    // 透過指標呼叫成員函式
    (obj.*funcPtr)();

    // 透過指標呼叫靜態成員函式
    (*staticFuncPtr)();

    return 0;
}

11. this指標

在C++中,this指標是一個特殊的指標,它指向當前物件的地址。它是作為類的非靜態成員函式的隱含引數存在的,允許在類的成員函式中訪問當前物件的成員。可以選擇省略this->運算子,直接使用成員變數或呼叫成員函式的名稱,編譯器會自動將其解析為當前物件的成員。然而,顯式地使用this指標可以增強程式碼的可讀性,並且在某些情況下可能是必需的,例如當成員變數與引數名稱衝突時。

12. 常成員方法

在C++中,常成員方法是指在類中宣告為const的成員方法。常成員方法承諾不會修改類的成員變數。透過將成員方法宣告為常量,你可以確保在物件被視為常量時仍然可以呼叫該方法。在常成員方法內部,你不能修改類的成員變數,除非它們被宣告為mutable。

常成員方法具有以下特點:

  1. 可以被常量物件呼叫:常量物件是指被宣告為const的物件。常量物件只能呼叫常成員方法,因為常成員方法不會修改物件的狀態。
  2. 不能修改成員變數:在常成員方法內部,你不能直接修改類的非靜態成員變數,除非它們被宣告為mutable。
  3. 不能呼叫非常量成員方法:常成員方法只能呼叫類中被宣告為常量的成員方法。這是因為常量物件不允許對其狀態進行修改。
class MyClass {
private:
    int x;

public:
    void setX(int value) {
        this->x = value;
    }

    int getX() const {
        return this->x;
    }
};

int main() {
    const MyClass obj;  // 宣告一個常量物件
    int value = obj.getX();  // 可以呼叫常成員方法
    // obj.setX(42);  // 錯誤!常量物件不能呼叫非常量方法
    return 0;
}

getX()方法被宣告為常量方法,因此可以被常量物件obj呼叫。然而,setX()方法沒有被宣告為常量方法,所以常量物件不能呼叫它。

Note: 可以結合this指標記憶,每個非靜態成員函式,都是需要物件去呼叫的,編譯器會自動給非靜態成員函式新增this指標,而常量物件的指標就是常指標,比如程式碼中obj的指標型別是const MyClass *,而MyClass::setX()中的隱式this引數的型別是MyClass *,所以,常量物件不能呼叫非常量方法。程式設計實踐中,如果方法可以定義為常量方法,那麼我們就儘可能新增const。
以下情況下,將成員方法定義為常量可能是不合適的:

  • 當方法需要修改物件的狀態時,例如修改成員變數的值。
  • 當方法需要呼叫其他非常量成員方法時,例如在常量方法內部需要呼叫一個修改物件狀態的方法。

13. 函式模板與類别範本

函式模板

模板的意義:對型別進行引數化
以下是一個簡單的函式模板

template <typename T> // 模板引數列表,可以是型別引數(使用typename/class定義),也可以是非型別引數
bool compare(T a, T b) { // compare是一個函式模板,透過這個模板可以建立具體的函式
  return a > b;
}
// 編譯器在函式呼叫點,根據使用者指定的引數型別,由原模板例項化一份函式程式碼出來,得到模板函式
// 可以根據使用者傳入的實參的具體型別,推匯出模板型別引數的具體型別
int main() {
  // compare<int> 模板名 + 引數列表 才是函式名
  compare<int>(10, 20); // 函式呼叫點
  compare<double>(1.2, 2.1);
}

一些重要的概念:

  • 函式模板 用於建立函式的模板,不進行編譯,因為引數型別還不知道
  • 模板的例項化 在函式呼叫點進行例項化
  • 模板函式 對函式模板進行例項化得到的函式
  • 模板型別引數 使用typename/class定義一個型別引數
  • 模板非型別引數 不是一個型別,而是一個具體的值。這意味著非型別引數可以是整數、列舉、指標或引用等。
  • 模板的實參推演 根據使用者傳入的實參型別,推匯出模板的引數型別
  • 模板的特例化 模板函式不是由編譯器提供,而是使用者提供的例項化版本
  • 模板函式、模板的特例化、非模板函式的過載關係 優先順序為 普通非模板函式 > 特例化 > 函式模板

模板可以有非型別引數
以下是一段使用模板非型別引數的示例程式碼,使用氣泡排序實現陣列的排序
img

Note: 非型別引數必須在編譯時確定其值,因此它們通常需要是常數表示式。

模板實參推演
img

函式模板特例化

// 這是一個函式模板的特例化示例, 提供了const char*型別的特例化版本
template <> 
bool compare(const char *a, const char *b) {
  return strcmp(a, b) > 0;
}

類别範本

用類别範本實現一個順序棧

#include <iostream>

template <typename T> class SeqStack {
public:
  SeqStack(int capacity = 10)
      : _data(new T[capacity]), _top(0), _capacity(capacity) {}

  ~SeqStack() {
    delete[] _data;
    _data = nullptr;
  }

  SeqStack(const SeqStack<T> &val) : _top(val._top), _capacity(val._capacity) {
    _data = new T[_capacity];
    for (int i = 0; i < _top; ++i) {
      _data[i] = val._data[i];
    }
  }
  SeqStack<T> &operator=(const SeqStack<T> &val) {
    if (this == &val) {
      // 自賦值
      return *this;
    }
    delete[] _data;

    _top = val._top;
    _capacity = val._capacity;
    _data = new T[_capacity];
    for (int i = 0; i < _top; ++i) {
      _data[i] = val._data[i];
    }
    return *this;
  }

  void push(const T &val) {
    if (full())
      expand();
    _data[_top++] = val;
  }
  void pop() {
    if (empty())
      return;
    --_top;
  }
  T top() const { return _data[_top - 1]; }
  bool full() const { return _top == _capacity; }
  bool empty() const { return _top == 0; }

private:
  // 順序棧底層陣列2倍擴容
  void expand() {
    T *ptmp = new T[_capacity * 2];
    for (int i = 0; i < _top; ++i) {
      ptmp[i] = _data[i];
    }

    delete[] _data;
    _data = ptmp;
    _capacity *= 2;
  }

private:
  T *_data;
  int _top;
  int _capacity;
};

int main() {
  // 類别範本中呼叫了的方法才會例項化
  SeqStack<int> s;
  s.push(1);
  s.push(2);
  s.push(3);
  s.pop();
  std::cout << s.top() << std::endl;
}

用類别範本實現vector

#include <iostream>
template <typename T> class vector {
public:
  vector(int capacity = 10) {
    _first = new T[capacity];
    _last = _first;
    _end = _first + capacity;
  }
  ~vector() {
    delete[] _first;
    _first = _last = _end = nullptr;
  }

  vector(const vector<T> &val) {
    auto capacity = val._end - val._first;
    auto size = val._last - val._first;
    _first = new T[capacity];
    for (int i = 0; i < size; ++i) {
      _first[i] = val._first[i];
    }
    _last = _first + size;
    _end = _first + capacity;
  }

  vector<T> &operator=(const vector<T> &val) {
    if (this == &val) {
      return *this;
    }
    delete[] _first;

    auto capacity = val._end - val._first;
    auto size = val._last - val._first;
    _first = new T[capacity];
    for (int i = 0; i < size; ++i) {
      _first[i] = val._first[i];
    }
    _last = _first + size;
    _end = _first + capacity;

    return *this;
  }

  void push_back(const T &val) {
    if (full())
      expand();
    *_last++ = val;
  }

  void pop_back() {
    if (empty())
      return;
    --_last;
  }

  T back() const { return *(_last - 1); }
  bool full() const { return _last == _end; }

  bool empty() const { return _first == _last; }
  int size() const { return _last - _first; }

private:
  void expand() {
    int capacity = _end - _first;
    int size = _last - _first;
    T *ptmp = new T[capacity * 2];
    for (int i = 0; i < size; ++i) {
      ptmp[i] = _first[i];
    }
    delete[] _first;
    _first = ptmp;
    _last = _first + size;
    _end = _first + capacity * 2;
  }

private:
  T *_first;
  T *_last;
  T *_end;
};

int main() {
  vector<int> vec;
  for (size_t i = 0; i < 10; i++) {
    vec.push_back(i);
  }
  while (!vec.empty()) {
    std::cout << vec.back() << std::endl;
    vec.pop_back();
  }
}

14. 容器空間配置器

在學習容器空間配置器之前,先了解其存在的必要性。繼續看上面用類别範本實現的vector容器類,它存在以下幾個問題:

  1. 記憶體開闢與物件建立沒有分開
    img
    建立一個類,在構造和解構函式中分別列印一些東西,然後建立這個物件的vector容器,這是容器類的構造方法中,就會透過new運算子開闢空間,同時呼叫物件的構造方法,但其實這只是一個空容器,物件構造既無必要結果也不對

  2. 記憶體釋放與物件析構沒有分開
    img

  3. 新增元素時應該是複製構造而非複製賦值
    這個很顯然,往容器中新增元素時,正確的步驟就應該是在對應記憶體位置發生複製構造,而不是複製賦值。發生複製賦值還是因為容器的構造方法中,把記憶體開闢和物件構造都執行了,所以一個空容器裡面也裝滿了物件(透過物件的預設構造方法,這在物件沒有無參預設構造時又是另一個潛在的問題)。所以這個問題的解決辦法,也是需要把容器的記憶體開闢和物件構造分開。

  4. 刪除元素時僅移動top指標而沒有析構物件
    刪除元素沒有用delete是因為會把記憶體釋放掉,而我們不想釋放記憶體,只是想把物件析構而已。移動top指標可以把這個物件從容器中刪除,使這個物件不再可用,但卻沒有真正釋放這個物件,如果這個物件佔用了外部資源的話,這就會導致資源洩露了。
    解決辦法也很明顯,做到能夠析構物件的同時,又不釋放相應的記憶體空間,用delete運算子肯定是做不到的,這時解構函式就可以派上用場了,這是少數的需要人為呼叫解構函式的場景。

Note: 在使用了placement new時,大多都需要用到直接呼叫解構函式

以下是透過容器空間配置器實現的vector容器,它完美的解決了這四個問題

#include <iostream>
// 容器空間配置器
template <typename T> class Allocator {
public:
  T *allocate(size_t size) { return (T *)malloc(sizeof(T) * size); }
  void deallocate(void *p) { free(p); }
  void construct(T *p, const T &val) { new (p) T(val); } // placement new,也叫定位new
  void destry(T *p) { p->~T(); }
};
template <typename T, typename Alloc = Allocator<T>> class vector {
public:
  vector(int capacity = 10) {
    // _first = new T[capacity];
    _first = _alloc.allocate(capacity);
    _last = _first;
    _end = _first + capacity;
  }
  ~vector() {
    // delete[] _first;
    for (T *p = _first; p != _last; ++p) {
      _alloc.destry(p);
    }
    _alloc.deallocate(_first);
    _first = _last = _end = nullptr;
  }

  vector(const vector<T> &val) {
    auto capacity = val._end - val._first;
    auto size = val._last - val._first;
    // _first = new T[capacity];
    _first = _alloc.allocate(capacity);
    for (int i = 0; i < size; ++i) {
      // _first[i] = val._first[i];
      _alloc.construct(_first + i, val._first[i]);
    }
    _last = _first + size;
    _end = _first + capacity;
  }

  vector<T> &operator=(const vector<T> &val) {
    if (this == &val) {
      return *this;
    }
    // delete[] _first;
    for (T *p = _first; p != _last; ++p) {
      _alloc.destry(p);
    }
    _alloc.deallocate(_first);

    auto capacity = val._end - val._first;
    auto size = val._last - val._first;
    // _first = new T[capacity];
    _first = _alloc.allocate(capacity);
    for (int i = 0; i < size; ++i) {
      // _first[i] = val._first[i];
      _alloc.construct(_first + i, val._first[i]);
    }
    _last = _first + size;
    _end = _first + capacity;

    return *this;
  }

  void push_back(const T &val) {
    if (full())
      expand();
    // *_last++ = val;
    _alloc.construct(_last, val);
    ++_last;
  }

  void pop_back() {
    if (empty())
      return;
    // --_last;
    --_last;
    _alloc.destry(_last);
  }

  T back() const { return *(_last - 1); }
  bool full() const { return _last == _end; }

  bool empty() const { return _first == _last; }
  int size() const { return _last - _first; }

private:
  void expand() {
    int capacity = _end - _first;
    int size = _last - _first;
    // T *ptmp = new T[capacity * 2];
    T *ptmp = _alloc.allocate(2 * capacity);
    for (int i = 0; i < size; ++i) {
      // ptmp[i] = _first[i];
      _alloc.construct(ptmp + i, _first[i]);
    }
    // delete[] _first;
    for (T *p = _first; p != _last; ++p) {
      _alloc.destry(p);
    }
    _alloc.deallocate(_first);
    _first = ptmp;
    _last = _first + size;
    _end = _first + capacity * 2;
  }

private:
  T *_first;
  T *_last;
  T *_end;
  Alloc _alloc;
};

class Foo {
public:
  Foo() { std::cout << "Foo()" << std::endl; }
  ~Foo() { std::cout << "~Foo()" << std::endl; }
};
int main() { vector<Foo> vec; }

15. 運算子過載

運算子過載的作用:使物件的運算表現得和編譯器內建型別一樣

以下示例程式碼演示瞭如何進行運算子的過載,實現一個Complex類

#include <iostream>

class Complex {
public:
  friend std::ostream &operator<<(std::ostream &cout, const Complex &val);
  friend Complex operator+(const Complex &l, const Complex &r);

  Complex(int real = 0, int image = 0) : _real(real), _image(image) {}

  Complex operator+(const Complex &val) {
    return {_real + val._real, _image + val._image};
  }

  // 前置加
  Complex &operator++() {
    _real += 1;
    _image += 1;
    return *this; // 在返回*this的函式里,我們都可以考慮返回值新增引用
  }

  // 後置加
  Complex operator++(int) {
    // 一種寫法
    // Complex tmp = *this;
    // _real += 1;
    // _image += 1;
    // return tmp;

    // 最佳化後的另一種寫法
    return {_real++, _image++};
  }

  void operator+=(const Complex &val) {
    _real += val._real;
    _image += val._image;
  }

private:
  int _real;
  int _image;
};

std::ostream &operator<<(std::ostream &cout, const Complex &val) {
  cout << val._real << "," << val._image;
  return cout;
}

Complex operator+(const Complex &l, const Complex &r) {
  return {l._real + r._real, l._image + r._image};
}
int main() {
  Complex c1{1, 2}, c2{3, 4};
  Complex c3 = c1 + c2;
  std::cout << c3 << std::endl;

  Complex c4 = c1 + 10; // int -> Complex
  std::cout << c4 << std::endl;

  // 上面能夠成功執行是因為 c1.operator+(10),
  // 10會自動進行隱式型別轉換,變為Complex
  // 當然,這個隱式型別轉換要求Complex有相應的構造方法,即由一個int構造Complex的方法(這種寫法並不推薦)
  // 下面不可行,則是因為左運算元是一個int,而int沒有相應的operator+(Complex)方法
  // 如果要讓下面的程式碼可行,我們需要定義一個全域性的operator+(const Complex& l,
  // const Complex& r);
  //
  // Complex c5 = 10 + c1; // Invalid operands to binary
  // expression ('int' and 'Complex')

  std::cout << ++c4 << std::endl;
  std::cout << c4++ << std::endl;
  c4 += c1;
  std::cout << c4 << std::endl;
}

上述程式碼中需要注意以下要點:
0. 前置加和後置加的函式名相同,但是後置加有一個int引數

  1. 在返回物件時,儘量考慮程式碼最佳化,不構造臨時物件,這有助於編譯器對程式碼進行最佳化,方便直接在呼叫處構造物件。比如在前置加時,既然要返回的物件就是修改後的,我們可以返回*this即當前物件,那麼就可以讓返回值型別新增&,以減少不必要的複製,而後置加,則可以直接{member1++,member2++,...},這樣既把當前狀態返回了,又在返回之後將當前狀態修改了,編譯器會最佳化為直接在呼叫處構造新物件,而不必發生區域性物件的建立和複製構造。 todo: add one more section to specify this problem
  2. 運算子過載有全域性和成員之分,全域性的運算子過載函式將不限制必須使用類的物件進行呼叫,任何可以構造為或者隱式轉換為第一個形參型別的都能呼叫該全域性運算子過載函式

16. 實現一個string類

以下是一個實現了包括普通構造析構複製構造複製賦值移動構造移動賦值運算子過載的string類

#include <cstring>
#include <iostream>

class string {
public:
  friend string operator+(const string &l, const string &r);
  string(const char *p = nullptr) {
    std::cout << "default ctor" << std::endl;
    if (p != nullptr) {
      _data = new char[strlen(p) + 1];
      strcpy(_data, p);
    } else {
      _data = new char[1];
      *_data = '\0';
    }
  }

  ~string() {
    delete[] _data;
    _data = nullptr;
  }

  string(const string &str) {
    std::cout << "copy ctor" << std::endl;
    _data = new char[strlen(str._data) + 1];
    strcpy(_data, str._data);
  }

  string &operator=(const string &str) {
    if (this == &str) {
      return *this;
    }
    delete[] _data;
    _data = new char[strlen(str._data) + 1];
    strcpy(_data, str._data);
    return *this;
  }

  string(string &&str) {
    std::cout << "move ctor" << std::endl;
    _data = str._data;
    str._data = nullptr;
  }

  string &operator=(string &&str) {
    if (this == &str)
      return *this;
    delete[] _data;
    _data = str._data;
    str._data = new char[1];
    str._data[0] = '\0';
    return *this;
  }

  bool operator>(const string &val) const {
    return strcmp(_data, val._data) > 0;
  }

  bool operator<(const string &val) const {
    return strcmp(_data, val._data) < 0;
  }

  bool operator==(const string &val) const {
    return strcmp(_data, val._data) == 0;
  }

  // 僅普通物件能呼叫,可以讀,也可以寫
  // 讀 char ch = str[1];
  // 寫 str[1] = 'c';
  char &operator[](int idx) { return _data[idx]; }

  // 普通物件和常物件都能呼叫,只可以讀
  // 讀 char ch = str[1];
  const char &operator[](int idx) const { return _data[idx]; }
  //
  const char *c_str() const { return _data; }

  int length() const { return strlen(_data); }

private:
  char *_data;
};

std::ostream &operator<<(std::ostream &out, const string &val) {
  out << val.c_str();
  return out;
}

string operator+(const string &l, const string &r) {
  string tmp; // default
  delete[] tmp._data;
  tmp._data = new char[strlen(l._data) + strlen(r._data) + 1];
  strcpy(tmp._data, l._data);
  strcpy(tmp._data + strlen(l._data), r._data);
  // 預設情況下,編譯器會把這種區域性物件的複製構造或者複製賦值最佳化掉,減少一次複製
  // 使用move進行強制移動構造或移動賦值反而會畫蛇添足
  return std::move(tmp);
}

int main() {
  string s1 = "hello"; // default
  string s2 = "world"; // default
  string s3 = s1 + s2; // move
  std::cout << s3 << std::endl;
}

這些函式中,我們重點關注一下函式string operator+(const string &l, const string &r)
版本一
首先,我們還可以對該方法進行以下實現:

string operator+(const string& l,const string& r){
    char * tmp = new char[strlen(l._data) + strlen(r._data) + 1];
    strcpy(tmp, l._data);
    strcat(tmp, r._data);
    string s{tmp};
    delete[] tmp;
    return tmp;
}

注意到這個版本中,會進行三次new,其中第一次new是為了構造tmp指標,第二次new是為了構造臨時物件s(在s的建構函式中,發生了new和記憶體複製),第三次則是為了構造或者賦值到接收物件。這個版本可謂是效率低下。

版本二

string operator+(const string &l, const string &r) {
  string tmp; // default
  delete[] tmp._data;
  tmp._data = new char[strlen(l._data) + strlen(r._data) + 1];
  strcpy(tmp._data, l._data);
  strcpy(tmp._data + strlen(l._data), r._data);
  return tmp;
}

這個版本中,透過直接構造string物件,然後透過友元直接修改成員屬性,這減少了不必要的記憶體開闢和複製操作,同時編譯器會自動最佳化,去掉了返回到接收物件的複製操作。

版本三

string operator+(const string &l, const string &r) {
  string tmp; // default
  delete[] tmp._data;
  tmp._data = new char[strlen(l._data) + strlen(r._data) + 1];
  strcpy(tmp._data, l._data);
  strcpy(tmp._data + strlen(l._data), r._data);
  return std::move(tmp);
}

這個版本有點弄巧成拙了,雖然在返回時,想要透過移動構造或者移動賦值減少物件構造,但其實也讓編譯器最佳化失去了效果,所以很多時候,不用太關注這種細節,讓編譯器最佳化就能有很好的效能。

17. 容器迭代器實現原理

迭代器可以用於透明地訪問容器中的元素。

給自定義string類新增迭代器

在上面的自定義string類的基礎上,實現string容器內部的迭代器型別。

#include <cstring>
#include <iostream>

class string {
public:
  friend string operator+(const string &l, const string &r);
  string(const char *p = nullptr) {
    std::cout << "default ctor" << std::endl;
    if (p != nullptr) {
      _data = new char[strlen(p) + 1];
      strcpy(_data, p);
    } else {
      _data = new char[1];
      *_data = '\0';
    }
  }

  ~string() {
    delete[] _data;
    _data = nullptr;
  }

  string(const string &str) {
    std::cout << "copy ctor" << std::endl;
    _data = new char[strlen(str._data) + 1];
    strcpy(_data, str._data);
  }

  string &operator=(const string &str) {
    if (this == &str) {
      return *this;
    }
    delete[] _data;
    _data = new char[strlen(str._data) + 1];
    strcpy(_data, str._data);
    return *this;
  }

  string(string &&str) {
    std::cout << "move ctor" << std::endl;
    _data = str._data;
    str._data = nullptr;
  }

  string &operator=(string &&str) {
    if (this == &str)
      return *this;
    delete[] _data;
    _data = str._data;
    str._data = new char[1];
    str._data[0] = '\0';
    return *this;
  }

  bool operator>(const string &val) const {
    return strcmp(_data, val._data) > 0;
  }

  bool operator<(const string &val) const {
    return strcmp(_data, val._data) < 0;
  }

  bool operator==(const string &val) const {
    return strcmp(_data, val._data) == 0;
  }

  // 僅普通物件能呼叫,可以讀,也可以寫
  // 讀 char ch = str[1];
  // 寫 str[1] = 'c';
  char &operator[](int idx) { return _data[idx]; }

  // 普通物件和常物件都能呼叫,只可以讀
  // 讀 char ch = str[1];
  const char &operator[](int idx) const { return _data[idx]; }
  //
  const char *c_str() const { return _data; }

  int length() const { return strlen(_data); }

  // 迭代器可以透明地訪問容器內部的元素
  class iterator {
  public:
    iterator(char *p = nullptr) : _p(p) {}
    bool operator!=(const iterator &it) { return _p != it._p; }
    void operator++() { ++_p; }
    char &operator*() { return *_p; }

  private:
    char *_p;
  };

  iterator begin() { return {_data}; }
  iterator end() { return {_data + length()}; }

private:
  char *_data;
};

std::ostream &operator<<(std::ostream &out, const string &val) {
  out << val.c_str();
  return out;
}

string operator+(const string &l, const string &r) {
  string tmp; // default
  delete[] tmp._data;
  tmp._data = new char[strlen(l._data) + strlen(r._data) + 1];
  strcpy(tmp._data, l._data);
  strcpy(tmp._data + strlen(l._data), r._data);
  // 預設情況下,編譯器會把這種區域性物件到接收物件的複製構造或者複製賦值最佳化掉,減少一次複製
  // 使用move進行強制移動構造或移動賦值反而會畫蛇添足
  return std::move(tmp);
}

int main() {
  string s1 = "hello"; // default
  string s2 = "world"; // default
  string s3 = s1 + s2; // move
  std::cout << s3 << std::endl;
  std::cout << "================" << std::endl;
  for (auto it = s3.begin(); it != s3.end(); ++it) {
    std::cout << *it;
  }
  std::cout << std::endl;

  std::cout << "================" << std::endl;
  // 增強for依賴容器的begin和end方法
  for (char c : s3) {
    std::cout << c;
  }
  std::cout << std::endl;
}

給自定義vector類新增迭代器

#include <algorithm>
#include <iostream>
#include <iterator>
// 容器空間配置器
template <typename T> class Allocator {
public:
  T *allocate(size_t size) { return (T *)malloc(sizeof(T) * size); }
  void deallocate(void *p) { free(p); }
  void construct(T *p, const T &val) {
    new (p) T(val);
  } // placement new,也叫定位new
  void destry(T *p) { p->~T(); }
};
template <typename T, typename Alloc = Allocator<T>> class vector {
public:
  vector(int capacity = 10) {
    // _first = new T[capacity];
    _first = _alloc.allocate(capacity);
    _last = _first;
    _end = _first + capacity;
  }
  ~vector() {
    // delete[] _first;
    for (T *p = _first; p != _last; ++p) {
      _alloc.destry(p);
    }
    _alloc.deallocate(_first);
    _first = _last = _end = nullptr;
  }

  vector(const vector<T> &val) {
    auto capacity = val._end - val._first;
    auto size = val._last - val._first;
    // _first = new T[capacity];
    _first = _alloc.allocate(capacity);
    for (int i = 0; i < size; ++i) {
      // _first[i] = val._first[i];
      _alloc.construct(_first + i, val._first[i]);
    }
    _last = _first + size;
    _end = _first + capacity;
  }

  vector<T> &operator=(const vector<T> &val) {
    if (this == &val) {
      return *this;
    }
    // delete[] _first;
    for (T *p = _first; p != _last; ++p) {
      _alloc.destry(p);
    }
    _alloc.deallocate(_first);

    auto capacity = val._end - val._first;
    auto size = val._last - val._first;
    // _first = new T[capacity];
    _first = _alloc.allocate(capacity);
    for (int i = 0; i < size; ++i) {
      // _first[i] = val._first[i];
      _alloc.construct(_first + i, val._first[i]);
    }
    _last = _first + size;
    _end = _first + capacity;

    return *this;
  }

  void push_back(const T &val) {
    if (full())
      expand();
    // *_last++ = val;
    _alloc.construct(_last, val);
    ++_last;
  }

  void pop_back() {
    if (empty())
      return;
    // --_last;
    --_last;
    _alloc.destry(_last);
  }

  T back() const { return *(_last - 1); }
  bool full() const { return _last == _end; }

  bool empty() const { return _first == _last; }
  int size() const { return _last - _first; }

  T &operator[](int index) { return _first[index]; }

  class iterator {
  public:
    iterator(T *ptr = nullptr) : _p(ptr) {}

    bool operator!=(const iterator &val) const { return _p != val._p; }

    void operator++() { ++_p; }

    T &operator*() { return *_p; }

  private:
    T *_p;
  };

  iterator begin() { return {_first}; }
  iterator end() { return {_last}; }

private:
  void expand() {
    int capacity = _end - _first;
    int size = _last - _first;
    // T *ptmp = new T[capacity * 2];
    T *ptmp = _alloc.allocate(2 * capacity);
    for (int i = 0; i < size; ++i) {
      // ptmp[i] = _first[i];
      _alloc.construct(ptmp + i, _first[i]);
    }
    // delete[] _first;
    for (T *p = _first; p != _last; ++p) {
      _alloc.destry(p);
    }
    _alloc.deallocate(_first);
    _first = ptmp;
    _last = _first + size;
    _end = _first + capacity * 2;
  }

private:
  T *_first;
  T *_last;
  T *_end;
  Alloc _alloc;
};

int main() {
  vector<int> v;
  for (int i = 0; i < 10; ++i) {
    v.push_back(i);
  }

  for (auto i : v) {
    std::cout << i << " ";
  }
  std::cout << std::endl;

  for (auto it = v.begin(); it != v.end(); ++it) {
    std::cout << *it << " ";
  }
  std::cout << std::endl;
}

18. 迭代器失效問題

首先考慮刪除元素導致的迭代器失效問題

  std::vector<int> v = {84, 87, 78, 16, 94, 36, 87, 93, 50, 22};

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

  for (auto it = v.begin(); it != v.end(); ++it) {
    if (*it % 2 == 0) {
      v.erase(it); // 第一次呼叫erase方法後,迭代器就失效了
    }
  }
  std::copy(v.begin(), v.end(), std::ostream_iterator<int>(std::cout, " "));
  std::cout << std::endl;

在第一次呼叫erase方法後,vector內部刪除一個元素,會將後續元素向前移動一個位置,這就導致後續的所有迭代器均失效。
上述程式碼執行的過程中,vector中的元素變化過程為:

84, 87, 78, 16, 94, 36, 87, 93, 50, 22
87, 78, 16, 94, 36, 87, 93, 50, 22
87, 16, 94, 36, 87, 93, 50, 22
87, 16, 36, 87, 93, 50, 22
87, 16, 36, 87, 93, 22

在判斷84為偶數時,刪除84,後續元素向前移動,然後it加一指向了78(因為87移動到了84元素所在位置)
之後刪除元素均是如此,每次刪除元素之後執行++it,相當於指標向後移動了兩個單位(刪除元素會導致後續元素移動一個單位,之後執行++it也是移動一個單位)

正確的刪除操作如下:

for (auto it = v.begin(); it != v.end();) {
  if (*it % 2 == 0) {
    v.erase(it); // 不更新迭代器
  } else {
    ++it; // 如果沒有刪除,則迭代器自增
  }
}

上述程式碼在執行刪除操作之後,刪除位置之後的元素往前移一個,同時it不更新
img

 // 連續插入元素,迭代器失效
  for (auto it = v2.begin(); it != v2.end(); ++it) {
    if (*it % 2 == 0) {
      // free(): invalid pointer
      v2.insert(it, *it - 1); // 第一次呼叫insert方法後,迭代器就失效了
    }
  }

再考慮插入元素的情況,這種情況比較複雜,因為容器在插入元素時有可能發生擴容操作,也可能底層空間足夠,不需要擴容。不管是哪一種情況,都會導致插入位置之後的元素向後移動,之後的迭代器均失效。

參考上面的解決方案,很容易想到,在插入成功時,更新it,使其能夠順利指向下一個元素。

  for (auto it = v.begin(); it != v.end(); ++it) {
    if (*it % 2 == 0) {
      it = v.insert(it, *it - 1);
      ++it; // 新元素插入後,更新迭代器
    }
  }

19. new和delete的過載

可以過載全域性的newdelete運算子,也可以對某個類的newdelete進行過載。
new在底層就是使用malloc來分配記憶體,然後呼叫物件的建構函式來初始化物件;
delete首先呼叫解構函式,在底層使用free來釋放記憶體。

#include <cstddef>
#include <iostream>
#include <new>

void *operator new(size_t size) {
  void *p = malloc(size);
  if (p == nullptr) {
    throw std::bad_alloc();
  }

  std::cout << "operator new: " << p << ", size: " << size << std::endl;
  return p;
}

void operator delete(void *p) {
  std::cout << "operator delete:" << p << std::endl;
  free(p);
}

void *operator new[](size_t size) {
  void *p = malloc(size);
  if (p == nullptr) {
    throw std::bad_alloc();
  }

  std::cout << "operator new[]: " << p << ", size: " << size << std::endl;
  return p;
}

void operator delete[](void *p) {
  std::cout << "operator delete[]:" << p << std::endl;
  free(p);
}

int main() {
  int *i = new int(9);
  int *j = new int[10];

  delete i;
  delete[] j;
}

20. 過載new和delete實現物件池

首先實現一個鏈式佇列,然後過載佇列節點的new和delete運算子。
當第一次建立佇列時,會呼叫建構函式Queue(),此時會呼叫new QueueNode()QueueNode的new運算子會在首次呼叫時開闢一塊大記憶體,然後之後的new都會在這塊大記憶體中透過頭刪法分配記憶體,在delete時,則透過頭插法將記憶體放回到池中,而不返回給作業系統。

不過,需要注意的是,這個方法並沒有對整塊大記憶體進行釋放,且在頻繁插入節點時,pool池會消耗完,然後建立新的pool,之前的pool則會洩漏。

除此之外,佇列頻繁插入和刪除節點都會使用物件池中的記憶體。

#include <cstddef>
#include <iostream>
#include <new>

template <typename T> class Queue {
public:
  Queue() { _front = _rear = new QueueNode(); }
  ~Queue() {
    QueueNode *cur = _front;
    while (cur != nullptr) {
      _front = _front->next;
      delete cur;
      cur = _front;
    }
  }

  void push(const T &val) {
    QueueNode *node = new QueueNode(val);
    _rear->next = node;
    _rear = node;
  }

  void pop() {
    if (empty()) {
      return;
    }
    QueueNode *first = _front->next;
    _front->next = first->next;
    if (_front->next == nullptr) {
      _rear = _front;
    }
    delete first;
  }

  T front() const { return _front->next->data; }

  bool empty() const { return _front == _rear; }

private:
  struct QueueNode {
    QueueNode(T data = T()) : data(data), next(nullptr) {}
    T data;
    QueueNode *next;
    static const int POOL_SIZE = 100000;
    static QueueNode *pool;

    // void *operator new(size_t size) {
    //   std::cout << "operator new" << std::endl;
    //   void *p = malloc(size);
    //   if (p)
    //     return p;
    //   else
    //     throw std::bad_alloc();
    // }
    //
    // void operator delete(void *p) {
    //   std::cout << "operator delete" << std::endl;
    //   free(p);
    // }
    //

    void *operator new(size_t size) {
      if (!pool) {
        pool = (QueueNode *)new char[POOL_SIZE * sizeof(QueueNode)];
        QueueNode *p = pool;
        for (; p < pool + POOL_SIZE - 1; ++p) {
          p->next = p + 1;
        }
        p->next = nullptr;
      }

      QueueNode *p = pool;
      pool = pool->next;
      return p;
    }

    void operator delete(void *p) {
      QueueNode *ptr = (QueueNode *)p;
      ptr->next = pool;
      pool = ptr;
    }
  };

  QueueNode *_front;
  QueueNode *_rear;
};

template <typename T>
typename Queue<T>::QueueNode *Queue<T>::QueueNode::pool = nullptr;

int main() {
  Queue<int> q;
  for (int i = 0; i < (99999 + 1); ++i) {
    q.push(i);
  }
  std::cout << q.empty() << std::endl;
}

21. 繼承結構中的訪問限定

類與類之間的關係:

  • 繼承 子類是父類的一種
  • 組合 一部分的關係

三種訪問限定修飾符 在類內部、派生類中、外部的訪問限定

繼承方式 基類訪問限定 派生類訪問限定 (main)外部訪問限定
public public public Y
protected protected N
private 不可見 N
protected public protected N
protected protected N
private 不可見 N
private public private N
protected private N
private 不可見 N

繼承方式控制父類成員在派生類中的訪問限定。
從表可以看出,在派生類中繼承而來的基類成員的訪問限定 是不超過繼承方式的,
且基類私有成員在派生類中不可見

雖然基類私有成員僅在基類內部可見,但是在繼承結構中是能被子類繼承的,佔記憶體空間。
實際上,派生類能夠繼承所有的父類成員和方法。
派生類中透過呼叫基類的建構函式來初始化從基類繼承而來的成員變數。

預設的訪問限定
在class中是private,使用class定義派生類,預設繼承方式就是private,
在struct中是public,使用struct定義派生類,預設繼承方式就是public。

22. 過載、隱藏、覆蓋(重寫)

#include <iostream>

class Base {
public:
  Base(int a) : a(a) {}
  void show() { std::cout << "Base::show()" << std::endl; }
  void show(int i) { std::cout << "Base::show(int)" << std::endl; }

  virtual void func() { std::cout << "Base::func()" << std::endl; }

protected:
  int a;
};

class Derived : public Base {

public:
  Derived(int d) : Base(d), b(d) {}
  void show() { std::cout << "Derived::show()" << std::endl; }

  void func() override { std::cout << "Derived::func()" << std::endl; }

private:
  int b;
};

int main() {
  // 函式過載:在同一個作用域下,同名函式,不同引數列表(引數個數、引數型別),靜態多型的一種
  // Base 中show()和show(int)構成函式過載
  Base b{1};
  b.show();
  b.show(42);

  std::cout << "=============" << std::endl;
  // Derived中的show隱藏了基類中的同名show()和show(int)
  Derived d{2};
  d.show(); // 訪問的是派生類自己的方法
  // 構成隱藏關係時,要想訪問基類的同名方法,必須包含作用域
  d.Base::show();
  d.Base::show(42);

  std::cout << "=============" << std::endl;
  // 基類指標(引用)指向派生類物件,基類方法使用virtual限定,此時構成覆蓋(必須使用virtual限定,才能構成覆蓋)
  Base *pb = &d;
  pb->func();
}

23. 多型、虛擬函式、虛擬函式指標、虛擬函式表

多型包括靜態多型和動態多型。
靜態多型指的是,在編譯時期發生的多型,也就是在編譯時函式的繫結,一般包括模板函式過載
動態多型則是指,在執行時期發生的多型,即在執行時發生的函式動態繫結,也就是透過繼承結構中的虛擬函式來實現。

  • 當基類中的成員函式被宣告為虛擬函式(virtual)時,它們可以在派生類中被覆蓋(也叫重寫,可以用override修飾,推薦加上override修飾)。
  • 覆蓋要求派生類中的方法與基類中的虛擬函式方法具有相同的函式簽名(返回值、函式名、引數列表都相同),且派生類中的覆蓋方法自動處理為虛擬函式。
  • 覆蓋關係指的是在派生類的虛擬函式表中對繼承自基類的虛擬函式的地址進行覆蓋
  • 定義了虛擬函式(virtual)的類,編譯階段,編譯器給這個類產生一個唯一的虛擬函式表(vftable),表中存放RTTI指標和各虛擬函式的地址,程式執行時每一張虛擬函式表都會載入到.rodata段。
  • 定義了虛擬函式(virtual)的類,這個類的物件在記憶體起始位置會額外儲存一個虛擬函式指標(vfptr),指向對應型別的虛擬函式表。
  • 一個含有虛擬函式的類的多個物件,它們的vfptr指向同一張虛擬函式表,虛擬函式的個數不影響物件記憶體大小,僅影響虛擬函式表的大小
  • 動態多型的底層實現原理:動態繫結函式呼叫,基類指標(引用)指向子類物件,透過基類指標呼叫虛擬函式 ==>> 訪問子類物件的vfptr ==>> 訪問對應的vftable ==>> 訪問對應的覆蓋後的虛擬函式。

以下面這段程式為例

#include <iostream>
class Base {
public:
  Base(int a) : a(a) {}
  virtual void func() { std::cout << "Base::func()" << std::endl; }

protected:
  int a;
};
class Derived : public Base {
public:
  Derived(int d) : Base(d), b(d) {}
  void func() override { std::cout << "Derived::func()" << std::endl; }

private:
  int b;
};
int main() {
  Base b{1};
  Derived d{2};
  Base *p = &d;
  Base &r = d;

  b.func();
  d.func();
  p->func();
  r.func();
}

使用以下命令,檢視它們的虛擬函式表的記憶體結構

clang++ -Xclang -fdump-vtable-layouts ./main.cpp  > vtable_layouts.txt

得到vtable_layouts.txt內容如下所示

Vtable for 'Base' (3 entries).
   0 | offset_to_top (0)
   1 | Base RTTI
       -- (Base, 0) vtable address --
   2 | void Base::func()

VTable indices for 'Base' (1 entries).
   0 | void Base::func()

Vtable for 'Derived' (3 entries).
   0 | offset_to_top (0)
   1 | Derived RTTI
       -- (Base, 0) vtable address --
       -- (Derived, 0) vtable address --
   2 | void Derived::func()

VTable indices for 'Derived' (1 entries).
   0 | void Derived::func()

Note: 如果是在Visual Studio中,可以使用命令cl .\main.cpp /d1reportSingleClassLayoutBase檢視Base的記憶體佈局

透過gdb除錯,來檢查一下,Base bDerived d的記憶體結構,以及他們對應的虛擬函式表的記憶體結構,如下圖所示

img
Base bDerived d的vfptr所指向的地址分別為0x555555557d580x555555557d40,棧中的記憶體增長方向是由高地址向低地址方向增長。透過-16能夠獲取到vfptr所指地址到虛擬函式列表的偏移,-8得到RTTI指標。

>>> x/a 0x555555557d58-16  # 獲取虛擬函式列表偏移
0x555555557d48 <_ZTV4Base>:     0x0
>>> x/a 0x555555557d58-8   # 獲取RTTI
0x555555557d50 <_ZTV4Base+8>:   0x555555557d78 <_ZTI4Base>
>>> x/a 0x555555557d58     # 根據vfptr+offset 得到第一個虛擬函式地址
0x555555557d58 <_ZTV4Base+16>:  0x555555555308 <_ZN4Base4funcEv> 

一些練習題

  1. 下面這段程式碼,透過交換兩個不同派生類物件的vfptr,實現了呼叫本不屬於自己的虛擬函式
    img

  2. 構成覆蓋關係的兩個虛擬函式,在提供預設引數時,基類中的預設引數壓棧
    這是因為,引數壓棧發生在編譯期,而函式動態繫結則發生在執行時期。
    還應注意到,在下圖中的,派生類中,函式f()是private的,但是也能正常發生多型呼叫。
    這是因為,函式呼叫的訪問許可權檢查是發生在編譯期,而多型呼叫發生在執行時,故能正常呼叫。
    img

  3. 下面這段程式碼,執行出錯,因為建構函式中,呼叫clear將vfptr置為0,導致無法找到對應的虛擬函式。
    img
    但是,派生類的多型呼叫可以正常執行,這是因為派生類物件有自己的虛擬函式表,使用的是派生類的vfptr。
    img

24. 虛解構函式

首先需要明確的是,不是所有的成員函式都可以宣告為虛擬函式。
以下兩種函式型別不能宣告為虛擬函式。

  • 建構函式
  • 靜態成員函式

虛擬函式會產生虛擬函式表vftable,虛擬函式表需要虛擬函式指標vfptr指定,而虛擬函式指標vfptr則存在於物件記憶體中,故而,虛擬函式依賴物件存在。
因此建構函式與靜態成員函式不能宣告為虛擬函式

在繼承結構中,如果基類存在虛擬函式,則必須將基類的解構函式定義為虛擬函式
如果不定義為虛解構函式,那麼在釋放資源時,不會發生動態繫結,也就不會呼叫派生類的解構函式,而是直接呼叫基類的解構函式,造成資源洩漏。如下所示:
img

25. 虛基類、虛繼承、菱形繼承

虛繼承是 C++ 中的一種特性,用於解決菱形繼承(Diamond Inheritance)問題,即當一個類同時繼承自兩個或更多個具有共同基類的類時可能出現的問題。

     A
    / \
   B   C
    \ /
     D

在菱形繼承中,如果類 A 中有一些成員變數或函式,那麼在類 D 中就會有兩份這樣的成員,一份來自類 B,一份來自類 C,這可能導致二義性和記憶體浪費。

虛繼承透過使用關鍵字 **virtual** 來解決菱形繼承問題。在虛繼承中,被繼承的基類會被標記為虛基類,這樣在派生類中就只會保留一份該虛基類的成員

例如,透過將類 B 和類 C 繼承自類 A 的方式改為虛繼承,即:

#include <iostream>

class A {
public:
  A(int a) : _a(a) { std::cout << "A::A(int)" << std::endl; }
  ~A() { std::cout << "A::~A()" << std::endl; }

private:
  int _a;
};

class B : virtual public A {
public:
  B(int b) : A(b), _b(b) { std::cout << "B::B(int)" << std::endl; }
  ~B() { std::cout << "B::~B()" << std::endl; }

private:
  int _b;
};

class C : virtual public A {
public:
  C(int c) : A(c), _c(c) { std::cout << "C::C(int)" << std::endl; }
  ~C() { std::cout << "C::~C()" << std::endl; }

private:
  int _c;
};

class D : public B, public C {
public:
  D(int d) : A(d), B(d), C(d), _d(d) { std::cout << "D::D(int)" << std::endl; }
  ~D() { std::cout << "D::~D()" << std::endl; }

private:
  int _d;
};

int main() { D d{10}; }

需要注意的是,在使用虛繼承之後,類A的構造需要由D完成。

26. C++中的四種型別轉換

型別轉換 說明 使用場景 注意事項
靜態轉換 (static_cast) 在編譯時進行型別轉換,通常用於非多型型別之間的轉換,例如基本資料型別之間的轉換或者基類指標向派生類指標的轉換。 - 將指標或引用轉換為不同的指標或引用型別。
- 將較大的整數型別轉換為較小的整數型別。
- 在無符號和有符號型別之間進行轉換。
- 靜態轉換不會執行執行時型別檢查,因此可能導致未定義的行為。
- 靜態轉換無法用於處理多型型別,因為它不會執行動態型別檢查。
動態轉換 (dynamic_cast) 在執行時進行型別轉換,主要用於處理多型型別之間的轉換。它會進行動態型別檢查,並確保安全地轉換指標或引用。 - 將基類指標或引用轉換為派生類指標或引用。
- 在涉及多型型別的情況下進行型別轉換。
- 只能用於處理多型型別。
- 當轉換失敗時(例如指標不指向有效的派生類物件),dynamic_cast 返回 nullptr(對於指標)或引發 std::bad_cast 異常(對於引用)。
重新解釋轉換 (reinterpret_cast) 執行低階別的轉換,將一個指標轉換為另一種型別的指標,或者將一個整數型別的值轉換為指標。這種轉換的結果是未定義的,除非轉換是類似於 memcpy 的位元組複製。 - 在底層表示不同但大小相同的型別之間進行轉換。
- 在需要執行強制型別轉換的底層程式設計中使用。
- reinterpret_cast 是最不安全的型別轉換,可能導致未定義的行為。
- 應該避免在普通 C++ 程式碼中使用 reinterpret_cast
常量轉換 (const_cast) 用於去除物件的 constvolatile 限定符。它只能改變指標或引用的常量屬性,而不能改變物件本身的常量性質。 - 在需要修改指向常量物件的指標或引用的情況下使用。
- 在需要修改指向 volatile 物件的指標或引用的情況下使用。
- 修改指向非常量物件的指標或引用是未定義行為。
- 應該謹慎使用 const_cast,確保不會破壞程式碼的常量性質。

27. STL 六大元件

STL包含容器(container),演算法(algorithm),迭代器(iterator),仿函式(functor),介面卡(adapter),空間配置器(allocator)六大元件。

容器(container)

標準庫中提供的容器有很多,包括順序容器(string,array,vector,deque,list)、有序關聯容器(set,multiset,map,multimap)、無序關聯容器(unordered_set,unordered_multiset,unordered_map,unordered_multimap)。

順序容器

順序容器 底層資料結構 常用介面 適用場景 注意事項
string 動態陣列 - size()
- empty()
- append(str)
- insert(pos, str)
- substr(pos, len)
處理字串,包括連線、查詢、替換等操作 字串拼接可能導致記憶體重分配;迭代器失效問題需要注意
array (c++11) 靜態陣列 - size()
- empty()
- at(index)
- front()
- back()
固定大小的陣列,無需動態調整大小 大小在編譯時確定,不能動態改變;沒有插入和刪除操作; at()函式超出邊界會丟擲 std::out_of_range 異常
vector 動態陣列 - size()
- empty()
- push_back(elem)
- pop_back()
- insert(pos, elem)
- erase(pos)
動態調整大小的陣列,支援快速的尾部插入和刪除操作 插入和刪除元素可能導致記憶體重分配;使用 reserve() 提前預留容量以減少重分配次數
deque 雙端佇列 - size()
- empty()
- push_back(elem)
- pop_back()
- push_front(elem)
- pop_front()
需要在頭部和尾部頻繁插入和刪除元素的場景 頻繁插入和刪除元素時效能優於vector;訪問元素的效率比vector低
list 雙向連結串列 - size()
- empty()
- push_back(elem)
- pop_back()
- push_front(elem)
- pop_front()
- insert(pos, elem)
需要在任意位置頻繁插入和刪除元素的場景 頻繁插入和刪除元素時效能優於vector和雙端佇列

關聯容器

容器 特點 注意事項
set 有序不重複集合,基於紅黑樹實現 插入和刪除操作的時間複雜度為 O(log n),不支援直接修改元素值
multiset 有序可重複集合,基於紅黑樹實現 插入和刪除操作的時間複雜度為 O(log n),允許重複元素存在
map 有序鍵值對集合,基於紅黑樹實現 插入和刪除操作的時間複雜度為 O(log n),不支援直接修改鍵值
multimap 有序鍵值對集合,允許鍵重複,基於紅黑樹實現 插入和刪除操作的時間複雜度為 O(log n),允許重複鍵存在,適用於鍵值對的多對多關係
unordered_set 無序不重複集合,基於雜湊表實現 插入和刪除操作的時間複雜度為平均 O(1),不保證元素順序
unordered_multiset 無序可重複集合,基於雜湊表實現 插入和刪除操作的時間複雜度為平均 O(1),不保證元素順序,允許重複元素存在
unordered_map 無序鍵值對集合,基於雜湊表實現 插入和刪除操作的時間複雜度為平均 O(1),不保證鍵值對順序
unordered_multimap 無序鍵值對集合,允許鍵重複,基於雜湊表實現 插入和刪除操作的時間複雜度為平均 O(1),不保證鍵值對順序,允許重複鍵存在,適用於鍵值對的多對多關係

演算法(algorithm)

演算法 描述
find(first, last, value) 在範圍 [first, last) 中查詢值為 value 的元素。
find_if(first, last, predicate) 在範圍 [first, last) 中查詢滿足條件 predicate 的元素。
binary_search(first, last, value) 在已排序範圍 [first, last) 中使用二分查詢演算法查詢值為 value 的元素。
count(first, last, value) 在範圍 [first, last) 中計算值為 value 的元素個數。
sort(first, last) 對範圍 [first, last) 中的元素進行升序排序。
stable_sort(first, last) 對範圍 [first, last) 中的元素進行穩定的升序排序。
partial_sort(first, middle, last) 對範圍 [first, last) 中的元素部分排序。
nth_element(first, nth, last) 對範圍 [first, last) 中的元素進行重排。
copy(first1, last1, first2) 將範圍 [first1, last1) 中的元素複製到以 first2 開始的另一個範圍中。
merge(first1, last1, first2, last2, result) 合併兩個已排序的範圍到以 result 開始的範圍中。
transform(first1, last1, result, unary_op) 對範圍中的每個元素應用一元操作,並將結果儲存到另一個範圍中。
accumulate(first, last, init) 計算範圍 [first, last) 中元素的累加值。
inner_product(first1, last1, first2, init) 計算兩個範圍中相應元素的內積。
partial_sum(first, last, result) 計算範圍中的部分和。
max_element(first, last) 返回範圍 [first, last) 中的最大元素的迭代器。
min_element(first, last) 返回範圍 [first, last) 中的最小元素的迭代器。
next_permutation(first, last) 生成給定範圍中元素的下一個排列。
prev_permutation(first, last) 生成給定範圍中元素的上一個排列。

迭代器(iterator)

  • 提供了一種統一的訪問容器元素的方法,使演算法和容器分離,即使容器的內部結構發生變化,只要迭代器介面不變,就不需要修改演算法的程式碼。
  • 行為類似於指標,可以透過解引用運算子 * 來訪問元素,也可以使用箭頭運算子 -> 來訪問元素的成員。
  • 通常使用[first, last)表示一段範圍。
  • 在多次插入或刪除元素時,需要考慮迭代器失效問題。

仿函式(functor)

仿函式是一個過載了函式呼叫運算子 operator()的類,使得類的物件可以像函式一樣被呼叫,而不需要像普通類物件那樣呼叫成員函式。
仿函式提供了一種函數語言程式設計的方式,使得程式碼更加靈活和可讀,將演算法和操作分離,增加程式碼複用和可維護性。

#include <iostream>

// 定義一個加法運算的仿函式
class Add {
public:
    int operator()(int a, int b) const {
        return a + b;
    }
};

int main() {
    Add add; // 建立一個加法運算的仿函式物件
    int result = add(3, 5); // 使用仿函式物件進行加法運算
    std::cout << "Result: " << result << std::endl;
    return 0;
}

介面卡(adapter)

容器介面卡 底層預設容器 特點
棧(Stack) 雙端佇列(deque) 後進先出(LIFO)
佇列(Queue) 雙端佇列(deque) 先進先出(FIFO)
優先佇列(Priority Queue) 二叉堆(vector) 元素按照優先順序排序

空間配置器(allocator)

容器空間配置器

相關文章