2024-10-17|閱讀時間 ‧ 約 20 分鐘

C 語言指標-程式碼圖解

大魔王指標:初學者的天堂路。

指標(Pointer)是 C 語言裡的「大魔王」,是資工系學生花了至少 9 小時上的課。我們一起用 18 分鐘文章快速了解指標的基本概念,中間在字串的部分我將結果和程式碼做對照,希望有助於理解。最後,我會將我跟 ChatGPT 對話放上來跟大家一起學習,希望能解決跟我一樣腦筋快打結的學生(╯°□°)╯︵ ┻━┻

指標跟記憶體的關係:

每個變數的值都儲存在記憶體裡,記憶體位置有個唯一的「地址」(Address)。指標 (Pointer)就是一種特殊的變數,用來儲存這些「地址」。

為什麼有指標?更靈活的操作記憶體

  • 動態記憶體管理 (Dynamic Memory Management):透過 malloc()free() 函數,指標可以分配和釋放記憶體,靈活控制記憶體的大小。
  • 陣列與字串操作 (Array and String Manipulation):指標讓我們可以更靈活地操作陣列和字串。特別是字串,實際上是用指標來處理的。

在了解指標之前,我們要先了解記憶體跟十六進位,方便之後解釋。

十六進位與記憶體

什麼是十六進位?

在電腦科學中,我們常常使用不同的數字系統來表示數據。你可能已經熟悉十進位(Decimal),也就是我們日常生活中使用的數字系統,數字從0到9。然而,在電腦的世界裡,除了二進位(Binary)0 和 1 之外,還有另一種非常重要的數字系統,那就是十六進位(Hexadecimal)。

十六進位是以16為基數的數字系統,數字從0到9之後,接著會使用字母A到F來代表額外的值:

  • 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
  • A = 10, B = 11, C = 12, D = 13, E = 14, F = 15

為什麼需要十六進位?

電腦實際上是以二進位(Binary)來處理數據的,只有0和1兩個數字。然而,當我們處理大量數據時,用二進位來表示數字會變得非常長且難以閱讀。十六進位能夠幫助我們更簡單地表示二進位的數字,因為每個十六進位的數字可以剛好表示4個二進位的位元(bits)。

例如:

  • 二進位:1010 代表十進位的 10
  • 十六進位:A 也代表十進位的 10

十六進位的應用

1. 顏色代碼

你可能在網頁設計或圖片編輯軟體中看過類似於 #FF5733 這樣的顏色代碼。這些顏色代碼通常是使用十六進位來表示RGB顏色值。每一對數字(或字母)代表紅(R)、綠(G)、藍(B)三個顏色的強度。

例如,#FF5733 中的:

  • FF 表示紅色 (255 in decimal)
  • 57 表示綠色 (87 in decimal)
  • 33 表示藍色 (51 in decimal)

2. 記憶體地址

在電腦的記憶體中,每個資料都儲存在一個特定的位置上,這些位置被稱為記憶體地址。由於電腦中的記憶體地址通常是很大的數字,使用十六進位來表示這些地址可以更簡潔和容易辨認。例如,記憶體地址 0x1A3F 就比用長長的二進位來表示更方便。

3. MAC 位址與 IP 位址

在網路通訊中,MAC 地址(Media Access Control Address)也是用十六進位表示的,通常會以六對兩個字母或數字的形式出現,例如 00:1A:2B:3C:4D:5E。這些值幫助網路設備之間進行唯一識別。


指標變數與取值

圖像說明:

每個變數都有自己的記憶體地址。當我們宣告指標變數 p 時,p 儲存的是某個變數的地址(例如 10 的地址)。

但一般我們不需要關心 p 自己的地址或它儲存的具體數值,因為 p 主要是用來幫我們找到 10 這個值。

在程式上的解釋:

同個 * 不同意思。這裡超重要!

1. 定義指標變數:

  • 資料型別* 來定義指標變數,例如 int* ptr = &var,這表示 ptr 是指向 var 的地址的指標。
  • int* ptr = &var; 的意思是,ptr 是一個指向 int 類型變數的指標,它存的是 var 這個變數的記憶體地址。

補充* 是定義指標時的一部分,如 int *ptrint* ptr 都是常見的寫法。

2. 取值/解引用(Dereferencing):

  • 當你使用 *ptr 時,你是「解引用/取值」這個指標,也就是去讀取 ptr 所指向的記憶體位置的值。例如 printf("%i\n", *ptr); 輸出的是 ptr 指向的那個值,也就是 var 的值。
  • * 當取值的用法,前面不會有資料型別。
  • 容易誤會的地方*ptr 這種寫法只在變數已經是指標時有意義,它會取得指標所指向的值。如果不是指標變數,使用 * 會導致錯誤。

3. 取得地址:

  • & 是用來取得變數的記憶體地址。例如 &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 會跑出跟其他兩個不一樣的結果。


字串 char*

還記得之前在 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) 會確保我們不會讀取超過陣列的大小。

字串比較:

現在有兩個字串 st,我們需要比較它們是否相同。但我們不能直接使用 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)
}



以下是我鑽牛角尖問 ChatGPT 的問題:

為什麼我不用 *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
  • 字串與指標:
    • 字串在 C 中是字元陣列,而指標(例如 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。

關鍵概念:

  • 字元陣列:字串常量本質上是一個字元陣列,而在 C 中,陣列的名字本身就代表這個陣列的首地址。也就是說,"hello" 等同於 &"hello"[0]
  • 自動解釋為地址當你寫 "hello",C 語言會自動把這個字串的首地址傳遞給 char* 變數,這就是為什麼可以直接用 char *s = "hello";
  • 與整數變數的不同:
  • 在整數變數中,變量本身儲存的是一個數值,像 int var = 10;,你需要 &var 來取得它的地址。
  • 但字串常數如 "hello" 本身就是地址,所以可以直接賦給指標變數 char *



分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.