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

其实,只要控制住指针的指向,使用中就可避免出错。

把它与整型变量对比一下,就容易理解指针的使用。整型变量存储整型类型数据的变量,也就是存储规定范围的整数。指针变量存储指针,也就是存储表示地址的正整数。由此可见,一个指针变量的值就是某个内存单元的地址,或称为某个内存单元的指针。可以讲:指针的概念就是地址。

由此可见,通过指针可以对目标对象进行存取(*操作符),故又称指针指向目标对象。指针可以指向各种基本数据类型的变量,也可以指向各种复杂的导出数据类型的变量,如指向数组元素。

《C语言解惑》