這一堂終於來到 C 語言裡被稱為最難的東西:指標。不過不用太緊張,前幾堂其實已經偷偷把這個概念帶給大家了:scanf 要加 &、陣列名稱是開頭的記憶體位置、* 可以把位置上的值拿出來。這份講義整理 Vol. 20 直播的內容,把這些零散的東西一次串起來。沒看過前面幾堂的話,建議先讀 #3 的講義,尤其是「陣列名稱其實是記憶體位置」那一節。
這是 2021 年 9 月的內容。工具一樣用 Dev-C++,換成其他 IDE 也可以,這裡教的語法跟工具沒有關係。直播把記憶體位置用
%d印成十進位的整數,講義的範例改用正式的%p,原因在後面說明。
因為知道這個主題很燒腦,這一堂的簡報特別少,只講三件事:
- 指標是什麼:存記憶體位置的變數,
&和*兩個運算 - 指標用在哪:偷看下週的函式,以及陣列和指標的關係
- 動態記憶體配置:
malloc和free,再延伸到指標陣列、指標的指標
整堂課如果只記得一句話,就記這句:指標也是一種變數,它存的東西是記憶體位置。
一、指標是什麼
記憶體位置也是一種資料
之前一直強調:跟記憶體要一個變數的時候,電腦會在記憶體裡找一塊空間,這塊空間在整個記憶體裡有一個位置(編號)。前幾堂我們用各種方式把這個位置印出來,玩了不少小技巧,也知道它就是一個數字。
程式裡處理的資料有整數、浮點數、字元、字串。那記憶體位置能被我們拿來操作,它算不算是一種資料?
算。所以:
- 整數變數存整數
- 浮點數變數存浮點數
- 「記憶體位置變數」存記憶體位置
這種存記憶體位置的變數比較特別,我們把它叫做指標(pointer)。它不是特別設計出來的新東西,就是一種變數;只是因為存的是位置,用起來會變得有點複雜。
什麼時候會用到
一般宣告一個整數變數,直接用它的名字就好了,為什麼還要特地去拿它的位置?通常用到指標,表示你手上有一塊空間,但沒辦法用名字拿到它:
- 動態記憶體配置:跟記憶體要一塊空間,但不幫它命名,只用指標記住它在哪。這一堂的後半會講。
- 函式之間操作資料:下一堂講函式時會真正用到,今天會先偷看一下。
- 資料管理:比較進階,跟怎麼存放資料的結構有關。
順便給兩個名詞:分析怎麼儲存資料的結構,叫資料結構;解決問題的方法,叫演算法。之前講的選擇排序法就已經有點這個味道了。這兩門是比較進階的課,入門階段先專心認識程式語言有哪些功能。
宣告、取址、取值
用一張圖來看。宣告一個整數變數 a,裡面存 70,假設它在記憶體裡的位置是 1000:
| 變數 | 位置 | 存的東西 |
|---|---|---|
int a |
1000 | 70 |
int *b |
(另外某個位置) | 1000 |
- 在型態後面加一個
*,例如int *b,意思是b是一個「存整數變數位置」的變數,也就是整數指標。 - 用
&a可以拿到a的位置,也就是 1000,把它存進b。這叫取址。 - 對
b做*,也就是*b,會拿著b裡存的 1000 回去找,找到的就是a那塊空間,所以會拿到 70。這叫取值。
& 和 * 是相反的運算:& 從變數拿到位置,* 從位置走回那塊空間。
b 本身也是一個變數,所以它自己也存在記憶體的某個位置,只是那個位置現在不是重點。
為什麼指標還要分「整數的」指標、「浮點數的」指標?因為只知道位置不夠。拿到 1000 之後,要從那裡抓多大的空間、抓回來要當成什麼資料,都得靠型態才知道。知道是整數指標,就知道要從 1000 開始抓 4 個 byte,當成整數來用。
*b 拿到的是那塊空間本身,所以不只能讀,也能寫:
y = *b + 1;:拿出 70,加 1 之後存進y*b = 80;:把 80 存進b指向的那塊空間,也就是a會變成 80
實際寫寫看
一樣複製之前寫過的程式,把中間挖空再開始寫,不用背骨架:
#include <stdio.h>
int main(){
int x;
int *p;
printf("請輸入一個整數:");
scanf("%d", &x);
p = &x; // 把 x 的位置存進 p
printf("x = %d\n", x);
printf("x 的位置 &x = %p\n", (void *)&x);
printf("p 存的位置 p = %p\n", (void *)p);
printf("p 指向的值 *p = %d\n", *p);
*p = 10; // 把 10 放進 p 指向的那塊空間
printf("改了 *p 之後,x = %d\n", x);
return 0;
}
輸入 5,其中一次執行的結果(位置每台電腦、每次執行都不一樣):
請輸入一個整數:5
x = 5
x 的位置 &x = 0x7fffdeb1b03c
p 存的位置 p = 0x7fffdeb1b03c
p 指向的值 *p = 5
改了 *p 之後,x = 10
- 輸入的 5 存在
x裡,x有自己的位置。 p是整數指標,把x的位置交給它,所以&x和p印出來一樣。- 拿著這個位置回去找,
*p找到的就是x,所以是 5。 - 最後改的明明是
*p,x卻變成 10,因為p指的那塊空間就是x。
直播是用
%d把位置印成十進位的整數(例如 6487572),比較好讀,但這不是正確寫法。印記憶體位置要用%p,並把指標轉型成(void *),印出來是 16 進位。64 位元電腦上的位置是 8 個 byte,int不一定放得下。
地址的比喻
很多人講指標時會用地址來比喻。你家附近的麥當勞,你平常就叫它「家附近那間麥當勞」,但它也有一個地址,例如新北市新店區某某路幾號。這兩個講法指的是同一個地方。
平常用名字找就好;但有時候需要一個絕對的位置,才知道那間店到底在哪。指標就像一張專門寫地址的紙,把地址寫下來,之後就能拿著地址找到那間店。
聊天室問:「過程不一樣,但結果是一樣的,這樣的意思嗎?」是的。用 x 或用 *p,走的路不一樣,到的是同一塊空間。
二、指標用在哪:偷看下週的函式
變數有生存範圍
記憶體位置是唯一的。之前講過一個例子:有位教授把變數的記憶體位置印出來,再用遊戲修改大師去改那個位置上的值,程式裡的變數也就跟著被改掉了。
但變數的名字不是唯一的。在大括號裡宣告的變數,名字只活在那對大括號裡:
#include <stdio.h>
int main(){
int x;
scanf("%d", &x);
if (x > 2) {
int n = 3; // n 只活在這對大括號裡
printf("大括號裡面:n = %d\n", n);
}
// printf("%d\n", n); // 拿掉註解就編譯不過:n 沒有宣告
return 0;
}
輸入 5 會印出 大括號裡面:n = 3。如果把最後那行 printf 的註解拿掉,編譯器會說 n 沒有被定義。
一離開大括號,程式就會把 n 那塊空間收回去,外面就不存在 n 了。所以一個名字只能在某一段時間、某個範圍裡代表某塊記憶體空間。
同名,但不是同一個變數
下一堂才會正式講函式,這裡先簡單帶一下,讓你知道指標可以用在哪。函式就是把常用的運算獨立出來。例如做一個叫 sum 的程式片段,給它兩個整數,它把總和回傳給你:
#include <stdio.h>
int sum(int a, int b){
a = a + 2; // 想讓 a 先加 2
printf("sum 裡的 a = %d\n", a);
return a + b;
}
int main(){
int a = 2, b = 3;
int result;
result = sum(a, b);
printf("main 的 a = %d\n", a);
printf("result = %d\n", result);
return 0;
}
sum 裡的 a = 4
main 的 a = 2
result = 7
int sum(int a, int b):sum要接兩個整數,命名成a、b,最後回傳一個整數。sum(a, b)這一整段,會被它回傳的值蓋掉。2 加 2 再加 3,所以是 7。
總和是 7,表示 a 確實有被加 2;但 main 裡的 a 還是 2。為什麼?
因為 sum 的 a 是在 sum 的大括號裡宣告的,只活在那裡;main 的 a 是在 main 的大括號裡宣告的。可以想成「main 王國的 a」和「sum 王國的 a」,就像不同班級裡剛好有同名的同學,其實是兩個人。
呼叫 sum(a, b) 時,做的事比較像是「sum 的 a = main 的 a」、「sum 的 b = main 的 b」:把值複製過去,而不是把變數本身交過去。
為什麼要這樣設計?寫程式要分工,大家各寫一些小函式。如果名字不能重複,你得先看別人用過哪些名字才能取名,程式會超級難寫。
傳位置過去
可是如果真的想讓 main 的 a 被加 2 呢?名字被隔開了,那就用大家都認得的東西:記憶體位置。就像遊戲修改大師,只要有位置,從外面也改得到。
#include <stdio.h>
int sum(int *a, int b){
*a = *a + 2; // 改的是 a 指向的那塊空間
return *a + b;
}
int main(){
int a = 2, b = 3;
int result;
result = sum(&a, b); // 把 a 的位置傳過去
printf("main 的 a = %d\n", a);
printf("result = %d\n", result);
return 0;
}
main 的 a = 4
result = 7
- 傳過去的是
&a,也就是位置,所以sum那邊要用整數指標int *a來接。 sum裡的a現在存的不是 2,而是main的a的位置。要加 2 的是那個位置上的值,所以寫*a = *a + 2。- 算總和時也一樣要寫
*a + b,直接寫a + b是拿位置去加,不對。
聊天室問:為什麼選擇用指標,而不是 reference?因為 reference(參考)是 C++ 才加進來的東西,在 C 語言裡能用的就只有指標。
這就是指標讓人覺得複雜的地方:程式拆成很多小函式之後,要改到外面的東西,唯一能溝通的就是記憶體位置;什麼時候該傳位置、什麼時候不用,就得自己想清楚,程式裡也會到處都是 *。後來的程式語言大多想辦法不讓你直接碰記憶體位置,這是後來的共識。這部分下一堂會講得更詳細。
宣告時和使用時意思不同的符號
有些符號在宣告時和在使用(寫在運算式裡)時,意思不一樣:
中括號 []
- 宣告時:
int a[10];是要 10 格的空間。 - 使用時:
a[4]是拿編號 4 的那一格,也就是第五格。
米字號 *
- 宣告時:
int *p;是宣告一個存記憶體位置的變數,也就是指標。 - 使用時有兩種意思:
- 從位置走到那塊空間:
*p。*前面不是一個值、後面接著一個變數(通常是指標)時,就是這個意思。 - 乘號:夾在兩個值中間,例如
a * b。
- 從位置走到那塊空間:
所以 int *p = &x; 的 * 和 *p = 10; 的 * 意思不一樣:前者是說 p 是指標,後者是把 p 存的位置轉成它指向的那塊空間。這部分建議自己動手試,看哪裡不懂再問。
三、指標和陣列
指標可以存陣列的位置
#3 講過,陣列名稱代表陣列開頭的位置。既然是位置,就能存進指標裡:
#include <stdio.h>
int main(){
int a[5] = {10, 20, 30, 40, 50};
int *p;
int i;
p = a; // 陣列名稱就是位置,不用加 &
for (i = 0; i < 5; i++) {
printf("p[%d] = %d,*(p + %d) = %d\n", i, p[i], i, *(p + i));
}
p = &a[2]; // 指標可以換成指向別的地方
printf("換位置之後 p[0] = %d\n", p[0]);
return 0;
}
p[0] = 10,*(p + 0) = 10
p[1] = 20,*(p + 1) = 20
p[2] = 30,*(p + 2) = 30
p[3] = 40,*(p + 3) = 40
p[4] = 50,*(p + 4) = 50
換位置之後 p[0] = 30
p是整數指標,指向一塊整數空間,而它現在指的正是陣列的第一格。- 要拿後面的格子,一樣用中括號。#3 講過,中括號就是「往後跳幾格,再取值」的簡寫:
p[i]就是*(p + i)。
陣列和指標一樣的地方是:都代表某一塊資料的記憶體位置。不一樣的是:陣列代表的位置不能換;指標就是一個變數,裡面存哪個位置可以換。上面最後讓 p 改指向 a[2],p[0] 就變成 30。
再整理一次:資料櫃的比喻
聊天室有人說「聽了十分鐘還是覺得好難」。確實難,但概念跟大家熟悉的都一樣,再換個比喻講一次。
想像有一排資料櫃,要存學生的分數。某個同學的分數 70,放在 B3 那一櫃。
- 要知道他的分數,可以直接拿分數;也可以記住「在 B3 那一櫃」。
- 對 B3 做加減乘除有意義嗎?沒有。B3 只是為了拿到資料的一個記號,你在意的是裡面的 70。
- 拿著 B3,就走到 B3 那一櫃,把抽屜拉出來,看到裡面的成績單寫著 70。B3 要轉成它上面存的東西才有意義。
對應到程式:
&:對那一櫃取&,會告訴你櫃子的編號 B3。- 存 B3 要用相對應的型態:它不是分數,是「櫃子的位置」,所以要用存位置的變數,也就是指標。
*:拿著 B3,走過去把那一櫃拉出來。拉出來以後,要讀裡面的分數、把它加 1,還是換成 80,都隨你。
這裡說「位置做加減沒有意義」,指的是單一個變數的位置。如果那個位置是陣列的開頭,加 1 就是「往後跳一格」,這就有意義了,上一節的
p + i就是這樣用的。
休息聊天
休息時聊到,學 C# 寫遊戲的人大多不會碰到指標,C# 已經把指標藏起來了;近代的程式語言裡,會直接動到指標的機會很低。所以現在聽不懂也沒關係,但理解這個概念還是有好處。
也聊到之後的課程規劃:先用最貼近電腦的 C 把基礎打好,之後如果開 C#、JavaScript 之類的課,就只講語法上怎麼寫、有什麼差異,因為迴圈這些概念都已經會了。
四、動態記憶體配置
為什麼需要
之前所有的宣告,陣列也好、變數也好,大小都要先固定。上一堂的字串也是:字串有一個長度,但我們永遠要宣告一個比它更大的空間來存。
能不能等使用者告訴我們要多少,再去要剛好的空間?可以,這就是動態記憶體配置。這塊空間是你自己要來的,所以也要自己管理它。
先看問題:陣列不夠大
讓使用者輸入 n 個整數,算出總和。先用以前的方法,宣告 5 格的陣列:
#include <stdio.h>
int main(){
int arr[5];
int n, i, sum;
printf("請輸入個數:");
scanf("%d", &n);
for (i = 0; i < n; i++) {
printf("輸入第 %d 個值:", i);
scanf("%d", &arr[i]);
}
sum = 0;
for (i = 0; i < n; i++) {
sum = sum + arr[i];
}
printf("總和是 %d\n", sum);
return 0;
}
輸入 3、1 2 3,會印出 總和是 6,沒有問題。
但個數輸入 10 呢?直播輸入到第 6 個,程式就炸了。陣列只有 5 格,怎麼可能存 10 個值?多出來的會寫到陣列外面去。
那開超大,1000 格總行了吧?可是如果實際上一直都只用 10 個值,每次都跟記憶體要一大堆用不到的空間,這樣不好。
用 n 宣告陣列
比較新的 C(C99 之後)可以直接用變數當陣列大小,叫做可變長度陣列,寫起來很直覺:
#include <stdio.h>
int main(){
int n, i, sum;
printf("請輸入個數:");
scanf("%d", &n);
if (n <= 0) { // 自己擋掉不可能的情況
printf("個數要大於 0\n");
return 0;
}
int arr[n]; // 用 n 當大小
for (i = 0; i < n; i++) {
printf("輸入第 %d 個值:", i);
scanf("%d", &arr[i]);
}
sum = 0;
for (i = 0; i < n; i++) {
sum = sum + arr[i];
}
printf("總和是 %d\n", sum);
return 0;
}
不過有幾件事要注意:
- 使用者輸入 -1 怎麼辦?跟電腦要 -1 格,不知道會發生什麼事,所以前面要自己用
if擋掉不可能的情況。這點用動態配置也一樣。 - 這種陣列有大小上限。直播試著要一百萬格,程式就炸了。
補充:上限是多少要看環境,這種陣列跟一般變數放在同一個地方(後面會提到的 stack),空間不大,常見的預設值只有 1 到 8 MB,一百萬個
int就要 4 MB。另外,C11 把可變長度陣列改成編譯器可以不支援的選用功能,C++ 標準也不支援,所以接下來的malloc才是通用的做法。
malloc:跟記憶體要一塊沒有名字的空間
要動態配置,就用指標加上 malloc 這個函式。名字來自 memory allocation,記憶體配置。用之前要 #include <stdlib.h>。
int *pointer = (int *)malloc(sizeof(int) * n);
- 小括號裡寫要多大的空間,單位是 byte。一個整數多大?#3 看過,用
sizeof(int);要 n 個,就乘上 n。 malloc要到空間之後,會回傳那塊空間的記憶體位置。這塊空間要存整數、浮點數、字元都隨你。- 我們要拿來存整數,所以把這個單純的位置轉型成整數指標
(int *),再存進pointer。
為什麼要轉型?跟陣列一樣,只有位置的話,不知道下一格在哪裡;知道型態,才知道一格多大、跳一格要跳多遠。轉成整數指標之後,pointer 就能當成陣列來用,用中括號拿第幾格。
C 語言裡
(int *)這個轉型其實可以不寫:存進整數指標變數的時候會自動轉型。直播還是寫出來,讓意思明確一點。存成.cpp用 C++ 編譯的話就一定要寫。
把上面的程式改成 malloc 版本:
#include <stdio.h>
#include <stdlib.h>
int main(){
int *pointer;
int n, i, sum;
printf("請輸入個數:");
scanf("%d", &n);
pointer = (int *)malloc(sizeof(int) * n); // 要 n 個整數的空間
for (i = 0; i < n; i++) {
printf("輸入第 %d 個值:", i);
scanf("%d", &pointer[i]); // 也可以寫 pointer + i
}
sum = 0;
for (i = 0; i < n; i++) {
sum = sum + pointer[i]; // 也可以寫 *(pointer + i)
}
printf("總和是 %d\n", sum);
free(pointer); // 用完要還回去
return 0;
}
輸入 5、1 2 3 4 5,會印出 總和是 15;輸入 10 也沒問題了。
只是把原本的 arr 換成 pointer,其他都一樣。看一下 scanf 那行:
pointer[i]就是*(pointer + i):從pointer存的位置往後跳 i 格(整數一格是 4 byte,所以每跳一格加 4),再取出那一格的整數空間。scanf要的是位置,所以再取一次&:&pointer[i]就是&*(pointer + i)。- 先
*再&,等於繞一圈又回來,所以&pointer[i]就等於pointer + i,兩種寫法都對。
用完要 free
用一般方式宣告的變數,離開它的生存範圍(大括號)就會被自動釋放。
但 malloc 要來的空間本來就沒有名字,我們只是用 pointer 記住它的位置。它不知道自己的生存範圍在哪,就算離開了大括號,這塊空間也還是被你佔著。所以要記得自己用 free 釋放,給它的是當初 malloc 回傳的那個開頭位置。
空間很小的時候感受不到差別,用到很大的空間時就會開始出問題。
動態配置還有一個好處:可以要比較大的空間。剛剛用 n 宣告陣列要一百萬格會被拒絕,用 malloc 就要得到。
一般宣告的變數和 malloc 要來的空間,存放的區塊不一樣:一般宣告的放在叫 stack 的區域,動態配置的放在叫 heap 的區域。這是比較進階的概念,有興趣可以用這兩個關鍵字查查看。
用 malloc 也能只要一個整數(乘 1 就好),但這樣很沒意義,直接宣告一個 int 就好了。
補充:檢查 malloc 有沒有要到
上面的程式是直播上課的寫法,照著寫就能動。真正寫程式時,還有兩件事要顧:
- 個數要先檢查,跟前面可變長度陣列一樣,0 或負數要自己擋掉。
malloc不一定要得到空間。要不到的時候,它會回傳NULL(空指標,不指向任何地方),這時不能拿來用。
#include <stdio.h>
#include <stdlib.h>
int main(){
int *pointer;
int n, i, sum;
printf("請輸入個數:");
if (scanf("%d", &n) != 1 || n <= 0) {
printf("個數要是正整數\n");
return 1;
}
pointer = (int *)malloc(sizeof(int) * n);
if (pointer == NULL) { // 沒要到空間
printf("記憶體不夠\n");
return 1;
}
for (i = 0; i < n; i++) {
printf("輸入第 %d 個值:", i);
scanf("%d", &pointer[i]);
}
sum = 0;
for (i = 0; i < n; i++) {
sum = sum + pointer[i];
}
printf("總和是 %d\n", sum);
free(pointer);
return 0;
}
輸入 3、4 5 6 會印出 總和是 15;個數輸入 -1 會印出 個數要是正整數。
五、指標陣列和指標的指標
這部分比較進階,直播也說不是今天的重點,先有個印象就好。直播講這段時投影片沒切過去,後來又補講了一次,講義照投影片的內容整理。
指標陣列
指標也是變數,所以也能有很多個,也就是陣列。int *a[3]; 是三格「存整數位置」的變數,a[0]、a[1]、a[2] 各自都是一個整數指標。
每一個指標都能各自去 malloc 一塊空間,而且大小可以不一樣,例如一格、兩格、三格,就會變成一個長得很「畸形」的陣列:
#include <stdio.h>
#include <stdlib.h>
int main(){
int *a[3]; // 三個整數指標
int i, j;
for (i = 0; i < 3; i++) {
a[i] = (int *)malloc(sizeof(int) * (i + 1)); // 分別要 1、2、3 格
for (j = 0; j <= i; j++) {
a[i][j] = j + 1;
}
}
for (i = 0; i < 3; i++) {
for (j = 0; j <= i; j++) {
printf("%d ", a[i][j]);
}
printf("\n");
}
for (i = 0; i < 3; i++) {
free(a[i]);
}
return 0;
}
1
1 2
1 2 3
a[i] 用中括號拿到第 i 個指標,那個指標再用中括號跳幾格拿到裡面的值,所以 a[i][j] 看起來就很像二維陣列。
指標的指標
指標是一個變數,所以它自己也有記憶體位置。那能不能有一個變數,存「整數指標」的位置?可以,型態寫成 int **:
#include <stdio.h>
int main(){
int x = 70;
int *p = &x; // p 存 x 的位置
int **pp = &p; // pp 存 p 的位置
printf("x = %d\n", x);
printf("*p = %d\n", *p);
printf("**pp = %d\n", **pp); // 先走到 p,再走到 x
**pp = 80;
printf("改了 **pp 之後,x = %d\n", x);
return 0;
}
x = 70
*p = 70
**pp = 70
改了 **pp 之後,x = 80
拆開來看:int ** 就是「int *」的指標,存的是整數記憶體位置變數的記憶體位置。三個、四個、五個 * 也都可以,一直遞迴下去。
用 int ** 做出二維的空間
一層的指標可以撐起一個像陣列的空間,兩層就能撐起二維的空間:先 malloc 一層「整數指標」的空間,再讓每個指標各自 malloc 一層整數的空間。
#include <stdio.h>
#include <stdlib.h>
int main(){
int **student;
int n, i, j;
printf("請輸入 n:");
scanf("%d", &n);
student = (int **)malloc(sizeof(int *) * n); // n 個整數指標
for (i = 0; i < n; i++) {
student[i] = (int *)malloc(sizeof(int) * n); // 每個指標再要 n 個整數
}
for (i = 0; i < n; i++) {
for (j = 0; j < n; j++) {
student[i][j] = i * n + j;
}
}
for (i = 0; i < n; i++) {
for (j = 0; j < n; j++) {
printf("%2d ", student[i][j]);
}
printf("\n");
}
for (i = 0; i < n; i++) {
free(student[i]); // 先還每一列
}
free(student); // 再還那一排指標
return 0;
}
輸入 3:
請輸入 n:3
0 1 2
3 4 5
6 7 8
這樣就有 n 乘 n 格的空間可以用,用起來就像二維陣列。如果有三個 *,就有點像三維陣列。
指標的大小都一樣
注意第一個 malloc 寫的是 sizeof(int *):要的是 n 個「整數指標」的空間,不是 n 個整數。
不管是整數的位置、浮點數的位置還是字元的位置,記憶體位置本身都是同一種編號,所以大小都一樣。多大要看是 32 位元還是 64 位元的程式:
#include <stdio.h>
int main(){
printf("sizeof(int *) = %zu\n", sizeof(int *));
printf("sizeof(double *) = %zu\n", sizeof(double *));
printf("sizeof(char *) = %zu\n", sizeof(char *));
printf("sizeof(int **) = %zu\n", sizeof(int **));
return 0;
}
64 位元的程式會印出四個 8;32 位元的程式則是四個 4。
六、Q&A:存成 .cpp 找不到 malloc
聊天室有人打了 malloc 的程式卻跑不出來。原因是檔案存成 .cpp,會用 C++ 的方式編譯,這時一定要 #include <stdlib.h>,加上去就找得到了。
直播提到存成 .c 時不一定要 include。舊的 C 編譯器確實會放過,但這是已經被標準拿掉的寫法,新的編譯器會警告甚至直接報錯。用到 malloc、free 就一律 #include <stdlib.h>。
七、畫圖再講一次
最後用滑鼠畫圖,把整堂再走一遍。下面的位置 1000、1004 都是舉例用的數字。
變數和指標
int a;:在記憶體生出一塊整數空間,假設在 1000。a = 2;讓裡面存 2。int *b;:b也是一個變數,在某個別的位置。- 寫
a拿到的是裡面的值 2;寫&a拿到的是位置 1000。 b = &a;:把 1000 存進b,所以b的值是 1000。*b:拿著 1000 去找那個位置上是誰,找到a,拿到 2。*b = 3;:走到 1000 那塊空間,把 3 放進去。之後問a或*b都是 3。- 但
&a還是 1000,b裡存的也還是 1000,位置不會變。
陣列和指標
int a[5];:要 5 格空間,假設從 1000 開始,a就代表 1000。一個整數佔 4 個 byte,所以後面幾格在 1004、1008、1012、1016。a[0] = 5;、a[2] = 3;:分別放進第 0 格和第 2 格。int *b = a;:b裡存 1000,*b就是 5。b + 1:b是整數指標,跳一格要跳一個整數的大小,所以是 1004,不是 1001。*(b + 1)就是 1004 那格的值;而*加上+的組合,簡寫就是中括號b[1]。
malloc 要來的空間
malloc(sizeof(int) * 5):sizeof(int)是 4,所以其實是跟記憶體要 20 個 byte,假設是 1000 到 1019。- 這時它只知道總共多大,不知道裡面要怎麼切格子。
- 轉型成
int *之後,才知道要 4 個 byte 4 個 byte 地切,切成 1000、1004、1008、1012、1016 五格。 - 有了這種切法,對指標加 1 時才會跳對位置。
把上面的圖寫成程式,看看位置之間差了多少:
#include <stdio.h>
#include <stdlib.h>
int main(){
int a = 2;
int *b = &a;
int arr[5] = {5, 6, 3, 0, 0};
int *c = arr;
int *m;
*b = 3;
printf("a = %d,*b = %d\n", a, *b);
printf("&a 和 b 一樣嗎?%d\n", &a == b);
printf("*c = %d,*(c + 1) = %d,c[1] = %d\n", *c, *(c + 1), c[1]);
printf("c + 1 比 c 多了 %d 個 byte\n", (int)((char *)(c + 1) - (char *)c));
m = (int *)malloc(sizeof(int) * 5); // 20 個 byte,切成 5 格整數
printf("malloc 要了 %d 個 byte\n", (int)(sizeof(int) * 5));
printf("m + 1 比 m 多了 %d 個 byte\n", (int)((char *)(m + 1) - (char *)m));
free(m);
return 0;
}
a = 3,*b = 3
&a 和 b 一樣嗎?1
*c = 5,*(c + 1) = 6,c[1] = 6
c + 1 比 c 多了 4 個 byte
malloc 要了 20 個 byte
m + 1 比 m 多了 4 個 byte
((char *) 轉型是為了以 byte 為單位算兩個位置差多少,看結果就好。)
小複習
- 指標是一種變數,存的東西是記憶體位置;
int *p宣告一個整數指標。 &x拿到x的位置;*p從p存的位置走回那塊空間,可以讀也可以寫。兩者是相反的運算。- 指標要有型態,才知道從那個位置要抓多大、當成什麼資料。
- 印位置用
%p,並轉型成(void *)。 - 大括號裡宣告的變數,離開大括號就不見了;同名的變數在不同大括號裡是不同的變數。想讓函式改到外面的變數,就把位置傳過去。
*和[]在宣告和使用時意思不同;*夾在兩個值中間時是乘號。- 陣列名稱是位置,可以存進指標;
p[i]就是*(p + i),&p[i]就是p + i。陣列的位置不能換,指標可以。 malloc(sizeof(型態) * 個數)動態要一塊空間,轉型成對應的指標就能當陣列用;用完要free,記得#include <stdlib.h>。- 指標陣列、
int **可以做出像二維陣列的空間;所有指標的大小都一樣。
課後
今天如果試不出來沒關係,下一堂會實際用到指標,到時候會比較能理解它在做什麼。今天的程式可以自己打一遍、改改看,例如把 *p = 10 換成別的運算,看看 x 會變成什麼。
有寫作業想分享的話,截圖發到 Twitter,加上 tag #悠太作業。有問題可以在下一堂的聊天室、棉花糖或 Discord 群組問。
下一堂預告:函式
下一堂講函式,今天偷看的 sum 會正式登場,也會看到指標除了動態記憶體配置以外更多的用途。
再下一週剛好是中秋節,打算來做一個烤肉小遊戲的直播:把肉放上烤盤、快焦之前翻面、烤好端給客人。所以函式之後的 C 語言課可能會往後延。