看完這篇你還能不懂C語言/C++內存管理?

C 語言內存管理指對系統內存的分配、建立、使用這一系列操做。在內存管理中,因爲是操做系統內存,使用不當會形成畢竟麻煩的結果。本文將從系統內存的分配、建立出發,而且使用例子來舉例說明內存管理不當會出現的狀況及解決辦法。ios

1、內存

在計算機中,每一個應用程序之間的內存是相互獨立的,一般狀況下應用程序 A 並不能訪問應用程序 B,固然一些特殊技巧能夠訪問,但此文並不詳細進行說明。例如在計算機中,一個視頻播放程序與一個瀏覽器程序,它們的內存並不能訪問,每一個程序所擁有的內存是分區進行管理的。c++

在計算機系統中,運行程序 A 將會在內存中開闢程序 A 的內存區域 1,運行程序 B 將會在內存中開闢程序 B 的內存區域 2,內存區域 1 與內存區域 2 之間邏輯分隔。程序員

1.1 內存四區

在程序 A 開闢的內存區域 1 會被分爲幾個區域,這就是內存四區,內存四區分爲棧區、堆區、數據區與代碼區。web

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

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

數據區指的是主要存放全局變量、常量和靜態變量的區域,數據區又能夠進行劃分,分爲全局區與靜態區。全局變量與靜態變量將會存放至該區域。瀏覽器

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

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);

}

運行結果爲:編輯器

咱們能夠觀察到變量 a 的地址是 2293324 變量 b 的地址是 2293320,因爲 int 的數據大小爲 4 因此二者之間間隔爲 4;再查看變量 c,咱們發現變量 c 的地址爲 2293319,與變量 b 的地址 2293324 間隔 1,由於 c 的數據類型爲 char,類型大小爲 1。在此咱們觀察發現,明明我建立變量的時候順序是 a 到 b 再到 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", &a, &b, &c);
 
 printf("靜態變量d的地址是:%d\n", &d);

}

運行結果以下:

以上代碼中建立了一個變量 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", &a, &b, &c);
 
 printf("靜態變量d的地址是:%d\n", &d);
 printf("全局變量e的地址是:%d\n", &e);

}

運行結果以下:

從以上運行結果中證明了上述內容的真實性,而且也獲得了一個知識點,棧區、數據區都是使用棧結構對數據進行存儲。

在以上內容中還說明了一點棧的特性,就是容量具備固定大小,超過最大容量將會形成棧溢出。查看以下代碼:

#include<stdio.h>

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

以上代碼定義了一個字符數組 arr_char,而且設置了大小爲 1024*1000000,設置該數據是方便查看大小;隨後在數組頭部進行賦值。運行結果以下:

這是程序運行出錯,緣由是形成了棧的溢出。在日常開發中若須要大容量的內存,須要使用堆。

堆並無棧同樣的結構,也沒有棧同樣的先進後出。須要人爲的對內存進行分配使用。代碼以下:

#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,在棧中那麼大的空間一定會形成棧溢出,而堆自己就是大容量,則不會出現該狀況。隨後輸出開闢的內存中內容,運行結果以下:

在此要注意p1是表示開闢的內存空間地址。

2、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, 0, 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 程序員在開發過程當中,不可避免的面對內存操做的問題,特別是頻繁的申請動態內存時會及其容易形成內存泄漏事故的發生。如申請了一塊內存空間後,未初始化便讀其中的內容、間接申請動態內存但並無進行釋放、釋放完一塊動態申請的內存後繼續引用該內存內容;如上所述這種問題都是出現內存泄漏的緣由,每每這些緣由因爲過於隱蔽在測試時不必定會徹底清楚,將會致使在項目上線後的長時間運行下,致使災難性的後果發生。

以下是一個在子函數中進行了內存空間的申請,可是並未對其進行釋放:

#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, "r"); 
 fscanf(f, "%d", &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,此時形成的結果出現了極大的改變,並且最恐怖的是該錯誤很難發現,隱蔽性很強,可是形成的後顧難以承受。運行結果以下:

3、 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
  1. 針對簡單類型 使用 new 分配後的無論是數組仍是非數組形式內存空間用兩種方式都可 如:
int *a = new int[10];
delete a;
delete[] a;

此種狀況中的釋放效果相同,緣由在於:分配簡單類型內存時,內存大小已經肯定,系統能夠記憶而且進行管理,在析構時,系統並不會調用析構函數。

它直接經過指針能夠獲取實際分配的內存空間,哪怕是一個數組內存空間(在分配過程當中 系統會記錄分配內存的大小等信息,此信息保存在結構體 _CrtMemBlockHeader 中,具體狀況可參看 VC 安裝目錄下 CRTSRCDBGDEL.cpp)。

  1. 針對類 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
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語言與CPP編程(cwdushu)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索