C++入門解惑(3)——初探指標(下) (轉)

amyz發表於2007-11-13
C++入門解惑(3)——初探指標(下) (轉)[@more@]

2.形形色色的指標

All Kinds of Pointers

:namespace prefix = o ns = "urn:schemas--com::office" />

前一章我們引入了指標及其定義,這一節我們繼續研究各種不同的指標及其定義方式(注:由於指標較為特殊,本章暫不作討論,但凡出現“指標”一詞,如非特別說明均指資料指標)。

1)指向指標的指標

我們已經知道,指標變數是用於儲存特定資料型別地址的變數,假如我們定義

int *pInt;

那麼,pInt為一個指向整型變數的指標變數。好,我們把前面這句話的主幹提取出來,就是:pInt為變數。既然pInt是變數,在中就會有與之對應的存放資料的地址值,那麼理論上也就應該有對應的指標來,嗯,實際上也如此,我們可以向這樣來定義可以指向變數pInt的指標:

int **pIntPtr;

按前一章的方法很好理解這樣的定義:**pIntPtr是一個int型別,則去掉一個*,*pIntPtr就是指向int的指標,再去一個*,我們最終得到的pIntPtr就是一個“指向int型指標變數的指標變數”,呵呵,是點拗口,不管怎麼說我們現在可以寫:

pIntPtr = &pInt;

令其指向pInt變數,而*pIntPtr則可以得回pInt變數。假如pInt指向某個整型變數如a,*pInt可以代表a,因此*(*pIntPtr)此時也可以更間接地得到a,當然我們如果省去括號,寫成**pIntPtr也是可以的。

以此類推,我們還可以得到int ***p這樣的“指向指向指向int型變數的指標的指標的指標”,或者再複雜:int ****p,“指向指向指向指向……”喔,說起來已經很暈了,不過原理擺在這裡,自己類比一下即可。

2)指標與常量

C++的常量可以分兩種,一種是“文字”常量,比如我們中出現的18,3.14,’a’等等;另一種則是用關鍵字const定義的常量。大多數時候可以把這兩種常量視為等同,但還是有一些細微差別,例如,“文字”常量不可直接用&尋找其在記憶體中對應的地址,但const定義的常量則可以。也就是說,我們不能寫&18這樣的,但假如我們定義了

const int ClassNumber = 18;

則我們可以透過&ClassNumber表示式得到常量ClassNumber的地址(不是常數18的地址!)。其實在儲存特點上常量與變數基本是一樣的(有對應的地址,並且在對應地址上存有相應的值),我們可以把常量看作一種“受限”的變數:只可讀不可寫。既然它們如此相似,而變數有對應的指標,那麼常量也應該有其對應的指標。比如,一個指向int型常量的指標pConstInt定義如下:

const int *pConstInt;

它意味著*pConstInt是一個整型常量,因此pConstInt就是一個指向整型常量的指標。我們就可以寫

pConstInt = &ClassNumber;

來令pConstInt指向常量ClassNumber. 給你三秒鐘,請判斷pConstInt是常量還是變數。1,2,3!OK,假如你的回答是變數,那麼說明你對常量變數的概念認識得還不錯,否則應該翻本C++的書看看const部分的內容。

唔,既然int、float、double甚至我們自己定義的class都可以有對應的常量型別,那麼指標應該也有常量才對,現在的問題是,我們應該如何定義一個指標常量呢?我們通常定義常量的作法是在型別名稱前面加上const,像const int a等等,但如果在指標定義前面加const,由於*是右結合的,語義上會把const int *p 視為 (const int) (*p)(括號是為了突出其結合形式所用,但不是合法的C++語法),即*p是一個const int型常量,p就為一個指向const int常量的指標。也就是說,我們所加的const並非修飾p,而是修飾*p,換成int const *p又如何呢?噢,這和const int *p沒有區別。為了讓我們的const能夠修飾到p,我們必須越過*號的阻撓將const送到p跟前,假如我們先在前面定義了一個int變數a,則語句

int * const p = &a;

就最終如我們所願地定義了一個指標常量p,它總是表示a的地址,也就是說,它恆指向變數a.

嗯,小結一下:前面我們講了兩種指標,一種是“指向常量的指標變數”,而之後是“指向變數的指標常量”,它們定義的區別就在於const所修飾的是*p還是p. 同樣,還會有“指向常量的指標常量”,顯然,必須要有兩個const,一個修飾*p,另一個修飾p:

const int * const p = &ClassNumber;

以*為界,我們同樣很好理解:*表示我們宣告的是指標,它前面的const int表示它指向某個整型常量,後面的const表示它是的個常量指標。

為方便區別,許多文章都介紹了“從右到左”讀法,其中把“*”讀作“指標”:

const int *p1 = &ClassNumber; // p1是一個指標,它指向int型常量

int * const p2 = &a;  // p2是一個指標常量,它指向int型變數

const int * const p3 = &ClassNumber; // p3是一個指標常量,它指向int型常量

好了,我們前面定義指標常量時,受到了*號右結合的困擾,使得前置的const修飾不到p,假如*號能與int結合起來(就像前一章所說的“前置派”的理解),成為一種“指向整型指標的型別”,如

const (int*) p;

const就可以修飾到p了。但C++的括號只能用於改變表示式的優先順序而不能改變宣告語句的結合次序,能不能想出另一種方法來實現括號的功能呢?

答案是肯定的:使用關鍵字typedef.

typedef的一個主要作用是將多個變數/常量修飾符捆梆起來作為一種混合性的新修飾符,例如要定義一個無符號的整型常量,我們要寫

const unsigned int ClassNumber = 18;

但我們也可以先用typedef將“無符號整型常量”定義成一個特定型別:

typedef const unsigned int ConstUInt;

這樣我們只須寫

ConstUInt ClassNumber = 18;

就可以達到與前面等價的效果。

咋看似乎與我們關注的內容沒有關係,其實typedef的“捆梆”就相當於加了括號,假如,我們定義:

typedef int * IntPtr;

這意味著什麼?這意味著IntPtr是一個“整型指標變數”型別,這可是前面所沒有出現過的新複合型別,實際上這才是上章“前置派”所理解的“int*”型別:我們當初即使寫

int* p1, p2;

雖然有了空格作為我們視覺上的區分,但不幸的是不吃這一套,仍會把*與p1結合,變成

int (*p1), p2;

所以可憐的p2無依無靠只得成為一個整型變數。但現在我們寫

IntPtr p1, p2;

結論就不一樣了:有了typedef的捆梆,IntPtr已經成為了名符其實的整型指標型別,所以p1,p2統統成為了貨真介實的指標。那麼我們寫

const IntPtr p;

噢,不好意思,編譯出錯了:沒有初始化常量p……咦,看見了沒有?在const IntPtr的修飾下p已經成為指標常量了(而不是const int *p這樣的指向常量的指標),哦,明白了,由於typedef的捆梆,const與IntPtr都同心協力地修飾p,即理解為:

(const)  (int *) p;

而不是前面的

(const int)  (*p);

所以,不要小瞧了typedef,不要隨意將它看作是一個簡單的宏替換。事實上《C++ Primer》就曾經出了這樣的類似考題,大約也是考你:const IntPtr p中的p是指向const int的指標呢還是指向int的指標常量。我知道現在你可以毫不猶豫地正確地回答這個問題了。

BTW:當初第一次看到的時候,我也是毫不猶豫,可惜答錯了^_^

3.指標、動態記憶體、陣列

 

我們上一章談到變數時已經知道,變數實際上就是編譯為我們程式分配的一塊記憶體,編譯器會將變數名稱與這塊記憶體正確地聯絡起來以供我們方面地讀寫。設想一下,假如一塊這樣的儲存單元沒有“變數名”,我們應該如何訪問它呢?噢,如果有這個單元的地址,我們透過*運算子也可以得回該對應的變數。

變數定義可以看作兩個功能的實現:1.分配記憶體;2.將記憶體與變數名聯絡起來。

按前面所說,如果知道地址,也可以不需要變數名,所以上兩個功能如果變成:1.分配記憶體;2.將分配所得的記憶體的地址儲存起來;

理論上也可以實現上面的功能。在C++中,我們使用new運算子就可以實現第二種方法。new表示式會為我們分配一適當的記憶體,並且返會該記憶體的首地址(確切說應該是一個指標)。在表示式中,關鍵字new後面通常緊跟著資料型別,以指示分配記憶體的大小及返回的指標型別,例如new int表示式會為我們分配一塊整型變數所需的記憶體(32位機上通常為4位元組),然後這個表示式的值就是一個指向該記憶體的整型指標值。因此我們可以寫:

int *p;

p = new int;  // 分配一塊用於儲存一個整型變數的記憶體,並將地址賦給指標p

這樣我們就可以透過*p來對這塊“沒有變數名”的記憶體進行相同的操作。

前面我們僅僅在記憶體中分配了一個整型儲存單元,我們還可以分配一塊能儲存多個整型值的記憶體,方法是在int後面加上用“[ ]”括起來的數字,這個數字就是你想分配的單元數目。如:

int *p;

p = new int[18];  // 分配一塊用於儲存18個整型變數的記憶體,並將首地址賦給指標p

但這時候我們用*p只能對18個整型單元的第一個進行存取,如何訪問其它17個單元呢?由於這些單元都是連續存放的,所以我們只要知道首地址的值以及每個整型變數所佔用的空間,就可以計算出其它17個單元的起始地址值。在C++中,我們甚至不必為“每個整形變數所佔空間”這樣的問題所累,因為C++可以“自動地”為我們實現這一點,我們只需要告訴它我們打算訪問的是相對當前指標值的第幾個單元就可以了。這一點透過指標運算可以實現,例如,按前面的宣告,現在p已經指向18塊儲存單元的第一塊,如果我想訪問第二塊,也就是p當前所指的下一塊記憶體呢?很簡單,只要寫p+1,這個表示式的結果就會神奇地得出第二塊記憶體單元的地址,如果你的機器是32位,那麼你感興趣的話可以列印一下p的地址值與p+1的地址值,你會發現它們之間相差的是4個位元組,而不是1個,編譯器已經自動為我們做好了轉換的工作:它會自動將1乘上指標所指的一個變數(整型變數)所佔的記憶體(4位元組)。於是我們如果想要給第二記憶體單元賦值為3 ,則只須寫:

*(p + 1) = 3;  // 注意:*號優先順序比+號要高,所以要加上括號

要列印的時候就寫:

cout << *(p+1);  // 輸出3

總之這些和一般的變數一樣使用沒有什麼兩樣了。我們當然也可以將它的地址值賦給另外的指標變數:

int *myPtr;

myPtr = p + 1;  // OK,現在myPtr就指向第二記憶體單元的地址

也可以進行自加操作:

myPtr++;  // 按上面的初值,自加後myPtr已經指向第三記憶體單元的地址

*myPtr = 18;  // 現在將第三個記憶體單元賦予整型值18,也就相當於*(p + 2) = 18

到目前為止一切都很好,但*(p +1)這樣的寫法太麻煩,C++為此引入了簡記的方法,就是“[ ]”運算子(當初定義的時候也用過它哦):要訪問第二單元記憶體,我們只需要寫p[1]就可以,它實際上相當於*(p + 1):

p[1] = 3;  // *(p + 1) = 3;

cout << p[15];  // cout << *(p + 15);

p[0] = 6;  // *(p + 0) = 6;  也就是 *p = 6;

為了說明“[ ]”與*(… + …)的等效性,下面再看一組奇怪的例子:

1[p] = 3;  // *(1 + p) = 3;

cout << 15[p];  // cout << *(15 + p);

0[p] = 6;  // *(0 + p) = 6; 也就是 *p = 6;

看起來是不是很怪異?其實這一組只不過了一下加數位置而已,功能與上一組是完全一樣的。

前面我們介紹了一種分配記憶體的新方法:利用new運算子。new運算子分配的記憶體除了沒有變數分配時附帶有的變數名外,它與變數分配還有一個重要的區別:new運算子是在堆(heap)中分配空間,而通常的變數定義是在棧(stack)上分配記憶體。堆和棧是程式記憶體的兩大部分,初學可以不必細究其異同,有一點需要明白的是,在棧上分配的記憶體系統會自動地為其釋放,例如在函式結束時,區域性變數將不復存在,就是系統自動清除棧記憶體的結果。但堆中分配的記憶體則不然:一切由你負責,即使你退出了new表示式的所處的函式或者作用域,那塊記憶體還處於被使用狀態而不能再利用。好處就是如果你想在不同模組中共享記憶體,那麼這一點正合你意,壞處是如果你不打算再利用這塊記憶體又忘了把它釋放掉,那麼它就會霸佔你寶貴的記憶體資源直到你的程式退出為止。如何釋放掉new分配的堆記憶體?答案是使用delete算符。delete的大概是C++中最簡單的部分之一(但也很容易粗心犯錯!),你只要分清楚你要釋放的是單個單元的記憶體,還是多個單元的記憶體,假如:

int *p = new int;  // 這裡把分配語句與初始化放在一起,效果和前面是一樣的

…  // 使用*p

delete p;  // 釋放p所指的記憶體,即用new分配的記憶體

如果是多個單元的,則應該是這樣:

int *p = new int[18];

… // 使用

delete[] p;  // 注意,由於p指向的是一塊記憶體,所以delete後要加“[]”

// 以確保整塊記憶體都被釋放,沒有“[]”只會釋放p指的第一塊記憶體

剛才我們是在堆中分配連續記憶體,同樣,在棧上也可以分配邊續記憶體,例如我們同樣要分配18個單元的整型記憶體空間,並將首地址賦予指標a,則定義如下:

int a[18];

類似於前面用new的版本,系統會在棧上分配18個整型記憶體單元,並將首地址賦予指標a,我們同樣可以透過“[ ]”運算子或者古老的“*(… + …)”來實現對它的訪問。需要注意的是a是一個指向整型的指標常量型別,不可以再對a賦值使其指向其它變數。同樣,由於是在棧中分配記憶體,釋放工作也不必由我們操心。由於a“看起來”包含了許多個相同型別的變數,因此C++將其稱為陣列。

由上面看來,棧分配的陣列似乎比堆分配要簡單好用,但棧分配有一個缺點,就是必須在編譯時刻確定記憶體的大小,也就是說,假如我要寫一個排序程式,每次參加排序的元素個數都不一樣,但我不能寫

int number;

cin >> number;

int a[number];  // 錯誤,number是變數,而作為棧上分數空間的陣列a的大小必須在

  // 編譯時就決定

但我可以寫

int number;

cin >> number;

int *a = new int[number];  // 沒有問題,堆空間分配可以在程式執行時才確定

當然最後別忘了釋放就成了:

delete[] a;

由於堆記憶體的分配比棧記憶體具有更大的靈活性,可以在程式期動態決定分配空間的大小,所以又稱為動態記憶體。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752019/viewspace-982121/,如需轉載,請註明出處,否則將追究法律責任。

相關文章