前往
大廳
主題 達人專欄

跟著豬腳 C 起來:變數的地址

解凍豬腳 | 2021-03-15 19:45:01 | 巴幣 5996 | 人氣 3266

 
  來寫一下很久沒有更新的 C 語言系列好了。

  在先前本系列的九篇文章裡面,我們講了一些 C 語言的基礎,包括資料型態、條件分歧、函數、陣列、迴圈等等的主題。一般而言,只要掌握了這些觀念,大部分的簡單任務都可以完成。然而,光是會了這些還不能說自己已經學會 C 語言!今天就來講講 C 語言裡最重要的精髓——指標。

  在這之前,我們先來複習一些變數的觀念吧。



。變數的地址

  我們知道,在程式裡宣告的每一個變數,都各自佔用了一點點電腦的記憶體空間(不然是要存去哪裡?)既然使用到了記憶體空間,那麼為了不把這堆資料搞混,每個變數都有自己專屬的地址,就好像我們在使用置物櫃的時候要記得自己用了編號幾號的櫃子,如此一來要取用的時候才不會誤開其他人的置物櫃。

  那麼,要怎麼知道變數的編號呢?其實很簡單,我們只要在變數前面加上一個「&」符號,就可以得到它的地址了。我們宣告一個變數 x,隨意地給它賦值為 1564545789:



  透過把 &x 輸出,可以看到它的地址以十六進制表示是 0x0065FE1C,存放了 5D4112FD(也就是十進位的 1564545789 這個數)。

  (%08X 的意思:以八位數的大寫十六進位制來表示數值,未達位數補 0)

  如果記憶體是一個超大的置物櫃,每一格都是一個 byte(一個 byte 可以放得下十六進位的 2 個位數,或是二進位的 8 個位數),你可以這樣理解它:



  可以想像成在記憶體的第 0x0065FE1C 格的位置開始,存放了變數 x 的內容。一般而言,在 32 位元或 64 位元的電腦,一個 int 佔用 4 bytes,因此記憶體位址 0x0065FE1C、0x0065FE1D、0x0065FE1E、0x0065FE1F 這四個 bytes,都屬於變數 x。

  不過實際情況可能還會更複雜,有些系統是把這四個 byte 倒過來存放的,這個因使用者的 CPU 設計而異:



  上面兩例中正序放置的方法,稱為 Big-Endian;反序放置的方法,則稱為 Little-Endian。總之,你只要至少能知道在這個例子裡我們可以看到變數 x 的值被存放在 0x0065FE1C 開始的四個 bytes 裡面就好了。

  當然,如果你不求甚解,只要知道求取 x 的時候指的是 x 變數裡存放的值、求取 &x 的時候指的是 x 在記憶體當中的地址,這樣就已經很足夠。



。傳值(Pass by value)

  在過去談過的函數用法,一般來說我們都是直接把一個數值傳進函數裡,讓它能夠運算。比如說,我們定義了攝氏溫度轉華氏溫度的函數,然後在 main 裡面呼叫它:

float CtoF(float C) {
    return C*9/5+32;
}

int main(void) {
    float m = 30;
    float n = CtoF(m);
    printf("攝氏 %.2f 度等於華氏 %.2f 度\n", m, n);
    return 0;
}

  雖然我們看來是把 m 丟進函式裡,實際上傳進去的東西只有「存放在 m 變數裡的 30」這個數值。那麼,接下來「30」這個數值被傳到函數裡面,電腦把它算完以後把結果傳回來。無論你在 CtoF 函式裡面如何去修改變數的值,這個外面的變數 m 的值依舊是 30,不會有任何改變。

  你可以想像,上例的「CtoF」這個函式只有收到「30」,它並不知道 m 這個變數的地址在哪裡,因此它能做的事情就有限——比如說,我們想要利用函式來「修改」一個原有的變數的值,那麼單憑這樣的寫法就做不到了。

  像這種平時最常見的用法,我們稱之為 pass by value(以值傳遞)。



。指標的宣告

  有了上面兩章節的觀念之後,我們就可以準備實作「以地址來操控變數」這件事了。

  在 C 語言裡面,我們知道 int 用來存放整數、float 用來存放浮點數、char 用來存放字元……不過,如果我們想要用來存放「地址」的話,該用什麼樣的變數呢?我們這麼做:

int main(void) {
    int x = 48763;
    int *p = &x;
    printf("The address of x: %08X\n", p);
}

  沒錯,用來存放 int 類型變數地址的變數,在宣告的時候只要在名稱前面加上一個「*」,用來表示它是專門存放 int 變數的地址,然後把 x 的地址放進去就可以了。上面這段相當於:

int main(void) {
    int x = 48763;
    int *p;
    p = &x;
    printf("The address of x: %08X\n", p);
}

  到目前為止,這裡都跟一般的變數一樣,僅僅是差在宣告的時候前面多了一個 * 字號而已。只要呼叫 p 就可以得到它裡面存放的地址編號,也同樣只要直接用 p = &x; 就可以把 x 的地址存進 p 這個變數。

  既然 p 本身用來儲存別的地址,那麼它當然也會佔用空間。我們使用 &p,同樣也可以找到它的地址:



  那也就說明了記憶體當中的狀態是這樣子的:



  由於這樣一個存放地址的變數,它存在的意義就是用來指向一個變數,所以這種變數就稱為「指標(pointer)」。



。操作指標指向的變數內容

  不過,單就上面這一段還僅僅是前置工作,畢竟你只是把變數的地址存進去而已。

  要想控制指標 p 所指向的變數 x,我們一樣使用「*」符號就可以了,只是這個符號用在這裡的時候,它的意義和宣告指標的時候是不同的:



  在 p 存放了 &x 的情況下,我們對「*p」的任何存取,都相當於是對「x」的操作。所以我們如果執行「*p = 5;」,那這句話的意思就相當於「x = 5;」了。

  在同一個函式裡直接這樣做的話當然不太有意義,一般來說,指標都會用在有函數傳入、傳出的情況。直接舉個實例吧!有的時候,我們會需要把兩個變數的值交換(例如排序演算法),假設現在有一個 x 變數和一個 y 變數,我們希望把它們的值互換,我們可以這麼做:

int main(void) {
    int x = 5, y = 3;
    printf("x: %d, y: %d\n", x, y);
    int temp;
    temp = x;
    x = y;
    y = temp;
    printf("x: %d, y: %d\n", x, y);
}

  不過,要是我們需要做很多次這樣的事,這種寫法實在太麻煩了。有了指標,我們可以直接讓函數對兩個地址的內容進行操作:

void swap(int*, int*);

int main(void) {
    int x=5, y=3;
    printf("x: %d, y: %d\n", x, y);
    swap(&x, &y);
    printf("x: %d, y: %d\n", x, y);
}

void swap(int *x, int *y) {
    int temp;
    temp = *x;
    *x = *y;
    *y = temp;
}



  我們可以看到,實際上我們是把 x 和 y 的地址直接傳進去,然後在函數裡操作它們。要是這種場合下沒有使用指標的功能,那我們是沒辦法把這段 swap 行為寫成函式的(當然如果你硬要用 define 的方式來達成也不是不行啦)。

  你還可以利用陣列(array)一次宣告很多個指標:



  其他的操作方法都一樣。

  像這種把地址傳進函式裡,讓函數能夠參照地址來操作變數,我們可以說它是一種 pass by reference 的實作。不過,這個名詞其實在 C 語言裡還是有點爭論,有人認為既然傳入的是一個地址(address),那就該稱為 pass by address,而 C 語言的發明人則是說 C 語言本身只有 pass by value(只是傳入的是一個 address),大家的講法都不太一樣,所以不要太糾結哪個正確,總之我是都叫它 IKEA 啦。



。指標的指標

  是的,既然指標本身是一個變數,那麼「指標的指標」這種東西也是存在的。我們可以設一個用來儲存指標地址的指標(即雙重指標):





  既然如此,當然也就可以設指標的指標的指標:





  1. 可以看到 ppp 的地址 0x0065FE00 存了 pp 的地址:00 65 FE 08
  2. 可以看到 pp 的地址 0x0065FE08 存了 p 的地址:00 65 FE 10
  3. 可以看到 p 的地址 0x0065FE10 存了 x 的地址:00 65 FE 1C
  4. 可以看到 x 的地址 0x0065FE1C 存了整數 5:00 00 00 05

  看起來很拗口,其實指標就跟一般的變數沒有太大的差別,只是使用的時候要稍微想一下自己在寫什麼,不要指到最後不知道自己在幹嘛。

  指標的應用當然不是只有這篇講的這樣,畢竟它能做的事情太多了,包括動態記憶體分配、多維陣列、連結串列,單單用一篇文章是絕對不可能講完的。我手上的旗標出版社的 C 語言教科書《C 語言教學手冊》光是講指標就花了整整 60 頁的篇幅才講完基礎(這還不包括應用)。

  那麼,對於指標的基本認識,差不多就講到這裡。指標的應用就之後有機會再來說說吧,希望這次的文章有讓你變得更好入眠。



。細節補充

  1. 宣告指標的時候把 * 符號和 int 擺在一起其實也是一種合規的寫法,也就是說「int *p;」和「int* p;」等價。但當我們使用「int* a, b;」的時候,實際上這句話代表的意思是「int *a; int b;」,一個是指標但一個不是,因此在宣告指標的時候我個人偏好讓 * 符號緊鄰變數名稱,如此一來也會比較易於理解。除此之外,多數情況下使用「int *a, *b;」這種方式來宣告多個指標是合法的。

  2. 記憶體圖例中灰色部分不全然都會是 0。一般來說沒有經過賦值的記憶體空間,可能會有先前其他程式使用過的痕跡,稱為「殘值」,這個觀念也是以前講過的。

  3. 正確來說在輸出地址的時候應該要用 %p 而不是 %08X,只是單純為了方便展示而這麼寫。
 

送禮物贊助創作者 !
0
留言

創作回應

⊰⊱求出處學術用⊰⊱
我是覺得宣告時用int* p宣告成整數指標(專門存位址)跟 *p 求整數指標那位址的內容 這樣
2021-03-16 07:34:51
void ***************p
2021-03-16 09:41:06
♙♲⚙\~O_O~/⚙♲♙
int main(const int argc,const char *argv[]){
const char *const str="豬腳要補充const+pointer一起使用的情形嗎?";
printf("%s",str);
}
2021-03-16 18:29:13
七星劍
豬腳大哥 請再要繼續發文經營這個勇者的小屋的喔 day 2 我過來的時候還是能看到許多的讀者在留言的啊 請您繼續的要加油下去的喔 我先說聲 晚安的喔
2021-03-16 19:53:42
露米諾斯 Lumynous
考古一下,其實準確來說應該是物件的位址而非變數。另外 pass by reference 的部分,可以說能夠藉由指標達到這種「語意」,但不該認為那就是 pass by reference,還是要記得傳進去的實際上是一個指標,而不是直接存取到原物件(與 indirection 相對)
2024-02-19 02:14:07

更多創作