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 ); }
其实,只要控制住指针的指向,使用中就可避免出错。
把它与整型变量对比一下,就容易理解指针的使用。整型变量存储整型类型数据的变量,也就是存储规定范围的整数。指针变量存储指针,也就是存储表示地址的正整数。由此可见,一个指针变量的值就是某个内存单元的地址,或称为某个内存单元的指针。可以讲:指针的概念就是地址。
由此可见,通过指针可以对目标对象进行存取(*操作符),故又称指针指向目标对象。指针可以指向各种基本数据类型的变量,也可以指向各种复杂的导出数据类型的变量,如指向数组元素。