文章來源:pconline 作者: titilima(翻譯)
原作者:Andrew Peace
原文連結:http://www.codeproject.com/cpp/pointers.asp
配套原始碼:http://www.codeproject.com/cpp/pointers/pointers.zip
何為指標?
指標基本上和其它的變數一樣,唯一的一點不同就是指標並不包含實際的資料,而是包含了一個指向記憶體位置的地址,你可以在這個地址找到某些資訊。這是一個很重要的概念,並且許多程式或者思想都是將指標作為它們的設計基礎,例如鏈表。
開始
如何定義一個指標?呃,就像定義其它的變數一樣,不過你還需要在變數名之前添加一個星號。例如,下面的代碼建立了兩個指向整數的指標:
int* pNumberOne;
int* pNumberTwo;
注意到變數名的首碼“p”了嗎?這是編寫代碼的一個習慣,用來表示這個變數是一個指標。
現在,讓我們把這些指標指向一些實際的值吧:
pNumberOne = &some_number;
pNumberTwo = &some_other_number;
“&”標誌應該讀作“the address of(……的地址)”,它的作用是返回一個變數的記憶體位址,而不是這個變數本身。那麼在這個例子中,pNumberOne就是some_number的地址,亦稱作pNumberOne指向some_number。
現在,如果我們想使用some_number的地址的話,那麼我們就可以使用pNumberOne了。如果我們希望經由pNumberOne而使用some_number的值的話,我們可以用*pNumberOne。“*”應該讀作“the memory location pointed to by(由……指向的記憶體位置)”,它用來取得指標所指向的值。不過指標聲明的情況例外,如“int *pNumber”。
到現在都學到什麼了(一個例子):
咻!要理解的東西太多了,所以在此我建議,如果你還是不理解以上的概念的話,那麼最好再通讀一遍;指標是一個複雜的主題,要掌握它是要花些時間的。
這裡有一個樣本,解說了上面討論的那些概念。它是由C編寫成,並不帶有C++的那些擴充。
#include
void main()
{
// 聲明變數:
int nNumber;
int *pPointer;
// 現在,給它們賦值:
nNumber = 15;
pPointer = &nNumber;
// 列印nNumber的值:
printf("nNumber is equal to : %d\n", nNumber);
// 現在,通過pPointer來控制nNumber:
*pPointer = 25;
// 證明經過上面的代碼之後,nNumber的值已經改變了:
printf("nNumber is equal to : %d\n", nNumber);
}
請通讀並編譯以上代碼,並確信你已經弄懂了它是如何工作的。然後,當你準備好了以後,就往下讀吧!
陷阱!
看看你是否能指出以下程式的缺陷:
#include
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將會指向一個不再屬於本程式的記憶體位置。如果你不懂這一點,那麼你應該去讀一讀有關本地變數、全域變數以及範圍的東西,這些概念非常重要。
那麼,如何解決這個問題呢?答案是使用一種名為動態分配的技術。請注意:在這一點上,C和C++是不同的。既然大多數開發人員正在使用C++,那麼下面的代碼就使用C++來編寫。
動態分配
動態分配也許可以算是指標的關鍵技術了。它被用於在沒有定義變數的情況下分配記憶體,然後由一個指標指向這段記憶體。雖然這個概念好像很讓人糊塗,其實它很簡單。以下的代碼解說了如何為一個整數分配記憶體空間:
int *pNumber;
pNumber = new int;
第一行代碼聲明了一個指標pNumber,第二行代碼分配了一個整數的空間,並使pNumber指向這一段新分配的記憶體。下面是另外一個例子,這一次使用了一個double:
double *pDouble;
pDouble = new double;
這些規則是相同的T,所以你應該可以很容易地掌握。
動態分配和本地變數的不同點是:你分配的記憶體在函數返回和語句塊結束的時候不會被釋放,所以,如果你用動態分配來重新編寫上面的代碼,那麼它就會正常工作了:
#include
int *pPointer;
void SomeFunction()
{
// 使pPointer指向一個new的整數
pPointer = new int;
*pPointer = 25;
}
void main()
{
SomeFunction(); // 讓pPointer指向某些東西
printf("Value of *pPointer: %d\n", *pPointer);
}
請通讀並編譯以上的範例程式碼,並確信你已經弄懂了它為何如此工作。當調用SomeFunction的時候,它分配了一段記憶體,並使pPointer指向這段記憶體。這一次當函數返回的時候,這段new的記憶體就會完好保留,所以pPointer仍然指向某些有用的內容。這就是動態分配了!請確信你已經搞懂了這一點,然後繼續閱讀關於這段代碼中的一個嚴重錯誤。
來得明白,去得明白
還有一個複雜的因素,並且是十分嚴重的——雖然它很好補救。問題是你分配的記憶體在離開的時候雖然仍然完好,但是這段記憶體永遠也不會自動銷毀。這就是說,如果你不通知電腦結束使用的話,這段記憶體就會一直存在下去,這樣做的結果就是記憶體的浪費。最終,系統就會因為記憶體耗盡而崩潰。所以,這是相當重要的一個問題。當你使用完記憶體之後,釋放它的代碼非常簡單:
delete pPointer;
這一切就這麼簡單。不管怎樣,在你傳遞一個有效指標——亦即一個指向一段你已經分配好的記憶體指標,而不是那些老舊的垃圾記憶體——的時候,你都需要無比細心。嘗試delete一段已經釋放的記憶體是十分危險的,這可能會導致你的程式崩潰。
好了,下面又是那個例子,這一次它就不會浪費記憶體了:
#include
int *pPointer;
void SomeFunction()
{
// 使pPointer指向一個new的整數
pPointer = new int;
*pPointer = 25;
}
void main()
{
SomeFunction(); // 讓pPointer指向某些東西
printf("Value of *pPointer: %d\n", *pPointer);
delete pPointer;
}
唯一的一行不同也就是最本質的一點。如果你不將記憶體delete掉,你的程式就會得到一個“記憶體流失”。如果出現了記憶體流失,那麼除非你關閉應用程式,否則你將無法重新使用這段泄漏的記憶體。
向函數傳遞指標
向函數傳遞指標的技術非常有用,但是它很容易掌握(譯註:這裡存在必然的轉折關係嗎?呃,我看不出來,但是既然作者這麼寫了,我又無法找出一個合適的關聯詞,只好按字面翻譯了)。如果我們要編寫一段程式,在其中要把一個數增加5,我們可能會像這麼寫:
#include
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的一份拷貝,而不是nMyNumber本身。因此,“Number = Number + 5”這一行則是向這份拷貝加上了5,而main()中的原始變數並沒有任何變化。你可以運行這個程式試著證明這一點。
對於這個程式,我們可以向函數傳遞這個數字記憶體位址的指標。這樣,我們就需要修改這個函數,使之能接收一個指向整數的指標。於是,我們可以添加一個星號,即把“void AddFive(int Number)”改為“void AddFive(int* Number)”。下面是這個修改過了的程式,注意到我們已經將nMyNumber的地址(而不是它本身)傳遞過去了嗎?此處改動是添加了一個“&”符號,它讀作(你應該回憶起來了)“the address of(……的地址)”。
#include
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之前的“*”的重要性了嗎?這就是告知編譯器我們要在指標Number指向的數字上加5,而不是向指標本身加5。
最後要注意的一點是,你亦可以在函數中返回指標,像下面這個樣子:
int * MyFunction();
在這個例子中,MyFunction返回了一個指向整數的指標。
指向類的指標
關於指標,我還有還有兩點需要提醒你。其中之一是指向結構或類的指標。你可以像這樣定義一個類:
class MyClass
{
public:
int m_Number;
char m_Character;
};
然後,你可以定義一個MyClass的變數:
MyClass thing;
你應該已經知道這些了,如果還沒有的話,你需要閱讀一下這方面的資料。你可以這樣定義一個指向MyClass的指標:
MyClass *thing;
就像你期望的一樣。然後,你可以為這個指標分配一些記憶體:
thing = new MyClass;
這就是問題之所在了——你將如何使用這個指標?呃,通常你會這麼寫:“thing.m_Number”,但是對於這個例子不行,因為thing並非一個MyClass,而是一個指向MyClass的指標,所以它本身並不包含一個名為“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;
}
指向數組的指標
你也可以使指標指向數組,如下:
int *pArray;
pArray = new int[6];
這將建立一個指標pArray,它會指向一個6個元素的數組。另一種不使用動態分配的方法如下:
int *pArray;
int MyArray[6];
pArray = &MyArray[0];
請注意,你可以唯寫MyArray來代替&MyArray[0]。當然,這種方法只適用於數組,是C/C++語言的實現使然(譯註:你也可以把函數名賦值給一個相應的函數指標)。通常出現的錯誤是寫成了“pArray = &MyArray;”,這是不正確的。如果你這麼寫了,你會獲得一個指向數組指標的指標(可能有些繞嘴吧?),這當然不是你想要的。
使用指向數組的指標
如果你有一個指向數組的指標,你將如何使用它?呃,假如說,你有一個指向整數數組的指標吧。這個指標最初將會指向數組的第一個值,看下面這個例子:
#include
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),因為在你使用指標的時候,編譯器不能檢查出來你是否已經移出了數組的末尾。所以,你可能很容易地使系統崩潰。下面仍然是這個例子,顯示了我們所設定的三個值:
#include
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就是pArray當前位置的前兩個元素。不過,請確定你是在操作指標,而不是操作它指向的值。這種使用指標的操作在迴圈的時候非常有用,例如for或while迴圈。
請注意,如果你有了一個指標(例如int* pNumberSet),你也可以把它看作一個數組。比如pNumberSet[0]相當於*pNumberSet,pNumberSet[1]相當於*(pNumberSet + 1)。
關於數組,我還有最後一句警告。如果你用new為一個數組分配空間的話,就像下面這個樣子:
int *pArray;
pArray = new int[6];
那麼必須這樣釋放它:
delete[] pArray;
請注意delete之後的[]。這告知編譯器它正在刪除一個整個的數組,而不是單獨的一個項目。你必須在使用數組的時候使用這種方法,否則可能會獲得一個記憶體流失。
最後的話
最後要注意的是:你不能delete掉那些沒有用new分配的記憶體,像下面這個樣子:
void main()
{
int number;
int *pNumber = number;
delete pNumber; // 錯誤:*pNumber不是用new分配的
}
常見問題及FAQ
Q:為什麼在使用new和delete的時候會得到“symbol undefined”錯誤?
A:這很可能是由於你的源檔案被編譯器解釋成了一個C檔案,因為new和delete操作符是C++的新特性。通常的改正方法是使用.cpp作為你的源副檔名。
Q:new和malloc的區別是什嗎?
A:new是C++特有的關鍵詞,並且是標準的分配記憶體方法(除了Windows程式的記憶體配置方法之外)。你絕不能在一個C C++程式中使用malloc,除非絕對必要。由於malloc並不是為C++物件導向的特色設計的,所以使用它為類對象分配記憶體就不會調用類的建構函式,這樣就會出現問題。由於這些原因,本文並不對它們進行討論,並且只要有可能,我亦會避免使用它們。
Q:我能一併使用free和delete嗎?
A:你應該使用和分配記憶體相配套的方法來釋放記憶體。例如,使用free來釋放由malloc分配的記憶體,用delete來釋放由new分配的記憶體。
引用
從某種角度上來說,引用已經超過了本文的範圍。但是,既然很多讀者問過我這方面的問題,那麼我在此對其進行一個簡要的討論。引用和指標十分相似,在很多情況下用哪一個都可以。如果你能夠回憶起來上文的內容——我提到的“&”讀作“the address of(……的地址)”,在聲明的時候例外。在聲明的這種情況下,它應該讀作“a reference to(……的引用)”,如下:
int& Number = myOtherNumber;
Number = 25;
引用就像是myOtherNumber的指標一樣,只不過它是自動解析地址的,所以它的行為就像是指標指向的實際值一樣。與其等價的指標代碼如下:
int* pNumber = &myOtherNumber;
*pNumber = 25;
指標和引用的另一個不同就是你不能更換引用的內容,也就是說你在聲明之後就不能更換引用指向的內容了。例如,下面的代碼會輸出20:
int myFirstNumber = 25;
int mySecondNumber = 20;
int &myReference = myFirstNumber;
myReference = mySecondNumber;
printf("%d", myFristNumber);
當在類中的時候,引用的值必須由建構函式設定,像下面這種方法一樣:
CMyClass::CMyClass(int &variable) : m_MyReferenceInCMyClass(variable)
{
// 這裡是構造代碼
}
總結
這一主題最初是十分難以掌握的,所以你最好讀上它個至少兩遍——因為大多數人不能立即弄懂。下面我再為你列出本文的重點:
1、指標是一種指向記憶體中某個位置的變數,你可以通過在變數名前添加星號(*)來定義一個指標(也就是int *number)。
2、你可以通過在變數名前添加“&”來獲得它的記憶體位址(也就是pNumber = &my_number)。
3、除了在聲明中以外(例如int *number),星號應該讀作“the memory location pointed to by(由……指向的記憶體位置)”。
4、除了在聲明中以外(例如int &number),“&”應該讀作“the address of(……的地址)”。
5、你可以使用“new”關鍵字來分配記憶體。
6、指標必須和它所指向的變數類型相配套,所以int *number不應該指向一個MyClass。
7、你可以向函數傳遞指標。
8、你必須使用“delete”關鍵字來釋放你分配的記憶體。
9、你可以使用&array[0]來獲得一個數組的指標。
10、你必須使用delete[]來釋放動態分配的數組,而不是簡單的delete。
這並非一個完全的指標指南,其中有一點我能夠涉及到的其它細節,例如指標的指標;還有一些我一點也未涉及到的東西,例如函數指標——我認為作為初學者的文章,這個有些複雜了;還有一些很少使用的東西,在此我亦沒有提到,省得讓這些不實用的細節使大家感到混亂。
就這樣了!你可以試著運行本文中的程式,並自己編寫一些樣本來弄懂關於指標的問題吧。