# C語言的指標(pointer) 指標(pointer): 「指標」可說是C或C++的大魔王了,指標易學難精,但由於很少有書或資料能夠把指標做清楚的解釋,可能連上課的老師都說不清楚(還是用術語解釋術語),造成多數初學者根本鴨子聽雷,光是連指標是什麼都搞不清楚,就失去學習興趣了。 指標(pointer)是源自於早期電腦科學和程式語言發展的一個必然過程。其實任何程式語言都一定會有指標的觀念和操作,只是後來比較新的程式語言考慮到指標的易學難精,使用難度高,而且有一些副作用,和自己主打簡單易用的理念不合,因此很多比較新的程式語言,都把指標巧妙的包裝成另外的形式,不讓使用者直接使用「 * 」和「&」運算子來操作指標。例如Java就把指標的觀念給巧妙的包裝成為「參考型態」,讓使用者能以更單純的方式操作指標,而不必被「 * 」和「&」運算子搞得頭昏腦脹。 所以反而指標變成C語言(主要發展期:1969年到1973年)最大的招牌特色了,就像市面上的車子已經都改為自動排檔,而還有手動排檔的老車,反而變成了它的特色。 指標用的好,對於熟練的人來說,指標提供了最大的靈活度,讓設計者能對記憶體的使用方式做很好的手動控制,就像開手動排檔的賽車手;而不使用指標,就像開自動排檔的一般人,雖然一樣能到達目的地,但有些過程就無法達到最佳化,要實現某些操作也不那麼方便。 不過,因為指標能夠直接操作電腦的底層硬體,能夠手動控制電腦使用記憶體的方式,因此指標是一把雙面刃。雖然對電腦來說都沒差,但對人類來說,只要觀念不清楚,一旦指標操作錯誤就會造成系統崩潰,或者影響程式的穩定性。而使用指標,對不那麼熟練的人來說,也造成無謂的複雜度,讓程式變得艱澀難懂,不只容易寫錯產生bug,也難以除錯(debug)。 指標的觀念易學難精,因此若要自稱為一個優秀的C語言(或C++)程式設計師,一定要具有深厚的指標功力才行,除了要徹底了解指標的觀念,也要多練習。 要徹底了解指標,一定要先了解電腦記憶體的結構,並且知道「變數」(Variable)是如何占用記憶體空間,「值」(Value)又是如何被放進變數中的。 電腦的記憶體空間可比喻成一個長長的收納櫃,這個櫃子可以收納許許多多的抽屜,或者說空箱子也行,就像下圖的感覺。當然這個長櫃可不像圖片一樣只能收納5個空箱子,而是能收納成千上萬、甚至幾十億個空箱子,視你電腦的記憶體是買多大的容量、或是使用什麼樣的裝置而定。記憶體中每一個空箱子(抽屜)可以容納1 byte的資料,而1 byte = 8 bits,也就是一個空箱子(抽屜)可以放入8個0或1的資料。 ![](https://i.imgur.com/KsIQr25.png) 並且,每個抽屜(空箱子)都有自己獨特的編號,可以比喻為「箱號」,像上圖的長櫃就有5個箱號,程式語言的專業術語就是「記憶體位址」。「指標」(Pointer)就是「記憶體位址」的另一種說法,記憶體位址是一組16進位的編號,不會和其他編號重複。 而「指標變數」(Pointer Variable)是指用來儲存「指標」(記憶體位址)的資料型態,就和整數的資料型態是用來存放整數的值(Value)是一樣的道理,指標變數就是拿來存放記憶體位址(一組16進位的值,例如OX22FF54)的資料型態。 所以使用指標變數,就可以利用指標去指定某一個變數的(第一個)記憶體位址,之後就可以把抽屜(變數)裡面的東西(值)給取出來使用、或者把該值給替換掉,就像我們日常生活中從長櫃的某個特定的抽屜中取物或替換物品一樣,後續將會詳細說明。 電腦在運算的過程中經常會使用到不同的資料型態,例如整數的資料型態int、浮點數的資料型態float、字元的資料型態char、布林的資料型態Boolean…等等。每種不同的資料型態會占用不同數量的記憶體空間,例如常見的「整數」的資料型態會占用掉4個byte的空間,也就是說為了存放一個整數,會占用掉4個空箱子(抽屜)的空間,而4 byte = 32 bits,每一個bit又可以填入1或0兩種值,所以共有「2的32次方」種可能性,所以我們才會說整數(int)的資料範圍是0 ~ 4294967295(或 -2147483648 ~ 2147483647),而一般我們用不到這麼小或這麼大的整數值,所以電腦預設幫我們預留4個byte的記憶體空間來填入整數值已經很夠用了。 ![](https://i.imgur.com/0tkl6gC.png) 至於要在記憶體空間中保留幾個空箱子(byte),依據宣告的資料型態的不同和環境(使用不同的程式語言、電腦是32 bits或64 bits、使用的作業系統是哪種…)的不同而有所差異。 極端的情況下,如果要放入一個超級大的值到記憶體空間,可以改為宣告為long或long double的資料型態,電腦就會幫我們預留更多的空箱子(抽屜)讓我們放值。例如宣告為long double的資料型態,電腦會預先幫我們劃位保留10個抽屜(10 byte),讓我們可以放入一個超級大的值(2的80次方!),但一般可能只有天文運算或超級電腦才比較有機會用到long double的資料型態。 雖然long double可用的數值範圍一定可以包含我們平常計算的值,但這樣未免太貪心了,給電腦占用了這麼多記憶體空間,卻把極大部分的空間都放入null(空),也就是俗話說的占著茅坑不拉屎,平白浪費電腦的記憶體空間。雖然現代的電腦記憶體空間都非常大,但也沒有必要這樣搞,所以我們會宣告一個最適合目前場合使用的資料型態。 註:而若是使用int(整數)的型態宣告了一個變數,但真的把它拿去放long double的值,當然是放不下的,這個情況就稱作「溢位」(overflow),多出來的0與1(位元、bit)就會被忽略、硬生生地被刪掉(無條件捨去),也就是說這個數值失真了。 經過上述說明,我們現在知道「變數」(Variable)和「值」(Value)是完全不同的概念了。當我們宣告了一個變數,電腦就會依據我們宣告的資料型態去預先幫我們劃位,保留對應數量的記憶體空間。例如程式碼寫作: int a; 就是告訴電腦,我們要使用到一個整數型態的變數,這個變數我們把它命名為a,請先幫我們預留4個抽屜,而這「4個抽屜」的名稱就叫做變數a。 當我們只宣告了變數卻沒有給這個變數「賦值」時,也就是只保留著這些空的抽屜,這樣並沒有什麼意義。把值(Value)放進變數中,聽起來很厲害的專業術語稱為「賦值」,程式碼寫作「a=28;」,也就是說我們把一個值28給轉換為二進位(轉換為二進位的過程電腦會自己處理好),並且依照記憶體空間的編號順序把28的二進位值給依序放入這4 byte的記憶體空間中。  註:注意這裡的「=」是程式語言的「指派」運算子,並不是一般數學的「等於」,意思是說把「=」符號右邊的值給丟到左邊的變數中保存。至於真正的「等於」算數運算子,在程式語言中要寫成「==」。 把剛才的說明濃縮為以下這張示意圖: ![](https://i.imgur.com/vNVf6yH.png)  註:實際上28是以2進位儲存到記憶體中的。 我們已經把值(Value)透過「賦值」的動作給丟到變數(Variable)中保存了,那我們要如何在程式中取用這個值呢? 在C語言中有兩種方法可以從「變數」中取出「值」來使用: 1. 「傳遞值」,就是「直接」取用這個值。例如程式碼寫作:「b=a」,就是把變數a的值「複製」一份丟到變數b中儲存,如果變數b已經有值的話就會被新的值給取代掉,舊的值就不見了。當然b也要事先宣告它的資料型態,若b的資料型態與a不同,可能會發生資料遺失或是溢位的情況。 2. 「傳遞記憶體位址」,就是使用「指標」(Pointer)來「間接」取值,這就是接下來要說明的重點。 前面說過指標(Pointer)是一種「資料型態」,就和整數的資料型態int、浮點數的資料型態float、字元的資料型態char、布林的資料型態Boolean是相同的意思。不過指標是種特別的資料型態,它儲存的是某個變數的「(第一個)記憶體位址」,有的人也會說成指標「指向」了某個變數的記憶體位址,只是說法不同而已,意思都是相同的。 「指標」就是某個變數的(第一個)記憶體位址,而「指標變數」則是指用來存放這個記憶體位址的變數。宣告一個指標變數,就和一般宣告變數一樣,是跟記憶體要一塊區域用來存放這個變數的值,只是這個變數的型態(型別)是指標,也就是說指標變數存放的是某個變數的(第一個)記憶體位址(指標)。 由於我們知道每種不同的資料型態它們各自會占用多少記憶體空間,所以只要知道變數的第一個byte的記憶體位址,電腦就能推算出這個變數的其他每個byte的記憶體位址,所以指標變數只要儲存某個變數的第一個byte的記憶體位址就夠了。 總結來說: 1. 變數的「指標」,是變數的「記憶體位址」的專業術語。 2. 「指標變數」是一種變數的資料型態,就和「int a;」是宣告變數a的型態是整數,能夠存放整數值的道理一樣,指標變數是能夠存放某個變數的記憶體位址(指標)的資料型態。而且指標變數既然本身也是個變數,當然也要占用記憶體空間,故指標變數本身也會有一個屬於它自己的記憶體位址,也就是指標變數的指標,聽起來蠻像繞口令的,但也不難,參考下圖即可明白。所以,指標變數的記憶體位址(指標變數自己的指標),和它所儲存的記憶體位址(其他變數的指標)是不同的。 ![](https://i.imgur.com/6Kkm3HP.png) 指標變數pointer_b 儲存的是浮點數b的指標,術語稱作:指標變數pointer_b「指向」浮點數b。 把以上的文字說明,用圖解表示出來: ![](https://i.imgur.com/dSrzviE.png) ![](https://i.imgur.com/20p6neW.png) 「 * 」運算子又被稱作「取值」或「間接」(indirection)運算子,和「&」被稱為「取址」運算子剛好是一對,這樣的稱呼也很方便我們記憶與使用。 不過要注意,「 * 」運算子可不能隨便亂用,因為若是指標變數在宣告時沒有設定一個初始值,也就是沒有把指標變數先透過類似「int *pointer_a = &a ; 」的語法先指派一個初始值給它(此例就是變數a的記憶體位址),則這時候指標就會「亂指」,甚至有可能指到已經有存在資料的記憶體位址,並且用新值取代舊值,如果這個記憶體位址存放的是作業系統的資料,就會把作業系統的資料給改寫掉,那問題就大了。 做個具體的比喻,沒有指向確定值的指標變數,就像下課後的五歲小孩,如果放任不管他就會橫衝直撞、發生危險,如果我們還沒決定好要讓它指向哪裡,就要暫時把它交給名為「空(NULL)」的托兒所。 ![](https://i.imgur.com/5Wax5kJ.png) 所幸現在的作業系統都有做保護,不會讓外部操作隨意變更作業系統記憶體區段內的值,編譯器也都設計的不錯,若指標亂指、隨意賦值,則最多只是編譯器當掉,或跳出錯誤訊息,不會擴大災情。但如果使用的是古早的作業系統,或者安全性比較差的作業系統(例如DOS),則就會直接毀掉作業系統,只能重灌或買新的電腦了。