指標變數
在撰寫此段說明的時候,
我忘記我已經寫過一遍了,
所以鬧了雙胞,
請參考另外的一份說明:
觀念應該是相同的,
但是解釋的角度和列舉的範例有一些不同,
我也是覺得很奇怪,
同樣的東西怎麼在不同的時間會寫出不同的東西呢?
不管怎樣,
請您多花一點時間欣賞了...
概說
指標變數常常會造成初學者的困擾,
尤其對於只想學習 C 語言中屬於高階的那一部份的同學而言,
更是很容易覺得位址可有可無,
不知道什麼是位址還是可以撰寫許多的程式,
不是嗎?
本來高階程式語言中的指標,
也沒人說一定要用記憶體的位址來製作,
(C/C++ 語言中為了執行的效率才會這樣子做的),
指標的概念算是讓 CPU 在存取資料時中間多一層的間接緩衝,
例如下面這句話:
當然也可以說
這兩種說法其實目的是一模一樣的,
可是你一定可以感覺到第一種說法的彈性,
系辦裡有好幾個助教,
你可以任找一個,
助教可以把講義收在任何一個地方,
但是你應該都可以透過助教的幫忙拿到講義,
對了!
就是為了增加程式的彈性,
所以我們使用間接的方法來存取資料 (或是程式)。
抽象的指標變數概念:
CPU 最終存取資料的地方是資料變數, 指標變數裡記錄著哪一個資料變數需要被
CPU 用來存取資料, 或者說 CPU 透過某一個指標變數可以去存取該指標變數內指向的那一個資料變數。
舉一個一般的例子:
有一個送貨員有一百個客戶輪流去 收貨/送貨,
他每天能夠處理的客戶個數視客戶當天貨物的多少而定,
有時處理 50 個,
有時處理 30 個,
有時處理 70 個,
不管前一天處理了多少個客戶,
他每天的工作就是要從接下去的那個客戶開始,
於是他就在小貨車的前座上掛了一塊牌子,
上面用白板筆寫著下一個該去收送貨的客戶,
這塊牌子就是一個指標,
上面記錄著應該要處理的資料 (貨物) 放在哪裡。
任意一種能夠記錄 "究竟是哪一個變數" 的方法理論上都可以用來做為指標,
例如:
在使用陣列變數的時候,
陣列變數的 index 變數其實具有指標的功能,
告訴 CPU 參與運算的 a[index] 到底是第幾個變數。
在 C/C++ 中為了增進執行碼的效率,
用資料變數在記憶体內的位址
做為指標。
如下節所述。
C/C++ 指標的語法
如何宣告一個指標變數:
int *iPtr; // 定義一個可以存放整數變數指標 (位址) 的變數
iPtr
double *dPtr; // 定義一個可以存放浮點變數指標 (位址) 的變數
dPtr
注意:
- * 是一個單元 (unary) 的取值 (dereference or indirection)
運算子 (operator), 功能是 "將其後所接的運算式之數值算出, 當成一個記憶体位址, 並讀取該記憶体位址內
(適當格式) 之資料做為此運算之結果"。
-
iPtr 稱為整數指標變數,dPtr 稱為浮點指標變數。
- 上面的定義是在定義 iPtr 為整數指標變數 (把 int* 看成是一種變數的型態), 但是你也可以把它想像成是在定義
*iPtr 為整數, 如下:
如此你更可以瞭解為什麼不另創一個型態叫做 intptr 來定義 iPtr 了。
如何使用指標變數:
任何一個變數的基本功能都是存放資料,
不同型態的變數之間最主要的差別就是所存放資料的意義及解釋方法是不同的。
-
存放資料在指標變數內:
在 C/C++ 中指標變數要存放的資料是記憶体的位址,
例如:
int *iPtr; int x; iPtr = &x; // 存放位址資料到 iPtr 變數內
注意:
& 是一個單元 (unary) 的運算符號,
其後必須要是一個變數,
它的功能是 "找出其後變數的位址,
做為該運算的結果"
-
讀取指標變數內資料:
相對應上面 "存放資料" 這種使用 iPtr 變數的模式,
"讀取資料" 是另一種使用 iPtr 變數的模式,
例如:
int *iPtr1, *iPtr2;
int x , y;
iPtr1 = &x
iPtr2 = iPtr1;
scanf("%d", iPtr1); // scanf("%d", &x); scanf("%d", iPtr2);
printf("%p%p", iPtr1, &x); // 以十六進位印出 iPtr1 之內容
上面紅字的 iPtr1 就是在讀取 iPtr1 指標變數內的位址資料。
-
間接存取指標變數所指向資料變數內之資料
除了上面兩種使用方式之外,
指標變數更常看到的使用法是與 * 運算子結合來間接地存取資料變數內的資料,
如下:
a. *iPtr1 = 20; // 在變數 x 內存放 20 這個數值資料
b. printf("%d", *iPtr2); // 將變數 x 內的資料交給 printf() 函式以10進位整數方式列印出來
y = *iPtr2 + 20; // 將變數 x 內資料讀出加上 20 後存入變數 y 之中
上面 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 這個變數裡存放著哪一個變數的位址來決定。
程式中為什麼需要有指標這種東西?
其理由如下:
-
可以動態配置/釋放記憶體
- 存取資料或是執行函式時有更多的彈性
-
製作有效率的資料結構 (串列、樹狀結構...)
-
使用傳值 (call-by-value) 當作函式呼叫時參數傳遞的基本機制時,
傳遞一個指標進入函式的話,
可以讓函式裡直接更改主程式內的資料,
可以說是函式呼叫時另一種資料的傳遞方式。
如下例:
double mean(int, double [], double *);
void main(void)
{
double mean, variance;
double x[10] = {1,2,3,4,5,6,7,8,9,10};
mean = mean(10, x, &variance);
}
double mean(int numItems, double x[], double *pVariance)
{
double sum = 0.0;
int i;
for (i=0; i<numItems; i++)
sum += x[i];
sum = sum / numItems;
*pVariance = 0.0; // 此時 CPU 實際上直接存取 main() 函式內的變數 variance
for (i=0; i<numItems; i++)
*pVariance += (x[i]-sum) * (x[i]-sum);
*pVariance = *pVariance / (numItems -1);
return sum;
}
使用指標的好處
-
前一節所提到的四項都是好處
-
用動態配置的記憶體,
其作用的範圍是由程式的邏輯去決定的,
只要你把該段記憶體的指標傳遞到需要存取的函式內就可以了。
-
可以把資料串連起來 (資料結構)
-
不同的函式可以共享大量的資料儲存空間。
-
wild pointer:
只要有一個指標內存放的資料錯誤,
根據這個錯誤的指標,
程式可能會損毀更多的資料(包括指標),
如此就像核子反應一樣,
一傳十、十傳百,
程式很快就當掉了。
-
dangling reference:
程式藉由指標指到已經不能使用 (已經釋放掉) 的記憶體。
-
memory leakage:
記憶體沒有釋放掉而又把指標變數的內容毀掉,
導致沒有任何方法去存取此區段之記憶體。
指標的運算
C 語言中指標以記憶體的位址來表示,
因此指標可以做加減的運算,
其運算原則如下:
-
指標加減常數 (或是 ++, --)
int x[20];
int *xPtr;
xPtr = &x[0]; // 也可以用 xPtr = x; 假設 xPtr 內的資料為 A000
xPtr++; // 也可以用 xPtr = xPtr + 1; 此運算完成後 xPtr 內之資料為 A002
// 剛好為變數 x[1] 之位址
xPtr = xPtr + 10; //此運算完成後 xPtr 內之資料為 A016,剛好為
// 變數 x[11] 之位址
注意:
指標之 +、
-、
++、
-- 等符號在運算時必須看指標變數之資料形態 (data type),
例如:
如果是字元指標的話,
char *xPtr, x[20];
xPtr = x;
xPtr = xPtr + 1;
上面的 + 號實際上會將變數 xPtr 內所存放的位址加上 1,
如果是浮點指標的話,
double x[20];
double *xPtr = x;
xPtr = xPtr + 1;
上面的 + 號實際上會將變數 xPtr 內所存放的位址加上 sizeof(*xPtr),
更複雜的情況也是依此原則來處理,
例如結構指標或是陣列指標,
如下:
FILE *fp;
fp = fp + 1; // 變數 fp 內所存放的位址資料加上 sizeof(*fp)
或是
int x[5][3];
int (*xap)[3];
xap = &x[3]; // 小心別寫 x[3],x[3] 的型態是 int *, 和 &x[3][0] 是等效的
xap = xap + 1; // 變數 xap 內所存放的位址資料加上 sizeof(int[3])
// 新的 xap 指向 x[4] 陣列存放的地方(此位址當然也就是 &x[4][0] 但是型態不同)
int *xp1, *xp2;
xp1 = &x[3][0]; //
xp1 = xp1 + 1; // 變數 xp1 內所存放的位址資料加上 sizeof(int)
// 新的 xp1 指向 x[3][1] 變數 (xp1 紀錄 x[3][1] 變數的位址 &x[3][1])
xp2 = x[3] + 1; // xp2 指向 x[3][1] 變數 (xp2 紀錄 x[3][1] 變數的位址 &x[3][1])
-
指標減指標:
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 語言中可以直接實現
memory-mapped I/O,例如:
假設個人電腦序列埠 (serial port, RS232) 有一個八位元的控制暫存器位址為 0xB000,
有一個八位元的資料暫存器位址為 0xB010 則下列程式可以直接輸出資料到這兩個暫存器去,
也可以直接讀取資料暫存器內由週邊裝置送來的資料,如下:
char x;
short int * const controlReg = 0xB000; // 指標常數數初始化
short int * const dataReg = 0xB010; // 指標常數初始化
*controlReg = 0xFA; // 輸出資料 0xFA 到控制暫存器
*dataReg = 0x1B; // 輸出資料 0x1B 到資料暫存器
x = *dataReg; // 由資料暫存器輸入資料並存於變數 x 之中
注意:
-
使用指標常數的目的是怕程式製作的時候不小心更改了指標變數的內容。
-
請小心區分指標常數 (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
-
陣列變數本身也是一個指標常數,
例如:
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),
這種資料格式很容易和指標陣列、
陣列的指標、
以及二維陣列三種型態混淆,
先看看兩層指標的標準用法:
int **ppData;
int *pData;
int data;
ppData = &pData;
pData = &data;
**ppData = 10; // 在變數 data 內存入數值資料 10
再看看陣列的指標 (pointer to array) 以及二維陣列 (two dimensional array) 的基本用法:
int (*pAry][20]; // pointer to array of 20 integers
int x[10][20];
ppData = x; // ERROR: can not convert int[20] * to int **
pAry = x; // OK: same type
pAry[5][10] = 100; // 將數值 100 置於變數 x[5][10] 之內
最後再來看看指標陣列 (pointer array) 的基本用法:
int *ptrAry[20]; // pointer array
ppData = ptrAry; // OK: same type
ppData[13] = &data; // 將變數 data 的位址存放到變數 ptrAry[13] 中