1. 程式人生 > 程式設計 >一文秒懂C語言/C++記憶體管理(推薦)

一文秒懂C語言/C++記憶體管理(推薦)

C 語言記憶體管理指對系統記憶體的分配、建立、使用這一系列操作。在記憶體管理中,由於是作業系統記憶體,使用不當會造成畢竟麻煩的結果。本文將從系統記憶體的分配、創建出發,並且使用例子來舉例說明記憶體管理不當會出現的情況及解決辦法。

一、記憶體

在計算機中,每個應用程式之間的記憶體是相互獨立的,通常情況下應用程式 A 並不能訪問應用程式 B,當然一些特殊技巧可以訪問,但此文並不詳細進行說明。例如在計算機中,一個視訊播放程式與一個瀏覽器程式,它們的記憶體並不能訪問,每個程式所擁有的記憶體是分割槽進行管理的。

在計算機系統中,執行程式 A 將會在記憶體中開闢程式 A 的記憶體區域 1,執行程式 B 將會在記憶體中開闢程式 B 的記憶體區域 2,記憶體區域 1 與記憶體區域 2 之間邏輯分隔。

一文秒懂C語言/C++記憶體管理(推薦)

1.1 記憶體四區

在程式 A 開闢的記憶體區域 1 會被分為幾個區域,這就是記憶體四區,記憶體四區分為棧區、堆區、資料區與程式碼區。

一文秒懂C語言/C++記憶體管理(推薦)

棧區指的是儲存一些臨時變數的區域,臨時變數包括了局部變數、返回值、引數、返回地址等,當這些變數超出了當前作用域時將會自動彈出。該棧的最大儲存是有大小的,該值固定,超過該大小將會造成棧溢位。

堆區指的是一個比較大的記憶體空間,主要用於對動態記憶體的分配;在程式開發中一般是開發人員進行分配與釋放,若在程式結束時都未釋放,系統將會自動進行回收。

資料區指的是主要存放全域性變數、常量和靜態變數的區域,資料區又可以進行劃分,分為全域性區與靜態區。全域性變數與靜態變數將會存放至該區域。

程式碼區就比較好理解了,主要是儲存可執行程式碼,該區域的屬性是隻讀的。

1.2 使用程式碼證實記憶體四區的底層結構

由於棧區與堆區的底層結構比較直觀的表現,在此使用程式碼只演示這兩個概念。首先檢視程式碼觀察棧區的記憶體地址分配情況:

#include<stdio.h>
int main()
{
 int a = 0;
 int b = 0;
 char c='0';
 printf("變數a的地址是:%d\n變數b的地址是:%d\n變數c的地址是:%d\n",&a,&b,&c);

}

執行結果為:

一文秒懂C語言/C++記憶體管理(推薦)

我們可以觀察到變數 a 的地址是 2293324 變數 b 的地址是 2293320,由於 int 的資料大小為 4 所以兩者之間間隔為 4;再檢視變數 c,我們發現變數 c 的地址為 2293319,與變數 b 的地址 2293324 間隔 1,因為 c 的資料型別為 char,型別大小為 1。在此我們觀察發現,明明我建立變數的時候順序是 a 到 b 再到 c,為什麼它們之間的地址不是增加而是減少呢?那是因為棧區的一種資料儲存結構為先進後出,如圖:

一文秒懂C語言/C++記憶體管理(推薦)

首先棧的頂部為地址的“最小”索引,隨後往下依次增大,但是由於堆疊的特殊儲存結構,我們將變數 a 先進行儲存,那麼它的一個索引地址將會是最大的,隨後依次減少;第二次儲存的值是 b,該值的地址索引比 a 小,由於 int 的資料大小為 4,所以在 a 地址為 2293324 的基礎上往上減少 4 為 2293320,在儲存 c 的時候為 char,大小為 1,則地址為 2293319。由於 a、b、c 三個變數同屬於一個棧內,所以它們地址的索引是連續性的,那如果我建立一個靜態變數將會如何?在以上內容中說明了靜態變數儲存在靜態區內,我們現在就來證實一下:

#include<stdio.h>
int main()
{
 
 int a = 0;
 int b = 0;
 char c='0';
 static int d = 0;
 
 printf("變數a的地址是:%d\n變數b的地址是:%d\n變數c的地址是:%d\n",&c);
 
 printf("靜態變數d的地址是:%d\n",&d);

}

執行結果如下:

一文秒懂C語言/C++記憶體管理(推薦)

以上程式碼中建立了一個變數 d,變數 d 為靜態變數,執行程式碼後從結果上得知,靜態變數 d 的地址與一般變數 a、b、c 的地址並不存在連續,他們兩個的記憶體地址是分開的。那接下來在此建一個全域性變數,通過上述內容得知,全域性變數與靜態變數都應該儲存在靜態區,程式碼如下:

#include<stdio.h>
int e = 0;
int main()
{
 
 int a = 0;
 int b = 0;
 char c='0';
 static int d = 0;
 
 printf("變數a的地址是:%d\n變數b的地址是:%d\n變數c的地址是:%d\n",&d);
 printf("全域性變數e的地址是:%d\n",&e);

}

執行結果如下:

一文秒懂C語言/C++記憶體管理(推薦)


從以上執行結果中證實了上述內容的真實性,並且也得到了一個知識點,棧區、資料區都是使用棧結構對資料進行儲存。

在以上內容中還說明了一點棧的特性,就是容量具有固定大小,超過最大容量將會造成棧溢位。檢視如下程式碼:

#include<stdio.h>

int main()
{
 char arr_char[1024*1000000];
 arr_char[0] = '0';
}

以上程式碼定義了一個字元陣列 arr_char,並且設定了大小為 1024*1000000,設定該資料是方便檢視大小;隨後在陣列頭部進行賦值。執行結果如下:

一文秒懂C語言/C++記憶體管理(推薦)


這是程式執行出錯,原因是造成了棧的溢位。在平常開發中若需要大容量的記憶體,需要使用堆。

堆並沒有棧一樣的結構,也沒有棧一樣的先進後出。需要人為的對記憶體進行分配使用。程式碼如下:

#include<stdio.h>
#include<string.h>
#include <malloc.h>
int main()
{
 char *p1 = (char *)malloc(1024*1000000);
 strcpy(p1,"這裡是堆區");
 printf("%s\n",p1);
}

以上程式碼中使用了strcpy 往手動開闢的記憶體空間 p1 中傳資料“這裡是堆區”,手動開闢空間使用 malloc,傳入申請開闢的空間大小 1024*1000000,在棧中那麼大的空間必定會造成棧溢位,而堆本身就是大容量,則不會出現該情況。隨後輸出開闢的記憶體中內容,執行結果如下:

一文秒懂C語言/C++記憶體管理(推薦)


在此要注意p1是表示開闢的記憶體空間地址。

二、malloc 和 free

在 C 語言(不是 C++)中,malloc 和 free 是系統提供的函式,成對使用,用於從堆中分配和釋放記憶體。malloc 的全稱是 memory allocation 譯為“動態記憶體分配”。

2.1 malloc 和 free 的使用

在開闢堆空間時我們使用的函式為 malloc,malloc 在 C 語言中是用於申請記憶體空間,malloc 函式的原型如下:

void *malloc(size_t size);

在 malloc 函式中,size 是表示需要申請的記憶體空間大小,申請成功將會返回該記憶體空間的地址;申請失敗則會返回 NULL,並且申請成功也不會自動進行初始化。

細心的同學可能會發現,該函式的返回值說明為 void *,在這裡 void * 並不指代某一種特定的型別,而是說明該型別不確定,通過接收的指標變數從而進行型別的轉換。在分配記憶體時需要注意,即時在程式關閉時系統會自動回收該手動申請的記憶體 ,但也要進行手動的釋放,保證記憶體能夠在不需要時返回至堆空間,使記憶體能夠合理的分配使用。

釋放空間使用 free 函式,函式原型如下:

void free(void *ptr);

free 函式的返回值為 void,沒有返回值,接收的引數為使用 malloc 分配的記憶體空間指標。一個完整的堆記憶體申請與釋放的例子如下:

#include<stdio.h>
#include<string.h>
#include <malloc.h>

int main() {
 int n,*p,i;
 printf("請輸入一個任意長度的數字來分配空間:");
 scanf("%d",&n);
 
 p = (int *)malloc(n * sizeof(int));
 if(p==NULL){
 printf("申請失敗\n");
 return 0;
 }else{
 printf("申請成功\n");
 } 
 
 memset(p,n * sizeof(int));//填充0 
 
 //檢視 
 for (i = 0; i < n; i++)
  printf("%d ",p[i]);
 printf("\n");

 free(p);
 p = NULL;
 return 0;
}

以上程式碼中使用了 malloc 建立了一個由使用者輸入建立指定大小的記憶體,判斷了記憶體地址是否建立成功,且使用了 memset 函式對該記憶體空間進行了填充值,隨後使用 for 迴圈進行了檢視。最後使用了 free 釋放了記憶體,並且將 p 賦值 NULL,這點需要主要,不能使指標指向未知的地址,要置於 NULL;否則在之後的開發者會誤以為是個正常的指標,就有可能再通過指標去訪問一些操作,但是在這時該指標已經無用,指向的記憶體也不知此時被如何使用,這時若出現意外將會造成無法預估的後果,甚至導致系統崩潰,在 malloc 的使用中更需要需要。

2.2 記憶體洩漏與安全使用例項與講解

記憶體洩漏是指在動態分配的記憶體中,並沒有釋放記憶體或者一些原因造成了記憶體無法釋放,輕度則造成系統的記憶體資源浪費,嚴重的導致整個系統崩潰等情況的發生。

一文秒懂C語言/C++記憶體管理(推薦)


記憶體洩漏通常比較隱蔽,且少量的記憶體洩漏發生不一定會發生無法承受的後果,但由於該錯誤的積累將會造成整體系統的效能下降或系統崩潰。特別是在較為大型的系統中,如何有效的防止記憶體洩漏等問題的出現變得尤為重要。例如一些長時間的程式,若在執行之初有少量的記憶體洩漏的問題產生可能並未呈現,但隨著執行時間的增長、系統業務處理的增加將會累積出現記憶體洩漏這種情況;這時極大的會造成不可預知的後果,如整個系統的崩潰,造成的損失將會難以承受。由此防止記憶體洩漏對於底層開發人員來說尤為重要。

C 程式設計師在開發過程中,不可避免的面對記憶體操作的問題,特別是頻繁的申請動態記憶體時會及其容易造成記憶體洩漏事故的發生。如申請了一塊記憶體空間後,未初始化便讀其中的內容、間接申請動態記憶體但並沒有進行釋放、釋放完一塊動態申請的記憶體後繼續引用該記憶體內容;如上所述這種問題都是出現記憶體洩漏的原因,往往這些原因由於過於隱蔽在測試時不一定會完全清楚,將會導致在專案上線後的長時間執行下,導致災難性的後果發生。

如下是一個在子函式中進行了記憶體空間的申請,但是並未對其進行釋放:

#include<stdio.h>
#include<string.h>
#include <malloc.h>
void m() { 
 char *p1; 
 p1 = malloc(100); 
 printf("開始對記憶體進行洩漏...");
}
 
int main() {
 m();
 return 0;
}

如上程式碼中,使用 malloc 申請了 100 個單位的記憶體空間後,並沒有進行釋放。假設該 m 函式在當前系統中呼叫頻繁,那將會每次使用都將會造成 100 個單位的記憶體空間不會釋放,久而久之就會造成嚴重的後果。理應在 p1 使用完畢後新增 free 進行釋放:

free(p1);

以下示範一個讀取檔案時不規範的操作:

#include<stdio.h>
#include<string.h>
#include <malloc.h>
int m(char *filename) { 
 FILE* f;
 int key; 
 f = fopen(filename,"r"); 
 fscanf(f,"%d",&key); 
 return key; 
}
 
int main() {
 m("number.txt");
 return 0;
}

以上檔案在讀取時並沒有進行 fclose,這時將會產生多餘的記憶體,可能一次還好,多次會增加成倍的記憶體,可以使用迴圈進行呼叫,之後在工作管理員中可檢視該程式執行時所佔的記憶體大小,程式碼為:

#include<stdio.h>
#include<string.h>
#include <malloc.h>
int m(char *filename) { 
 FILE* f;
 int key; 
 f = fopen(filename,&key); 
 return key; 
}
 
int main() {
 int i;
 for(i=0;i<500;i++) {
  m("number.txt");
 }
 return 0;
}

可檢視新增迴圈後的程式與新增迴圈前的程式做記憶體佔用的對比,就可以發現兩者之間添加了迴圈的程式碼將會成本增加佔用容量。

未被初始化的指標也會有可能造成記憶體洩漏的情況,因為指標未初始化所指向不可控,如:

int *p;
*p = val;

包括錯誤的釋放記憶體空間:

pp=p;
free(p); 
free(pp);

釋放後使用,產生懸空指標。在申請了動態記憶體後,使用指標指向了該記憶體,使用完畢後我們通過 free 函式釋放了申請的記憶體,該記憶體將會允許其它程式進行申請;但是我們使用過後的動態記憶體指標依舊指向著該地址,假設其它程式下一秒申請了該區域內的記憶體地址,並且進行了操作。當我依舊使用已 free 釋放後的指標進行下一步的操作時,或者所進行了一個計算,那麼將會造成的結果天差地別,或者是其它災難性後果。所以對於這些指標在生存期結束之後也要置為 null。檢視一個示例,由於 free 釋放後依舊使用該指標,造成的計算結果天差地別:

#include<stdio.h>
#include<string.h>
#include <malloc.h>
int m(char *freep) { 
 int val=freep[0];
 printf("2*freep=:%d\n",val*2);
 free(freep);
 val=freep[0];
 printf("2*freep=:%d\n",val*2);
}
 
int main() {
 int *freep = (int *) malloc(sizeof (int));
 freep[0]=1;
 m(freep);
 return 0;
 
}

以上程式碼使用 malloc 申請了一個記憶體後,傳值為 1;在函式中首先使用 val 值接收 freep 的值,將 val 乘 2,之後釋放 free,重新賦值給 val,最後使用 val 再次乘 2,此時造成的結果出現了極大的改變,而且最恐怖的是該錯誤很難發現,隱蔽性很強,但是造成的後顧難以承受。執行結果如下:

一文秒懂C語言/C++記憶體管理(推薦)

三、 new 和 delete

C++ 中使用 new 和 delete 從堆中分配和釋放記憶體,new 和 delete 是運算子,不是函式,兩者成對使用(後面說明為什麼成對使用)。

new/delete 除了分配記憶體和釋放記憶體(與 malloc/free),還做更多的事情,所有在 C++ 中不再使用 malloc/free 而使用 new/delete。

3.1 new 和 delete 使用

new 一般使用格式如下:

指標變數名 = new 型別識別符號;
指標變數名 = new 型別識別符號(初始值);
指標變數名 = new 型別識別符號[記憶體單元個數];

在C++中new的三種用法包括:plain new, nothrow new 和 placement new。

plain new 就是我們最常使用的 new 的方式,在 C++ 中的定義如下:

void* operator new(std::size_t) throw(std::bad_alloc);
void operator delete( void *) throw();

plain new 在分配失敗的情況下,丟擲異常 std::bad_alloc 而不是返回 NULL,因此通過判斷返回值是否為 NULL 是徒勞的。

char *getMemory(unsigned long size) 
{ 
 char * p = new char[size]; 
 return p; 
} 
void main(void) 
{
 try{ 
  char * p = getMemory(1000000); // 可能發生異常
  // ... 
  delete [] p; 
 } 
 catch(const std::bad_alloc &amp; ex) 
 {
  cout &lt;&lt; ex.what();
 } 
}

nothrow new 是不丟擲異常的運算子new的形式。nothrow new在失敗時,返回NULL。定義如下:

void * operator new(std::size_t,const std::nothrow_t&) throw();
void operator delete(void*) throw();
void func(unsinged long length) 
{
 unsinged char * p = new(nothrow) unsinged char[length]; 
 // 在使用這種new時要加(nothrow) ,表示不使用異常處理 。
 
 if (p == NULL) // 不拋異常,一定要檢查
  cout << "allocte failed !"; 
  // ... 
 delete [] p;
}

placement new 意即“放置”,這種new允許在一塊已經分配成功的記憶體上重新構造物件或物件陣列。placement new不用擔心記憶體分配失敗,因為它根本不分配記憶體,它做的唯一一件事情就是呼叫物件的建構函式。定義如下:

void* operator new(size_t,void*);
void operator delete(void*,void*);

palcement new 的主要用途就是反覆使用一塊較大的動態分配的記憶體來構造不同型別的物件或者他們的陣列。placement new構造起來的物件或其陣列,要顯示的呼叫他們的解構函式來銷燬,千萬不要使用delete。

void main() 
{ 
 using namespace std; 
 char * p = new(nothrow) char [4]; 
 if (p == NULL) 
 {
  cout << "allocte failed" << endl; 
  exit( -1 );
 } 
 // ... 
 long * q = new (p) long(1000); 
 delete []p; // 只釋放 p,不要用q釋放。
}

p 和 q 僅僅是首址相同,所構建的物件可以型別不同。所“放置”的空間應小於原空間,以防不測。當”放置new”超過了申請的範圍,Debug 版下會崩潰,但 Release 能執行而不會出現崩潰!

該運算子的作用是:只要第一次分配成功,不再擔心分配失敗。

void main() 
{
 using namespace std; 
 char * p = new(nothrow) char [100]; 
 if (p == NULL) 
 { 
  cout << "allocte failed" << endl;
  exit(-1);
 } 
 long * q1 = new (p) long(100); 
 // 使用q1 ... 
 int * q2 = new (p) int[100/sizeof(int)]; 
 // 使用q2 ... 
 ADT * q3 = new (p) ADT[100/sizeof(ADT)]; 
 // 使用q3 然後釋放物件 ... 
 delete [] p; // 只釋放空間,不再析構物件。
}

注意:使用該運算子構造的物件或陣列,一定要顯式呼叫解構函式,不可用 delete 代替析構,因為 placement new 的物件的大小不再與原空間相同。

void main() 
{ 
 using namespace std; 
 char * p = new(nothrow) char [sizeof(ADT)+2]; 
 if (p == NULL) 
 { 
  cout << "allocte failed" &lt;&lt; endl;
  exit(-1); 
 } 
 // ... 
 ADT * q = new (p) ADT; 
 // ... 
 // delete q; // 錯誤
 q->ADT::~ADT(); // 顯式呼叫解構函式,僅釋放物件
 delete [] p;  // 最後,再用原指標來釋放記憶體
}

placement new 的主要用途就是可以反覆使用一塊已申請成功的記憶體空間。這樣可以避免申請失敗的徒勞,又可以避免使用後的釋放。

特別要注意的是對於 placement new 絕不可以呼叫的 delete,因為該 new 只是使用別人替它申請的地方。釋放記憶體是 nothrow new 的事,即要使用原來的指標釋放記憶體。free/delete 不要重複呼叫,被系統立即回收後再利用,再一次 free/delete 很可能把不是自己的記憶體釋放掉,導致異常甚至崩潰。

上面提到 new/delete 比 malloc/free 多做了一些事情,new 相對於 malloc 會額外的做一些初始化工作,delete 相對於 free 多做一些清理工作。

class A
{
 public:
  A()
  {
  cont<<"A()建構函式被呼叫"<<endl;
  }
  ~A()
  {
  cont<<"~A()建構函式被呼叫"<<endl;
  }
}

在 main 主函式中,加入如下程式碼:

A* pa = new A(); //類 A 的建構函式被呼叫
delete pa; //類 A 的解構函式被呼叫

可以看出:使用 new 生成一個類物件時系統會呼叫該類的建構函式,使用 delete 刪除一個類物件時,系統會呼叫該類的解構函式。可以呼叫建構函式/解構函式就意味著 new 和 delete 具備針對堆所分配的記憶體進行初始化和釋放的能力,而 malloc 和 free 不具備。

2.2 delete 與 delete[] 的區別

c++ 中對 new 申請的記憶體的釋放方式有 delete 和 delete[] 兩種方式,到底這兩者有什麼區別呢?

我們通常從教科書上看到這樣的說明:

  • delete 釋放 new 分配的單個物件指標指向的記憶體
  • delete[] 釋放 new 分配的物件陣列指標指向的記憶體 那麼,按照教科書的理解,我們看下下面的程式碼:
int *a = new int[10];
delete a;  //方式1
delete[] a;  //方式2

針對簡單型別 使用 new 分配後的不管是陣列還是非陣列形式記憶體空間用兩種方式均可 如:

int *a = new int[10];
delete a;
delete[] a;

此種情況中的釋放效果相同,原因在於:分配簡單型別記憶體時,記憶體大小已經確定,系統可以記憶並且進行管理,在析構時,系統並不會呼叫解構函式。

它直接通過指標可以獲取實際分配的記憶體空間,哪怕是一個數組記憶體空間(在分配過程中 系統會記錄分配記憶體的大小等資訊,此資訊儲存在結構體 _CrtMemBlockHeader 中,具體情況可參看 VC 安裝目錄下 CRTSRCDBGDEL.cpp)。

針對類 Class,兩種方式體現出具體差異

當你通過下列方式分配一個類物件陣列:

class A
 {
 private:
  char *m_cBuffer;
  int m_nLen;

 `` public:
  A(){ m_cBuffer = new char[m_nLen]; }
  ~A() { delete [] m_cBuffer; }
 };

 A *a = new A[10];
 delete a;   //僅釋放了a指標指向的全部記憶體空間 但是隻呼叫了a[0]物件的解構函式 剩下的從a[1]到a[9]這9個使用者自行分配的m_cBuffer對應記憶體空間將不能釋放 從而造成記憶體洩漏
 delete[] a;  //呼叫使用類物件的解構函式釋放使用者自己分配記憶體空間並且 釋放了a指標指向的全部記憶體空間

所以總結下就是,如果 ptr 代表一個用new申請的記憶體返回的記憶體空間地址,即所謂的指標,那麼:

delete ptr 代表用來釋放記憶體,且只用來釋放 ptr 指向的記憶體。delete[] rg 用來釋放rg指向的記憶體,!!還逐一呼叫陣列中每個物件的destructor!!

對於像 int/char/long/int*/struct 等等簡單資料型別,由於物件沒有 destructor ,所以用 delete 和 delete []是一樣的!但是如果是 C++ 物件陣列就不同了!

關於 new[] 和 delete[],其中又分為兩種情況:

(1) 為基本資料型別分配和回收空間;
(2) 為自定義型別分配和回收空間;
對於 (1),上面提供的程式已經證明了 delete[] 和 delete 是等同的。但是對於 (2),情況就發生了變化。

我們來看下面的例子,通過例子的學習瞭解 C++ 中的 delete 和 delete[] 的使用方法

#include <iostream>
using namespace std;

class Babe
{
public:
 Babe()
 {
  cout << \"Create a Babe to talk with me\" << endl;
 }

 ~Babe()
 {
  cout << \"Babe don\'t Go away,listen to me\" << endl;
 }
};

int main()
{
 Babe* pbabe = new Babe[3];
 delete pbabe;
 pbabe = new Babe[3];
 delete[] pbabe;
 return 0;
}

結果是:

Create a babe to talk with me
Create a babe to talk with me
Create a babe to talk with me
Babe don\'t go away,listen to me
Create a babe to talk with me
Create a babe to talk with me
Create a babe to talk with me
Babe don\'t go away,listen to me
Babe don\'t go away,listen to me

大家都看到了,只使用 delete 的時候只出現一個 Babe don't go away,listen to me,而使用 delete[] 的時候出現 3 個 Babe don't go away,listen to me。不過不管使用 delete 還是 delete[] 那三個物件的在記憶體中都被刪除,既儲存位置都標記為可寫,但是使用 delete 的時候只調用了 pbabe[0] 的解構函式,而使用了 delete[] 則呼叫了 3 個 Babe 物件的解構函式。

你一定會問,反正不管怎樣都是把儲存空間釋放了,有什麼區別。

答:關鍵在於呼叫解構函式上。此程式的類沒有使用作業系統的系統資源(比如:Socket、File、Thread等),所以不會造成明顯惡果。如果你的類使用了作業系統資源,單純把類的物件從記憶體中刪除是不妥當的,因為沒有呼叫物件的解構函式會導致系統資源不被釋放,這些資源的釋放必須依靠這些類的解構函式。所以,在用這些類生成物件陣列的時候,用 delete[] 來釋放它們才是王道。而用 delete 來釋放也許不會出問題,也許後果很嚴重,具體要看類的程式碼了。

到此這篇關於一文秒懂C語言/C++記憶體管理的文章就介紹到這了,更多相關C++記憶體管理內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!