指標變數

在撰寫此段說明的時候, 我忘記我已經寫過一遍了, 所以鬧了雙胞, 請參考另外的一份說明:

觀念應該是相同的, 但是解釋的角度和列舉的範例有一些不同, 我也是覺得很奇怪, 同樣的東西怎麼在不同的時間會寫出不同的東西呢? 不管怎樣, 請您多花一點時間欣賞了...

概說

指標變數常常會造成初學者的困擾, 尤其對於只想學習 C 語言中屬於高階的那一部份的同學而言, 更是很容易覺得位址可有可無, 不知道什麼是位址還是可以撰寫許多的程式, 不是嗎?

本來高階程式語言中的指標, 也沒人說一定要用記憶體的位址來製作, (C/C++ 語言中為了執行的效率才會這樣子做的), 指標的概念算是讓 CPU 在存取資料時中間多一層的間接緩衝, 例如下面這句話:

當然也可以說 這兩種說法其實目的是一模一樣的, 可是你一定可以感覺到第一種說法的彈性, 系辦裡有好幾個助教, 你可以任找一個, 助教可以把講義收在任何一個地方, 但是你應該都可以透過助教的幫忙拿到講義,

對了! 就是為了增加程式的彈性, 所以我們使用間接的方法來存取資料 (或是程式)。

抽象的指標變數概念:

CPU 最終存取資料的地方是資料變數指標變數裡記錄著哪一個資料變數需要被 CPU 用來存取資料, 或者說 CPU 透過某一個指標變數可以去存取該指標變數內指向的那一個資料變數。

舉一個一般的例子:

任意一種能夠記錄 "究竟是哪一個變數" 的方法理論上都可以用來做為指標, 例如: 在使用陣列變數的時候, 陣列變數的 index 變數其實具有指標的功能, 告訴 CPU 參與運算的 a[index] 到底是第幾個變數。

在 C/C++ 中為了增進執行碼的效率, 用資料變數在記憶体內的位址 做為指標。 如下節所述。

C/C++ 指標的語法

如何宣告一個指標變數

注意

  1. * 是一個單元 (unary) 的取值 (dereference or indirection) 運算子 (operator), 功能是 "將其後所接的運算式之數值算出, 當成一個記憶体位址, 並讀取該記憶体位址內 (適當格式) 之資料做為此運算之結果"。

  2. iPtr 稱為整數指標變數,dPtr 稱為浮點指標變數

  3. 上面的定義是在定義 iPtr 為整數指標變數 (把 int* 看成是一種變數的型態), 但是你也可以把它想像成是在定義 *iPtr 為整數, 如下:

    如此你更可以瞭解為什麼不另創一個型態叫做 intptr 來定義 iPtr 了。

如何使用指標變數

任何一個變數的基本功能都是存放資料, 不同型態的變數之間最主要的差別就是所存放資料的意義解釋方法是不同的。

  1. 存放資料在指標變數內:

    在 C/C++ 中指標變數要存放的資料是記憶体的位址, 例如:


    注意

  2. 讀取指標變數內資料:

    相對應上面 "存放資料" 這種使用 iPtr 變數的模式, "讀取資料" 是另一種使用 iPtr 變數的模式, 例如:

    上面紅字的 iPtr1 就是在讀取 iPtr1 指標變數內的位址資料。

  3. 間接存取指標變數所指向資料變數內之資料

    除了上面兩種使用方式之外, 指標變數更常看到的使用法是與 * 運算子結合來間接地存取資料變數內的資料, 如下:

    上面 a, b 這兩個用法尤其以 a 特別容易混淆。 請注意一般在等號的左邊我們必須放一個變數, 例如:

    這個 x 和在等號右邊或是別的地方出現的 x 是不一樣的, 例如:

    上面這個敘述中出現的兩個 x, CPU 在處理時都是去 x 那個變數裡把資料抓出來, 但是前一個敘述裡變數 x 出現在等號的左邊時 (所謂的 lvalue), 就不再是將 x 裡的資料讀出來了。 應該解讀為 CPU 見到等號左邊的 x 時會 "將 x 的位址找到, 以便在設定運算中存放等號右邊計算出來的數值", 再來看看前面的

    這個敘述, iPtr1 是個指標變數, 裡面存放著變數 x 的位址, *iPtr1 很明顯地和前面討論 x 放在等號左邊或是等號右邊的情形類似, *iPtr1 放在等號右邊的話, 就是將變數 x 內存放的資料讀出, 但是 *iPtr1 放在等號左邊時, CPU 在由變數 iPtr1 中讀出變數 x 的位址後, 不去讀變數 x 內存放的資料, 而將該位址 (變數 x 之位址) 留下來準備在設定運算中存放等號右邊計算出來的數值

    你也可以把程式中出現 *iPtr1 的地方看成是一個變數, 這個變數是 "iPtr1 變數內記錄的記憶體位址所代表的變數", 在程式編譯的時候 CPU 沒有辦法確定這個變數到底是指哪一個變數, 必須等到程式執行的時候 CPU 才根據 iPtr1 變數內的結果去確定 *iPtr1 這個變數到底是指哪一個, 這樣子的話程式的功能會突然增強了許多,本來 x = 10; 這樣的敘述 在程式執行的過程中永遠只能在一個確定的變數 x 內存放 10 這樣的資料, 現在 *iPtr1 = 10; 這樣的敘述則可以把 10 這個資料存放在很多不同的變數裡, 完全看程式執行到這一列敘述時 iPtr1 這個變數裡存放著哪一個變數的位址來決定。

程式中為什麼需要有指標這種東西?

其理由如下:

  1. 可以動態配置/釋放記憶體

  2. 存取資料或是執行函式時有更多的彈性

  3. 製作有效率的資料結構 (串列、樹狀結構...)

  4. 使用傳值 (call-by-value) 當作函式呼叫時參數傳遞的基本機制時, 傳遞一個指標進入函式的話, 可以讓函式裡直接更改主程式內的資料, 可以說是函式呼叫時另一種資料的傳遞方式。 如下例:

使用指標的好處

  1. 前一節所提到的四項都是好處

  2. 用動態配置的記憶體, 其作用的範圍是由程式的邏輯去決定的, 只要你把該段記憶體的指標傳遞到需要存取的函式內就可以了。

  3. 可以把資料串連起來 (資料結構)

  4. 不同的函式可以共享大量的資料儲存空間。

使用指標的壞處(容易犯的錯誤)

  1. wild pointer:

    只要有一個指標內存放的資料錯誤, 根據這個錯誤的指標, 程式可能會損毀更多的資料(包括指標), 如此就像核子反應一樣, 一傳十、十傳百, 程式很快就當掉了。

  2. dangling reference:

    程式藉由指標指到已經不能使用 (已經釋放掉) 的記憶體。

  3. memory leakage:

    記憶體沒有釋放掉而又把指標變數的內容毀掉, 導致沒有任何方法去存取此區段之記憶體。

指標的運算

C 語言中指標以記憶體的位址來表示, 因此指標可以做加減的運算, 其運算原則如下:

  1. 指標加減常數 (或是 ++, --)

    注意

  2. 指標減指標:

    C 語言裡面沒有定義兩個指標 (位址) 相加的運算, 但是有定義兩個相同形態指標的相減運算, 如下:

      int i, x[10]; int *xPtr1=&x[1], *xPtr2=&x[4]; i = xPtr2 - xPtr1; // 請注意:運算結果存放在變數 i 裡的數值是 3,並不是 6 // 是 ((int)xPtr2 - (int)xPtr1)/sizeof(int)

C 語言中陣列是用指標實作的

陣列是比較高階的資料表達方式, 記憶體位址則是非常低階的概念, C 語言中雖然有陣列的語法 (陣列宣告以及存取運算元), 但是 C 語言中百分之百地用指標來實現這些語法, 例如:

相當於:

在設計程式的時候雖然你知道這兩種表達方法完全等效, 但是請儘量用陣列的語法, 比較有整體資料的概念。

C語言中使用位址來製作指標使得在 C 語言中可以直接實現 memory-mapped I/O,例如:


注意

  1. 使用指標常數的目的是怕程式製作的時候不小心更改了指標變數的內容。

  2. 請小心區分指標常數 (pointer constant: a pointer that does not change), 例如:
      int data1, data2; int * const ptrData = &data1; *ptrData = 2; // OK ptrData = &data2; // ERROR
    與常數指標 (constant pointer: a pointer that points to a constant), 例如:
      const int data1 = 1, data2 = 2; // 或是 int const data1 = 1, data2; const int * ptrData; // 或是 int const * ptrData; ptrData = &data1; *ptrData = 2; // ERROR ptrData = &data2; // OK

  3. 陣列變數本身也是一個指標常數, 例如:
      int x[5], iArray[20]; iArray = &x; // ERROR *(iArray + 4) = 10; // OK: 存取變數 iArray[4] 請注意:&x 代表 5個整數資料的起始記憶體位址 (&x+1 為 &x[0] 記憶體位址加上 5*sizeof(int)), x 代表 1個整數資料的起始記憶體位址 (x+1 為 &x[0] 記憶體位址加上 sizeof(int))

兩層指標 (double pointer)

在較複雜的資料結構中難免會遇見需要用所謂的兩層指標 (double pointer), 這種資料格式很容易和指標陣列陣列的指標、 以及二維陣列三種型態混淆,

先看看兩層指標的標準用法:


再看看陣列的指標 (pointer to array) 以及二維陣列 (two dimensional array) 的基本用法:

最後再來看看指標陣列 (pointer array) 的基本用法:

指標陣列 (pointer array)

程式設計課程 首頁

製作日期: 98/12/05 by 丁培毅 (Pei-yih Ting)
E-mail: pyting@mail.ntou.edu.tw