1、簡述
記憶體管理是C++最令人切齒痛恨的問題,也是C++最有爭議的問題,C++高手從中獲得了更好的性能,更大的自由,C++菜鳥的識訓則是一遍一遍的檢查代碼和對C++的痛恨,但記憶體管理在C++中無處不在,記憶體泄漏幾乎在每個C++程式中都會發生,因此要想成為C++高手,記憶體管理一關是必須要過的,除非放棄C++,轉到Java或者.NET,他們的記憶體管理基本是自動的,當然你也放棄了自由和對記憶體的支配權,還放棄了C++超絕的性能,本期專題將從記憶體管理、記憶體泄漏、記憶體回收這三個方面來探討C++記憶體管理問題,
2、記憶體管理
Bill Gates 曾經失言:
640K ought to be enough for everybody — Bill Gates 1981
程式員們經常撰寫記憶體管理程式,往往提心吊膽,如果不想觸雷,唯一的解決辦法就是發現所有潛伏的地雷并且排除它們,躲是躲不了的,本文的內容比一般教科書的要深入得多,讀者需細心閱讀,做到真正地通曉記憶體管理,
2.1 C++記憶體管理詳解
2.1.1 記憶體的分配方式
(1)分配方式簡介
在C++中,記憶體分成5個區,分別是堆、堆疊、自由存盤區、全域/靜態存盤區和常量存盤區,
- 堆疊,在執行函式時,函式內區域變數的存盤單元都可以在堆疊上創建,函式執行結束時這些存盤單元自動被釋放,堆疊記憶體分配運算內置于處理器的指令集中,效率很高,但是分配的記憶體容量有限,
- 堆,就是那些由new分配的記憶體塊,他們的釋放編譯器不去管,由我們的應用程式去控制,一般一個new就要對應一個delete,如果程式員沒有釋放掉,那么在程式結束后,作業系統會自動回收,
- 自由存盤區,就是那些由malloc等分配的記憶體塊,他和堆是十分相似的,不過它是用free來結束自己的生命的,
- 全域/靜態存盤區,全域變數和靜態變數被分配到同一塊記憶體中,在以前的C語言中,全域變數又分為初始化的和未初始化的,在C++里面沒有這個區分了,他們共同占用同一塊記憶體區,
- 常量存盤區,這是一塊比較特殊的存盤區,他們里面存放的是常量,不允許修改,
(2)明確區分堆與堆疊
堆與堆疊的區分問題,似乎是一個永恒的話題,由此可見,初學者對此往往是混淆不清的,所以我決定拿他第一個開刀,首先,我們舉一個例子:
void f() {
int* p=new int[5];
}
這條短短的一句話就包含了堆與堆疊,看到new,我們首先就應該想到,我們分配了一塊堆記憶體,那么指標p呢?他分配的是一塊堆疊記憶體,所以這句話的意思就是:在堆疊記憶體中存放了一個指向一塊堆記憶體的指標p,在程式會先確定在堆中分配記憶體的大小,然后呼叫operator new分配記憶體,然后回傳這塊記憶體的首地址,放入堆疊中,匯編代碼如下:
00401028 push 14h
0040102A call operator new (00401060)
0040102F add esp,4
00401032 mov dword ptr [ebp-8],eax
00401035 mov eax,dword ptr [ebp-8]
00401038 mov dword ptr [ebp-4],eax
這里,我們為了簡單并沒有釋放記憶體,那么該怎么去釋放呢?是delete p么?
噢,錯了,應該是delete []p,這是為了告訴編譯器:我洗掉的是一個陣列,
(3)堆和堆疊究竟有什么區別?
-
管理方式不同:對于堆疊來講,是由編譯器自動管理,無需我們手工控制;對于堆來說,釋放作業由程式員控制,容易產生memory leak,
-
空間大小不同:一般來講在32位系統下,堆記憶體可以達到4G的空間,從這個角度來看堆記憶體幾乎是沒有什么限制的,但是對于堆疊來講,一般都是有一定的空間大小的,例如,在VS2017下面,默認的堆疊空間大小是1M,當然,我們可以修改:

注意:reserve最小值為4Byte;commit是保留在虛擬記憶體的頁檔案里面,它設定的較大會使堆疊開辟較大的值,可能增加記憶體的開銷和啟動時間, -
能否產生碎片不同:對于堆來講,頻繁的new/delete勢必會造成記憶體空間的不連續,從而造成大量的碎片,使程式效率降低,對于堆疊來講,則不會存在這個問題,因為堆疊是先進后出的佇列,它們是如此的一一對應,以至于永遠都不可能有一個記憶體塊從堆疊中間彈出,在它彈出之前,在它上面的后進的堆疊內容已經被彈出,詳細的可以參考資料結構,這里我們就不再一一討論了,
-
生長方向不同:對于堆來講,生長方向是向上的,也就是向著記憶體地址增加的方向;對于堆疊來講,它的生長方向是向下的,是向著記憶體地址減小的方向增長,
-
分配方式不同:堆都是動態分配的,沒有靜態分配的堆,堆疊有2種分配方式:靜態分配和動態分配,靜態分配是編譯器完成的,比如區域變數的分配,動態分配由alloca函式進行分配,但是堆疊的動態分配和堆是不同的,它的動態分配是由編譯器進行釋放,無需我們手工實作,
-
分配效率不同:堆疊是機器系統提供的資料結構,計算機會在底層對堆疊提供支持:分配專門的暫存器存放堆疊的地址,壓堆疊出堆疊都有專門的指令執行,這就決定了堆疊的效率比較高,堆則是C/C++函式庫提供的,它的機制是很復雜的,例如為了分配一塊記憶體,庫函式會按照一定的演算法(具體的演算法可以參考資料結構/作業系統)在堆記憶體中搜索可用的足夠大小的空間,如果沒有足夠大小的空間(可能是由于記憶體碎片太多),就有可能呼叫系統功能去增加程式資料段的記憶體空間,這樣就有機會分到足夠大小的記憶體,然后進行回傳,顯然,堆的效率比堆疊要低得多,
從這里我們可以看到,堆和堆疊相比,由于大量new/delete的使用,容易造成大量的記憶體碎片;由于沒有專門的系統支持,效率很低;由于可能引發用戶態和核心態的切換,記憶體的申請,代價變得更加昂貴,所以堆疊在程式中是應用最廣泛的,就算是函式的呼叫也利用堆疊去完成,函式呼叫程序中的引數,回傳地址,EBP和區域變數都采用堆疊的方式存放,所以,我們推薦大家盡量用堆疊,而不是用堆,
雖然堆疊有如此眾多的好處,但是由于和堆相比不是那么靈活,有時候分配大量的記憶體空間,還是用堆好一些,
無論是堆還是堆疊,都要防止越界現象的發生(除非你是故意使其越界),因為越界的結果要么是程式崩潰,要么是摧毀程式的堆、堆疊結構,產生以想不到的結果,就算是在你的程式運行程序中,沒有發生上面的問題,你還是要小心,說不定什么時候就崩掉,那時候debug可是相當困難的,
2.1.2 控制C++的記憶體分配
在嵌入式系統中使用C++的一個常見問題是記憶體分配,即對new 和 delete 運算子的失控,
具有諷刺意味的是,問題的根源卻是C++對記憶體的管理非常的容易而且安全,具體地說,當一個物件被消除時,它的解構式能夠安全的釋放所分配的記憶體,
這當然是個好事情,但是這種使用的簡單性使得程式員們過度使用new 和 delete,而不注意在嵌入式C++環境中的因果關系,并且,在嵌入式系統中,由于記憶體的限制,頻繁的動態分配不定大小的記憶體會引起很大的問題以及堆破碎的風險,
作為忠告,保守的使用記憶體分配是嵌入式環境中的第一原則,
但當你必須要使用new 和delete時,你不得不控制C++中的記憶體分配,你需要用一個全域的new 和delete來代替系統的記憶體分配符,并且一個類一個類的多載new 和delete,
一個防止堆破碎的通用方法是從不同固定大小的記憶體持中分配不同型別的物件,對每個類多載new 和delete就提供了這樣的控制,
(1)多載全域的new和delete運算子
可以很容易地多載new 和 delete 運算子,如下所示:
void * operator new(size_t size)
{
void *p = malloc(size);
return (p);
}
void operator delete(void *p);
{
free(p);
}
這段代碼可以代替默認的運算子來滿足記憶體分配的請求,出于解釋C++的目的,我們也可以直接呼叫malloc() 和free(),
也可以對單個類的new 和 delete 運算子多載,這是你能靈活的控制物件的記憶體分配,
class TestClass {
public:
void * operator new(size_t size);
void operator delete(void *p);
// .. other members here ...
};
void *TestClass::operator new(size_t size)
{
void *p = malloc(size); // Replace this with alternative allocator
return (p);
}
void TestClass::operator delete(void *p)
{
free(p); // Replace this with alternative de-allocator
}
所有TestClass 物件的記憶體分配都采用這段代碼,更進一步,任何從TestClass 繼承的類也都采用這一方式,除非它自己也多載了new 和 delete 運算子,通過多載new 和 delete 運算子的方法,你可以自由地采用不同的分配策略,從不同的記憶體池中分配不同的類物件,
(2)為單個的類多載 new[ ]和delete[ ]
必須小心物件陣列的分配,你可能希望呼叫到被你多載過的new 和 delete 運算子,但并不如此,記憶體的請求被定向到全域的new[ ]和delete[ ] 運算子,而這些記憶體來自于系統堆,
C++將物件陣列的記憶體分配作為一個單獨的操作,而不同于單個物件的記憶體分配,為了改變這種方式,你同樣需要多載new[ ] 和 delete[ ]運算子,
class TestClass {
public:
void * operator new[ ](size_t size);
void operator delete[ ](void *p);
// .. other members here ..
};
void *TestClass::operator new[ ](size_t size)
{
void *p = malloc(size);
return (p);
}
void TestClass::operator delete[ ](void *p)
{
free(p);
}
int main(void)
{
TestClass *p = new TestClass[10];
// ... etc ...
delete[ ] p;
}
但是注意:對于多數C++的實作,new[]運算子中的個數引數是陣列的大小加上額外的存盤物件數目的一些位元組,在你的記憶體分配機制重要考慮的這一點,你應該盡量避免分配物件陣列,從而使你的記憶體分配策略簡單,
2.1.3 常見的記憶體錯誤及其對策
發生記憶體錯誤是件非常麻煩的事情,編譯器不能自動發現這些錯誤,通常是在程式運行時才能捕捉到,而這些錯誤大多沒有明顯的癥狀,時隱時現,增加了改錯的難度,有時用戶怒氣沖沖地把你找來,程式卻沒有發生任何問題,你一走,錯誤又發作了, 常見的記憶體錯誤及其對策如下:
(1)記憶體分配未成功,卻使用了它
編程新手常犯這種錯誤,因為他們沒有意識到記憶體分配會不成功,常用解決辦法是,在使用記憶體之前檢查指標是否為NULL,如果指標p是函式的引數,那么在函式的入口處用**assert(NULL != p)進行檢查,如果是用malloc或new來申請記憶體,應該用if(NULL == p) 或if(NULL!=p)**進行防錯處理,
(2)記憶體分配雖然成功,但是尚未初始化就參考它
犯這種錯誤主要有兩個起因:一是沒有初始化的觀念;二是誤以為記憶體的預設初值全為零,導致參考初值錯誤(例如陣列), 記憶體的預設初值究竟是什么并沒有統一的標準,盡管有些時候為零值,我們寧可信其無不可信其有,所以無論用何種方式創建陣列,都別忘了賦初值,即便是賦零值也不可省略,不要嫌麻煩,
(3)記憶體分配成功并且已經初始化,但操作越過了記憶體的邊界
例如在使用陣列時經常發生下標“多1”或者“少1”的操作,特別是在for回圈陳述句中,回圈次數很容易搞錯,導致陣列操作越界,
(4)忘記了釋放記憶體,造成記憶體泄露
含有這種錯誤的函式每被呼叫一次就丟失一塊記憶體,剛開始時系統的記憶體充足,你看不到錯誤,終有一次程式突然死掉,系統出現提示:記憶體耗盡,
動態記憶體的申請與釋放必須配對,程式中malloc與free的使用次數一定要相同,否則肯定有錯誤(new/delete同理),
(5)釋放了記憶體卻繼續使用它,有3種情況:
- 程式中的物件呼叫關系過于復雜,實在難以搞清楚某個物件究竟是否已經釋放了記憶體,此時應該重新設計資料結構,從根本上解決物件管理的混亂局面,
- 函式的return陳述句寫錯了,注意不要回傳指向“堆疊記憶體”的“指標”或者“參考”,因為該記憶體在函式體結束時被自動銷毀,
- 使用free或delete釋放了記憶體后,沒有將指標設定為NULL,導致產生“野指標”,
對策:
【規則1】用malloc或new申請記憶體之后,應該立即檢查指標值是否為NULL,防止使用指標值為NULL的記憶體,
【規則2】不要忘記為陣列和動態記憶體賦初值,防止將未被初始化的記憶體作為右值使用,
【規則3】避免陣列或指標的下標越界,特別要當心發生“多1”或者“少1”操作,
【規則4】動態記憶體的申請與釋放必須配對,防止記憶體泄漏,
【規則5】用free或delete釋放了記憶體之后,立即將指標設定為NULL,防止產生“野指標”,
2.1.4 指標與陣列的對比
C++/C程式中,指標和陣列在不少地方可以相互替換著用,讓人產生一種錯覺,以為兩者是等價的,
- 陣列要么在靜態存盤區被創建(如全域陣列),要么在堆疊上被創建,陣列名對應著(而不是指向)一塊記憶體,其地址與容量在生命期內保持不變,只有陣列的內容可以改變,
- 指標可以隨時指向任意型別的記憶體塊,它的特征是“可變”,所以我們常用指標來操作動態記憶體,指標遠比陣列靈活,但也更危險,
下面以字串為例比較指標與陣列的特性:
(1)修改內容
下面示例中,字符陣列a的容量是6個字符,其內容為hello,a的內容可以改變,如a[0]= ‘X’,指標p指向常量字串“world”(位于靜態存盤區,內容為world),常量字串的內容是不可以被修改的,從語法上看,編譯器并不覺得陳述句p[0]= ‘X’有什么不妥,但是該陳述句企圖修改常量字串的內容而導致運行錯誤,
char a[] = “hello”;
a[0] = ‘X’;
cout << a << endl;
char *p = “world”; // 注意p指向常量字串
p[0] = ‘X’; // 編譯器不能發現該錯誤
cout << p << endl;
(2)內容復制與比較
不能對陣列名進行直接復制與比較,若想把陣列a的內容復制給陣列b,不能用陳述句 b = a ,否則將產生編譯錯誤,應該用標準庫函式strcpy進行復制,同理,比較b和a的內容是否相同,不能用if(b==a) 來判斷,應該用標準庫函式strcmp進行比較,
陳述句p = a 并不能把a的內容復制指標p,而是把a的地址賦給了p,要想復制a的內容,可以先用庫函式malloc為p申請一塊容量為strlen(a)+1個字符的記憶體,再用strcpy進行字串復制,同理,陳述句if(p==a) 比較的不是內容而是地址,應該用庫函式strcmp來比較,
// 陣列…
char a[] = "hello";
char b[10];
strcpy(b, a); // 不能用 b = a;
if(strcmp(b, a) == 0) // 不能用 if (b == a)
...
// 指標…
int len = strlen(a);
char *p = (char *)malloc(sizeof(char)*(len+1));
strcpy(p,a); // 不要用 p = a;
if(strcmp(p, a) == 0) // 不要用 if (p == a)
...
(3)計算記憶體容量
用運算子sizeof可以計算出陣列的容量(位元組數),
如下示例中,sizeof(a)的值是12(注意別忘了"/0"),指標p指向a,但是sizeof§的值卻是4,這是因為sizeof§得到的是一個指標變數的位元組數,相當于sizeof(char*),而不是p所指的記憶體容量,C++/C語言沒有辦法知道指標所指的記憶體容量,除非在申請記憶體時記住它,
char a[] = "hello world";
char *p = a;
cout<< sizeof(a) << endl; // 12位元組
cout<< sizeof(p) << endl; // 4位元組
注意當陣列作為函式的引數進行傳遞時,該陣列自動退化為同型別的指標,如下示例中,不論陣列a的容量是多少,sizeof(a)始終等于sizeof(char *),
void Func(char a[100])
{
cout<< sizeof(a) << endl; // 4位元組而不是100位元組
}
2.1.5 指標引數是如何傳遞記憶體的?
如果函式的引數是一個指標,不要指望用該指標去申請動態記憶體,如下示例中,Test函式的陳述句GetMemory(str, 200)并沒有使str獲得期望的記憶體,str依舊是NULL,為什么?
void GetMemory(char *p, int num)
{
p = (char *)malloc(sizeof(char) * num);
}
void Test(void)
{
char *str = NULL;
GetMemory(str, 100); // str 仍然為 NULL
strcpy(str, "hello"); // 運行錯誤
}
毛病出在函式GetMemory中,編譯器總是要為函式的每個引數制作臨時副本,指標引數p的副本是 _p,編譯器使 _p = p,如果函式體內的程式修改了_p的內容,就導致引數p的內容作相應的修改,這就是指標可以用作輸出引數的原因,在本例中,_p申請了新的記憶體,只是把_p所指的記憶體地址改變了,但是p絲毫未變,所以函式GetMemory并不能輸出任何東西,事實上,每執行一次GetMemory就會泄露一塊記憶體,因為沒有用free釋放記憶體,
如果非得要用指標引數去申請記憶體,那么應該改用“指向指標的指標”,見示例:
void GetMemory2(char **p, int num)
{
*p = (char *)malloc(sizeof(char) * num);
}
void Test2(void)
{
char *str = NULL;
GetMemory2(&str, 100); // 注意引數是 &str,而不是str
strcpy(str, "hello");
cout<< str << endl;
free(str);
}
由于“指向指標的指標”這個概念不容易理解,我們可以用函式回傳值來傳遞動態記憶體,這種方法更加簡單,見示例:
char *GetMemory3(int num)
{
char *p = (char *)malloc(sizeof(char) * num);
return p;
}
void Test3(void)
{
char *str = NULL;
str = GetMemory3(100);
strcpy(str, "hello");
cout<< str << endl;
free(str);
}
用函式回傳值來傳遞動態記憶體這種方法雖然好用,但是常常有人把return陳述句用錯了,這里強調不要用return陳述句回傳指向“堆疊記憶體”的指標,因為該記憶體在函式結束時自動消亡,見示例:
char *GetString(void)
{
char p[] = "hello world";
return p; // 編譯器將提出警告
}
void Test4(void)
{
char *str = NULL;
str = GetString(); // str 的內容是垃圾
cout<< str << endl;
}
用除錯器逐步跟蹤Test4,發現執行str = GetString陳述句后str不再是NULL指標,但是str的內容不是“hello world”而是垃圾,
如果把上述示例改寫成如下示例,會怎么樣?
char *GetString2(void)
{
char *p = "hello world";
return p;
}
void Test5(void)
{
char *str = NULL;
str = GetString2();
cout<< str << endl;
}
函式Test5運行雖然不會出錯,但是函式GetString2的設計概念卻是錯誤的,因為GetString2內的“hello world”是常量字串,位于靜態存盤區,它在程式生命期內恒定不變,無論什么時候呼叫GetString2,它回傳的始終是同一個“只讀”的記憶體塊,
2.1.6 杜絕“野指標”
“野指標”不是NULL指標,是指向“垃圾”記憶體的指標,人們一般不會錯用NULL指標,因為用if陳述句很容易判斷,但是“野指標”是很危險的,if陳述句對它不起作用, “野指標”的成因主要有兩種:
(1)指標變數沒有被初始化,任何指標變數剛被創建時不會自動成為NULL指標,它的預設值是隨機的,它會亂指一氣,所以,指標變數在創建的同時應當被初始化,要么將指標設定為NULL,要么讓它指向合法的記憶體,例如
char *p = NULL;
char *str = (char *) malloc(100);
(2)指標p被free或者delete之后,沒有置為NULL,讓人誤以為p是個合法的指標,
(3)指標操作超越了變數的作用域范圍,這種情況讓人防不勝防,示例程式如下:
class A
{
public:
void Func(void){ cout << “Func of class A” << endl; }
};
void Test(void)
{
A *p;
{
A a;
p = &a; // 注意 a 的生命期
}
p->Func(); // p是“野指標”
}
函式Test在執行陳述句p->Func()時,物件a已經消失,而p是指向a的,所以p就成了“野指標”,但奇怪的是我運行這個程式時居然沒有出錯,這可能與編譯器有關,
2.1.7 有了malloc/free為什么還要new/delete?
malloc與free是C++/C語言的標準庫函式,new/delete是C++的運算子,它們都可用于申請動態記憶體和釋放記憶體,
對于非內部資料型別的物件而言,光用maloc/free無法滿足動態物件的要求,物件在創建的同時要自動執行建構式,物件在消亡之前要自動執行解構式,由于malloc/free是庫函式而不是運算子,不在編譯器控制權限之內,不能夠把執行建構式和解構式的任務強加于malloc/free,
因此C++語言需要一個能完成動態記憶體分配和初始化作業的運算子new,以及一個能完成清理與釋放記憶體作業的運算子delete,注意new/delete不是庫函式,我們先看一看malloc/free和new/delete如何實作物件的動態記憶體管理,見示例:
class Obj
{
public :
Obj(void){ cout << “Initialization” << endl; }
~Obj(void){ cout << “Destroy” << endl; }
void Initialize(void){ cout << “Initialization” << endl; }
void Destroy(void){ cout << “Destroy” << endl; }
};
void UseMallocFree(void)
{
Obj *a = (obj *)malloc(sizeof(obj)); // 申請動態記憶體
a->Initialize(); // 初始化
//…
a->Destroy(); // 清除作業
free(a); // 釋放記憶體
}
void UseNewDelete(void)
{
Obj *a = new Obj; // 申請動態記憶體并且初始化
//…
delete a; // 清除并且釋放記憶體
}
類Obj的函式Initialize模擬了建構式的功能,函式Destroy模擬了解構式的功能,函式UseMallocFree中,由于malloc/free不能執行建構式與解構式,必須呼叫成員函式Initialize和Destroy來完成初始化與清除作業,函式UseNewDelete則簡單得多,
所以我們不要企圖用malloc/free來完成動態物件的記憶體管理,應該用new/delete,由于內部資料型別的“物件”沒有構造與析構的程序,對它們而言malloc/free和new/delete是等價的,
既然new/delete的功能完全覆寫了malloc/free,為什么C++不把malloc/free淘汰出局呢?這是因為C++程式經常要呼叫C函式,而C程式只能用malloc/free管理動態記憶體,
如果用free釋放“new創建的動態物件”,那么該物件因無法執行解構式而可能導致程式出錯,如果用delete釋放“malloc申請的動態記憶體”,結果也會導致程式出錯,但是該程式的可讀性很差,所以new/delete必須配對使用,malloc/free也一樣,
2.1.8 記憶體耗盡怎么辦?
如果在申請動態記憶體時找不到足夠大的記憶體塊,malloc和new將回傳NULL指標,宣告記憶體申請失敗,通常有三種方式處理“記憶體耗盡”問題,
(1)判斷指標是否為NULL,如果是則馬上用return陳述句終止本函式,例如:
void Func(void)
{
A *a = new A;
if(a == NULL)
{
return;
}
...
}
**(2)判斷指標是否為NULL,如果是則馬上用exit(1)終止整個程式的運行,**例如:
void Func(void)
{
A *a = new A;
if(a == NULL)
{
cout << “Memory Exhausted” << endl;
exit(1);
}
...
}
(3)為new和malloc設定例外處理函式,
上述(1)(2)方式使用最普遍,如果一個函式內有多處需要申請動態記憶體,那么方式(1)就顯得力不從心(釋放記憶體很麻煩),應該用方式(2)來處理,
很多人不忍心用exit(1),問:“不撰寫出錯處理程式,讓作業系統自己解決行不行?”
不行,如果發生“記憶體耗盡”這樣的事情,一般說來應用程式已經無藥可救,如果不用exit(1) 把壞程式殺死,它可能會害死作業系統,道理如同:如果不把歹徒擊斃,歹徒在老死之前會犯下更多的罪,
有一個很重要的現象要告訴大家,對于32位以上的應用程式而言,無論怎樣使用malloc與new,幾乎不可能導致“記憶體耗盡”,
但必須強調:不加錯誤處理將導致程式的質量很差,千萬不可因小失大,
2.1.9 malloc/free的使用要點
(1)函式malloc的原型:void * malloc(size_t size);
用malloc申請一塊長度為length的整數型別的記憶體,程式如下:
int *p = (int *) malloc(sizeof(int) * length);
我們應當把注意力集中在兩個要素上:“型別轉換”和“sizeof”,
- malloc回傳值的型別是void *,所以在呼叫malloc時要顯式地進行型別轉換,將void * 轉換成所需要的指標型別,
- malloc函式本身并不識別要申請的記憶體是什么型別,它只關心記憶體的總位元組數,我們通常記不住int, float等資料型別的變數的確切位元組數,
在malloc的“()”中使用sizeof運算子是良好的風格,但要當心有時我們會昏了頭,寫出 p = malloc(sizeof( p ))這樣的程式來,
(2)函式free的原型:void free( void * memblock );
為什么free函式不象malloc函式那樣復雜呢?這是因為指標p的型別以及它所指的記憶體的容量事先都是知道的,陳述句free§能正確地釋放記憶體,如果p是NULL指標,那么free對p無論操作多少次都不會出問題,如果p不是NULL指標,那么free對p連續操作兩次就會導致程式運行錯誤,
2.1.10 new/delete的使用要點
運算子new使用起來要比函式malloc簡單得多,例如:
int *p1 = (int *)malloc(sizeof(int) * length);
int *p2 = new int[length];
這是因為new內置了sizeof、型別轉換和型別安全檢查功能,對于非內部資料型別的物件而言,new在創建動態物件的同時完成了初始化作業,如果物件有多個建構式,那么new的陳述句也可以有多種形式,例如:
class Obj
{
public :
Obj(void); // 無引數的建構式
Obj(int x); // 帶一個引數的建構式
...
}
void Test(void)
{
Obj *a = new Obj;
Obj *b = new Obj(1); // 初值為1
...
delete a;
delete b;
}
如果用new創建物件陣列,那么只能使用物件的無引數建構式,例如:
Obj *objects = new Obj[100]; // 創建100個動態物件
不能寫成:
Obj *objects = new Obj[100](1);// 創建100個動態物件的同時賦初值1
在用delete釋放物件陣列時,留意不要丟了符號‘[]’,后者有可能引起程式崩潰和記憶體泄漏,例如:
delete []objects; // 正確的用法
delete objects; // 錯誤的用法
2.2 C++中的健壯指標和資源管理
2.2.1 第一條規則(RAII)
一個指標,一個句柄,一個臨界區狀態只有在我們將它們封裝入物件的時候才會擁有所有者,
這就是我們的第一規則:在建構式中分配資源,在解構式中釋放資源,
當你按照規則將所有資源封裝的時候,你可以保證你的程式中沒有任何的資源泄露,這點在當封裝物件(Encapsulating Object)在堆疊中建立或者嵌入在其他的物件中的時候非常明顯,
但是對那些動態申請的物件呢?不要急!任何動態申請的東西都被看作一種資源,并且要按照上面提到的方法進行封裝,這一物件封裝物件的鏈不得不在某個地方終止,它最終終止在最高級的所有者,自動的或者是靜態的,這些分別是對離開作用域或者程式時釋放資源的保證,
下面是資源封裝的一個經典例子,在一個多執行緒的應用程式中,執行緒之間共享物件的問題是通過用這樣一個物件聯系臨界區來解決的,每一個需要訪問共享資源的客戶需要獲得臨界區,例如,這可能是Win32下臨界區的實作方法:
class CritSect
{
friend class Lock;
public:
CritSect () { InitializeCriticalSection (&_critSection); }
~CritSect () { DeleteCriticalSection (&_critSection); }
private:
void Acquire ()
{
EnterCriticalSection (&_critSection);
}
void Release ()
{
LeaveCriticalSection (&_critSection);
}
private:
CRITICAL_SECTION _critSection;
};
這里聰明的部分是我們確保每一個進入臨界區的客戶最后都可以離開,
"進入"臨界區的狀態是一種資源,并應當被封裝,封裝器通常被稱作一個鎖(lock),
class Lock
{
public:
Lock (CritSect& critSect) : _critSect (critSect)
{
_critSect.Acquire ();
}
~Lock ()
{
_critSect.Release ();
}
private
CritSect & _critSect;
};
鎖的一般用法:
void Shared::Act () throw (char *)
{
Lock lock (_critSect);
// perform action —— may throw
// automatic destructor of lock
}
注意無論發生什么,臨界區都會借助于語言的機制保證釋放,
還有一件需要記住的事情–每一種資源都需要被分別封裝,這是因為資源分配是一個非常容易出錯的操作,是要資源是有限提供的,我們會假設一個失敗的資源分配會導致一個例外–事實上,這會經常的發生,所以如果你想試圖用一個石頭打兩只鳥的話,或者在一個建構式中申請兩種形式的資源,你可能就會陷入麻煩,只要想想在一種資源分配成功但另一種失敗拋出例外時會發生什么,因為建構式還沒有全部完成,解構式不可能被呼叫,第一種資源就會發生泄露,
這種情況可以非常簡單的避免,無論何時你有一個需要兩種以上資源的類時,寫兩個小的封裝器將它們嵌入你的類中,每一個嵌入的構造都可以保證洗掉,即使包裝類沒有構造完成,
2.2.2 Smart Pointers
我們至今還沒有討論最常見型別的資源–用運算子new分配,此后用指標訪問的一個物件,
我們需要為每個物件分別定義一個封裝類嗎?(事實上,C++標準模板庫已經有了一個模板類,叫做auto_ptr,其作用就是提供這種封裝,我們一會兒在回到auto_ptr,)讓我們從一個極其簡單、呆板但安全的東西開始,
看下面的Smart Pointer模板類,它十分堅固,甚至無法實作,
template <class T>
class SmartPointer
{
public:
~SmartPointer () { delete _p; }
T * operator->() { return _p; }
T const * operator->() const { return _p; }
protected:
SmartPointer (): _p (0) {}
explicit SmartPointer (T* p): _p (p) {}
T * _p;
};
為什么要把SmartPointer的建構式設計為protected呢?
如果我需要遵守第一條規則,那么我就必須這樣做,資源–在這里是class T的一個物件–必須在封裝器的建構式中分配,但是我不能只簡單的呼叫new T,因為我不知道T的建構式的引數,因為,在原則上,每一個T都有一個不同的建構式;我需要為它定義個另外一個封裝器,模板的用處會很大,為每一個新的類,我可以通過繼承SmartPointer定義一個新的封裝器,并且提供一個特定的建構式,
class SmartItem: public SmartPointer<Item>
{
public:
explicit SmartItem (int i)
: SmartPointer<Item> (new Item (i)) {}
};
為每一個類提供一個Smart Pointer真的值得嗎?
說實話–不!它很有教學的價值,但是一旦你學會如何遵循第一規則的話,你就可以放松規則并使用一些高級的技術,這一技術是讓SmartPointer的建構式成為public,但是只是是用它來做資源轉換(Resource Transfer),我的意思是用new運算子的結果直接作為SmartPointer的建構式的引數,像這樣:
SmartPointer<Item> item (new Item (i));
這個方法明顯更需要自控性,不只是你,而且包括你的程式小組的每個成員,他們都必須發誓出了作資源轉換外不把建構式用在人以其他用途,幸運的是,這條規矩很容易得以加強,只需要在源檔案中查找所有的new即可,
2.2.3 Resource Transfer
到目前為止,我們所討論的一直是生命周期在一個單獨的作用域內的資源,
現在我們要解決一個困難的問題–如何在不同的作用域間安全的傳遞資源,
這一問題在當你處理容器的時候會變得十分明顯,你可以動態的創建一串物件,將它們存放至一個容器中,然后將它們取出,并且在最終安排它們,為了能夠讓這安全的作業–沒有泄露–物件需要改變其所有者,
這個問題的一個非常顯而易見的解決方法是使用Smart Pointer,無論是在加入容器前還是還找到它們以后,這是他如何運作的,你加入Release方法到Smart Pointer中:
template <class T>
T * SmartPointer<T>::Release ()
{
T * pTmp = _p;
_p = 0;
return pTmp;
}
注意在Release呼叫以后,Smart Pointer就不再是物件的所有者了——它內部的指標指向空,現在,呼叫了Release都必須是一個負責的人并且迅速隱藏回傳的指標到新的所有者物件中,在我們的例子中,容器呼叫了Release,比如這個Stack的例子:
void Stack::Push (SmartPointer <Item> & item) throw (char *)
{
if (_top == maxStack)
throw "Stack overflow";
_arr [_top++] = item.Release ();
};
同樣的,你也可以再你的代碼中用加強Release的可靠性,
2.2.4 Strong Pointers
一個Strong Pointer會在許多地方和我們這個SmartPointer相似–它在超出它的作用域后會清除他所指向的物件,資源傳遞會以強指標賦值的形式進行,也可以有Weak Pointer存在,它們用來訪問物件而不需要所有物件–比如可賦值的參考,
任何指標都必須宣告為Strong或者Weak,并且語言應該來關注型別轉換的規定,例如,你不可以將Weak Pointer傳遞到一個需要Strong Pointer的地方,但是相反卻可以,Push方法可以接受一個Strong Pointer并且將它轉移到Stack中的Strong Pointer的序列中,Pop方法將會回傳一個Strong Pointer,把Strong Pointer的引入語言將會使垃圾回收成為歷史,
我可以自己實作Strong Pointers,畢竟,它們都很像Smart Pointers,給它們一個拷貝建構式并多載賦值運算子并不是一個大問題,事實上,這正是標準庫中的auto_ptr有的,重要的是對這些操作給出一個資源轉移的語法,但是這也不是很難,
template <class T>
SmartPointer<T>::SmartPointer (SmartPointer<T> & ptr)
{
_p = ptr.Release ();
}
template <class T>
void SmartPointer<T>::operator = (SmartPointer<T> & ptr)
{
if (_p != ptr._p)
{
delete _p;
_p = ptr.Release ();
}
}
使這整個想法迅速成功的原因之一是我可以以值方式傳遞這種封裝指標!
我有了我的蛋糕,并且也可以吃了,看這個Stack的新的實作:
class Stack
{
enum { maxStack = 3 };
public:
Stack ()
: _top (0)
{}
void Push (SmartPointer<Item> & item) throw (char *)
{
if (_top >= maxStack)
throw "Stack overflow";
_arr [_top++] = item;
}
SmartPointer<Item> Pop ()
{
if (_top == 0)
return SmartPointer<Item> ();
return _arr [--_top];
}
private
int _top;
SmartPointer<Item> _arr [maxStack];
};
Pop方法強制客戶將其回傳值賦給一個Strong Pointer,SmartPointer,任何試圖將他對一個普通指標的賦值都會產生一個編譯期錯誤,因為型別不匹配,此外,因為Pop以值方式回傳一個Strong Pointer(在Pop的宣告時SmartPointer后面沒有&符號),編譯器在return時自動進行了一個資源轉換,他呼叫了operator =來從陣列中提取一個Item,拷貝建構式將他傳遞給呼叫者,呼叫者最后擁有了指向Pop賦值的Strong Pointer指向的一個Item,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/289866.html
標籤:其他
上一篇:LINUX02_概述、檔案系統詳解、vim、cd、ls、mkdir、touch、rm、cp、less、tail、head、find、locate、打包或解壓tar
