1.地址的有效性
計算機的內存地址是有一定構成規律的。能被CPU訪問的地址才是有效的地址,除此之外都是無效的地址。
【例5.7】找出下面程序中的錯誤。
#include <stdio.h> int main () { char a=\'B\' , *p ; int addr ; p=&a ; addr=&a ; printf (\"0x%p ,0x%p ,0x%pn\" , &a ,addr ,p ); printf (\"%c ,%c ,%cn\" ,a ,*p ,*addr ); return 0 ; }
這個程序與例2.14類似,只是變量為字符型並聲明p為字符型指針。p是指針變量,存儲的是地址,直接使用「p=&a」是天經地義。但addr不行,它是整型數值而&a是地址,兩者不匹配,使用「addr=&a」就要給出警告信息。與例2.14的方法一樣,使用
addr= (int )&a ;
即可完成匹配。同理,可以使用「*p」輸出存儲在p變量地址裡的值,但不能使用「*addr」,因為addr是整數表示的地址。其實,它應該和p的類型一致,即使用「(char*)」轉換一下。下面給出改正後的程序和運行結果。
#include <stdio.h> int main () { char a=\'B\' ,*p ; int addr ; p=&a ; addr= (int )&a ; printf (\"0x%p ,0x%p ,0x%pn\" , &a ,addr ,p ); printf (\"%c ,%c ,%cn\" ,a ,*p ,* (char* )addr ); return 0 ; } 0x0012FF7C ,0x0012FF7C ,0x0012FF7C B ,B ,B
也可以直接把地址賦給指針變量。以例5.7為例,p是字符型指針,所以要進行類型轉換。*p是字符型,輸出時無需轉換,但addr需要使用「(char*)」轉換,然後再對它使用「*」號輸出字符值。詳見下面的例子,輸出結果與例5.7相同。
【例5.8】直接賦地址值的例子。
#include <stdio.h> int main () { char a=\'B\' ,*p ; int addr ; addr= (int )0x0012FF7C ; p= (char * )0x0012FF7C ; printf (\"0x%p , 0x%p , 0x%pn\" , &a ,addr ,p ); printf (\"%c ,%c ,%cn\" ,a ,*p ,* (char* )addr ); return 0 ; }
這裡給p賦的地址值是存儲字符B的地址值,這個地址是有效的。由此可見,可以隨便把一個地址賦給p,只要轉換匹配一下即可,p是來者不拒,並不管給它賦的什麼值,更不管其後果。聲明一個指針,必須賦給它一個合理的地址值,請看下面的例子。
【例5.9】演示給指針賦有效和無效地址的例子。
#include <stdio.h> int main () { char *p ,a=\'A\' ,b=\'B\' ; p=&a ; printf (\"0x%p , %cn\" , p ,*p ); p= (char * )0x0012FF74 ; printf (\"0x%p , %cn\" , p ,*p ); p= (char * )0x0012FF78 ; printf (\"0x%p , %cn\" , p ,*p ); p= (char * )0x0012FF7C ; printf (\"0x%p , %cn\" , p ,*p ); p= (char * )0x1234 ; printf (\"0x%pn\" , p ); printf (\"%cn\" , *p ); return 0 ; }
編譯正確,但運行時出現異常。
0x0012FF78 , A 0x0012FF74 , B 0x0012FF78 , A 0x0012FF7C , | 0x00001234
當指針賦予字符A的地址時,指針地址不僅有效,且*p具有確定的字符A。當將p改賦地址0x0012FF74時,這個地址恰恰是系統分給字符B的地址,這個地址不僅有效,且*p具有確定的字符B。地址有效,但內容不一定確定。0x0012FF7C是有效地址,但程序沒有使用這個地址,所以決定不了它的內容,輸出字符「|」是無法預知的。地址0x1234雖然能被指針p接受,也能輸出這個地址,但這個地址是無效的,所以執行語句
printf (\"%cn\" , *p );
時,產生運行時錯誤。也就是當賦一個無效的地址給p時,就不能對*p操作。
結論:使用指針必須對其初始化,必須給指針賦予有效的地址。
2.指針本身的可變性
編譯系統為變量分配的地址是不變的,為指針變量分配的地址也是如此,但指針變量所存儲的地址是可變的。
【例5.10】有如下程序:
#include <stdio.h> int main () { int a=15 , b=25 , c=35 ,i=0 ; //4 int *p=&a ; //5 printf (\"0x%p ,0x%p ,0x%pn\" , &a ,&b ,&c ); //6 printf (\"0x%p ,0x%p ,0x%p ,%dn\" , &p ,*&p ,p ,*p ); //7 for (i=0 ;i<3 ;i++ ,p-- ) //8 printf (\"%d \" , *p ); //9 printf (\"n%d ,0x%p ,0x%pn\" , *p ,p ,&p ); //10 for (i=0 ,++p ;i<3 ;i++ ,p++ ) //11 printf (\"%d \" , *p ); //12 printf (\"n%d ,0x%p ,0x%pn\" , *p ,p ,&p ); //13 --p ; //14 for (i=0 ;i<3 ;i++ ) //15 printf (\"%d \" , * (p-i )); //16 printf (\"n%d ,0x%p ,0x%pn\" , *p ,p ,&p ); //17 for (i=0 ;i<3 ;i++ ) //18 printf (\"%d \" , * (p-2+i )); //19 printf (\"n%d ,0x%p ,0x%pn\" , *p ,p ,&p ); //20 return 0 ; }
假設運行後,5、6兩行給出如下輸出信息。
0x0012FF7C ,0x0012FF78 ,0x0012FF74 0x0012FF6C ,0x0012FF7C ,0x0012FF7C ,15
請問能分析出程序後面的輸出結果嗎?
【解答】因為有兩個地址裡存儲的值不是由程序決定的,所以有兩個輸出不能確定。除此之外,其他的輸出值均可以根據給出的輸出語句,寫出確定的輸出結果。
為了便於分析,首先要清楚所給上述輸出結果的含義。
(1)從第一行可知,這依次是分配給變量a,b,c的地址。
(2)a的地址是0x0012FF7C。注意第2行的輸出中,第3個和第4個的值與它相等。
(3)第1行第1個0x0012FF6C對應「&p」,是編譯系統為指針分配的地址,用來存放指針p。因為已經給指針變量賦值(p=&a),所以「*&p」就是輸出指針地址0x0012FF6C裡的內容0x0012FF7C。它就是p指向a的地址,即p也輸出0x0012FF7C。也就是說,*&p,p,&a三個的值相同。
(4)「*p」就是輸出指針p指向地址0x0012FF7C裡所存儲的變量a的值,即15。
要分析輸出,需要掌握如下操作含義。
(1)編譯系統為聲明的變量a分配存儲地址,運行時可以改變a的數值,但不會改變存儲a的地址,即&a的地址值不變。同理,為聲明的指針變量p分配一個存儲地址,p指向的地址值可以變化,但&p的地址不會變化。
(2)可以對指針變量p做加、減操作。由第1行輸出結果知,p=p-1(可記做--p),則p指向的地址是0x0012FF78,*p輸出字符B,再執行p--,則*p輸出C。如果再執行p++,則*p輸出C。這時,對p操作後,不僅p指向的地址有效,其地址中存儲的內容也正確。
(3)如果p的操作超出這三個變量的地址,就無法得出輸出結果。
按照上述提示,預測如下。
(1)第8~10行中的for語句就是輸出三個變量的值(15 25 35),輸出完之後,可以預測p指向地址為0x0012FF70,但不能預測*p的內容。運行過程中&p保持為0x0012FF6C。
(2)第11~13行中的for語句是反向輸出三個變量的值(35 25 15),輸出完之後,可以預測p指向地址為0x0012FF80,但不能預測*p的內容。&p仍為0x0012FF6C。
(3)第14~17行中的for語句也是輸出三個變量的值(15 25 35),第14行將p調整指向存儲a的地址,循環語句中使用「*(p-i)」,因為只是使用p做基準,用i做偏移量,所以p的值不變,輸出完之後,p不變,仍為0x0012FF7C,*p=15,&p不變。
(4)第18~20行中的for語句是反向輸出三個變量的值(35 25 15),循環語句也使用p做基準中,即「*(p-2+i)」。輸出完之後,p不變,仍為0x0012FF7C,*p=15,&p不變。
由此可見,要小心對p的操作,以免進入程序非使用區或無效地址。如果使用不當,嚴重時會使系統崩潰,這是使用指針的難點之一。
程序實際運行結果如下。
0x0012FF7C ,0x0012FF78 ,0x0012FF74 0x0012FF6C ,0x0012FF7C ,0x0012FF7C ,15 15 25 35 3 ,0x0012FF70 ,0x0012FF6C 35 25 15 1245120 ,0x0012FF80 ,0x0012FF6C 15 25 35 15 ,0x0012FF7C ,0x0012FF6C 35 25 15 15 ,0x0012FF7C ,0x0012FF6C
3.沒有初始化指針和空指針
【例5.11】沒有初始化指針與初始化為空指針的區別。
#include <stdio.h> int main () { int a=15 ; int *p ; printf (\" 指針沒有初始化:n\" , p ,&p ); printf (\"0x%p ,0x%pn\" , p ,&p ); p=NULL ; printf (\" 指針初始化為NULL :n\" , p ,&p ); printf (\"0x%p ,0x%pn\" , p ,&p ); }
運行結果如下。
指針沒有初始化: 0xCCCCCCCC ,0x0012FF78 指針初始化為NULL : 0x00000000 ,0x0012FF78
顯然,這兩種情況下,不管如何初始化指針,&p分配的地址是一樣的,區別是指針變量存放的值。
指針在沒有初始化之前,指針變量沒有存儲有效地址,如果對「*p」進行操作,就會產生運行時錯誤。當用NULL初始化指針時,指針變量存儲的內容是0號地址單元,這雖然是有效的地址,但也不允許使用「*p」,因為這是系統地址,不允許應用程序訪問。
為了用好指針,應養成在聲明指針時,就予以初始化。既然初始化為NULL,也會產生運行時錯誤,何必要選擇這種初始化方式呢?其實,這是為了為程序提供一種判斷依據。例如,申請一塊內存塊,在使用之前要判斷是否申請成功(申請成功才能使用)。
int *p=NULL ; p= (int* )malloc (100 ); if (p==NULL );{ printf (\" 內存分配錯誤!n\" ); exit (1 ); // 結束運行 }
注意正確地包含必要的頭文件,下面給出一個完整的例子。
【例5.12】判斷空指針的完整例子。
#include <stdlib.h> #include <stdio.h> void main ( ) { char *p ; if ( (p = (char * )malloc (100 ) ) == NULL ) { printf (\" 內存不夠!n\" ); exit (1 ); } gets (p ); printf (\"%sn\" , p ); free (p ); }
其實,只要控制住指針的指向,使用中就可避免出錯。
把它與整型變量對比一下,就容易理解指針的使用。整型變量存儲整型類型數據的變量,也就是存儲規定範圍的整數。指針變量存儲指針,也就是存儲表示地址的正整數。由此可見,一個指針變量的值就是某個內存單元的地址,或稱為某個內存單元的指針。可以講:指針的概念就是地址。
由此可見,通過指針可以對目標對像進行存取(*操作符),故又稱指針指向目標對象。指針可以指向各種基本數據類型的變量,也可以指向各種複雜的導出數據類型的變量,如指向數組元素。