讀古今文學網 > C語言解惑 > 5.3 指針地址的有效性 >

5.3 指針地址的有效性

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
);
}
  

其實,只要控制住指針的指向,使用中就可避免出錯。

把它與整型變量對比一下,就容易理解指針的使用。整型變量存儲整型類型數據的變量,也就是存儲規定範圍的整數。指針變量存儲指針,也就是存儲表示地址的正整數。由此可見,一個指針變量的值就是某個內存單元的地址,或稱為某個內存單元的指針。可以講:指針的概念就是地址。

由此可見,通過指針可以對目標對像進行存取(*操作符),故又稱指針指向目標對象。指針可以指向各種基本數據類型的變量,也可以指向各種複雜的導出數據類型的變量,如指向數組元素。