1. 程式人生 > >C++指標 陣列 記憶體釋放

C++指標 陣列 記憶體釋放

尊重原創,原文出處:http://blog.163.com/[email protected]/blog/static/926673832009751923282/

和其它變數一樣,指標是基本的變數,所不同的是指標包含一個實際的資料,該資料代表一個可以找到實際資訊的記憶體地址。這是一個非常重要的概念。許多程式和思想依靠指標作為他們設計的基礎。

開始

       怎樣定義一個指標呢?除了你需要在變數的名稱前面加一個星號外,其它的和別的變數定義一樣。舉個例子,以下程式碼定義了兩個指標變數,它們都指向一個整數。

int* pNumberOne;

int* pNumberTwo;

注意到兩個變數名稱前的字首’p’了麼?這是一個慣例,用來表示這個變數是個指標。

       現在,讓我們將這些指標實際的指向某些東西:

pNumberOne = &some_number;

pNumberTwo = &some_other_number;

‘&’符號應該讀作”什麼什麼的地址”,它返回一個變數在記憶體中的地址,設定到左側的變數中。因此,在這個例子中,pNumberOne設定和some_number的地址相同,因此pNumberOne現在指向some_number。

       現在,如果我們想訪問some_number的地址,可以使用pNumberOne。如果我們想通過pNumberOne訪問some_number的值,那麼應該用*pNumberOne。這個星號表示解除指標的參照,應該讀作“什麼什麼指向的記憶體區域”。

到現在我們學到了什麼?舉個例子

喲,有許多東西需要理解。我的建議是,如果你有哪個概念沒有弄清楚的話,那麼,不妨再看一遍。指標是個複雜的物件,可能需要花費一段時間來掌握它。

這兒有一個例子示範上面所將的概念。這是用C寫的,沒有C++擴充套件。

#i nclude <stdio.h>

void main()

{

    // 申明變數

    int nNumber;

    int *pPointer;

    //賦值

    nNumber = 15;

    pPointer = &nNumber;

    // 輸出nNumber的值

    printf("nNumber is equal to : %d\n", nNumber);

    // 通過pPointer修改nNumber的值

    *pPointer = 25;

// 證明nNumber已經被改變了

// 再次列印nNumber的值

    printf("nNumber is equal to : %d\n", nNumber);

}

       通讀一遍,並且編譯樣例程式碼,確信你理解了它為什麼這樣工作。如果你準備好了,那麼繼續。

一個陷阱!

看看你能否發現下面這段程式的毛病:

#i nclude <stdio.h>

int *pPointer;

void SomeFunction();

{

    int nNumber;

    nNumber = 25;   

    //將pPointer指向nNumber

    pPointer = &nNumber;

}

void main()

{

    SomeFunction(); //用pPointer做些事情

    // 為什麼會失敗?

    printf("Value of *pPointer: %d\n", *pPointer);

}

這段程式先呼叫SomeFunction函式,該函式建立一個叫做nNumber的變數,並將pPointer指向它。那麼,問題是,當函式退出時,nNumber被刪除了,因為它是一個區域性變數。當程式執行到區域性變數定義的程式塊以外時,區域性變數總是被刪除了。這就意味著,當SomeFunction函式返回到main函式時,區域性變數將被刪除,因此pPointer將指向原先nNumber的地址,但這個地址已經不再屬於這段程式了。如果你不理解這些,那麼重新閱讀一遍關於區域性變數和全域性變數的作用範圍是明智的選擇。這個概念也是非常重要的。

       那麼,我們如何解決這個問題呢?答案是使用大家都知道的一個方法:動態分配。請明白C和C++的動態分配是不同的。既然現在大多數程式設計師都使用C++,那麼下面這段程式碼就是常用的了。

動態分配

       動態分配可以說是指標的關鍵所在。不需要通過定義變數,就可以將指標指向分配的記憶體。也許這個概念看起來比較模糊,但是確實比較簡單。下面的程式碼示範如何為一個整數分配記憶體:

int *pNumber;

pNumber = new int;

       第一行申明瞭一個指標pNumber,第二行分配一個整數記憶體,並且將pNumber指向這個新記憶體。下面是另一個例子,這次用一個浮點數:

double *pDouble;

pDouble = new double;

       動態分配有什麼不同的呢?當函式返回或者程式執行到當前塊以外時,你動態分配的記憶體將不會被刪除。因此,如果我們用動態分配重寫上面的例子,可以看到現在能夠正常工作了。

#i nclude <stdio.h>

int *pPointer;

void SomeFunction()

{

    // make pPointer point to a new integer

    pPointer = new int;

    *pPointer = 25;

}

void main()

{

    SomeFunction(); // make pPointer point to something

    printf("Value of *pPointer: %d\n", *pPointer);

}

       通讀一遍,編譯上面的程式碼,確信你已經理解它是如何工作的。當呼叫SomeFunction時,分配了一些記憶體,並且用pPointer指向它。這次,當函式返回時,新記憶體就完整無缺了。因此pPointer仍舊指向有用的東西。這是因為使用了動態分配。確信你已經理解它了。那麼繼續向下看,瞭解為什麼上面的程式還會有一系列的錯誤。

記憶體分配和記憶體釋放

這裡有一個問題,可能會變得十分嚴重,雖然它很容易補救。這個問題就是,雖然你用動態分配可以方便的讓記憶體完整無缺,確實不會自動刪除,除非你告訴計算機,你不再需要這塊記憶體了,否則記憶體將一直被分配著。因此結果就是,如果你不告訴計算機你已經使用完這塊記憶體,那麼它將成為被浪費的空間,因為其它程式或者你的應用程式的其它部分不能使用這塊記憶體。最終將導致系統因為記憶體耗盡而崩潰。因此這個問題相當重要。記憶體使用完後釋放非常容易:

delete pPointer;

       需要做的就是這些。但是你必須確定,你刪除的是一個指向你實際分配的記憶體的指標,而不是其它任何垃圾。嘗試用delete已經釋放的記憶體是危險的,並且可能導致程式崩潰。

       這裡再次舉個例子,這次修改以後就不會有記憶體浪費了。

#i nclude <stdio.h>

int *pPointer;

void SomeFunction()

{

// make pPointer point to a new integer

    pPointer = new int;

    *pPointer = 25;

}

void main()

{

    SomeFunction(); // make pPointer point to something

    printf("Value of *pPointer: %d\n", *pPointer);

    delete pPointer;

}

只有一行不同,但這行是要點。如果你不刪除記憶體,就會導致“記憶體洩漏”,記憶體將逐漸減少,除非應用程式重新啟動,否則將不能再生。

向函式傳遞指標

傳遞指標給函式非常有用,但不容易掌握。如果我們寫一個程式,傳遞一個數值並且給它加上5,我們也許會寫出如下的程式:

#i nclude <stdio.h>

void AddFive(int Number)

{

    Number = Number + 5;

}

void main()

{

    int nMyNumber = 18;

    printf("My original number is %d\n", nMyNumber);

    AddFive(nMyNumber);

    printf("My new number is %d\n", nMyNumber);

}

但是,程式中函式AddFive的引數Number只是變數nMyNumber的一個拷貝,而不是變數本身,因此,Number = Number + 5只是為變數的拷貝增加了5,而不是最初的在main()函式中的變數。當然,你可以執行程式,以證明這一點。

       為了將值傳遞出去,我們可以傳遞這個變數的指標到函式中,但我們需要修改一下函式,以便傳遞數值的指標而不是數值。因此將void AddFive(int Number)修改為void AddFive(int *Number),增加了一個星號。下面是修改了的函式,注意,我們必須確認傳遞了nMyNumber的地址,而不是它本身。這通過增加&符號來完成,通常讀作“什麼什麼的地址”。

#i nclude <stdio.h>

void AddFive(int* Number)

{

    *Number = *Number + 5;

}

void main()

{

    int nMyNumber = 18;

    printf("My original number is %d\n", nMyNumber);

    AddFive(&nMyNumber);

    printf("My new number is %d\n", nMyNumber);

}

大家可以試著自己做個例子來實驗一下。注意在AddFive函式中Number變數前那個重要的星號。只是必須的,用來告訴編譯器我們想將5加到變數Number指向的數值,而不是將5加到指標本身。

關於函式最後需要注意的是你也可以返回一個指標。比如:

int * MyFunction();

       在這個例子中,MyFunction函式返回一個指向整數的指標。

類的指標

關於指標還有兩個需要注意的問題。其中一個是結構或者類。你可以如下定義一個類:

class MyClass

{

public:

    int m_Number;

    char m_Character;

};

然後,你可以如下方式定義一個類變數:

MyClass thing;

       你應該已經知道這些了,如果還不知道的話,那麼再將上面的內容讀一遍。定義MyClass的指標應該這麼寫:

MyClass *thing;

       然後你需要分配記憶體,並將指標指向這個記憶體

       thing = new MyClass;

       問題來了,你如何使用這個指標呢?一般的,我們寫thing.m_Number,但你不能對指標用’.’操作,因為thing不是一個MyClass物件。只是指向一個MyClass物件的指標。因此,指標thing不包含m_Number這個變數。只是它指向的結構中包含這個變數。因此,我們必須使用一個不同的協定,用->取代’.’。以下是一個例子:

class MyClass

{

public:

    int m_Number;

    char m_Character;

};

void main()

{

    MyClass *pPointer;

    pPointer = new MyClass;

    pPointer->m_Number = 10;

    pPointer->m_Character = 's';

    delete pPointer;

}

陣列的指標

你也可以構造一個指向陣列的指標,如下:

int *pArray;

pArray = new int[6];

       將建立一個叫做pArray的指標,指向一個包含6個元素的陣列。另一種構造的方法是使用動態分配,如下:

int *pArray;

int MyArray[6];

pArray = &MyArray[0];

       注意,你這裡也可以不用&MyArray[0],而直接使用&MyArray取代。當然,這僅僅適用於陣列。

使用指向陣列的指標

       一旦你有了指向陣列的指標,那麼如何使用它呢?現在假設你有一個指向整數陣列的指標,那麼指標開始時將指向第一個整數。舉例如下:

#i nclude <stdio.h>

void main()

{

    int Array[3];

    Array[0] = 10;

    Array[1] = 20;

    Array[2] = 30;

    int *pArray;

    pArray = &Array[0];

    printf("pArray points to the value %d\n", *pArray);

}

將指標移到指向陣列的下一個值,可以用pArray++。也許你也可以猜出來了,我們可以用pArray+2的方式將指標向後移動兩個位置。要注意的問題是,你自己必須知道陣列的上限是多少(例子中是3),因為編譯器不能檢查你是否將指標移到了陣列以外,因此你可以很容易的將系統搞崩潰了。以下是個例子,顯示我們設定的三個值:

#i nclude <stdio.h>

void main()

{

    int Array[3];

    Array[0] = 10;

    Array[1] = 20;

    Array[2] = 30;

    int *pArray;

    pArray = &Array[0];

    printf("pArray points to the value %d\n", *pArray);

    pArray++;

    printf("pArray points to the value %d\n", *pArray);

    pArray++;

    printf("pArray points to the value %d\n", *pArray);

}

你也可以使用pArray-2這樣的方式來向前移動2個位置。不管是加或者減,你必須保證不是對指標所指向的資料的操作。這種操作指標和陣列的方式在迴圈中是最常用的。例如for和while迴圈。

       另外要提的是,如果你有一個指標比如int pNumberSet,你也可以把它當成陣列。例如pNumberSet[0]等於*pNumberSet,並且pNumberSet[1]等於*(pNumberSet+1)。

       對於陣列,還有一點要注意的,如果你用new為陣列分配記憶體,比如:

int *pArray;

pArray = new int[6];

你必須用以下方式進行刪除:

delete[] pArray;

注意delete後的[],它告訴編譯器,這是刪除整個陣列,而不僅僅是第一個元素。對於陣列你必須使用這種方法,否則就會有記憶體洩漏。

總結

一條要注意的:你不能刪除不是用new分配的記憶體。比如以下例子:

void main()

{

    int number;

    int *pNumber = number;

    delete pNumber; // wrong - *pNumber wasn't allocated using new.

}

一、相關概念

1.堆物件:在程式執行過程中根據需要隨時可以建立或刪除的物件。這種堆物件被建立在記憶體一些空閒儲存單元中,這些儲存單元被稱為堆。它們可以被建立的堆物件佔有,也可以通過刪除堆物件而獲得釋放。

2.記憶體洩露:通常所指的記憶體洩漏是指堆記憶體的洩漏。堆記憶體是指程式從堆中分配的,大小任意的,使用完後必須顯示釋放的記憶體。應用程式一般使用malloc(),realloc(),new等函式從堆中分配到一塊記憶體,使用完後,程式必須負責相應的呼叫free()或delete釋放該記憶體塊,否則,這塊記憶體就不能被再次使用,我們就說這塊記憶體洩漏了。

我們在程式除錯或執行的過程中,有時會突然彈出一個對話方塊,顯示資訊:

User breakpointcalled from code at 0x77fa018c

或者DAMAGE: after Normal block(#63) at0x003B3FE0

或者Unhandled exception at 0x77f767cd(ntdll.dll) in myapp.exe: Userbreakpoint.

則意味著我們需要對記憶體的分配和刪除指令做一個仔細全面的檢查了!

3.new:用來動態地建立堆物件,相當於C語言中的malloc()函式。

指標變數名=new 型別名[下標表達式];

aa=new float[100];

4.delete:用來刪除使用new建立的物件或一般型別的指標,相當於C語言中的free()函式。

delete []指標變數名;

delete []aa;

如果aa不是指向陣列而是指向一個普通物件或者變數,則可以直接delete aa;不用加 [ ]。

注意:方括號非常重要的,如果delete語句中少了方括號,因編譯器認為該指標是指向陣列第一個元素的,會產生回收不徹底的問題(只回收了第一個元素所佔空間),我們通常叫它“記憶體洩露”,加了方括號後就轉化為指向陣列的指標,回收整個陣列。delete []的方括號中不需要填陣列元素數,系統自知。即使寫了,編譯器也忽略。<<Thinkin c++>>上說以前的delete []方括號中是必須添加個數的,後來由於很容易出錯,所以後來的版本就改進了這個缺陷,不需要指定回收的陣列元素個數。

動態分配記憶體空間是有顯著好處的:僅在使用者使用相關功能時才分配記憶體,避免不必要的記憶體開銷。

二、用法與注意事項

1.指標變數或物件的初始化

aa=NULL;

在定義aa的類的建構函式中,把它賦為空指標,用來判斷使用者在使用過程中是否分配過堆記憶體給它。

2.指標變數或物件的釋放

if(aa)

    delete []aa;

釋放之前需要用if語句判斷程式執行時是否分配過記憶體給aa,直接delete []aa;是危險的!釋放一個沒有建立過的記憶體空間,必然會報錯。

3.動態分配的指標變數或物件的生存期

指標變數或物件一經分配,便不會自動釋放,一定要在程式退出之前手動使用delete釋放之,否則便會導致經典的記憶體洩露事件。釋放的原則就是用完就刪!至於何時用完,需要程式編寫者自行判斷。一般來說,全域性變數可能在很多函式成員中使用,建議在類的解構函式中釋放;區域性變數則必須在定義它的函式的最後釋放。

4.不正當的釋放

同一空間重複釋放也是危險的,因為該空間可能已另有分配,而這個時候又去釋放的話,程式會報錯。

有種情況很頭疼,釋放的位置正確,也沒有重複釋放,仍然報告記憶體訪問溢位的錯誤。此時極有可能是陣列的使用錯誤,即程式中實際使用的陣列元素總數超過了使用者的分配值。應該仔細檢查使用過程中陣列的下標最大值是否超過了動態分配值,保險的做法是分配一些多餘空間給陣列。