指针初始化,就是保证指针指向一个有效的地址。这有两个含义,一是保证指针指向一个地址;二是指针指向的地址是有效的。
1.数值表示地址
为了更深入地理解这一点,首先要记住指针是与地址相关的。指针变量的取值是地址值,但如何用数值来代表地址值呢?请看下面的例子。
【例14.4】演示表示地址值的例子。
#include <stdio.h>void main(){ int a=25,b=55; printf (/"%d,%dn/",&a,&b); printf (/"%d,%dn/",*&a,*&b); printf (/"%d,%dn/",*(int *)1245052,*(int *)1245048);}
程序输出结果如下。
1245052,124504825,5525,55
“&a”表示变量a的地址,“*&a”表示存入地址里的值,也就是变量a的值。既然&a代表的是存储变量a的十进制地址1245052,是否可以使用“*1245052”呢?
答案是否定的,编译器无法解释“*1245052”。要想让“1245052”表示地址,必须显式说明,即让它与指针关联起来,使用“int*”将这个十进制数字强制转换成地址。这就是通常说的“地址就是指针”的含义。
2.有效地址和无效地址
【例14.5】演示有效地址和地址值的例子。
#include <stdio.h>void main(){ int a=25,b=55,*p; //4 printf (/"%d,%dn/",&a,&b); //5 a=1245048; //6 p=(int *)a; //7 printf (/"%d,%d,%d,%dn/",&a,a,*(int *)a,*p); //8 *p=1245048; //9 printf (/"%d,%dn/",p,*p); //10}
第6行只是将与变量b的地址值相等的数值赋给变量a,所以变量a的值是十进制数值,不是地址。第7行则是将变量a的数值强制转换成十进制地址值赋给指针p,即指针指向变量b的地址。这表现在第8行的输出“&a”仍然是变量a的地址,而“a”则是和变量b的地址值相等的数值。已知“(int*)a”代表变量b的地址,所以“*(int*)a”和“*p”都是输出变量b的值。由此得这一行的输出为:
1245052,1245048,55,55
第9行给*p赋值,*p要求的是数值,这里是把与变量b地址值相等的数值赋给*p,p是指向变量b的地址,所以输出
1245048,1245048
是相同的。但要注意一个是地址的值,一个是数据的数值。由此可知,一个地址值,必须是指针类型的数据(这里是int*)。因为地址值是指针类型的数值,所以才有“地址就是指针”的说法。指针要用一个地址值初始化,而变量的地址肯定是有效的,所以用变量地址初始化指针是万无一失的。不过,有时并没有变量可用,这就要自己为它分配正确的地址。
这个例子不仅演示如何使用地址值,还演示有效地址的概念。这里是在确定变量地址之后,才使用它们,所以是有效的。一般认为,所谓地址有效是指在计算机能够有效存取的范围内。由此可见,这个有效并不能保证程序正确。指针超出本程序使用的地址范围,可能带来不可估计的错误。
为了保证赋予的地址有效,避免像上面例子那样直接使用强制转换,而是直接使用变量地址赋值。例如:
int a=25,b=55,*p=&b;
或者使用语句
int a=25,b=55,*p;p=&b;
由此可见,对于一个指针,需要赋给它一个地址值。上面的例子在赋给指针地址时,不是随意的,而是经过挑选的。如果随便选一个地址,可能是计算机不能使用的地址,也就是无效的地址。
【例14.6】演示无效地址的例子。
#include <stdio.h>void main(){ int a=25,b=55,*p; //4 printf (/"%d,%dn/",&a,&b); //5 a=12345; //6 p=(int *)a; //7 printf (/"%d,%d,%dn/",&a,a,p); //8 printf (/"%dn/",*p); //9}
如果注释掉第9行,运行结果如下:
1245052,12450481245052,12345,12345
从运行结果知道,把十进制地址12345赋给了指针p,程序可以将p指向的地址打印出来。但这是个无效的地址,所以不能使用“*p”。这时虽然编译没问题,但却产生运行时错误。
由此可见,使用指针的危险性就是赋予它一个无效地址。如果有效地避免了这一点,就可以运用自如。
3.无效指针和NULL指针
编译器保证由0转换而来的指针不等于任何有效的指针。常数0经常用符号NULL代替,即定义如下:
#define NULL 0
当将0赋值给一个指针变量时,绝对不能使用该指针所指向的内存中存储的内容。NULL指针并不指向任何对象,但可以用于赋值或比较运算。除此之外,任何因其他目的而使用NULL指针都是非法的。因为不同编译器对NULL指针的处理方式也不相同,所以要特别留神,以免造成不可收拾的后果。
如上所说,把指针初始化为NULL,就是用0号地址初始化指针,而且这个地址不允许程序操作,但可以为编程提供判别条件。尤其是申请内存时,假如没有分配到合适的地址,系统将返回NULL。
C编译程序都提供了内存分配函数,最主要的是malloc和calloc,它们是标准C语言函数库的一部分,都是为要写的数据在内存中分配一个安全区。一旦找到一个大小合适的内存空间,就分配给它们,并将这部分内存的地址作为一个指针返回。malloc和calloc的主要区别是:calloc清除所分配的内存中的所有字节,即置零所有字节;malloc仅分配一块内存,但所有字节的内容仍然是被分配时所含的随机值。
malloc和calloc所分配的内存空间,都可以用free函数释放出来。这3个函数的原型在文件stdlib.h中,但很多编译器又放在头文件malloc.h中,注意查阅手册。
在C目前所提供的最新编译程序中,malloc和calloc都返回一个void型的指针,也就是说,返回的地址值可以假设为任何合法的数据类型的指针。这个强制转换可以在声明中进行,如把它们声明为字符型、整型、长整型、双精度或其他任何类型。
【例14.7】找出程序中的错误并改正。
#include <stdlib.h>#include <stdio.h>void main (){ char *p; *p = malloc(100); gets(p); printf(p); free(p);}
malloc所返回的地址并未赋给指针p,而是赋给了指针p所指的内存位置。这一位置在此情况下也是完全未知的。下面语句
char *p;*p = ( char *) malloc(100);
只是将void指针强制转化为char类型的指针,所以它与上面的等效,都是错误的。如果改为
char *p;p = malloc(100);
的方式,则是可以的,但它也有另外一个更为隐蔽的错误。如果内存已经用完了,malloc将返回空(NULL)值,这在C语言中是一个无效的指针。正确的程序应该把对指针的有效性检查加入其中,并及时释放不用的动态内存空间。下面是一个正确而完整的实例。
#include <stdlib.h>#include <stdio.h>void main ( ){ char *p; p =(char *) malloc(100); if(p==NULL) { printf (/"内存分配错误!n/"); exit(1); } gets(p); printf(p); free(p);}
在设计C程序时,对指针的初始化有两种方法:使用已有变量的地址或者为它分配动态存储空间。好的设计方法是尽可能早点为指针赋值,以免遗忘造成使用没有初始化的指针。
对于上述程序,如果设置
p =NULL;
则程序执行if语句“{}”里的部分,输出“内存分配错误!”,然后退出程序。在程序设计中,有时正好利用NULL作为判别条件。下面就是一个典型的例子。
【例14.8】完善下面的程序。
#include <stdio.h>char s1[16];char *mycopy(char *dest,char *src){ while(*dest++=*src++); return dest;}void main ( ){ char s2[16]=/"how are you?/"; mycopy(s1,s2); printf(s1); printf(/"n/");}
这个程序编译没有错误,但不够完善。这主要是mycopy函数中没有采取预防指针为NULL(又称0指针)的情况。解决的方法很多,下面是简单处理的例子。
char *mycopy(char *dest,char *src){ if(dest == NULL || src == NULL) return dest; while(*dest++=*src++); return dest;}
由此可见,使用已有变量的地址初始化指针能保证地址总是有效的。如果使用分配动态存储空间来初始化指针,确保地址有效的方法是增加判断地址分配是否成功的程序段。