1. 指針的基本概念

第 12 章 棧與隊列講過,堆棧有棧頂指針,隊列有頭指針和尾指針,這些概念中的“指針”本質上是一個整數,是數組的索引,通過指針訪問數組中的某個元素。在圖 20.3 “間接定址”我們又看到另外一種指針的概念,把一個變數所在的內存單元的地址保存在另外一個內存單元中,保存地址的這個內存單元稱為指針,通過指針和間接定址訪問變數,這種指針在C語言中可以用一個指針類型的變數表示,例如某程序中定義了以下全局變數:

int i;
int *pi = &i;
char c;
char *pc = &c;

這幾個變數的內存佈局如下圖所示,在初學階段經常要借助于這樣的圖來理解指針。

圖 23.1. 指針的基本概念

指針的基本概念

這裡的&是取地址運算符(Address Operator)&i表示取變數i的地址,int *pi = &i;表示定義一個指向int型的指針變數pi,並用i的地址來初始化pi。我們講過全局變數只能用常量表達式初始化,如果定義int p = i;就錯了,因為i不是常量表達式,然而用i的地址來初始化一個指針卻沒有錯,因為i的地址是在編譯連結時能確定的,而不需要到運行時才知道,&i是常量表達式。後面兩行代碼定義了一個字元型變數c和一個指向c的字元型指針pc,注意pipc雖然是不同類型的指針變數,但它們的內存單元都占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可以做左值,*&EE等價,如果表達式E是指針類型,&*EE等價。

指針之間可以相互賦值,也可以用一個指針初始化另一個指針,例如:

int *ptri = pi;

或者:

int *ptri;
ptri = pi;

表示pi指向哪就讓ptri也指向哪,本質上就是把變數pi所保存的地址值賦給變數ptri

用一個指針給另一個指針賦值時要注意,兩個指針必須是同一類型的。在我們的例子中,piint *型的,pcchar *型的,pi = pc;這樣賦值就是錯誤的。但是可以先強制類型轉換然後賦值:

pi = (int *)pc;

圖 23.2. 把char *指針的值賦給int *指針

把char *指針的值賦給int *指針

現在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 *指針的用處。