C++面試常考題——編譯內存相關
轉自:https://leetcode-cn.com/leetbook/read/cpp-interview-highlights/e4ns5g/
C++程序編譯過程
編譯過程分為四個過程:編譯(編譯預處理、編譯、優化),匯編,鏈接。
編譯預處理:處理以 # 開頭的指令;
編譯、優化:將源碼 .cpp 文件翻譯成 .s 匯編代碼;
匯編:將匯編代碼 .s 翻譯成機器指令 .o 文件;
鏈接:匯編程序生成的目標文件,即 .o 文件,并不會立即執行,因為可能會出現:.cpp 文件中的函數引用了另一個 .cpp 文件中定義的符號或者調用了某個庫文件中的函數。那鏈接的目的就是將這些文件對應的目標文件連接成一個整體,從而生成可執行的程序 .exe 文件。
鏈接分為兩種:
- 靜態鏈接:代碼從其所在的靜態鏈接庫中拷貝到最終的可執行程序中,在該程序被執行時,這些代碼會被裝入到該進程的虛擬地址空間中。
- 動態鏈接:代碼被放到動態鏈接庫或共享對象的某個目標文件中,鏈接程序只是在最終的可執行程序中記錄了共享對象的名字等一些信息。在程序執行時,動態鏈接庫的全部內容會被映射到運行時相應進行的虛擬地址的空間。
二者的優缺點:
- 靜態鏈接:浪費空間,每個可執行程序都會有目標文件的一個副本,這樣如果目標文件進行了更新操作,就需要重新進行編譯鏈接生成可執行程序(更新困難);優點就是執行的時候運行速度快,因為可執行程序具備了程序運行的所有內容。
- 動態鏈接:節省內存、更新方便,但是動態鏈接是在程序運行時,每次執行都需要鏈接,相比靜態鏈接會有一定的性能損失。
C++內存管理
C++ 內存分區:棧、堆、全局/靜態存儲區、常量存儲區、代碼區。
- 棧:存放函數的局部變量、函數參數、返回地址等,由編譯器自動分配和釋放。
- 堆:動態申請的內存空間,就是由 malloc 分配的內存塊,由程序員控制它的分配和釋放,如果程序執行結束還沒有釋放,操作系統會自動回收。
- 全局區/靜態存儲區(.bss 段和 .data 段):存放全局變量和靜態變量,程序運行結束操作系統自動釋放,在 C 語言中,未初始化的放在 .bss 段中,初始化的放在 .data 段中,C++ 中不再區分了。
- 常量存儲區(.data 段):存放的是常量,不允許修改,程序運行結束自動釋放。
代碼區(.text 段):存放代碼,不允許修改,但可以執行。編譯后的二進制文件存放在這里。
說明:
從操作系統的本身來講,以上存儲區在內存中的分布是如下形式(從低地址到高地址):.text 段 --> .data 段 --> .bss 段 --> 堆 --> unused --> 棧 --> env
程序實例:
#include <iostream>
using namespace std;/*
說明:C++ 中不再區分初始化和未初始化的全局變量、靜態變量的存儲區,如果非要區分下述程序標注在了括號中
*/int g_var = 0; // g_var 在全局區(.data 段)
char *gp_var; // gp_var 在全局區(.bss 段)int main()
{int var; // var 在棧區char *p_var; // p_var 在棧區char arr[] = "abc"; // arr 為數組變量,存儲在棧區;"abc"為字符串常量,存儲在常量區char *p_var1 = "123456"; // p_var1 在棧區;"123456"為字符串常量,存儲在常量區static int s_var = 0; // s_var 為靜態變量,存在靜態存儲區(.data 段)p_var = (char *)malloc(10); // 分配得來的 10 個字節的區域在堆區free(p_var);return 0;
}
堆和棧的區別
- 申請方式:棧是系統自動分配,堆是程序員主動申請。
- 申請后系統響應:分配棧空間,如果剩余空間大于申請空間則分配成功,否則分配失敗棧溢出;申請堆空間,堆在內存中呈現的方式類似于鏈表(記錄空閑地址空間的鏈表),在鏈表上尋找第一個大于申請空間的節點分配給程序,將該節點從鏈表中刪除,大多數系統中該塊空間的首地址存放的是本次分配空間的大小,便于釋放,將該塊空間上的剩余空間再次連接在空閑鏈表上。
- 棧在內存中是連續的一塊空間(向低地址擴展)最大容量是系統預定好的,堆在內存中的空間(向高地址擴展)是不連續的。
- 申請效率:棧是有系統自動分配,申請效率高,但程序員無法控制;堆是由程序員主動申請,效率低,使用起來方便但是容易產生碎片。
- 存放的內容:棧中存放的是局部變量,函數的參數;堆中存放的內容由程序員控制。
變量的區別
全局變量、局部變量、靜態全局變量、靜態局部變量的區別
C++ 變量根據定義的位置的不同的生命周期,具有不同的作用域,作用域可分為 6 種:全局作用域,局部作用域,語句作用域,類作用域,命名空間作用域和文件作用域。
從作用域看:
- 全局變量:具有全局作用域。全局變量只需在一個源文件中定義,就可以作用于所有的源文件。當然,其他不包含全局變量定義的源文件需要用
extern
關鍵字再次聲明這個全局變量。 - 靜態全局變量:具有文件作用域。它與全局變量的區別在于如果程序包含多個文件的話,它作用于定義它的文件里,不能作用到其它文件里,即被
static
關鍵字修飾過的變量具有文件作用域。這樣即使兩個不同的源文件都定義了相同名字的靜態全局變量,它們也是不同的變量。 - 局部變量:具有局部作用域。它是自動對象(auto),在程序運行期間不是一直存在,而是只在函數執行期間存在,函數的一次調用執行結束后,變量被撤銷,其所占用的內存也被收回。
- 靜態局部變量:具有局部作用域。它只被初始化一次,自從第一次被初始化直到程序運行結束都一直存在,它和全局變量的區別在于全局變量對所有的函數都是可見的,而靜態局部變量只對定義自己的函數體始終可見。
從分配內存空間看:
- 靜態存儲區:全局變量,靜態局部變量,靜態全局變量。
- 棧:局部變量。
說明:
- 靜態變量和棧變量(存儲在棧中的變量)、堆變量(存儲在堆中的變量)的區別:靜態變量會被放在程序的靜態數據存儲區(.data 段)中(靜態變量會自動初始化),這樣可以在下一次調用的時候還可以保持原來的賦值。而棧變量或堆變量不能保證在下一次調用的時候依然保持原來的值。
- 靜態變量和全局變量的區別:靜態變量用 static 告知編譯器,自己僅僅在變量的作用范圍內可見。
在《C++PP》這本書中,這部分的內容,被描述為存儲持續性、作用域與鏈接性。
全局變量,在書中為外部鏈接性、靜態持續變量
靜態全局變量,在書中為內部鏈接性、靜態持續變量
靜態局部變量,在書中為無鏈接性、靜態持續變量
局部變量,在書中為自動存儲持續性的變量
全局變量定義在頭文件中會有什么問題?
- 如果在頭文件中定義全局變量,當該頭文件被多個文件
include
時,該頭文件中的全局變量就會被定義多次,導致重復定義,因此不能再頭文件中定義全局變量。
對象創建限制在堆或棧
說明:C++ 中的類的對象的建立分為兩種:靜態建立、動態建立。
- 靜態建立:由編譯器為對象在棧空間上分配內存,直接調用類的構造函數創建對象。例如:
A a;
- 動態建立:使用
new
關鍵字在堆空間上創建對象,底層首先調用operator new()
函數,在堆空間上尋找合適的內存并分配;然后,調用類的構造函數創建對象。例如:A *p = new A();
限制對象只能建立在堆上:
-
最直觀的思想:避免直接調用類的構造函數,因為對象靜態建立時,會調用類的構造函數創建對象。但是直接將類的構造函數設為私有并不可行,因為當構造函數設置為私有后,不能在類的外部調用構造函數來構造對象,只能用 new 來建立對象。但是由于 new 創建對象時,底層也會調用類的構造函數,將構造函數設置為私有后,那就無法在類的外部使用 new 創建對象了。因此,這種方法不可行。
-
解決方法一
-
將析構函數設置為私有。
原因:靜態對象建立在棧上,是由編譯器分配和釋放內存空間,編譯器為對象分配內存空間時,會對類非靜態函數進行檢查,即編譯器會檢查析構函數的訪問性。當析構函數設為私有時,編譯器創建的對象就無法通過訪問析構函數來釋放對象的內存空間,因此,編譯器不會在棧上為對象分配內存。
-
代碼
class A { public:A() {}void destory(){delete this;}private:~A(){} };
-
該方法存在的問題。
- 用
new
創建的對象,通常會使用delete
釋放該對象的內存空間,但此時類的外部無法調用析構函數,因此類內必須定義一個destory()
函數,用來釋放new
創建的對象。 - 無法解決繼承問題,因為如果這個類作為基類,析構函數要設置成
virtual
,然后在派生類中重寫該函數,來實現多態。但此時,析構函數是私有的,派生類中無法訪問。
- 用
-
-
解決方法二
-
構造函數設置為
protected
,并提供一個public
的靜態函數來完成構造,而不是在類的外部使用new
構造;將析構函數設置為protected
。原因:類似于單例模式,也保證了在派生類中能夠訪問析構函數。通過調用 create() 函數在堆上創建對象。
-
代碼
class A { protected:A() {}~A() {}public:static A *create(){return new A();}void destory(){delete this;} };
-
限制對象只能建立在棧上:
解決方法:將 operator new()
設置為私有。原因:當對象建立在堆上時,是采用 new
的方式進行建立,其底層會調用 operator new()
函數,因此只要對該函數加以限制,就能夠防止對象建立在堆上。
class A
{
private:void *operator new(size_t t) {} // 注意函數的第一個參數和返回值都是固定的void operator delete(void *ptr) {} // 重載了 new 就需要重載 delete
public:A() {}~A() {}
};
內存對齊
什么是內存對齊?內存對齊的原則?為什么要進行內存對齊,有什么優點?
內存對齊:編譯器將程序中的每個“數據單元”安排在字的整數倍的地址指向的內存之中。
內存對齊的原則:
- 結構體變量的首地址能夠被其最寬基本類型成員大小與對齊基數中的較小者所整除;
- 結構體每個成員相對于結構體首地址的偏移量 (offset) 都是該成員大小與對齊基數中的較小者的整數倍,如有需要編譯器會在成員之間加上填充字節 (internal padding);
- 結構體的總大小為結構體最寬基本類型成員大小與對齊基數中的較小者的整數倍,如有需要編譯器會在最末一個成員之后加上填充字節 (trailing padding)。
實例:
/*
說明:程序是在 64 位編譯器下測試的
*/
#include <iostream>using namespace std;struct A
{short var; // 2 字節int var1; // 8 字節 (內存對齊原則:填充 2 個字節) 2 (short) + 2 (填充) + 4 (int)= 8long var2; // 12 字節 8 + 4 (long) = 12char var3; // 16 字節 (內存對齊原則:填充 3 個字節)12 + 1 (char) + 3 (填充) = 16string s; // 48 字節 16 + 32 (string) = 48
};int main()
{short var;int var1;long var2;char var3;string s;A ex1;cout << sizeof(var) << endl; // 2 shortcout << sizeof(var1) << endl; // 4 intcout << sizeof(var2) << endl; // 4 longcout << sizeof(var3) << endl; // 1 charcout << sizeof(s) << endl; // 32 stringcout << sizeof(ex1) << endl; // 48 structreturn 0;
}
進行內存對齊的原因(主要是硬件設備方面的問題):
- 某些硬件設備只能存取對齊數據,存取非對齊的數據可能會引發異常;
- 某些硬件設備不能保證在存取非對齊數據的時候的操作是原子操作;
- 相比于存取對齊的數據,存取非對齊的數據需要花費更多的時間;
- 某些處理器雖然支持非對齊數據的訪問,但會引發對齊陷阱(alignment trap);
- 某些硬件設備只支持簡單數據指令非對齊存取,不支持復雜數據指令的非對齊存取。
內存對齊的優點:
- 便于在不同的平臺之間進行移植,因為有些硬件平臺不能夠支持任意地址的數據訪問,只能在某些地址處取某些特定的數據,否則會拋出異常;
- 提高內存的訪問效率,因為 CPU 在讀取內存時,是一塊一塊的讀取。
類的大小
類大小的計算
說明:類的大小是指類的實例化對象的大小,用 sizeof 對類型名操作時,結果是該類型的對象的大小。
計算原則:
- 遵循結構體的對齊原則。
- 與普通成員變量有關,與成員函數和靜態成員無關。即普通成員函數,靜態成員函數,靜態數據成員,靜態常量數據成員均對類的大小無影響。因為靜態數據成員被類的對象共享,并不屬于哪個具體的對象。
- 虛函數對類的大小有影響,是因為虛函數表指針的影響。
- 虛繼承對類的大小有影響,是因為虛基表指針帶來的影響。
- 空類的大小是一個特殊情況,空類的大小為 1,當用 new 來創建一個空類的對象時,為了保證不同對象的地址不同,空類也占用存儲空間。
實例:
簡單情況和空類情況
/*
說明:程序是在 64 位編譯器下測試的
*/
#include <iostream>using namespace std;class A
{
private:static int s_var; // 不影響類的大小const int c_var; // 4 字節int var; // 8 字節 4 + 4 (int) = 8char var1; // 12 字節 8 + 1 (char) + 3 (填充) = 12
public:A(int temp) : c_var(temp) {} // 不影響類的大小~A() {} // 不影響類的大小
};class B
{
};
int main()
{A ex1(4);B ex2;cout << sizeof(ex1) << endl; // 12 字節cout << sizeof(ex2) << endl; // 1 字節return 0;
}
帶有虛函數的情況:(注意:虛函數的個數并不影響所占內存的大小,因為類對象的內存中只保存了指向虛函數表的指針。)
/*
說明:程序是在 64 位編譯器下測試的
*/
#include <iostream>using namespace std;class A
{
private:static int s_var; // 不影響類的大小const int c_var; // 4 字節int var; // 8 字節 4 + 4 (int) = 8char var1; // 12 字節 8 + 1 (char) + 3 (填充) = 12
public:A(int temp) : c_var(temp) {} // 不影響類的大小~A() {} // 不影響類的大小virtual void f() { cout << "A::f" << endl; }virtual void g() { cout << "A::g" << endl; }virtual void h() { cout << "A::h" << endl; } // 24 字節 12 + 4 (填充) + 8 (指向虛函數的指針) = 24
};int main()
{A ex1(4);A *p;cout << sizeof(p) << endl; // 8 字節 注意:指針所占的空間和指針指向的數據類型無關cout << sizeof(ex1) << endl; // 24 字節return 0;
}
內存泄露
什么是內存泄漏
內存泄漏:由于疏忽或錯誤導致的程序未能釋放已經不再使用的內存。
進一步解釋:
-
并非指內存從物理上消失,而是指程序在運行過程中,由于疏忽或錯誤而失去了對該內存的控制,從而造成了內存的浪費。
-
常指堆內存泄漏,因為堆是動態分配的,而且是用戶來控制的,如果使用不當,會產生內存泄漏。
-
使用 malloc、calloc、realloc、new 等分配內存時,使用完后要調用相應的 free 或 delete 釋放內存,否則這塊內存就會造成內存泄漏。
-
指針重新賦值
char *p = (char *)malloc(10); char *p1 = (char *)malloc(10); p = np;
開始時,指針
p
和p1
分別指向一塊內存空間,但指針p
被重新賦值,導致p
初始時指向的那塊內存空間無法找到,從而發生了內存泄漏。
大概分為這么3類內存泄漏:
- 堆內存泄漏:new/mallc分配內存,未使用對應的delete/free回收
- 系統資源泄漏, Bitmap, handle,socket等資源未釋放
- 沒有將基類析構函數定義稱為虛函數,(使用基類指針或者引用指向派生類對象時)派生類對象釋放時將不能正確釋放派生對象部分。
如何檢測及防止內存泄漏
防止內存泄漏的方法
-
內部封裝:將內存的分配和釋放封裝到類中,在構造的時候申請內存,析構的時候釋放內存。
#include <iostream> #include <cstring>using namespace std;class A { private:char *p;unsigned int p_size;public:A(unsigned int n = 1) // 構造函數中分配內存空間{p = new char[n];p_size = n;};~A() // 析構函數中釋放內存空間{if (p != NULL){delete[] p; // 刪除字符數組p = NULL; // 防止出現野指針}};char *GetPointer(){return p;}; }; void fun() {A ex(100);char *p = ex.GetPointer();strcpy(p, "Test");cout << p << endl; } int main() {fun();return 0; }
說明:但這樣做并不是最佳的做法,在類的對象復制時,程序會出現同一塊內存空間釋放兩次的情況,請看如下程序:
void fun1() {A ex(100);A ex1 = ex; char *p = ex.GetPointer();strcpy(p, "Test");cout << p << endl; }
簡單解釋:對于
fun1
這個函數中定義的兩個類的對象而言,在離開該函數的作用域時,會兩次調用析構函數來釋放空間,但是這兩個對象指向的是同一塊內存空間,所以導致同一塊內存空間被釋放兩次,可以通過增加計數機制來避免這種情況,看如下程序:#include <iostream> #include <cstring>using namespace std; class A { private:char *p;unsigned int p_size;int *p_count; // 計數變量 public:A(unsigned int n = 1) // 在構造函數中申請內存{p = new char[n];p_size = n;p_count = new int;*p_count = 1;cout << "count is : " << *p_count << endl;};A(const A &temp){p = temp.p;p_size = temp.p_size;p_count = temp.p_count;(*p_count)++; // 復制時,計數變量 +1cout << "count is : " << *p_count << endl;}~A(){(*p_count)--; // 析構時,計數變量 -1cout << "count is : " << *p_count << endl; if (*p_count == 0) // 只有當計數變量為 0 的時候才會釋放該塊內存空間{cout << "buf is deleted" << endl;if (p != NULL) {delete[] p; // 刪除字符數組p = NULL; // 防止出現野指針if (p_count != NULL){delete p_count;p_count = NULL;}}}};char *GetPointer(){return p;}; }; void fun() {A ex(100);char *p = ex.GetPointer();strcpy(p, "Test");cout << p << endl;A ex1 = ex; // 此時計數變量會 +1cout << "ex1.p = " << ex1.GetPointer() << endl; } int main() {fun();return 0; }
程序運行結果:
count is : 1 Test count is : 2 ex1.p = Test count is : 1 count is : 0 buf is deleted
解釋下:程序運行結果的倒數 2、3 行是調用兩次析構函數時進行的操作,在第二次調用析構函數時,進行內存空間的釋放,從而會有倒數第 1 行的輸出結果。
-
智能指針
智能指針是 C++ 中已經對內存泄漏封裝好了一個工具,可以直接拿來使用,將在下一個問題中對智能指針進行詳細的解釋。
內存泄漏檢測工具的實現原理
內存檢測工具有很多,這里重點介紹下 valgrind 。
valgrind 是一套 Linux 下,開放源代碼(GPL V2)的仿真調試工具的集合,包括以下工具:
- Memcheck:內存檢查器(valgrind 應用最廣泛的工具),能夠發現開發中絕大多數內存錯誤的使用情況,比如:使用未初始化的內存,使用已經釋放了的內存,內存訪問越界等。
- Callgrind:檢查程序中函數調用過程中出現的問題。
- Cachegrind:檢查程序中緩存使用出現的問題。
- Helgrind:檢查多線程程序中出現的競爭問題。
- Massif:檢查程序中堆棧使用中出現的問題。
- Extension:可以利用 core 提供的功能,自己編寫特定的內存調試工具。
Memcheck 能夠檢測出內存問題,關鍵在于其建立了兩個全局表:
-
Valid-Value 表:對于進程的整個地址空間中的每一個字節(byte),都有與之對應的 8 個 bits ;對于 CPU 的每個寄存器,也有一個與之對應的 bit 向量。這些 bits 負責記錄該字節或者寄存器值是否具有有效的、已初始化的值。
-
Valid-Address 表:對于進程整個地址空間中的每一個字節(byte),還有與之對應的 1 個 bit,負責記錄該地址是否能夠被讀寫。
檢測原理:
- 當要讀寫內存中某個字節時,首先檢查這個字節對應的 Valid-Address 表中對應的 bit。如果該 bit 顯示該位置是無效位置,Memcheck 則報告讀寫錯誤。
- 內核(core)類似于一個虛擬的 CPU 環境,這樣當內存中的某個字節被加載到真實的 CPU 中時,該字節在 Valid-Value 表對應的 bits 也被加載到虛擬的 CPU 環境中。一旦寄存器中的值,被用來產生內存地址,或者該值能夠影響程序輸出,則 Memcheck 會檢查 Valid-Value 表對應的 bits,如果該值尚未初始化,則會報告使用未初始化內存錯誤。
智能指針
智能指針是為了解決動態內存分配時帶來的內存泄漏以及多次釋放同一塊內存空間而提出的。C++11 中封裝在了 <memory> 頭文件中。
C++11 中智能指針包括以下三種:
- 共享指針(shared_ptr):資源可以被多個指針共享,使用計數機制表明資源被幾個指針共享。通過 use_count() 查看資源的所有者的個數,可以通過 unique_ptr、weak_ptr 來構造,調用 release() 釋放資源的所有權,計數減一,當計數減為 0 時,會自動釋放內存空間,從而避免了內存泄漏。
- 獨占指針(unique_ptr):獨享所有權的智能指針,資源只能被一個指針占有,該指針不能拷貝構造和賦值。但可以進行移動構造和移動賦值構造(調用 move() 函數),即一個 unique_ptr 對象賦值給另一個 unique_ptr 對象,可以通過該方法進行賦值。
- 弱指針(weak_ptr):指向 share_ptr 指向的對象,能夠解決由shared_ptr帶來的循環引用問題。
智能指針的實現原理: 計數原理。
智能指針的實現原理: 計數原理。
#include <iostream>
#include <memory>template <typename T>
class SmartPtr
{
private : T *_ptr;size_t *_count;public:SmartPtr(T *ptr = nullptr) : _ptr(ptr){if (_ptr){_count = new size_t(1);}else{_count = new size_t(0);}}~SmartPtr(){(*this->_count)--;if (*this->_count == 0){delete this->_ptr;delete this->_count;}}SmartPtr(const SmartPtr &ptr) // 拷貝構造:計數 +1{if (this != &ptr){this->_ptr = ptr._ptr;this->_count = ptr._count;(*this->_count)++;}}SmartPtr &operator=(const SmartPtr &ptr) // 賦值運算符重載 {if (this->_ptr == ptr._ptr){return *this;}if (this->_ptr) // 將當前的 ptr 指向的原來的空間的計數 -1{(*this->_count)--;if (this->_count == 0){delete this->_ptr;delete this->_count;}}this->_ptr = ptr._ptr;this->_count = ptr._count;(*this->_count)++; // 此時 ptr 指向了新賦值的空間,該空間的計數 +1return *this;}T &operator*(){assert(this->_ptr == nullptr);return *(this->_ptr);}T *operator->(){assert(this->_ptr == nullptr);return this->_ptr;}size_t use_count(){return *this->count;}
};
一個 unique_ptr 怎么賦值給另一個 unique_ptr 對象?
借助 std::move()
可以實現將一個 unique_ptr
對象賦值給另一個 unique_ptr
對象,其目的是實現所有權的轉移。
// A 作為一個類
std::unique_ptr<A> ptr1(new A());
std::unique_ptr<A> ptr2 = std::move(ptr1);
智能指針可能出現的問題:循環引用
在如下例子中定義了兩個類 Parent、Child,在兩個類中分別定義另一個類的對象的共享指針,由于在程序結束后,兩個指針相互指向對方的內存空間,導致內存無法釋放。
#include <iostream>
#include <memory>using namespace std;class Child;
class Parent;class Parent {
private:shared_ptr<Child> ChildPtr;
public:void setChild(shared_ptr<Child> child) {this->ChildPtr = child;}void doSomething() {if (this->ChildPtr.use_count()) {}}~Parent() {}
};class Child {
private:shared_ptr<Parent> ParentPtr;
public:void setPartent(shared_ptr<Parent> parent) {this->ParentPtr = parent;}void doSomething() {if (this->ParentPtr.use_count()) {}}~Child() {}
};int main() {weak_ptr<Parent> wpp;weak_ptr<Child> wpc;{shared_ptr<Parent> p(new Parent);shared_ptr<Child> c(new Child);p->setChild(c);c->setPartent(p);wpp = p;wpc = c;cout << p.use_count() << endl; // 2cout << c.use_count() << endl; // 2}cout << wpp.use_count() << endl; // 1cout << wpc.use_count() << endl; // 1return 0;
}
循環引用的解決方法: weak_ptr
循環引用:該被調用的析構函數沒有被調用,從而出現了內存泄漏。
weak_ptr
對被shared_ptr
管理的對象存在 非擁有性(弱)引用,在訪問所引用的對象前必須先轉化為shared_ptr
;weak_ptr
用來打斷shared_ptr
所管理對象的循環引用問題,若這種環被孤立(沒有指向環中的外部共享指針),shared_ptr
引用計數無法抵達 0,內存被泄露;令環中的指針之一為弱指針可以避免該情況;weak_ptr
用來表達臨時所有權的概念,當某個對象只有存在時才需要被訪問,而且隨時可能被他人刪除,可以用weak_ptr
跟蹤該對象;需要獲得所有權時將其轉化為shared_ptr
,此時如果原來的shared_ptr
被銷毀,則該對象的生命期被延長至這個臨時的shared_ptr
同樣被銷毀。
#include <iostream>
#include <memory>using namespace std;class Child;
class Parent;class Parent {
private://shared_ptr<Child> ChildPtr;weak_ptr<Child> ChildPtr;
public:void setChild(shared_ptr<Child> child) {this->ChildPtr = child;}void doSomething() {//new shared_ptrif (this->ChildPtr.lock()) {}}~Parent() {}
};class Child {
private:shared_ptr<Parent> ParentPtr;
public:void setPartent(shared_ptr<Parent> parent) {this->ParentPtr = parent;}void doSomething() {if (this->ParentPtr.use_count()) {}}~Child() {}
};int main() {weak_ptr<Parent> wpp;weak_ptr<Child> wpc;{shared_ptr<Parent> p(new Parent);shared_ptr<Child> c(new Child);p->setChild(c);c->setPartent(p);wpp = p;wpc = c;cout << p.use_count() << endl; // 2cout << c.use_count() << endl; // 1}cout << wpp.use_count() << endl; // 0cout << wpc.use_count() << endl; // 0return 0;
}