1. 程式人生 > >Linux C/C++ 記憶體分配與釋放 [摘抄整理]

Linux C/C++ 記憶體分配與釋放 [摘抄整理]

 no malloc no free
no new no delete


寫了一個簡單類,執行的時候報了個錯 ,下決心好好看下記憶體相關知識

class ConstChar{
	public:
		ConstChar(const char *data, int size){
			m_data = new char[size];
			m_data = data;// 在這裡 m_data 已經指向新的記憶體塊,所以在解構函式被呼叫的時候,就會報錯了,也不符合模組化的編碼規範
		}
		~ConstChar(){
			printf("%s desctruted !\n",__func__);
			delete m_data;
		}
	private:
		const char* m_data;
};

正確的應該是:

class ConstChar{
	public:
		ConstChar(const char *data, int size){
			char *tchar = new char[size];
                        strncpy(tchar,data,size);
                        m_data = tchar;
		}
~ConstChar(){printf("%s desctruted !\n",__func__);delete m_data;}private:const char* m_data;};
因為這個建構函式主要功能是用一個 const 字串去構造一個新的 const 字串,所以可以不用 new /delete :
class ConstChar{
	public:
		ConstChar(const char *data, int size){
			m_data = data;
		}
		~ConstChar(){
			printf("%s desctruted !\n",__func__);
		}
	private:
		const char* m_data;
};

關於 char 和 string :

準確的來說,char是資料型別,而string是類,不嚴格算基礎資料型別。當string的生命週期結束時,會自動呼叫string類的解構函式,釋放記憶體,不用手動釋放。

用char時需要申請記憶體,使用完了要釋放記憶體,使用string是否需要申請和釋放?

使用string時不需要用到 new/delete, 而 string 自己的底層實現需要,否則就失去方便性了。

string a  = "ok";
string * b = new string("ok two");
delete b;
string * c = new string[5];
delete [] c;

一、對內的分配

  32位作業系統支援4GB記憶體的連續訪問,但通常把記憶體分為兩個2GB的空間,每個程序在執行時最大可以使用2GB的私有記憶體(0x00000000—0x7FFFFFFF)。即理論上支援如下的大陣列:


C++程式碼
    1. char szBuffer[2*1024*1024*1024]; 

       當然,由於在實際執行時,程式還有程式碼段、臨時變數段、動態記憶體申請等,實際上是不可能用到上述那麼大的陣列的。

  至於高階的2GB記憶體地址(0x80000000—0xFFFFFFFF),作業系統一般內部保留使用,即供作業系統核心程式碼使用。在Windows和Linux平臺上,一些動態連結庫(Windows的dll,Linux的so)以及ocx控制元件等,由於是跨程序服務的,因此一般也在高2GB記憶體空間執行。

  可以看到,每個程序都能看到自己的2GB記憶體以及系統的2GB記憶體,但是不同程序之間是無法彼此看到對方的。當然,作業系統在底層做了很多工作,比如磁碟上的虛擬記憶體交換(請看下以標題),不同的記憶體塊動態對映等等。

  二、虛擬記憶體

  虛擬記憶體的基本思想是:用廉價但緩慢的磁碟來擴充快速卻昂貴的記憶體。在一定時刻,程式實際需要使用的虛擬記憶體區段的內容就被載入實體記憶體中。當實體記憶體中的資料有一段時間未被使用,它們就可能被轉移到硬碟中,節省下來的實體記憶體空間用於載入需要使用的其他資料。

  在程序執行過程中,作業系統負責具體細節,使每個程序都以為自己擁有整個地址空間的獨家訪問權。這個幻覺是通過“虛擬記憶體”實現的。所有程序共享機器的實體記憶體,當記憶體使用完時就用磁碟儲存資料。在程序執行時,資料在磁碟和記憶體之間來回移動。記憶體管理硬體負責把虛擬地址翻譯為實體地址,並讓一個程序始終運行於系統的真正記憶體中,應用程式設計師只看到虛擬地址,並不知道自己的程序在磁碟與記憶體之間來回切換。

  從潛在的可能性上說,與程序有關的所有記憶體都將被系統所使用,如果該程序可能不會馬上執行(可能它的優先順序低,也可能是它處於睡眠狀態),作業系統可以暫時取回所有分配給它的實體記憶體資源,將該程序的所有相關資訊都備份到磁碟上。

  程序只能操作位於實體記憶體中的頁面。當程序引用一個不在實體記憶體中的頁面時,MMU就會產生一個頁錯誤。記憶體對此事做出響應,並判斷該引用是否有效。如果無效,核心向程序發出一個“segmentation violation(段違規)”的訊號,核心從磁碟取回該頁,換入記憶體中,一旦頁面進入記憶體,程序便被解鎖,可以重新執行--程序本身並不知道它曾經因為頁面換入事件等待了一會。

  三、記憶體的使用

  對於程式設計師,我們最重要的是能理解不同程序間私有記憶體空間的含義。C和C++的編譯器把私有記憶體分為3塊:基棧、浮動棧和堆。如下圖:

      (1)基棧:也叫靜態儲存區,這是編譯器在編譯期間就已經固定下來必須要使用的記憶體,如程式的程式碼段、靜態變數、全域性變數、const常量等。

      (2)浮動棧:很多書上稱為“棧”,就是程式開始執行,隨著函式、物件的一段執行,函式內部變數、物件的內部成員變數開始動態佔用記憶體,浮動棧一般都有生命週期,函式結束或者物件析構,其對應的浮動棧空間的就拆除了,這部分內容總是變來變去,記憶體佔用也不是固定,因此叫浮動棧。

    (3)堆:C和C++語言都支援動態記憶體申請,即程式執行期可以自由申請記憶體,這部分記憶體就是在堆空間申請的。堆位於2GB的最頂端,自上向下分配,這是避免和浮動棧混到一起,不好管理。我們用到malloc和new都是從堆空間申請的記憶體,new比malloc多了物件的支援,可以自動呼叫建構函式。另外,new建立物件,其成員變數位於堆裡面。

  我們來看一個例子:

C++程式碼
    const int n = 100;   //基棧
    void Func(void)   
    {   
        char ch = 0;   //浮動棧
        char* pBuff = (char*)malloc(10);   //堆疊
        //…   
    }  


       這個函式如果執行,其中n由於是全域性靜態變數,位於基棧,ch和pBuff這兩個函式內部變數,ch位於浮動棧,而pBuff指向的由malloc分配的記憶體區,則位於堆疊。

  在記憶體理解上,最著名的例子就是執行緒啟動時的引數傳遞。

  函式啟動一個執行緒,很多時候需要向執行緒傳引數,但是執行緒是非同步啟動的,即很可能啟動函式已經退出了,而執行緒函式都還沒有正式開始執行,因此,絕不能用啟動函式的內部變數給執行緒傳參。道理很簡單,函式的內部變數在浮動棧,但函式退出時,浮動棧自動拆除,記憶體空間已經被釋放了。當執行緒啟動時,按照給的引數指標去查詢變數,實際上是在讀一塊無效的記憶體區域,程式會因此而崩潰。

  那怎麼辦呢?我們應該直接用malloc函式給需要傳遞的引數分配一塊記憶體區域,將指標傳入執行緒,執行緒收到後使用,最後執行緒退出時,free釋放。


四、記憶體bug

  無規則的濫用記憶體和指標會導致大量的bug,程式設計師應該對記憶體的使用保持高度的敏感性和警惕性,謹慎地使用記憶體資源。

  使用記憶體時最容易出現的bug是:

  (1)壞指標值錯誤:在指標賦值之前就用它來引用記憶體,或者向庫函式傳送一個壞指標,第三種可能導致壞指標的原因是對指標進行釋放之後再訪問它的內容。可以修改free語句,在指標釋放之後再將它置為空值。

    free(p);    
    p = NULL;  

這樣,如果在指標釋放之後繼續使用該指標,至少程式能在終止之前進行資訊轉儲。

  (2)改寫(overwrite)錯誤:越過陣列邊界寫入資料,在動態分配的記憶體兩端之外寫入資料,或改寫一些堆管理資料結構(在動態分配記憶體之前的區域寫入資料就很容易發生這種情況)

    p = malloc(256);    
    p[-1] = 0;    
    p[256] = 0;  

        (3)指標釋放引起的錯誤:釋放同一個記憶體塊兩次,或釋放一塊未曾使用malloc分配的記憶體,或釋放仍在使用中的記憶體,或釋放一個無效的指標。一個極為常見的與釋放記憶體有關的錯誤就像下面這樣:
    struct node *p, *tart, *temp;      
    for(p = start; p ; p = p->next)      
    {      
        free(p);      
    }   

       上面的程式碼會在第二次迭代時對已經釋放的指標再次進行釋放,這樣就會導致不可預料的錯誤。正確的迭代方法:(沒看明白)

       
     struct node *p, *tart, *temp;   
     for(p = start; p ; p = temp)   
     {   
         temp = p->next;   
         free(p);   
     } 


轉自:CSDN 關於 new/delete  點選開啟連結 關於

C++中指標在new和delete操作的時候對記憶體堆都做了些什麼呢,以下解:

1、指標的new操作:

指標在new之後,會在記憶體堆中分配一個空間,而指標中存放的是這個空間的地址。如:

void main(){
 int *p = new int(4);
 cout << p << endl;
 cout << *p << endl;
}
輸出為:
0x00431BF0
4

分別為分配的空間地址和地址記憶體放的值。

如果寫為:

void main(){
 int *p = new int(4);
 cout << *(int *)0x00431BF0 << endl;
}
輸出為:4

程式的意思就是將0x00431BF0記憶體地址開始的4個byte的塊取出轉換為int型別輸出,即直接讀取記憶體。

2、指標的delete操作:

指標在delete的時候,會將指標指向的記憶體區域釋放掉,而指標同記憶體區域的聯絡並沒有被切斷,仍然會只想原來指向的記憶體區域。如:

void main(){
 int *p = new int(4);
 cout << p << endl;
 cout << *p << endl;
 delete p;
 cout << p << endl; // delete / free 之後地址還是原來的地址
 cout << *p << endl; // 但是內容已經改變
}

程式輸出:
0x00431BF0
4
0x00431BF0
-572662307

可以看到p前後指向的地址是相同的,而指向地址的記憶體區域被釋放。

3、空指標:

空指標指向的記憶體區域為記憶體的首地址,是不可讀寫的區域,即空指標不提供操作。刪除空指標是安全的(因為它什麼也沒做)。所以,在寫建構函式,
賦值操作符,或其他成員函式時,類的每個指標成員要麼指向有效的記憶體,要麼就指向空,那在你的解構函式裡你就可以只用簡單地delete 掉他們,而不用
擔心他們是不是被new 過。如:

void main(){
 int *p = NULL;
 cout << p << endl;
} //輸出0x00000000

4、取出記憶體區域的值

在取某記憶體地址開始的一個區域的值的時候,取出的值取決於用來取值的型別,譬如int為4個byte,char為1個byte,程式如:

void main(){
 int a[2] = {261,0};
 int *pi = a;
 char *p = (char*)pi;
 cout << *(int *)p++ << endl;  //取出p地址,轉化為取4個byte,並取出內容,之後p向後移動一位(這裡注意不是 4 位,對於 char 型別的操作)
 cout << *(int *)p << endl;    //取出p地址,轉化為取4個byte,並取出內容
 cout << (int)*p << endl;      //取出1個char型別,並轉換為int型
 cout << (int)*(char *)pi << endl;  //取出pi地址,轉換為char型別,取出內容,並轉換為int型
}
程式輸出:
261
1
1
5

a的儲存區域安排為:byte1=5,byte2=1,byte3~byte8 = 0;所以*(int *)p++取的為byte1到byte4;

之後的*(int *)p取的是byte2到byte5;(int)*p取的是byte2;(int)*(char *)pi取的是byte1,之後轉換為int型

做一些修改:
 int a[2] = {261,1}; // {261,1}; 和 {261,0}; 結果差別很大
 bitset<32>bs(261);
 cout << "bitset of 261 =  " << bs <<endl; //(261,1) 在記憶體的狀態 [ ....0000 0001]  [0000 0000 0000 0000 0000 0001 0000 0101]
 int *pi = a;
 char *pt = (char*)pi;

 cout << "(int*)pt=" << (int*)pt << " pt=" << pt << " *(int*)pt++="<< *(int*)pt++ <<endl; 

     //取出pt地址,轉化為取4個byte,並取出內容,之後pt向後移動一位  261

 cout << "(int*)pt=" << (int*)pt << " pt=" << pt << " *(int*)pt="<< *(int*)pt <<endl;     

     //取出pt地址,轉化為取4個byte,並取出內容  16777217

 cout << (int)*pt << endl;          

     //取出1個char型別(8bit),並轉換為int型(32bit) -- 1

 cout << (int)*(char *)pi << endl; 

     //取出pi地址(32bit),轉換為char*型別(8bit),取出內容(5),並轉換為int型 --- 5

bitset of 261 =  00000000000000000000000100000101
(int*)pt=0x28fecd pt= *(int*)pt++=261
(int*)pt=0x28fecd pt= *(int*)pt=16777217
1
5


關於指標和陣列:

int arr[100];

int *p = new int[100];

陣列也可以不用[],而通過+號來得到指定元素: //當然,對於陣列,更常用的還是 [] 操作符。
cout << *(arr + 0) << endl;  //*(arr+0) 等於 *arr
cout << *(arr + 1) << endl;
cout << *(arr + 1) << endl;

//其實,對於指標,這樣的+及-操作用得還要多點。
cout << *(p + 0) << endl; //*(p + 0) 等於 *p
cout << *(p + 1) << endl;
cout << *(p + 1) << endl;
 

當指標變數 P 通過 new [] 指向一連續的記憶體空間:

1、p[N] 得到第N個元素 (0 <= N < 元素個數);

2、*(p + N) 同樣得到第N個元素 (0 <= N < 元素個數) 如 p[0] 或 *(p + 0) 得到記憶體空間第0個元素;  

把上面右邊程式碼中的大部分 p 替換為 arr,則和左邊程式碼變得一模一樣。

下面再來比較二者的不同:

//定義並且初始化:                                                                       int arr[20] = {0,1,2,3,4,5,6,7,8,9,0,……,19};

//定義、並且生成空間,但不能直接初始空間的內容:        int* p = new int[20] {0,1,2,3,4 ……} // 錯!  只得通過迴圈一個個設定

//不能通過對陣列本身 + 或 - 來改變陣列的位置:

arr = arr + 1;  // 錯!
cout << *arr << endl;
 
arr++;  // 錯!
cout << *arr << endl;
 
arr--;  // 錯!
cout << *arr << endl;
 
輸出結果:
無,因為程式有語法錯誤,通不過編譯。

//可以通過 + 或 - 操作直接改變指標: p = p + 1;
cout << *p << endl;
 
p++;
cout << *p << endl;
 
p--;
cout << *p << endl;

釋放空間:

//陣列 : 所帶的空間由系統自動分配及回收,無須也無法由程式來直接釋放。
//指向 : 連續空間的指標,必須使用delete [] 來釋放   delete [] p;

接下來的問題也很重要:

1.指標在某些方面的表現似乎有些像“花心大蘿蔔”。請看下面程式碼,演示令人心酸的一幕。
 
/* 初始化 p  ----- p 的新婚 通過 new ,將一段新建的記憶體“嫁給”指標p這一段分配的記憶體,就是p的原配夫妻*/
int* p = new int[100];  
 
 
/*  使用 p  ----- 恩愛相處
   N 多年恩愛相處,此處略去不表*/
……
 
/* p 改變指向 ---- 分手*/
 
int girl [100];   //第三者出現
p = girl;         //p 就這樣指向 girl
 
 
/* delete [] p ----  落幕前的災難  終於有一天,p老了,上帝選擇在這一時刻懲罰他*/
 
delete [] p;
 
扣除註釋,上面只有4行程式碼。這4行程式碼完全符合程式世界的憲法:語法。也就是說對它們進行編譯,編譯器會認為它們毫無錯誤,輕鬆放行。
 
但在災難在 delete [] p 時發生。我們原意是要釋放 p 最初通過 new int[100]而得到的記憶體空間,但事實上,p那時已經指向girl[100]了。結果,

第一、最初的空間並沒有被釋放。

第二、girl[100] 本由系統自行釋放,現在我們卻要強行釋放它。

2.一個指標被刪除時,應指向最初的地址 ,當一個指標通過 +,- 等操作而改變了指向;那麼在釋放之前,應確保其回到原來的指向。
 
比如:
 
int* p = new int[3];
 
*p = 1;
cout << *p << endl;
 
p++;    //p的指向改變了,指向了下一元素
*p = 2;
cout << *p << endl;
 
//錯誤的釋放:
delete [] p;
 
在 delete [] p 時,p指向的是第二個元素,結果該釋放將產生錯位:第一個元素沒有被釋放,而在最後多刪除了一個元素。相當你蓋房時蓋的是前3間,可以在拆房時,漏了頭一間,從第二間開始拆起,結果把不是你蓋的第4房間倒給一併拆了。
 
如何消除這一嚴重錯誤呢?
第一種方法是把指標正確地"倒"回原始位置:
 
p--;
delete [] p;
 
但當我們的指標指向變化很多次時,在釋放前要保證一步不錯地一一退回,會比較困難。所以另一方法是在最初時“備份”一份。在釋放時,直接釋放該指標即可。
 
int* p = new int[3];
int* pbak = *p;    //備份
 
//移動 p
……
 
//釋放:
delete [] pbak;  //( 這個在程式碼實際編寫中經常會碰到)
 
由於pbak正是指向p最初分配後的地址,我們刪除pbak,就是刪除p最初的指向。此時我們不能再刪除一次p。這也就引出new / delete 及 new[] / delete[] 在本章的最後一個問題。

3.已釋放的空間,不可重複釋放
第一種情況,錯了最直接:
 
int* p = new int(71);
cout << *p << endl;
 
delete p; //OK!
delete p; //ERROR! 重複刪除p
 
當然,如果同一指標在delete之後,又通過new 或 new[] 分配了一次記憶體,則需要再刪除一次:
 
int* p = new int(71);
cout << *p << endl;
 
delete p; //OK!
...
p = new int(81);
delete p; //OK!
...
 
p = new int[10];
for (int i=0; i<10; i++)
  *p = i;
 
...
delete [] p; //OK!
 
上面程式碼中,共計三次對p進行delete 或 delete[],但不屬於重複刪除。因為每次delete都對應一次新的new。
我們下面所說的例子,均指一次delete之後,沒有再次new,而重複進行delete。
 
第二種情況,重複刪除同一指向的多個指標
 
int* p1 = new int(71);
int* p2 = p1;   //p2和p1 現在指向同一記憶體地址
 
cout << *p1 << endl;
cout << *p2 << endl;
 
delete p1;  //OK
delete p2;  //ERROR! p2所指的記憶體,已通過delete p1而被釋放,不可再delete一次。
 
同樣的問題,如果你先刪除了p2,則同樣不可再刪除p1。
 
...
delete p2; //OK
delete p1; //ERROR

第三種情況,刪除指向某一普通變數的指標
 
int a = 100;
int* p = &a;
delete p;  //ERROR
 
p 不是通過new 得到新的記憶體空間,而是直接指向固定變數:a。所以刪除p等同要強行剝奪a的固有空間,會導致出錯。