下面來介紹C語言功能最強大的特點,同時也是相對而言比較難掌握的概念之一——指針。
一、指針的基本概念
如同其它基本類型的變量一樣,指針也是一種變量,但它是一種把內存地址作為其值的變量。因為指針通常包含的是一個擁有具體值的變量的地址,所以它可以間接地引用一個值。
二、指針變量的聲明、初始化和運算符
聲明語句
int *ptra, a;
聲明了一個整型變量a與一個指向整數值的指針ptra,也就是說,在聲明語句中使用*(稱為“間接引用運算符”)即表示被聲明的變量是一個指針。指針可被聲明為指向任何數據類型。需要強調的是,在此語句中變量a只被聲明為一個整型變量,這是因為間接引用運算符*並不針對一條聲明語句中的所有變量,所以每一個指針都必須在其名字前面用前綴*聲明。指針應該用聲明語句或賦值語句初始化,可以把指針初始化為0、NULL或某個地址,具有值0或NULL的指針不指向任何值,而要想把某個變量地址賦給指針,需使用單目運算符&(稱為“地址運算符”)。
例如程序已用聲明語句
int *ptra, a=3;
聲明了整型變量a(值為3)與指向整數值的指針a,那麼通過賦值語句
ptra=&a;
就可以把變量a的地址賦給指針變量ptra。需要注意的是不可將運算符&用於常量、表達式或存儲類別被聲明為register的變量上。被賦值後的指針可以通過運算符*獲得它所指向的對象的值,這叫做“指針的復引用”,例如打印語句
printf("%d", *ptra);
就會打印出指針變量ptra所指向的對象的值(也就是a的值)3。如果被復引用的指針沒有被正確的初始化或沒有指向具體的內存單元都會導致致命的執行錯誤或意外地修改重要的數據。Printf的轉換說明符%p以十六進制整數形式輸出內存地址,例如在以上的賦值後,打印語句
printf("%p", &a);
printf("%p", ptra);
都會打印出變量a的地址。
三、指針表達式和算術運算以及數組、字符串和指針的關系
在算術表達式、賦值表達式和比較表達式中,指針是合法的操作數,但是並非所有的運算符在與指針變量一起使用時都是合法的,可以對指針進行的有限的算術運算包括自增運算(++)、自減運算(--)、加上一個整數(+、+=)、減去一個整數(-或-=)以及減去另一個指針。
數組的各元素在內存中是連續存放的,這是指針運算的基礎。現在假設在一台整數占4個字節的機器上,指針ptr被初始化指向整型數組a(共有三個元素)的元素a[0],而a[0]的地址是40000,那麼各個變量的地址就會如下表所示:
表達式 ptra &a[0] &a[1] &a[2] 表達式的意義 指針ptra的值 元素a[0]的地址 元素a[1]的地址 元素a[2]的地址 表達式的值 40000 40000 40004 40008
必須注意,指針運算不同於常規的算術運算,一般地,40000+2的結果是40002,但當一個指針加上或減去一個整數時,指針並非簡單地加上或減去該整數值,而是加上該整數與指針引用對象大小的乘積,而對象的大小則和機器與對象的數據類型有關。例如在上述情況下,語句
ptra+=2;
的結果是40000+4*2=40008, ptra也隨之指向元素a[2],同理,諸如語句
ptra-=2;
ptra++;
++ptra;
ptra--;
ptra--;
等的運算原理也都與此相同,至於指針與指針相減,則會得到在兩個地址之間所包含的數組元素的個數,例如ptra1包含存儲單元40008,ptra2包含存儲單元40000,那麼語句
x = ptra1 - ptra2;
得到的結果就是2(仍假設整數在內存中占4個字節)。因為除了數組元素外,我們不能認為兩個相同類型的變量是在內存中連續存儲的,所以指針算數運算除了用於數組外沒有什麼意義。
如果兩個指針類型相同,那麼可以把一個指針賦給另一個指針,否則必須用強制類型轉換運算符把賦值運算符右邊的指針的類型轉換為賦值運算符左邊指針的類型。例如ptr1是指向整數的指針,而ptr2是指向浮點數的指針,那麼要把ptr2的值賦給ptr1, 則須用語句
ptr1 = (int *) ptr2;
來實現。唯一例外的是指向void類型的指針(即void *),因為它可以表示任何類型的指針。任何類型的指針都可以賦給指向void類型的指針,指向void類型的指針也可以賦給任何類型的指針,這兩種情況都不需要使用強制類型轉換。
但是由於編譯器不能根據類型確定void *類型的指針到底要引用多少個字節數,所以復引用void *指針是一種語法錯誤。
可以用相等測試運算符和關系運算符比較兩個指針,但是除非他們指向同一個數組中的元素,否則這種比較一般沒有意義。相等測試運算符則一般用來判斷某個指針是否是NULL,這在本文後面提到內存操作中有一定的用途。
C語言中的數組和指針有著密切的關系,他們幾乎可以互換,實際上數組名可以被認為是一個常量指針,假設a是有五個元素的整數數組,又已用賦值語句
ptra = a;
將第一個元素的地址賦給了指向整數的指針ptra,那麼如下的一組表達式是等價的:
a[3] ptra[3] *(ptra+3) *(a+3)
它們都表示數組中第四個元素的值。
又因為C語言中的字符串是用空字符('\0')結束的字符數組,所以事實上,字符串就是指向其第一個字符的指針。但是還是要提醒大家,數組名和字符串名都是常量指針,他們的值是不可被改變的,例如程序段
char s[]="this is a test.";
for (;*s='\0';s++)
printf("%c", *s);
是錯誤的,因為它試圖在循環中改變s的值,而s實際上是一個常量指針。
在本部分的最後,說一說指向函數的指針。指向函數的指針包含了該函數在內存中的地址,函數名實際上就是完成函數任務的代碼在內存中的起始地址。函數指針常用在菜單驅動系統中。
四、指針的應用、內存操作和簡單的數據結構
其實初學者在學習指針時,其困難之處往往不在於理解其中的基本概念,而在於不知道指針究竟有何用處,什麼時候應該用指針去解決實際問題。以下我就將指針的主要功能作一個簡單的介紹。
A——調用函數時能修改兩個或兩個以上的值並將其返回調用函數
我們在沒有學習指針之前便涉及了函數的使用,在函數中使用return語句可以把一個值從被調用的函數返回給調用函數(或從被調用函數返回控制權而不返回一個值),但是在實際應用中常常需要能夠修改調用函數中的多個值,這就必須用到指針。
給函數傳遞參數的方式有兩種,即傳值和傳引用,C語言中的所有函數調用都是傳值調用,但可以用指針和間接引用運算符模擬傳引用調用。在調用某個函數時,如果需要在被調用的函數中修改參數值,應該給函數傳遞參數的地址。當把變量的地址傳遞給函數時,可以在函數中用間接引用運算符*修改調用函數中內存單元中的該變量的值。比如我們無法在只用傳值調用的情況下在函數中完成兩個數的交換,因為它要求修改兩個參數的值,但通過傳地址模擬傳引用調用即可輕松地實現這一點,例如函數:
void swap(int *a, int *b)
{
int temp;
temp=*a;
*a=*b;
*b=temp;
}
就實現了兩個整數的交換,在調用函數swap時,需要用兩個地址(或兩個指向整數的指針)作為參數,比如
swap(&num1, &num2);
其中num1和num2是兩個整型變量,這樣他們的值通過調用swap函數便得到了交換。需要指出的是,傳遞數組不需要使用運算符&, 因為數組名實際上是一個常量指針,然而傳遞數組元素就不同了,仍需要以元素的地址作為參數才能在函數中修改元素的值並返回調用函數。
B——能更加方便地處理字符串與數組
如前文所述,數組名與字符串名都是常量指針,指針和數組名有時候是可以替代的。用指針編寫數組下標標達式可節省編譯時間,而且有時表示相對偏移量會更加方便,這方面的技巧本文就不多介紹了。
C——能動態地分配空間並直接處理內存地址
不像BASIC等一些其他高級語言,C語言要求同一層次中聲明語句不能被置於任何執行語句之後,這決定了C中的數組是完全靜態的,因為數組的長度必須在聲明的時候確定,也就是說不能通過輸入語句之類的方法臨時決定數組的大小,要想建立和維護動態的數據結構就需要實現動態的內存分配並運用指針進行操作。
現在將關於內存操作的幾個重要函數列表如下:
函數:
void *calloc(size_t nmemb, size_t size); 作用:
為含nmemb個對象的數組分配空間,每個對象的大小為size.所分配的空間初始化成全零。函數calloc返回空指針或一個指向所分配空間的指針。 函數:
void free(void *ptr); 作用:
回收ptr所指向的空間,即使該空間可用於再分配。 函數:
void malloc(size_t size); 作用:
分配由size指定大小的對象空間。函數返回一個空指針或指向所分配空間的指針。 函數:
void *realloc(void *ptr, size_t size); 作用:
將ptr所指對象的大小改為size指定的大小。在新舊空間的較小空間中的對象的內容不被改變。如果新空間大,該對象新分配部分的值是不確定的。若ptr是空指針,函數realloc的行為類似指定空間的函數malloc。如果size為零且ptr不是空指針,其釋放所指向的對象。函數realloc返回一個空指針或指向可能移動了的分配空間的指針。 注:這些函數的原型在通用工具頭文件中。
運用這些函數我們可以實現動態數據結構的處理,舉個簡單的例子,我們要模擬建立變長數組,只需使用如下語句:
int n, *ptr;
scanf("%d", &n);
ptr=malloc(n*sizeof(int));
這樣,系統就分配了存放有n個元素的數組所需要的空間,ptr存儲了分配內存的首地址,之後通過指針的復引用即可進行賦值等操作。需要提醒的是,如果沒有可用的內存,在調用malloc函數時就返回NULL指針,所以這時一般應判斷一下指針是否為NULL再進行下一步操作。
由此可見,盡管指針通常以變量的地址作為其值,但有時候它指向的地址根本就沒有變量名,這時候只能通過復引用指針進行一系列的操作。
D——能有效地表示復雜的數據結構(本部分內容建議初學者在弄清一切基本概念之前可以先不看)
在初學者接觸令人費解的數據結構之前,必須先了解兩個新的概念——“指向指針的指針”(也稱二級指針)和“自引用結構”。
一級指針包含一個地址,該地址中存放具體的值,而二級指針包含的地址中存放的是另一個地址,因此也可以說二級指針是指向指針的指針。
聲明一個二級指針要使用**,例如:
int **ptr;
聲明了一個指向整數的二級指針。二級指針到底有什麼用呢?其實就像通過給函數傳遞地址,我們就可以修改具體的值一樣,給一個函數傳遞二級指針,就可以將修改地址並將其返回調用函數,這在數據結構的管理上相當重要。
一般的結構包含若干成員,而自引用結構必定包含一個指針成員,該指針指向與自身同一個類型的結構。例如,如下結構定義就定義了一個自引用結構:
struct node{
int data;
struct node *nextptr;
};
通過鏈節nextptr可以把一個struct node類型的結構與另一個同類型的結構鏈在一起。我們可以通過自引用結構建立有用的數據結構,如鏈表、隊列、堆棧和樹。考慮到篇幅問題與難度關系,筆者僅就比較基本的單向鏈表給出機械工業出版社《C程序設計教程》(第二版)中的源程序。我不推薦初學者過早接觸數據結構,因為其中涉及了二級指針和自引用結構的大量應用,在打牢基礎之前基本不可能完全讀懂程序就更不要說運用了。對數據結構有興趣的同學可以再和我進行交流。
五、學習指針與內存的一點建議
很多人在剛接觸指針的時候可能會覺得比較抽象,所以在練習的時候總是回避指針的應用,盡量用原有的方法解決問題,越是這樣就越難進步。我建議大家即使面對不用指針也能解決的問題,也應該嘗試使用指針,尤其是模擬函數的傳引用調用等,只要多多練習,其實掌握基本概念還是很輕松的。再接下來就是要多思考什麼時候用指針顯得更方便和實用,逐漸掌握怎麼用指針的真谛。毫不誇張地說指針是C的核心,學好指針是駕馭C語言的必由之路,大家加油干吧。