【Day 22】C 語言直接讓我放棄的記憶體指標

C 語言直接讓我放棄的記憶體指標

讓我放棄的記憶體指標

在學習 C 語言的過程中, ifforwhile 這些流程控制語法對我來說不算難,因為曾經寫過遊戲腳本,邏輯其實很接近。原以為自己算是挺聰明的,沒想到一個指標就讓我瞬間從天堂墜落,還沒真正起飛就直接殞落。

「前面的各種流程判斷,大家應該都沒問題吧?」老師環顧教室,語氣平淡地問。

教室裡一片寂靜,只有鍵盤敲擊聲零星響起。這種場景在電腦課上很常見,因為大多數人其實都在偷偷打自己的遊戲。

「期中考的時候,會有幾題是關於指標的題目喔!」老師接著補了一句。

「蛤???」我心裡只冒出這一個字,毫無懸念。

蛤(圖源 tenor)

「說明指標之前,我們先來複習一下上週的變數內容,因為變數和指標有一定的關聯……」老師站在講台上開始解說。

聽到「期中考會考指標」這句話,我整個人瞬間慌了,心裡只冒出一句:

「糟了,這下電腦課會不會被當啊?」

我完全搞不懂指標到底是什麼,只好偷偷戳一下旁邊那個跟我一起打《魔獸爭霸》的同學。

他頭也不抬,還在操作英雄放技能,淡淡回一句:「怕屁,不會啦,繼續打《信長》啦!」

我愣了三秒,覺得這傢伙比城牆還穩。

結果……我就真的放下焦慮,繼續操滑鼠跟著衝鋒陷陣,彷彿期中考根本不存在。

但你各位知道嗎?

讀書時期總有一種人特別討厭 —— 每次都嘴上說不會讀、不會看,結果一到考試就穩穩拿下班上前三名。

同學(攤手):「啊~這次我都沒看啦,一定會考爛。」
我(心想):「呵,這次總算有機會贏他了!」

考卷發下來 ——
同學:✨滿分✨
我:……

老師(盯著我):「你,下課後記得來補考。」
我:Σ(°△°|||)

同學:😎「早就說不會啊。」
我:Orz

記憶體指標與位址

在電腦世界中,不管你宣告的是函式、變數,還是陣列,它們最後都會被存放在記憶體裡,而每一個存放的位置都有一個唯一的「位址」,C 語言中的指標,正是用來存取和操作這些位址的工具。

那麼,該怎麼查看變數的位址呢?其實非常簡單 —— 只要在變數前加上 & 符號即可,這個稱之為取址運算子(Address-of Operator),透過它,我們就能直接取得變數在記憶體中的位址。

底下就讓我們看一個非常簡單的範例,來認識一下指標的基本概念:

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main(void) {
int a = 10; // 宣告一個整數變數 a 並賦值為 10

printf("a 的值是: %d\n", a); // 輸出 a 的值
printf("a 的位址是: %p\n", &a); // 輸出 a 的位址
return 0;
}

Note
%p 用來印出記憶體位址,通常會顯示成十六進位數字。
最嚴謹的寫法是 (void*)&a
printf("a 的位址是: %p\n", (void*)&a);
不過大部分情況下 &a 也能正常印出。

執行之後,你可能會看到以下訊息:

1
2
a 的值是: 10
a 的位址是: 0x16dd6eac8

這裡的 0x16dd6eac8 就是變數 a 在記憶體中的位址,順帶一提,你看到的位址不一定跟我一樣,因為每次程式跑的時候,記憶體分配都可能不一樣喔!

對前面讀過二進位或十六進位的朋友來說, 0x16dd6eac8 應該不陌生,需要注意的是,這並不是說記憶體真的用十六進位存,而是因為位址本質上是一個數字,只是用十六進位表示會比較精簡,因此通常會以十六進位來顯示。

那麼為什麼要理解指標呢?

因為指標可以讓我們直接操作記憶體中的位址,舉例來講,我們可以將 a 的位址存放到一個指標變數中,這樣我們就可以透過這個指標來存取 a 的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

int main(void) {
int a = 10; // 宣告一個整數變數 a 並賦值為 10
int *p = &a; // 宣告一個指標變數 p,並將 a 的位址存放到 p 中

printf("a 的值是: %d\n", a); // 輸出 a 的值
printf("a 的位址是: %p\n", &a); // 輸出 a 的位址
printf("p 指向的位址是: %p\n", p); // 輸出 p 指向的位址
printf("p 指向的值是: %d\n", *p); // 使用解引用運算子 (*) 取得 p 指向的值

return 0;
}

執行之後,你可能會看到以下訊息:

1
2
3
4
a 的值是: 10
a 的位址是: 0x16f0c6ac8
p 指向的位址是: 0x16f0c6ac8
p 指向的值是: 10

這代表什麼意思呢?我們可以直接修改 p 指向的值,這樣就可以間接地修改 a 的值:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main(void) {
int a = 10; // 宣告一個整數變數 a 並賦值為 10
int *p = &a; // 宣告一個指標變數 p,並將 a 的位址存放到 p 中

printf("修改前 a 的值是: %d\n", a); // 輸出修改前 a 的值
*p = 20; // 使用解引用運算子修改 p 指向的值
printf("修改後 a 的值是: %d\n", a); // 輸出修改後 a 的值

return 0;
}

接著執行之後,你就會看到以下訊息:

1
2
修改前 a 的值是: 10
修改後 a 的值是: 20

這樣我們就成功地透過指標修改了 a 的值。

如果你是 JavaScript 開發者,應該會覺得「欸,這不就跟物件、陣列的參考差不多嗎?」沒錯,概念上有點像。

差別在於 —— JS 的參考就像小朋友用安全剪刀,最多剪不直;但 C 的指標是直接給你一把武士刀,你一不小心就可能把程式砍爆,所以阿,指標很強大,但用起來真的要非常小心。

這裡要特別注意一點,在 C 語言中,如果要作為指標使用,必須要先宣告一個指標變數,並且使用 * 符號來表示這是一個指標類型的變數,這也是前面尚未提到的重要部分。

接著你可能會好奇:

「如果我沒有使用 *& 符號,會發生什麼事情呢?」

很好的問題,我們不如來實驗看看比較快:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main(void) {
int a = 10; // 宣告一個整數變數 a 並賦值為 10
int p = a; // 宣告一個整數變數 p,並將 a 的值賦給 p

printf("修改前 a 的值是: %d\n", a); // 輸出修改前 a 的值
p = 20; // 嘗試修改 p 的值
printf("修改後 a 的值是: %d\n", a); // 輸出修改後 a 的值

return 0;
}

基本上你會看到以下訊息:

1
2
修改前 a 的值是: 10
修改後 a 的值是: 10

因為我們是將 a 的值也就是 10 賦值給 p,所以當我們修改 p 的值時,並不會影響到 a 的值。

那如果將 a 的位址賦值給 p,然後再修改 p 的值,會發生什麼事情呢?我們來試試看:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main(void) {
int a = 10; // 宣告一個整數變數 a 並賦值為 10
int p = &a; // 宣告一個整數變數 p,並將 a 的位址賦給 p

printf("修改前 a 的值是: %d\n", a); // 輸出修改前 a 的值
p = 20; // 嘗試修改 p 的值
printf("修改後 a 的值是: %d\n", a); // 輸出修改後 a 的值
return 0;
}

不出意外的話,現在就會看到錯誤訊息了:

1
2
3
4
main.c:4:9: error: incompatible pointer to integer conversion initializing 'int' with an expression of type 'int *'; remove & [-Wint-conversion]
4 | int p = &a; // 宣告一個整數變數 p,並將 a 的位址賦給 p
| ^ ~~
1 error generated.

這是因為位址必須要使用指標類型的變數來存放,而我們在這裡使用了 int 類型的變數 p,因為指標是一種特殊的型別,不同於整數,所以必須用 int *p 來宣告。

是不是感覺滿有趣的(並沒有)?

指標對我來說,就像 C 語言裡的 Final Boss。
問題是……我還沒練滿等級,連裝備都沒齊,就直接衝進去挑戰,結果當然是被一招秒殺。
那一刻我只能心裡默念:「Game Over… QQ」

由此可知,沒天分沒關係,上課還是要認真聽課,打不贏魔王沒差,至少別被路邊的史萊姆打爆。

結語

不知道大家以前在學時期有沒有那種不讀書也能考高分的同學?

還是說你就是那個同學呢?

同步更新

本文將同步更新至以下網站: