在第 12 章 棧與隊列講過,堆棧有棧頂指針,隊列有頭指針和尾指針,這些概念中的“指針”本質上是一個整數,是數組的索引,通過指針訪問數組中的某個元素。在圖 20.3 “間接定址”我們又看到另外一種指針的概念,把一個變數所在的內存單元的地址保存在另外一個內存單元中,保存地址的這個內存單元稱為指針,通過指針和間接定址訪問變數,這種指針在C語言中可以用一個指針類型的變數表示,例如某程序中定義了以下全局變數:
int i; int *pi = &i; char c; char *pc = &c;
這幾個變數的內存佈局如下圖所示,在初學階段經常要借助于這樣的圖來理解指針。
這裡的&
是取地址運算符(Address Operator),&i
表示取變數i
的地址,int *pi = &i;
表示定義一個指向int
型的指針變數pi
,並用i
的地址來初始化pi
。我們講過全局變數只能用常量表達式初始化,如果定義int p = i;
就錯了,因為i
不是常量表達式,然而用i
的地址來初始化一個指針卻沒有錯,因為i
的地址是在編譯連結時能確定的,而不需要到運行時才知道,&i
是常量表達式。後面兩行代碼定義了一個字元型變數c
和一個指向c
的字元型指針pc
,注意pi
和pc
雖然是不同類型的指針變數,但它們的內存單元都占4個位元組,因為要保存32位的虛擬地址,同理,在64位平台上指針變數都占8個位元組。
我們知道,在同一個語句中定義多個數組,每一個都要有[]
號:int a[5], b[5];
。同樣道理,在同一個語句中定義多個指針變數,每一個都要有*
號,例如:
int *p, *q;
如果寫成int* p, q;
就錯了,這樣是定義了一個整型指針p
和一個整型變數q
,定義數組的[]
號寫在變數後面,而定義指針的*
號寫在變數前面,更容易看錯。定義指針的*
號前後空格都可以省,寫成int*p,*q;
也算對,但*
號通常和類型int
之間留空格而和變數名寫在一起,這樣看int *p, q;
就很明顯是定義了一個指針和一個整型變數,就不容易看錯了。
如果要讓pi
指向另一個整型變數j
,可以重新對pi
賦值:
pi = &j;
如果要改變pi
所指向的整型變數的值,比如把變數j
的值增加10,可以寫:
*pi = *pi + 10;
這裡的*
號是指針間接定址運算符(Indirection Operator),*pi
表示取指針pi
所指向的變數的值,也稱為Dereference操作,指針有時稱為變數的引用(Reference),所以根據指針找到變數稱為Dereference。
&
運算符的操作數必須是左值,因為只有左值才表示一個內存單元,才會有地址,運算結果是指針類型。*
運算符的操作數必須是指針類型,運算結果可以做左值。所以,如果表達式E
可以做左值,*&E
和E
等價,如果表達式E
是指針類型,&*E
和E
等價。
指針之間可以相互賦值,也可以用一個指針初始化另一個指針,例如:
int *ptri = pi;
或者:
int *ptri; ptri = pi;
表示pi
指向哪就讓ptri
也指向哪,本質上就是把變數pi
所保存的地址值賦給變數ptri
。
用一個指針給另一個指針賦值時要注意,兩個指針必須是同一類型的。在我們的例子中,pi
是int *
型的,pc
是char *
型的,pi = pc;
這樣賦值就是錯誤的。但是可以先強制類型轉換然後賦值:
pi = (int *)pc;
現在pi
指向的地址和pc
一樣,但是通過*pc
只能訪問到一個位元組,而通過*pi
可以訪問到4個位元組,後3個位元組已經不屬於變數c
了,除非你很確定變數c
的一個位元組和後面3個位元組組合而成的int
值是有意義的,否則就不應該給pi
這麼賦值。因此使用指針要特別小心,很容易將指針指向錯誤的地址,訪問這樣的地址可能導致段錯誤,可能讀到無意義的值,也可能意外改寫了某些數據,使得程序在隨後的運行中出錯。有一種情況需要特別注意,定義一個指針類型的局部變數而沒有初始化:
int main(void) { int *p; ... *p = 0; ... }
我們知道,在堆棧上分配的變數初始值是不確定的,也就是說指針p
所指向的內存地址是不確定的,後面用*p
訪問不確定的地址就會導致不確定的後果,如果導致段錯誤還比較容易改正,如果意外改寫了數據而導致隨後的運行中出錯,就很難找到錯誤原因了。像這種指向不確定地址的指針稱為“野指針”(Unbound Pointer),為避免出現野指針,在定義指針變數時就應該給它明確的初值,或者把它初始化為NULL
:
int main(void) { int *p = NULL; ... *p = 0; ... }
NULL
在C標準庫的標頭檔stddef.h
中定義:
#define NULL ((void *)0)
就是把地址0轉換成指針類型,稱為空指針,它的特殊之處在於,操作系統不會把任何數據保存在地址0及其附近,也不會把地址0~0xfff的頁面映射到物理內存,所以任何對地址0的訪問都會立刻導致段錯誤。*p = 0;
會導致段錯誤,就像放在眼前的炸彈一樣很容易找到,相比之下,野指針的錯誤就像埋下地雷一樣,更難發現和排除,這次走過去沒事,下次走過去就有事。
講到這裡就該講一下void *
類型了。在編程時經常需要一種通用指針,可以轉換為任意其它類型的指針,任意其它類型的指針也可以轉換為通用指針,最初C語言沒有void *
類型,就把char *
當通用指針,需要轉換時就用類型轉換運算符()
,ANSI在將C語言標準化時引入了void *
類型,void *
指針與其它類型的指針之間可以隱式轉換,而不必用類型轉換運算符。注意,只能定義void *
指針,而不能定義void
型的變數,因為void *
指針和別的指針一樣都占4個位元組,而如果定義void
型變數(也就是類型暫時不確定的變數),編譯器不知道該分配幾個位元組給變數。同樣道理,void *
指針不能直接Dereference,而必須先轉換成別的類型的指針再做Dereference。void *
指針常用於函數介面,比如:
void func(void *pv) { /* *pv = 'A' is illegal */ char *pchar = pv; *pchar = 'A'; } int main(void) { char c; func(&c); printf("%c\n", c); ... }
下一章講函數介面時再詳細介紹void *
指針的用處。