C 語言指標-程式碼圖解

閱讀時間約 18 分鐘

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

指標(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 的地址)。

raw-image

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

raw-image

在程式上的解釋:

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

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 會跑出跟其他兩個不一樣的結果。

raw-image

字串 char*

還記得之前在 CS50 的環境下,我們使用 String 資料型別就能表示字串嗎?但在 C 語言中,字串是用 char* 來表示的。

讓我們複習一下,字串其實是一個「連續的字元陣列」(array of characters)。當我們使用指標來指向這個字元陣列的開頭地址時,因為字元是連續儲存在記憶體中的,所以指標可以依序存取後續的字元。因此,在 C 語言中,字串的表示方式是使用 char* 指標來指向這個陣列的開頭。

char 指的是一個字符。

raw-image
#include <stdio.h>

int main() {
// 定義一個字串,指向 "HI!" 的起始地址,s 已經接受H的地址了。
char* s = "HI!";

// 使用 printf 來輸出字串
printf("%s\n", s);

return 0;
}

指標程式碼輸出結果,差別

我用同時輸出各種不同的寫法,去比較他們的差別,讓你更了解指標。

raw-image

你可以貼以下程式碼跑跑看,看結果有沒有跟你想的一樣。

#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() 會逐字元比較兩個字串,如果它們相同,則返回輸出相同。

raw-image
#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)
}
raw-image



以下是我鑽牛角尖問 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 的值)。

raw-image
咦那為什麼字串就可以用指標變數直接等於字串? 而不用寫 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 *



留言0
查看全部
avatar-img
發表第一個留言支持創作者!
本篇文章探討計算機理論中的 P/NP 問題,分析其在演算法和理論計算中的重要性。透過回溯法及圖靈機的介紹,讀者將更瞭解 P 問題與 NP 問題的區別,以及它們在解決複雜問題中的挑戰。最後,我們將提及停機問題,揭示計算機在面對某些問題時的侷限性。
上次我們提到了演算法(algorithm),它是一種解決問題的方式。但演算法只是資料結構與演算法(Data Structures and Algorithms, DSA)這個領域的一部分。今天,我們要進一步探索這個主題,了解它的核心概念。 什麼是資料結構與演算法呢?簡單來說,資料結構是用來組織和存
上回提到,演算法是一種解決問題的方法。光是簡單的將數字有小排到大就有很多種不同的排序演算法可以選擇。這次,我們來介紹幾個常見的排序演算法,看看它們是怎麼運作的。
演算法是一種解決問題的虛擬邏輯,他不像 C 語言有直接的程式碼,而是一種虛擬的問題解決方式。 想像一下,今天要在字典裡面找到 Zoo,有幾種方法: 逐頁查找:如果字典有 1000 頁,最糟情況下需要翻 1000 次 才能找到。 兩頁兩頁找:這樣的話,1000 頁最多要翻 500 次。 二分查
在上一篇文章中,我們介紹了「陣列」的基本使用方式。本篇將帶你深入探討 C 語言中字串的運作原理,了解如何以陣列形式儲存字串。此外,我們還會介紹如何將英文字母透過 ASCII 表轉換成數值,並說明其在電腦中的實際應用。最後,解析 Command Line Argument(命令列參數)的使用方法。
程式並不是寫完就可以自動運行,還需要經過「編譯」的階段。此篇文章會介紹編譯的四個階段,更貼近電腦科學。另外,也說明「陣列」的資料儲存模式跟實際的運用,提供優化程式的建議,最後再分享三個除錯的技巧。
本篇文章探討計算機理論中的 P/NP 問題,分析其在演算法和理論計算中的重要性。透過回溯法及圖靈機的介紹,讀者將更瞭解 P 問題與 NP 問題的區別,以及它們在解決複雜問題中的挑戰。最後,我們將提及停機問題,揭示計算機在面對某些問題時的侷限性。
上次我們提到了演算法(algorithm),它是一種解決問題的方式。但演算法只是資料結構與演算法(Data Structures and Algorithms, DSA)這個領域的一部分。今天,我們要進一步探索這個主題,了解它的核心概念。 什麼是資料結構與演算法呢?簡單來說,資料結構是用來組織和存
上回提到,演算法是一種解決問題的方法。光是簡單的將數字有小排到大就有很多種不同的排序演算法可以選擇。這次,我們來介紹幾個常見的排序演算法,看看它們是怎麼運作的。
演算法是一種解決問題的虛擬邏輯,他不像 C 語言有直接的程式碼,而是一種虛擬的問題解決方式。 想像一下,今天要在字典裡面找到 Zoo,有幾種方法: 逐頁查找:如果字典有 1000 頁,最糟情況下需要翻 1000 次 才能找到。 兩頁兩頁找:這樣的話,1000 頁最多要翻 500 次。 二分查
在上一篇文章中,我們介紹了「陣列」的基本使用方式。本篇將帶你深入探討 C 語言中字串的運作原理,了解如何以陣列形式儲存字串。此外,我們還會介紹如何將英文字母透過 ASCII 表轉換成數值,並說明其在電腦中的實際應用。最後,解析 Command Line Argument(命令列參數)的使用方法。
程式並不是寫完就可以自動運行,還需要經過「編譯」的階段。此篇文章會介紹編譯的四個階段,更貼近電腦科學。另外,也說明「陣列」的資料儲存模式跟實際的運用,提供優化程式的建議,最後再分享三個除錯的技巧。
你可能也想看
Google News 追蹤
Thumbnail
Hi 我是 VK~ 在 8 月底寫完〈探索 AI 時代的知識革命:NotebookLM 如何顛覆學習和創作流程?〉後,有機會在 INSIDE POSSIBE 分享兩次「和 NotebookLM 協作如何改變我學習和創作」的主題,剛好最近也有在許多地方聊到關於 NotebookLM 等 AI 工具
Thumbnail
這是張老師的第三本書,我想前二本應該也有很多朋友們都有讀過,我想絕對是受益良多,而這次在書名上就直接點出,著重在從投資的角度來切入
Thumbnail
這篇內容,將會講解什麼是運算子,以及與運算子相關的知識。包括運算子的簡介、賦值運算子、算術運算子、遞增/遞減、比較運算子、邏輯運算子。
Thumbnail
這篇內容,將會講解什麼是變數,以及與變數相關的知識。包括變數、資料型態、變數賦值、變數的命名規則、變數的作用區域、變數的可重複性、內建變數。
Thumbnail
這篇內容,將透過實戰教學,來講解「滑鼠點方塊」的程式碼。包括如何測試遊戲、座標系統、自訂參數和內建參數、if else、and、遊戲的邏輯設計、程式碼解析。
步驟 1: 計劃每天花點時間學習大語言模型的技術部分。 步驟 2: 選擇合適的教材。我選擇了Manning出版的《Build a Large Language Model (From Scratch)》,這本書有配套的程式碼和詳細的講解,是我信賴的學習素材。 步驟 3: 瀏覽教材中的程式
前言 在閱讀《強化式學習:打造最強 AlphaZero 通用演算法》時,對一些看似基本,但是重要且會影響到之後實作的項目概念有點疑惑,覺得應該查清楚,所以搞懂後記錄下來,寫下這篇文章(應該說是筆記?)。 正文 下面這段程式碼: model = Sequential() model.add
Thumbnail
解決電腦上遇到的問題、證明正確性、探討效率 並且很著重溝通,說服別人你做的事是正確且有效率的。 內容: 計算模型、資料結構介紹、演算法介紹、時間複雜度介紹。
Thumbnail
Hi 我是 VK~ 在 8 月底寫完〈探索 AI 時代的知識革命:NotebookLM 如何顛覆學習和創作流程?〉後,有機會在 INSIDE POSSIBE 分享兩次「和 NotebookLM 協作如何改變我學習和創作」的主題,剛好最近也有在許多地方聊到關於 NotebookLM 等 AI 工具
Thumbnail
這是張老師的第三本書,我想前二本應該也有很多朋友們都有讀過,我想絕對是受益良多,而這次在書名上就直接點出,著重在從投資的角度來切入
Thumbnail
這篇內容,將會講解什麼是運算子,以及與運算子相關的知識。包括運算子的簡介、賦值運算子、算術運算子、遞增/遞減、比較運算子、邏輯運算子。
Thumbnail
這篇內容,將會講解什麼是變數,以及與變數相關的知識。包括變數、資料型態、變數賦值、變數的命名規則、變數的作用區域、變數的可重複性、內建變數。
Thumbnail
這篇內容,將透過實戰教學,來講解「滑鼠點方塊」的程式碼。包括如何測試遊戲、座標系統、自訂參數和內建參數、if else、and、遊戲的邏輯設計、程式碼解析。
步驟 1: 計劃每天花點時間學習大語言模型的技術部分。 步驟 2: 選擇合適的教材。我選擇了Manning出版的《Build a Large Language Model (From Scratch)》,這本書有配套的程式碼和詳細的講解,是我信賴的學習素材。 步驟 3: 瀏覽教材中的程式
前言 在閱讀《強化式學習:打造最強 AlphaZero 通用演算法》時,對一些看似基本,但是重要且會影響到之後實作的項目概念有點疑惑,覺得應該查清楚,所以搞懂後記錄下來,寫下這篇文章(應該說是筆記?)。 正文 下面這段程式碼: model = Sequential() model.add
Thumbnail
解決電腦上遇到的問題、證明正確性、探討效率 並且很著重溝通,說服別人你做的事是正確且有效率的。 內容: 計算模型、資料結構介紹、演算法介紹、時間複雜度介紹。