指標入門指導 — A Beginner’s guide to Pointers

NO IMAGE

A Beginner’s guide to Pointers

這是我翻譯的一篇文章,主要向初學者介紹C/C 的靈魂——指標。

原作者是:Andrew Peace

原文連結:http://www.codeproject.com/cpp/pointers.asp
 
What are Pointers?
指標是什麼?

基本上,指標同其他變數是一樣的.只是,它們的不同之處在於:其它變數包含實際的資料,而指標包含一個指示器,這個指示器指向一塊能夠找到資訊的記憶體區域.這是一個非常重要的概念,許多程式和思想依賴於指標把指標作為設計的基礎,比如連結串列.
 
Getting Started

我如何定義一個指標?Well, 像定義其它變數一樣, 只是需要在它的名字前加一個星號(*).例如,下面的程式碼建立了兩個指標, 它們都指向一個整型.
 

    int* pNumberOne;    
    int* pNumberTwo;     

注意到兩個變數名前的字首 ‘p’了嗎?這是一個習慣的用法, 指出一個變數是一個指標.
現在,讓這些指標實際地指向一些東西:
& 標記應當讀作”…的地址”( ‘the address of’),因為得到了一個變數的儲存區域的地址,而不是變數本身.所以,在這個例子裡, pNumberOne 被設定為等於some_number的地址, pNumberOne現在指向some_number.   

    pNumberOne = &some_number;
    pNumberTwo = &some_other_number; 

What we’ve learnt so far: an example:  

   
Phew! 有許多需要注意的地方,我建議如果你沒有理解這些概念,你應當再讀一次. 指標是一個複雜的主題,需要花一段時間才能掌握.  

這兒是一個例子,示範上面討論的一些概念思想.它是用C語言寫的, 不是C (C的擴充).

 

 

#include <stdio.h>

void main()
…{
    // 宣告變數:
    int nNumber;
    int *pPointer;

    // 現在, 給變數賦值:
    nNumber = 15;
    pPointer = &nNumber;

    // 輸出 :
    printf(“nNumber is equal to : %d “, nNumber);

    // 現在, 通過pPointer 修改nNumber:
    *pPointer = 25;

    // 再次輸出nNumber的值,證明nNumber的值被改變:
    printf(“nNumber is equal to : %d “, nNumber);
}

通讀上面的程式碼示例,並編譯, 確定你理解它是如何工作的.然後, 準備好,繼續!

A trap!

看看你能不能找出下面程式的錯誤:
 

#include <stdio.h>

int *pPointer;

void SomeFunction();
…{
    int nNumber;
    nNumber = 25;    

    // 使pPointer 指向 to nNumber:
    pPointer = &nNumber;
}

void main()
…{
    SomeFunction(); // 使pPointer 指向 某個東西

    // 為什麼失敗?
    printf(“Value of *pPointer: %d “, *pPointer);

 

這段程式首先呼叫SomeFunction()函式,SomeFunction()首先建立一個名叫nNumber的變數,然後使pPointer指向它.然後,但是,問題出現了.當這個函式執行完畢,nNumber被釋放了,因為它是一個區域性變數.當程式的執行離開定義區域性變數的塊時,區域性變數總是被釋放.這意味著,當SomeFunction()返回main()時,這個變數被釋放了, 所以pPointer 指向的地方過去屬於,但是以後不再屬於這個程式.如果你不理解這個, 可以回去複習一下關於區域性變數和全域性變數的知識,以及作用域.這個概念同樣重要.

那麼如何結局問題?答案是, 使用一個叫做動態分配(dynamic allocation)的技術.請首先明白C和C 的不同.因為多數開發人員現在使用C , 下面使用的程式碼是C 的一類.

動態分配(dynamic allocation)

動態分配或許是指標的關鍵.它用來分配(申請)記憶體, 不必定義一個變數並使指標指向它.儘管這個概念容易產生混淆,它實際上很簡單.下面的程式碼演示如何為整型分配記憶體.

int *pNumber;
pNumber = new int;

 

第一行宣告瞭一個變數, pNumber.然後,第二行為一個整型分配了記憶體,並使pNumber指向這塊記憶體.這裡是另一個例子,這次使用 double:
   

double *pDouble;
pDouble = new double;

 

The formula is the same every time, so you can’t really fail with this bit.

動態分配的特點, 你分配的這塊記憶體不會在函式返回或作用域結束的時候被釋放.所以,如果我們使用動態分配重寫上面的例子,可以看到它正常工作:
   

#include <stdio.h>

int *pPointer;

void SomeFunction()
…{
    // make Pointer point to a new integer
    pPointer = new int;
    *pPointer = 25;
}

void main()
…{
    SomeFunction(); // make pPointer point to something
    printf(“Value of *pPointer: %d “, *pPointer);
}

 

通讀上面的程式碼示例,並編譯, 確定你理解它是如何工作的.當SomeFunction 被呼叫,它分配了一些記憶體.並使pPointer指向它.這次,當函式返回時,新的記憶體仍然是完整的, 所以pPointer還是指向有用的東西.這使用了動態分配!一定要理解這個, 並繼續往下讀來學習更多的技巧和指導上面程式碼的一個嚴重錯誤.  

Memory comes, memory goes

這兒有一個併發症,並且會變得非常嚴重,儘管它很容易補救.這個問題是,儘管你使用動態分配得到的記憶體仍然完整,但是它沒有被自動的釋放.就是,記憶體始終被佔用,直到你告訴計算機你不再使用它.結果是,如果你不告訴計算機你不再使用這塊記憶體, 將浪費空間.最後將導致你的系統因記憶體耗盡而崩潰, 所以它非常重要.當你不再使用它時, 釋放是簡單的:

    delete pPointer;

這就是所有需要做的.你一定要小心, 確保傳遞一個有效的指標,也就是它確實指向你分配的某塊記憶體, 而不是任何的垃圾.試圖 delete 一塊已經釋放的記憶體是危險的,可能導致程式崩潰.

現在重新寫出例子, 這次不會浪費任何記憶體:
   

#include <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 “, *pPointer);

    delete pPointer;
}

 

有一行不同,但是這一行是精華!如果你不釋放記憶體, 將產生 記憶體洩露(‘memory leak’) ,記憶體空間逐漸的被洩露, 並且不會被重新利用指導應用程式關閉.

Passing pointers to functions

向函式傳遞指標的能力是非常有用的,但是很容易掌握.如果我們要做一個程式, 得到一個數, 並把這個數加5 , 可能編寫類似下面的程式碼:

#include <stdio.h>

void AddFive(int Number)
…{
    Number = Number   5;
}

void main()
…{
    int nMyNumber = 18;
    
    printf(“My original number is %d “, nMyNumber);
    AddFive(nMyNumber);
    printf(“My new number is %d “, nMyNumber);

 

然而, 問題在於 AddFive 引用的 Number 是 傳遞給函式的變數 nMyNumber 的副本(拷貝), 不是變數本身 .因此, 這行 ‘Number = Number 5’, 對變數的副本加5, 而原來在main()的變數沒有變化.試著執行以下程式, 證明這個問題.

要解決這個問題, 我們可以傳遞一個指向保持在記憶體裡的數的指標給函式, 但是我們必須修改函式使它接受指向一個數的指標, 而不是一個數.所以,我們把 void AddFive(int Number)改成void AddFive(int* Number), 加上星號.列出修改後的程式.注意我們是否確實傳遞了nMyNumber的地址而不是它本身? 這是通過加上&記號完成的, 你應當把它讀作”…的地址” (‘the address of’).

試著寫出你自己的程式,驗證一下.注意在AddFive函式裡的Number前面的星號的重要性了嗎?這是告訴編譯器我們要給一個數加5, 變數Number指向這個數,而不是給指標本身加5.

#include <stdio.h>
void AddFive(int* Number)
…{
    *Number = *Number   5;
}

void main()
…{
    int nMyNumber = 18;
    
    printf(“My original number is %d “, nMyNumber);
    AddFive(&nMyNumber);
    printf(“My new number is %d “, nMyNumber);
}

最後注意一下這個函式,你可以從函式返回指標:

 int * MyFunction();

在這個例子裡, MyFunction返回指向整數的指標.

Pointers to Classes

這裡還有有幾個關於指標的告誡, 一個是關於結構體和類.你可以像下面一樣定義一個類:

 

class MyClass
…{
public:
    int m_Number;
    char m_Character;
};

 

然後, 可以像下面一樣定義一個MyClass型別的變數:

MyClass thing;

 

你應當已經知道這個, 如果不知道應該重新讀一下上面的內容.定義指向MyClass的指標: 

MyClass *thing; 

 

像你預見的一樣.你可以分配一些記憶體,並使這個指標指向這塊記憶體:
 

thing = new MyClass; 

 

又有一個問題, 你應當怎樣使用這個指標.Well, 一般情況下,你像這樣寫’thing.m_Number’,但是你不能這樣使用一個指標因為 thing 不是一個MyClass, 而是一個指向MyClass的指標,所以thing本身不包含一個叫做德變數m_Number;是指標指向的內容包含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;
}

Pointers to Arrays

你也可以是指標指向陣列.像下面這樣寫:

int *pArray;
pArray = new int[6];

 

這將建立一個指標,pArray, 並且使它指向有六個元素的陣列.有另外的方法, 不使用動態分配,像下下面這樣:

int *pArray;
int MyArray[6];
pArray = &MyArray[0]; 

 

注意, 你可以簡單的寫MyArray, 而不是&MyArray[0].當然, 這個僅僅用於陣列, 是C/C 語言實現陣列方法的結果.一個常見的缺陷是,這樣寫: pArray = &MyArray;但這是錯誤的.如果你這樣寫, 將得到指向一個陣列的指標的指標(確實很拗口), 這不是你想要的.

Using pointers to arrays

一旦有一個指向陣列的指標, 應當如何使用它?Well, 有一個指向 int型陣列的指標.指標將初始指向陣列的第一個值, 像下面的示例:

 

 

#include <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 “, *pArray);
}

為了使指標移動到陣列裡的下一個值, 我們可以用pArray .就像你能夠猜到的一樣,我們也可以 pArray 2, 這將把指標移動兩個元素.小心, 有需要指導這個陣列的上界(在這個例子裡是 3), 因為使用指標時編譯器不會檢查越過陣列的邊界, 所以你很容易讓系統崩潰.在下面的例子裡, 注意我們設定溝娜 鮒? 
 

#include <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 “, *pArray);
    pArray ;
    printf(“pArray points to the value %d “, *pArray);
    pArray ;
    printf(“pArray points to the value %d “, *pArray);

 

你也可以使用減, pArray – 2, 指標pArray從當前位置移動了兩個元素.但是,確定你是對指標進行加減而不是它的值.這類使用指標和陣列的操作在迴圈時有很大的作用, 比如forwhile迴圈.

再注意一點, 如果你有一個指向某個值的指標, 如int* pNumberSet, 你可以把它當作一個陣列, 例如pNumberSet[0]*pNumberSet相等, pNumberSet[1]*(pNumberSet 1)相等.
最後關於陣列的警告是, 如果你使用new為陣列動態分配了記憶體空間:

int *pArray;
pArray = new int[6];

你必須用下面的方法釋放記憶體:

delete[] pArray;

注意delete後面的[] ! 這告訴編譯器, 釋放整個陣列,而不是一個元素. 無論如何你必須使用這種方法, 否則將導致記憶體洩露.

Last Words

最後要注意:沒有使用 new 分配的記憶體, 你一定不要把它釋放, 就像下面這樣:

void main()
…{
    int number;
    int *pNumber = number;
    
    delete pNumber; // wrong – *pNumber wasn’t allocated using new.
}

Common Questions and FAQ

Q:在newdelete上, 為什麼出現(沒有定義的標誌符)’symbol undefined’ 的錯誤?
A:這最可能是因為你的原始檔被編譯器看作是通常的 C 檔案, 而newdelete運算子是C 的新特性.通常的解決辦法是保證你的原始檔使用 .cpp副檔名.

Q: newmalloc 有什麼不同?
A: new是一個僅在C 中出現的關鍵字, 並且現在是分配記憶體的標準方法.你不應當在C 應用程式中再使用malloc, 除非確實需要.因為 malloc 不是為C 的物件導向特性設計的, 對一個類使用malloc分配記憶體時將阻止呼叫這個類的建構函式, 這將產生一個問題程式.

Q:我能 一塊使用freedelete嗎?
A:你應當使用與分配方式對應的方式來釋放記憶體.例如,使用free釋放malloc分配的記憶體, delete僅僅用於 new分配的記憶體.

References – 引用

引用,在某種程度上超出了本文的範圍.但是,讀這些內容的人經常問我, 我有必要簡單的討論一下.如果你回憶一下上面的內容, 我說與運算子(&) 讀作’…的地址’, 除非在宣告裡.當它出現在宣告裡的情況下, 像下面的一樣, 它應當被讀作’…的引用'(‘a reference to’)

int& Number = myOtherNumber;
Number = 25;

這個引用像一個指向myOtherNumber的指標, 處了它會自動的被解除引用(‘dereferenced’), 所以它的行為就像一個實際的值型別而不是一個指標.使用指標的等價程式碼如下:

int* pNumber = &myOtherNumber;
*pNumber = 25;

指標和引用的另一個不同是, 你不能重置(‘reseat’)一個引用, 就是說宣告一個引用後你不能修改它的指向. 例如, 下面的程式碼將輸出 ’20’:

int myFirstNumber = 25;
int mySecondNumber = 20;
int &myReference = myFirstNumber;

myReference = mySecondNumber;

printf(” %d”, myFristNumber);

 

在類裡,引用的值必須在類的建構函式裡設定, 使用如下的方式:

CMyClass::CMyClass(int &variable) : m_MyReferenceInCMyClass(variable)
…{
    // constructor code here
}

 

Summary
開始,這個主題很難掌握,所以很有必要仔細看至少兩次:大多數人不會馬上理解.下面要點:
  1.指標是一個變數,這個變數指向一塊記憶體區域.要定義一個指標,在變數名前加個星號(*), 比如:int *number.
  2.你可以通過在變數前加個&,得到任何變數的地址, 如:pNumber = &my_number.
  3.不是用在宣告語句裡的星號(*), 應當讀作 ‘the memory location pointed to by’.
  4.不用再宣告語句裡的&應當讀作’…的地址'(‘the address of’).
  5.你可以使用new運算子分配記憶體
  6.指標必須與你想要這個指標指向的變數有相同的型別, 所以int *number 不能指向一個MyClass.
  7.你可以把指標傳遞給函式.
  8.你必須使用’delete‘關鍵字釋放分配的記憶體.
  9.你可以使用&array[0]之指標指向一個已經存在的陣列.
  10.要釋放使用動態分配方式得到的陣列, 必須使用delete[],而不是delete.

這不是絕對完整的指標使用指導, 這裡涉及了少數指標更詳細的細節, 如指向指標的指標; 還有一個東西這裡根本沒有涉及到, 如指向函式的指標,我認為這些內容對這篇面向初學者的文章來說有點複雜.  

就這些!試著執行一下這裡寫出的程式, 並寫出一些你自己的東西.