文章程式碼顯示

2018年2月5日 星期一

《筆記》C語言 - 07:指標與位址、指標變數

指標是學習 C 語言中很多人的夢饜,也可說是第二個門檻,但要能夠到可以自稱 "熟悉 C 語言" 的地步,這難關是肯定要咬牙撐過去的。

如果有一天程式語言要一較高下誰才是天下第一
C 語言的「指標」就如同郭靖的降龍十八掌

知道 C 語言能在程式語言界佔有一席之地靠的是什麼絕學了吧 !?

然而絕學兩個字意謂著要花一番苦心啊 ... 用的好用的精,一下子就能靠這個區別出你是 C 語言初學者還是熟悉 C 語言的人了

正文開始

首先我們從之前就學過的著手,宣告一個普通的變數

int value;

讀作「value 是一個普通變數,裡面存放 int 型態的數值。」

這時我們思考一下,當我們宣告了一個普通變數,在電腦裡是怎樣存放這個數值的呢?

其實就跟我們日常生活中的 “住家地址” 是一樣的意思,在談陣列的時候我們就有稍微提到地址的概念,在談數據型別所占據的 bytes 長度時也有稍微提到。

首先回顧一下, int 型別 已知為  4 bytes( 32bits ) 的長度;且已知電腦在儲存數據的時候是以 1 byte ( 8bits)  為單位(可參考此)。

接下來就要讓大家發揮一下想像力了

想像電腦的記憶體(儲存這些數值的地方)是一棟大樓,而大樓裡面又有許許多多的套房

每一個套房的大小都是 1byte ( 8 bits ) 

假設現在有這麼一棟大樓,由政府就出資蓋分給大家住,但也不是人人平等,要看人種來決定分到幾間套房的

整數部分:


int 型別(4 bytes)的人種算是普通平民百姓,就分給它四間連在一起的套房

short 型別(2 bytes)的比較低賤一些,只分到兩間

char ( 1 byte) 屬於貧民窟,只分到一間

浮點數部分:

floag 型別( 4 bytes)也是四間


最高檔的貴族是 double 型別( 8 bytes ) ,這人種一次就分到八間連在一起的套房


好了,現在我們知道不同的數據型別會分配到不同的房間數目,接著我們將大樓內的每一個套房都標上位址(房號) 例如 0x0000 、 0x0001 ... 等等

0x 兩個字代表著後面的數字是十六進制的,知道這個後我們就不鳥他了,就用 ”帝寶” 兩個字替代吧!所以就有 帝寶0000 帝寶0001 ... 以此類推

假設現在有兩個 int 型別 的變數(人)分別叫 小王、小李

這兩人是普通平民百姓,會分到四間連在一起的套房

int 小王;
int 小李;

那麼有可能他們的房號(位址)會是這樣

小王分到 -> 帝寶5200 帝寶5201 帝寶5202 帝寶5203

小李分到 -> 帝寶884A 帝寶884B 帝寶884C 帝寶884D


這個位址是由電腦自行去分配的,重點在於分到的四間套房一定是連在一起的

某天小王經過警衛室,警衛問他「你是哪間的阿?」

小王本來要回答我是 「帝寶5200、帝寶5201、帝寶5202、帝寶5203」

但要把四間都講出來太麻煩了,小王就回答「 5200 的 」

小李就會回答「我是 884A的」,也就是只講起始房號就好

在此,我們理解了一個普通 int 型態的變數在記憶體內的模樣,原來對於一個變數而言,除了存在房間裡的那些家當(數值)以外,還都會有屬於自己的房號(起始房號)


#include "stdio.h"

int main(void) {

 int room1 = 123377; //小王他的房間內擺放著家當(數值) 123377
 int room2 = 43149; //小李他的房間內擺放著家當(數值) 43149


 printf("room1 家中財產為 %d, 地址(起始房號)為 %p\n", room1, &room1);// & 為取址運算子
 printf("room2 家中財產為 %d, 地址(起始房號)為 %p\n", room2, &room2);

 return 0;
}


上述程式碼第八行的地方我們使用了 & 取指運算子(在這不當作 AND 使用),並且搭配 %p 來列印出變數 room1 (小王),在大樓中的位址(也就是起始房號)

由上述的結果我們可以發現, room1 (小王) 的位址為 0x0028FF3C;room2 (小李) 的位址為 0x0028FF38

他們兩者之間正好差 "4"

我們可以用下圖來看出這兩個變數在記憶體中的擺放



Hint : 為什麼 room1 (小王) 變數先宣告,記憶體位置為 0x0028FF3C
room2 (小李) 後宣告,記憶體位置是 0x0028FF38 呢?

直觀上來說,你可能會認為後宣告的變數其記憶體位置應該要放在比較後面的地方,但事實上不同的處理器會有不同的架構,這與記憶體的"堆疊"有關。此部分不在我們討論的範圍,你只要知道較晚產生的變數,其記憶體位址會被擺放在比較低的地方就行了

如同在蓋大樓的時候,是從頂樓的房間開始分配,最後一個人會被分配到一樓的房間一樣

指標變數(pointer variable)

前文我們知道了原來每個數據型態 ( char, short, int, float ... ) 所分配到的房間數量不同,並且我們可以使用 & 來取出各個變數的記憶體位址。

接著我們談談指標變數是什麼

指標變數對應於普通變數而言,就只是存放的東西不一樣而已

我們在宣告普通變數時,通常會將一個 "數值" (例如 : 1234 ) 存放在裡面,也可以說宣告一個普通變數的目的在於存放一個數值。其型別為常見的 int , float 等等,用來存放整數與浮點數的數值

對於 C 語言 來說,記憶體位址是一個特別的東西,所以它額外用一個特殊的容器(指標變數)進行儲存,故宣告一個指標變數的目的是用來存放某個 指標(pointer) (例如 : 0x0028FF3C)。其型別為 int*, float* 等等,用來存放整數與浮點數數值的位址

宣告一個指標變數的目的是用來存放一個位址

有天我們宣告了

int* door;

讀作 「door 是一個存放位址的指標變數,
存放的位址指向一個"空間",
且這個 "空間",裡頭存放的"數值"為 int 型別」

更簡潔且被大家慣用的讀法為「door 是一個指向 int型別 的指標」( door is a pointer points to integer)

先不管上面那句什麼意思,我們把這語句進行擬物化一下

int* 高科技任意門;

有天小李想搬家,但他家偏偏在八樓又沒電梯(!?),好在科技進步已經發展了任意門

這任意門挺大台的,要四間連間套房的空間才放的下,正好在警衛室隔壁有四間連間套房

警衛就把這台 “高科技任意門” 放在這了

這任意門要怎用呢? 

挺簡單,只要在任意門的操作面板輸入想要到達的起始房號就好了

例如我們在任意門的面板輸入小李的起始房號 “帝寶884A” ,然後任意門就會啟動了
只要走進任意門,就直接會通到小李的房間,然後就可以搬它家的東西(數值)

:任意門也是有分等級的,例如我們宣告成 int* 就是針對 int 型別的,只能用來存放 int 變數的位址。任意門機器的大小是固定 4bytes 的,與宣告成 int* 、 char* 、float* 或是 double* 無關

#include "stdio.h"

int main(void) {

 int room1 = 123377; //小王他的房間內擺放著家當(數值)12337
 int room2 = 43149; //小李他的房間內擺放著家當(數值)43149

 int* door = NULL; //door為一個指標變數(用來存放地址),該地址指向一個空間(NULL)
                   //該空間存放 int型別 的(整數)變數

 printf("room1 家中財產為 %d, 位址為 %p\n", room1, &room1);
 printf("room2 家中財產為 %d, 位址為 %p\n", room2, &room2);
 printf("任意門通往位址 %x, 任意門機器本身的位址 %p\n\n", door, &door);

 printf("設置任意門通往 room2 (將 door 設置為 room2 的位址)\n");
 door = &room2; //讓 door 指向 room2 (或可讀做 door 為指向 room2 的指標)
 printf("任意門通往位址 %x\n\n", door);

 printf("穿過任意門取得 room2 的數值\n");
 printf("穿過任意門得到 %d", *door); //*door 的 *(米字號) 在此與宣告時所用的米字號意義不同
                                   // 在此用做 "(依地址間接)取值運算子"或稱 反參考運算子
 return 0;
}



首先我們如前述的程式碼,宣告兩個普通的 int 變數分別為 room1(小王) 以及 room2(小李),並且有擺放數值在房間裡頭

接著我們在第 8 行宣告一個名為 door 的指標變數,指向 NULL

宣告了指標變數卻又不給初始值(也就是宣告了一個指向不知名位址的指標變數)不是一個很好的習慣,如果真的沒辦法給予該指標變數一個明確的指標(位址),較好的寫法是 int* door = NULL(或0) ;

由結果中我們知道
room1 的位址為 0x0028FF3C ,存放的數據為 123377
room2 的位址為 0x0028FF38 ,存放的數據為 43149
任意門(door)本身的位址為 0x0028FF34 ,存放的數據為 0 (這個數據應該要是一個 int 數據的位址)

接著我們在第 16 行設置任意門所存放的數據為 room2 的位址
如此一來,任意門所存放的數據變為 0x0028FF38
以專業的術語來說這樣的行為讀為「設置 door 為指向 room2 的指標」

最後,我們在第 20 行就可以藉由 *(米字號) 來經過 door 去存取 room2 的數據

: 第 20 行的 *(米字號) 在此與第 8 行宣告時所用的 *(米字號) 含義不同,在 20 行用做 "(依地址間接)取值運算子"或稱 反參考運算子。

看到這裡,你需要做的事為靜下心來揣摩這些專有名詞以及他們之間的關係。第一次看不懂就看第二次,第二次看不懂就看第三次,第三次看不懂代表我的說法不合你意,可嘗試尋找其它參考資料

↓↓↓ 連結到部落格方針與索引 ↓↓↓

Blog 使用方針與索引