National Taiwan University
Uedu Main Site
Explore Uedu
Student Console
Register as Member/Login
Research Informed Consent Center
Survey Center
Educational Big Data Challenge
Claim or create a course
Try a Scenario Task
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

2

Local AI

--

Uedu Code

--

台灣大學 AQI 70 24°C PM2.5 17
AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

Unit 08

指標

位址、取值、指標運算與 nullptr
Interaction實作 25 minute(s)

Learning Objectives

  • 說明每個變數在記憶體裡都有位址,用 & 取得位址、用 * 透過指標讀取與修改
  • 宣告與初始化指標,分辨 nullptr 與沒有初始化的指標
  • 用指標走訪陣列,說明指標加 1 是前進一個元素
  • 用指標參數讓函式修改呼叫端的變數
  • 辨認常見的指標錯誤:對空指標取值、野指標與懸空指標

記憶體、位址與指標

程式執行時,每個變數都佔用記憶體裡的一段空間。記憶體可以想成一長排編了號的置物櫃,每一格是一個位元組(byte), 編號就是位址(address),通常用十六進位表示,例如 0x7ffd5e8c4b2c。

int score = 80;     // score 佔 4 個位元組,從某個位址開始
int* p = &score;    // &score 取得 score 的位址,存進指標 p
cout << *p;         // *p:到 p 存的位址去拿值,得到 80

指標(pointer)是一種變數,它存的不是一般的數字或文字,而是另一個變數的位址。有了位址,就能找到那個變數、讀取或修改它。

本課的逐步執行動畫會在每個變數旁邊顯示位址;指標的值旁邊有一個圓點,箭頭連到它指向的變數。

動畫裡的位址是模擬 Linux 上 g++ 的配置方式產生的。真正執行時,作業系統每次都會把程式放到不同位置(位址空間配置隨機化,ASLR), 所以你在自己電腦上看到的位址會不一樣,但「誰在誰前面、相差幾個位元組」的關係是一樣的。

取得位址、透過指標讀寫

注意右邊每個變數最後一欄的位址:p 的值和 score 的位址一模一樣,所以箭頭從 p 指向 score。第 12 行透過 *p 改值,被改的是 score。最後一行印出各型別的大小:在 64 位元系統上,不論指向什麼型別,指標都是 8 個位元組。

宣告指標的寫法

寫法 意思
int* p; 宣告 p 是指向 int 的指標(* 寫在型別旁或變數旁都可以:int *p;)
p = &x; & 放在變數前面:取得 x 的位址
*p * 放在指標前面:取值(dereference),存取 p 所指的變數
int* p = nullptr; 空指標:明確表示「目前沒有指向任何東西」

& 與 * 在宣告和運算式裡意思不同:宣告時 int* 表示「指標型別」,運算式裡 *p 表示「取值」;&x 是取位址, 宣告時的 int& 則是下一課的參考。

一次宣告多個指標時要特別注意:

int* a, b;     // 只有 a 是指標,b 是一般的 int
int *c, *d;    // c、d 都是指標

指標宣告時一定要給初始值:區域變數不會自動歸零,沒有初始值的指標裡是一個隨機的位址(野指標,wild pointer), 透過它讀寫是未定義行為。還不知道要指向哪裡時,就先設成 nullptr,使用前檢查 if (p != nullptr)。

nullptr 是 C++11 加入的關鍵字。舊的程式碼常用 NULL 或 0 表示空指標,看得懂即可,新程式請用 nullptr。

nullptr 與野指標

empty 是空指標,沒有箭頭;wild 宣告時沒有給值,動畫顯示「?」。執行到第 14 行時,程式會停下來說明原因。真正的程式不會停下來說明,而是讀到一個不知道是哪裡的位址:可能印出奇怪的數字,也可能讓程式當掉(Segmentation fault)。

指標與陣列

陣列的元素在記憶體裡一個接一個連續排列。在大部分運算式裡,陣列名稱會自動轉成「指向第一個元素的指標」:

int nums[4] = {10, 20, 30, 40};
int* p = nums;          // 等於 int* p = &nums[0];

指標運算以「元素」為單位,不是以位元組為單位:p + 1 指向下一個元素,位址會加上 sizeof(int),也就是 4。 所以:

  • *(p + i) 就是第 i 個元素,C++ 也允許寫成 p[i]
  • 兩個指向同一個陣列的指標相減,得到的是相差幾個元素
  • 可以用 p++ 讓指標往後走,走到「最後一個元素的下一格」(nums + 4)時停止。這個位置可以拿來比較,但不能取值

用指標走訪陣列

看陣列每一格下方的位址尾數:每格相差 4(一個 int 的大小)。第 11 行印出 p 和 p + 1,相差的也是 4。第 13~15 行的迴圈讓 it 從第一個元素走到 nums + 4,箭頭會一格一格往右移。

想一想:位址、取值與指標運算

1.

下面的程式會印出什麼?

int a = 5;
int* p = &a;
*p = *p + 1;
cout << a;
2.

下面的程式會印出什麼?

int arr[3] = {2, 4, 6};
int* p = arr;
p++;
cout << *p + 1;
3.

在函式裡寫 int* p; 之後還沒有給值,這時 p 的值是什麼?

第 1 次作答

傳指標給函式

C++ 呼叫函式時預設是傳值:參數是引數的複本,函式裡改參數不會影響呼叫端的變數。 如果希望函式能改呼叫端的變數,可以把變數的位址傳進去,函式再透過指標修改:

void addOne(int* n) { *n = *n + 1; }

int score = 9;
addOne(&score);    // 傳 score 的位址,score 變成 10

指標參數也可以是 nullptr,用來表示「這次沒有東西」。所以函式在取值前要先檢查,否則對空指標取值會讓程式當掉。

下一課的參考(reference)可以做到同樣的事,而且寫法比較簡潔、不會是空的;需要表達「可能沒有」時才用指標。

函式透過指標修改變數

呼叫 swapByPointer 時,右邊多出一個框架。它的參數 a、b 有自己的位址,值則是 main 裡 x、y 的位址,所以箭頭從新框架指回 main 的變數。函式結束、框架消失之後,x、y 已經交換了。

常見的指標錯誤

錯誤 例子 會發生什麼
對空指標取值 int* p = nullptr; cout << *p; 通常立刻當掉(Segmentation fault)
野指標 int* p; *p = 3; 寫到不知道是哪裡的記憶體,結果無法預期
懸空指標 指向已經結束的區域變數,或已經 delete 的記憶體 那塊記憶體可能已經被別人使用,讀到錯誤的值或當掉
超出陣列範圍 int a[3]; int* p = a; p[5] = 1; 改到別的變數,或讓程式當掉

這些都是未定義行為(undefined behavior):C++ 標準不規定結果,編譯器也不會在執行時幫你檢查。 有時程式看起來「正常」執行,錯誤卻在很久之後才出現,這是指標錯誤難找的原因。CodeLab 的直譯器會在發生的當下停下來說明, 真正編譯出來的程式不會。

函式結束時,它的區域變數就消失了。下面這個函式回傳區域變數的位址,呼叫端拿到的就是懸空指標:

懸空指標

注意第 7 行回傳之後,makeNumber 的框架連同 n 一起消失,但 p 還記得 n 的舊位址。用 g++ 編譯這段程式會看到警告 address of local variable 'n' returned;不要忽略編譯器的警告。

想一想:指標參數與錯誤

1.

下面的程式會印出什麼?

int x = 1, y = 2;
int* p = &x;
p = &y;
*p = 9;
cout << x << y;
2.

void setZero(int* p) { *p = 0; },要讓 main 裡的 int n = 5; 變成 0,應該怎麼呼叫?

3.

下列哪一種情況會產生懸空指標?

第 1 次作答

動手寫:加分函式

完成 addBonus:如果 score 不是 nullptr,就把 bonus 加到它指向的分數上。main 已經寫好,執行後的輸出要和預期相同。

你的輸出
預期輸出

重點回顧

  • 每個變數都有位址;&x 取得位址,*p 透過指標存取它指向的變數
  • 指標的型別要和它指向的資料一致:int* 指向 int、double* 指向 double;64 位元系統上指標本身都是 8 個位元組
  • 宣告指標時就給初始值,不確定時用 nullptr,使用前先檢查
  • 陣列名稱會轉成指向第一個元素的指標;p + i 前進 i 個元素,p[i] 等於 *(p + i)
  • 傳指標給函式,函式就能修改呼叫端的變數
  • 對空指標取值、野指標、懸空指標、超出陣列範圍,都是未定義行為;下一課的參考在多數情況下更安全
這一課完成了!
Next lesson:參考
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。