大魔王指標:初學者的天堂路。
指標(Pointer)是 C 語言裡的「大魔王」,是資工系學生花了至少 9 小時上的課。我們一起用 18 分鐘文章快速了解指標的基本概念,中間在字串的部分我將結果和程式碼做對照,希望有助於理解。最後,我會將我跟 ChatGPT 對話放上來跟大家一起學習,希望能解決跟我一樣腦筋快打結的學生(╯°□°)╯︵ ┻━┻
每個變數的值都儲存在記憶體裡,記憶體位置有個唯一的「地址」(Address)。指標 (Pointer)就是一種特殊的變數,用來儲存這些「地址」。
malloc()
和 free()
函數,指標可以分配和釋放記憶體,靈活控制記憶體的大小。在了解指標之前,我們要先了解記憶體跟十六進位,方便之後解釋。
在電腦科學中,我們常常使用不同的數字系統來表示數據。你可能已經熟悉十進位(Decimal),也就是我們日常生活中使用的數字系統,數字從0到9。然而,在電腦的世界裡,除了二進位(Binary)0 和 1 之外,還有另一種非常重要的數字系統,那就是十六進位(Hexadecimal)。
十六進位是以16為基數的數字系統,數字從0到9之後,接著會使用字母A到F來代表額外的值:
電腦實際上是以二進位(Binary)來處理數據的,只有0和1兩個數字。然而,當我們處理大量數據時,用二進位來表示數字會變得非常長且難以閱讀。十六進位能夠幫助我們更簡單地表示二進位的數字,因為每個十六進位的數字可以剛好表示4個二進位的位元(bits)。
例如:
你可能在網頁設計或圖片編輯軟體中看過類似於 #FF5733
這樣的顏色代碼。這些顏色代碼通常是使用十六進位來表示RGB顏色值。每一對數字(或字母)代表紅(R)、綠(G)、藍(B)三個顏色的強度。
例如,#FF5733
中的:
在電腦的記憶體中,每個資料都儲存在一個特定的位置上,這些位置被稱為記憶體地址。由於電腦中的記憶體地址通常是很大的數字,使用十六進位來表示這些地址可以更簡潔和容易辨認。例如,記憶體地址 0x1A3F
就比用長長的二進位來表示更方便。
在網路通訊中,MAC 地址(Media Access Control Address)也是用十六進位表示的,通常會以六對兩個字母或數字的形式出現,例如 00:1A:2B:3C:4D:5E
。這些值幫助網路設備之間進行唯一識別。
每個變數都有自己的記憶體地址。當我們宣告指標變數 p 時,p 儲存的是某個變數的地址(例如 10 的地址)。
但一般我們不需要關心 p 自己的地址或它儲存的具體數值,因為 p 主要是用來幫我們找到 10 這個值。
在程式上的解釋:
資料型別*
來定義指標變數,例如 int* ptr = &var
,這表示 ptr
是指向 var
的地址的指標。int* ptr = &var;
的意思是,ptr
是一個指向 int
類型變數的指標,它存的是 var
這個變數的記憶體地址。 補充: *
是定義指標時的一部分,如 int *ptr
或 int* ptr
都是常見的寫法。
2. 取值/解引用(Dereferencing):
*ptr
時,你是「解引用/取值」這個指標,也就是去讀取 ptr
所指向的記憶體位置的值。例如 printf("%i\n", *ptr);
輸出的是 ptr
指向的那個值,也就是 var
的值。*
當取值的用法,前面不會有資料型別。*ptr
這種寫法只在變數已經是指標時有意義,它會取得指標所指向的值。如果不是指標變數,使用 *
會導致錯誤。&
是用來取得變數的記憶體地址。例如 &var
返回 var
的記憶體位置,而不是 var
的值。#include <stdio.h>
int main()
{
int var = 10;
int *ptr = &var; // 令ptr為指標變數,這是第1個定義
printf("%i\n",var);
return 0;
}
以上這個程式碼如果看懂就已經前進一大步了!以上這個程式碼要印出 var 的值也就是 10。
#include <stdio.h>
int main()
{
int var = 10;
int *ptr = &var;
printf("%p\n",&var);//印出十六進位的地址型態的表示方法是%p,並非整數%i
return 0;
}
剛剛提到 &var
是指 var
的 地址,所以他印出來的會是 var
的地址(記憶體位置) 0x7fffeeacc30c
。因為每個人電腦的記憶體儲存位置不一樣,所以印出來跟我的結果不一樣是正常的,即使是我重新跑一次結果都會不一樣。
接下來,我印出四行程式,來更清楚解釋指標變數之間的關係。
#include <stdio.h>
int main()
{
int var = 10;
int *ptr = &var; //注意int *是令一個指標變數,不是指向ptr哦!
printf("%p\n",&var); //這裡我印出列,示範&var跟ptr的共同點
printf("%p\n",ptr); //ptr儲存的是十六進位的地址,所以使用%p
printf("%p\n",&ptr); //如果我想知道ptr本身的所在位置,就要加上&
printf("%i\n",*ptr); // ptr所指向的值,所以會是 10
return 0;
}
因為 var
的地址就是 ptr
的值,所以兩個出來的答案一樣。都會是 0x7fff66b03fbc
(因為我重新跑第二次,所以記憶體位置跟上一個程式碼不同);而因為 p 自己也有地址,所以輸入 &ptr
會跑出跟其他兩個不一樣的結果。
還記得之前在 CS50 的環境下,我們使用 String
資料型別就能表示字串嗎?但在 C 語言中,字串是用 char*
來表示的。
讓我們複習一下,字串其實是一個「連續的字元陣列」(array of characters)。當我們使用指標來指向這個字元陣列的開頭地址時,因為字元是連續儲存在記憶體中的,所以指標可以依序存取後續的字元。因此,在 C 語言中,字串的表示方式是使用 char*
指標來指向這個陣列的開頭。
char
指的是一個字符。
#include <stdio.h>
int main() {
// 定義一個字串,指向 "HI!" 的起始地址,s 已經接受H的地址了。
char* s = "HI!";
// 使用 printf 來輸出字串
printf("%s\n", s);
return 0;
}
我用同時輸出各種不同的寫法,去比較他們的差別,讓你更了解指標。
你可以貼以下程式碼跑跑看,看結果有沒有跟你想的一樣。
#include <stdio.h>
int main()
{
// 定義一個字串,指向 "HI!" 的起始地址,s 已經接受H的地址了。
char *s = "HI!";
// 使用 printf 來輸出字串
printf("%s\n", s);
// 字串是以連續的陣列組成,所以也可以逐個輸出
printf("%c\n", s[0]);
printf("%c\n", s[1]);
printf("%c\n", s[2]);
printf("%c\n", s[3]); // 空值
// 看 s 指向的底址
printf("%p\n", s);
// s 「自己」的地址
printf("%p\n", &s);
// s 指向的逐個字元的地址
printf("%p\n", &s[0]);
printf("%p\n", &s[1]);
printf("%p\n", &s[2]);
return 0;
}
如果要得到字串,在沒有用 get_string
的功能,可以用以下的方式實現:
#include <stdio.h>
int main() {
// 定義一個字元陣列來儲存輸入的字串
char input[100]; // 這裡的 100 是陣列大小,最多可存 99 個字元 + 1 個結束符號 '\0'
// 提示使用者輸入字串
printf("請輸入一個字串: ");
// 使用 fgets() 來接收輸入
fgets(input, sizeof(input), stdin);
// 輸出剛才輸入的字串
printf("你輸入的字串是:%s", input);
return 0;
}
fgets(input, sizeof(input), stdin);
:fgets()
函數會從標準輸入 stdin
中讀取資料,並將結果儲存到 input
陣列中。sizeof(input)
會確保我們不會讀取超過陣列的大小。
現在有兩個字串 s
和 t
,我們需要比較它們是否相同。但我們不能直接使用 if(s == t)
,因為這樣比較的是字串 s
和字串 t
在記憶體中的地址,而不是它們的內容。
因此,我們需要使用 <string.h>
函式庫中的 strcmp()
函數來進行字串內容的比較。strcmp()
會逐字元比較兩個字串,如果它們相同,則返回輸出相同。
#include<cs50.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main() {
// 定義兩個字串
char *s = get_string("s: ");
char *t = get_string("t: ");
// 使用 strcmp() 來比較字串內容
if (strcmp(s, t) == 0) {
printf("字串 s 和 t 相同!\n");
} else {
printf("字串 s 和 t 不相同!\n");
}
}
malloc()
前面了解到字串是透過指標的方式表示,那如果我要複製相同字串,在複製的字串編輯怎麼辦?
由於字串是用指標表示的,所以當我們要複製字串時,必須先為新的字串分配記憶體空間。這時我們會用到 malloc()
函數來新增空間。當我們不再需要這些空間時,應該使用 free()
來釋放記憶體。
題目:我要把t字串的第一個變大寫,但同時保留s字串。
#include <cs50.h>
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h> // 加上才有malloc
#include <string.h>
int main()
{
char *s = get_string("s: ");
char *t = malloc(strlen(s) + 1);
if (t == NULL) { // 檢查 malloc 是否成功
printf("記憶體分配失敗!\n");
return 1; // 若分配失敗,結束程式
}
for (int i = 0, n = strlen(s); i <= n; i++)
{
t[i] = s[i]; // 把每個s的值複製到t
}
if (strlen(t) > 0)
{
t[0] = toupper(t[0]);
}
printf("t = %s\n", t);
printf("s = %s\n", s)
}
為什麼我不用*t
輸出字串,但是我前面要印出10(值) 就要用指標*ptr
指回去?
我在學習的時候,第一次我輸出字串輸入printf("t = %s\n", *t);
以為 t 是指標變數,所以我就要指回去原來的地方,沒想到輸出的結果只有開頭第一個字母。為什麼會這樣呢?
int var = 10;
時,var
是一個存儲整數值的變數。int *ptr = &var;
中,ptr
是一個指向 var
的指標,儲存的是 var
的地址。*ptr
時,你告訴程式要「解參考」(dereference),即從 ptr
指向的記憶體地址取得其儲存的值。所以 *ptr
會給你 var
的值(即 10)。//參考上面簡介
printf("%i\n", *ptr); // 輸出10
char *s
)指向這個字元陣列的第一個字元。s
指向字串的起始位置,而 printf("%s", s);
「帶我去 s 的位置,並讀取後面的字元,直到遇到字串結尾的 \0
(null 結束符號)。printf("%s", *s);
就是「只」讀取第一個位置。printf("%s", s);
已經自動處理了字串的輸出。我可以一開始就寫 int *ptr = var ,並直接 printf("%i", ptr) 嗎?
如果你寫成 int *ptr = var;
,會產生編譯錯誤,因為 ptr
期待的是一個地址(pointer),而你給了它一個整數(var
的值)。
咦那為什麼字串就可以用指標變數直接等於字串? 而不用寫 Char* = &"hello"?
當你寫 char *s = "hello";
時,字串 "hello"
其實是在程式運行時被儲存在內存的某個位置,並且編譯器會自動分配一塊連續的記憶體來儲存這個字串。這個記憶體區域的首地址就是字串 "hello"
的地址,而這個地址會被賦值給指標變數 s
。
#include <stdio.h>
int main() {
char *s = "hello";
printf("%p\n", s); // 輸出字串 "hello" 的首地址
printf("%p\n", &s[0]); // 這也是字串的首地址
return 0;
}
// 這裡 s 和 &s[0] 是相同的,因為 "hello" 的字元 'h' 的地址被賦給了 s。
"hello"
等同於 &"hello"[0]
。"hello"
,C 語言會自動把這個字串的首地址傳遞給 char*
變數,這就是為什麼可以直接用 char *s = "hello";
。int var = 10;
,你需要 &var
來取得它的地址。"hello"
本身就是地址,所以可以直接賦給指標變數 char *
。