本节来解决如何给一个指针变量初始化。即怎样使一个指针变量指向另一个变量。
前面章节中的某些程序实际上已经使用了,即可以用赋值语句使一个指针变量得到另一个变量的地址,从而使它指向该变量。比如:
int i, *j;
j = &i;
这样就将变量 i 的地址放到了指针变量 j 中,通过 i 的地址,j 就能找到 i 中的数据,所以 j 就“指向”了变量 i。其中 & 是“取地址运算符”,与 scanf 中的 & 是一样的概念;* 为“指针运算符”,功能是取其内部所存变量地址所指向变量中的内容。因为 j 是定义成指针型变量,所以 j 中只能存放变量的地址,所以变量i前一定要加 &。需要注意的是,指针变量中只能存放地址,不要将一个整数或任何其他非地址类型的数据赋给一个指针变量。
此外,还有两点需要注意:
下面给大家写一个程序:
# include <stdio.h>
int main(void)
{
int i = 3, *j; //*j表示定义了一个指针变量j
j = &i;
printf("*j = %d\n", *j); //此时*j完全等同于i
printf("j = %d\n", j); //j里面存储的是变量i的地址
return 0;
}
输出结果是:
*j = 3
j = 1245052
下面再将上面这个程序修改一下:
# include <stdio.h>
int main(void)
{
int i = 3;
int *j = &i; //*j表示定义了一个指针变量j, 并将变量i的地址赋给它
printf("*j = %d\n", *j); //此时*j完全等同于i
printf("j = %d\n", j); //j里面存储的是变量i的地址
return 0;
}
输出结果是:
*j = 3
j = 1245052
这个程序与第一个程序有什么不同?同样是将变量 i 的地址赋给指针变量 j,第一个程序是“j=&i;”,而第二个程序是“*j=&i;”。原因是,前者是定义指针变量后对它初始化,即先定义后初始化;而后者是定义指针变量时对它进行初始化,即定义时初始化。通过这个对比我们可以更鲜明地看出定义指针变量时的“*j”和程序中用到的“*j”含义的不同。
那么指针变量和指针变量之间可不可以相互赋值呢?我们看看下面这个程序:
# include <stdio.h>
int main(void)
{
int *i, *j;
int k = 3;
i = &k;
j = i; //直接指针变量名之间进行赋值
printf("*j = %d\n", *j); //此时*j完全等同于k
printf("j = %d\n", j); // j里面存储的是变量k的地址
return 0;
}
输出结果是:
*j = 3
j = 1245044
可见,可以直接将一个指针变量赋给另一个指针变量,只要将指针变量名赋给另一个指针变量名即可。但是需要注意的是:
同样,也可以在定义指针变量时就给它赋初值:
# include <stdio.h>
int main(void)
{
int k = 3;
int *i = &k;
int *j = i;
printf("*j = %d\n", *j); //此时*j完全等同于k
printf("j = %d\n", j); //j里面存储的是变量k的地址
return 0;
}
输出结果是:
*j = 3
j = 1245048
注意,“int*j=i;”千万不要写成“int*j=*i;”。因为此时 *i 不是定义指针变量 i,而是完全等同于变量 k。所以 int 型变量不能赋给 int* 型的变量。
试图引用未初始化的指针变量是初学者最容易犯的错误。未初始化的指针变量就是“野”指针,它指向的是无效的地址。
有些书上说:“如果指针变量不初始化,那么它可能指向内存中的任何一个存储单元,这样就会很危险。如果正好指向存储着重要数据的内存单元,而且又不小心向这个内存单元中写入了数据,把原来的重要数据给覆盖了,这样就会导致系统崩溃。”这种说法是不正确的!如果真是这样的话就是编译器的一个严重的 BUG!
编译器的设计人员是不会允许这么大的 BUG 存在的。那么如果指针变量未初始化,编译器的设计人员是如何处理这个问题的呢?肯定不可能让它乱指。以VC++6.0这个编译器为例,如果指针变量未初始化,那么编译器会让它指向一个固定的、不用的地址。下面来写一个程序:
# include <stdio.h>
int main(void)
{
int *p, *q;
printf("p = %#X\n", p);
printf("q = %#X\n", q);
return 0;
}
输出结果是:
p = 0XCCCCCCCC
q = 0XCCCCCCCC
可见,在 VC++6.0 中只要指针变量未初始化,那么编译器就让它指向 0XCCCCCCCC 这个内存单元。而且这个内存单元是程序所不能访问的,访问就会触发异常,所以也不怕往里面写东西。
而如果在 VS 2008 这个编译器中,程序虽然能编译通过,但是在运行的时候直接出错,它并不会像 VC++6.0 那样还能输出所指向的内存单元的地址。
下面来看一个程序:
# include <stdio.h>
int main(void)
{
int i = 3, *j;
*j = i;
return 0;
}
程序中,j 是 int* 型的指针变量。j 中存放的应该是内存空间的地址,然后“变量 i 赋给 *j”表示将变量i中的值放到该地址所指向的内存空间中。但是现在 j 中并没有存放一个地址,程序中并没有给它初始化,那么它指向的就是 0XCCCCCCCC 这个内存单元。这个内存单元是不允许访问的,即不允许往里面写数据。而把 i 赋给 *j 就是试图往这个内存空间中写数据,程序执行时就会出错。但这种错误在编译的时候并不会报错,只有在执行的时候才会出错,即传说中的“段错误”。所以,一定要确保指针变量在引用之前已经被初始化为指向有效的地址。
在实际编程中,这种错误常见的另一个地方是用 scanf 给指针变量所指向的内存单元赋值。我们看看下面这个程序:
# include <stdio.h>
int main(void)
{
int *i;
scanf("%d", i);
return 0;
}
该程序试图给指针变量 i 所指向的内存单元赋值。但现在指针变量 i 并没有初始化,所以程序执行时出错。所以同样,在使用 scanf 时必须要先给指针变量 i 初始化。比如像下面这样写:
# include <stdio.h>
int main(void)
{
int *i, j;
i = &j; //先给指针变量i初始化
scanf("%d", i); //i本身就是地址, 所以不用加&
printf("%d\n", *i);
return 0;
}
输出结果是:
10
10
能不能使用 scanf 给指针变量初始化?指针变量里面存放的是地址,而内存中有数不清的单元,每个单元都有一个地址,你知道每个单元的地址吗?你知道哪些地址是空闲可用的,而哪些地址正存储着重要数据不能用吗?不知道的话怎么用scanf给它初始化呢?万一随便写一个地址正好是存储着非常重要的数据的内存单元地址,那系统就真的崩溃了!
随着大家编程能力的不断提高,慢慢地就会发现,其实编程最重要、最核心的就是如何处理内存的问题,如何与内存打交道。
这也是编程中最容易犯的错误,不仅是初学编程的,即使是有一些经验的程序员也会不小心犯这个错误。我们把前面的程序改一下:
# include <stdio.h>
int main(void)
{
int i = 3;
int *j = NULL;
*j = i;
return 0;
}
之前是没有给指针变量j初始化,现在初始化了,但是将它初始化为指向 NULL。NULL 也是一个指针变量。NULL 指向的是内存中地址为 0 的内存空间。以 32 位操作系统为例,内存单元地址的范围为 0x00000000~0xffff ffff。其中 0x00000000 就是 NULL 所指向的内存单元的地址。但是在操作系统中,该内存单元是不可用的。凡是试图往该内存单元中写入数据的操作都会被视为非法操作,从而导致程序错误。同样,这种错误在编译的时候也不会报错,只有在执行的时候才会出错。这种错误也属于“段错误”。
然而虽然这么写是错误的,但是将一个指针变量初始化为指向 NULL,这在实际编程中是经常使用的。就跟前面讲普通变量在定义时给它初始化为 0 一样,指针变量如果在定义时不知道指向哪里就将其初始化为指向 NULL。只是此时要注意的是,在该指针变量指向有效地址之前不要往该地址中写入数据。也就是说,该指针变量还要二次赋值。
既然不能往里面写数据,而且还容易犯错,为什么还要这样给它初始化呢?直接同前面定义普通变量时一样,在定义时也不初始化,等到后面知道该给它赋什么值时再给它赋值不行吗?可以!但还是建议大家将它初始化为 NULL,就同前面将普通变量在定义时初始化为 0 一样。这是很好的一种编程习惯。
最后关于 NULL 再补充一点,NULL 是定义在 stdio.h 头文件中的符号常量,它表示的值是 0。