上一节介绍了 C 语言指针的概念,并且给出了通过指针修改变量值的例子,相信看了上一节的朋友应该对 C 语言的指针没那么陌生了。
指针变量占多少内存空间?
稍稍思考一下,应该能够发现,指针的确是通过修改内存来修改变量的值的。例如下图,指针变量 p1 指向地址为 4000 的内存,而这里记录着变量 i 的值,通过 C 语言提供的指针运算来修改 p1 指向的内存里的值,也就相当于修改了变量 i 的值,这与直接对 i 赋值是一样的。
既然指针变量存储的是内存地址,那么指针变量的位宽就应该保证能够存储最大的地址。例如在大多数 32 位计算机中,指针变量的位宽为 4 字节,因为多数情况下,在 32 位计算机中,最大的内存地址为 0xffffffff,至少需要 4 字节才能完整保存。相应的,在大多数 64 位计算机中,指针变量的位宽为 8 字节。
为何要有不同类型的指针变量
继续讨论,既然指针是通过修改内存来修改变量的值的,那么,一个指针一次修改多少内存呢?这就涉及到指针的类型了。请看下面的例子:
signed char i[8] = {1, 2, 3, 4, 5, 6, 7, 8};
int j = 8;
signed char *p1 = i;
int *p2 = (int*)i;
*(p1+1) = 5;
*(p2+1) = 9;
对于数组 i[8],i 其实就表示这个数组的首地址,所以可以直接把它赋值给指针变量 p1。这样一来,我们就可以通过 p1 来修改数组 i 了。
指针 p1 是 signed char* 类型的,通过 p1 修改 i 所在内存时,一次修改 sizeof(signed char) 字节,也即 1 字节。那么,p1+1 指向的就是 i 的第二个元素(i[1]),执行 * (p1+1) = 5; 以后,i[1] 就等于 5 了。
按照这个逻辑,p2 是 int* 类型的指针变量,请看上图,通过 p2 访问数组 i 时,一次访问的实际上是 sizeof(int) = 4 字节内存。所以 * (p2+1) = 9; 实际上修改的是 i 的第 5~8 字节。
我们把代码写完整些,通过 p1 修改后,把 i 全部打印出来;通过 p2 修改后,再把 i 全部打印出来,请看如下代码:
#include <stdio.h>
int main()
{
signed char i[8] = {1, 2, 3, 4, 5, 6, 7, 8};
int j = 8;
signed char *p1 = i;
int cnt;
*(p1+1) = 5;
for(cnt=0;cnt<8;cnt++)
printf("%d ", i[cnt]);
printf("\n");
int* p2 = (int*)i;
*(p2+1) = 9;
for(cnt=0;cnt<8;cnt++)
printf("%d ", i[cnt]);
printf("\n");
return 0;
}
以上代码输出:
1 5 3 4 5 6 7 8
1 5 3 4 9 0 0 0
指针的加法
看到这里,你可能会有疑问了,i 的地址为 4000,那 p1 和 p2 指向的也是 4000,p1+1 指向 4001 地址,这没什么好说的。但是 p2+1 指向的却是 4004? 4000+1 等于 4004 ,这不是扯淡吗?!
这还真不是扯淡,还记得我们在第 19 节一起讨论的 C 语言的数据类型吗?“+”运算符要求两边的操作数是同一类型的,如果不同则会自动转换。p1 和 p2 是指针类型的,而 “+1” 的这个“1”是整型的,因此在做加法之前,会有自动数据类型转换的过程。p1 是 signed char* 型的指针变量,所以“+1”就相当于“+1 x sizeof(signed char)”,因此 p1+1 = 4001。类似的,p2 加上整型 1 就相当于“+ 1 x sizeof(int)”,因此 p2+1=4004。
为了验证我们的分析,下面写 C 代码做实验,我们分别定义 signed char* 型的指针变量 p1 和 int* 型的指针变量 p2,均赋值为 1,然后分别对 p1 和 p2 加一,打印它们原来的值,和加一后的值,请看如下代码:
#include <stdio.h>
int main()
{
signed char *p1 = (signed char*)1;
int *p2 = (int*)1;
printf("p1=%p, p1+1=%p\n", p1, p1+1);
printf("p2=%p, p2+1=%p\n", p2, p2+1);
return 0;
}
以上程序输出:
p1=0x1, p1+1=0x2
p2=0x1, p2+1=0x5
这就验证了我们的分析。类似的,读者可以自行分析 long* 、float* 、double* 等任意类型的指针变量的加法运算。
可以将指针的加数“1”看作有“单位”的,单位大小取决于指针的类型。这样就好理解 “1+1”不等于 2 的情况了,因为 1千克 + 1毫克 不等于 2 千克,对不?
指针这么强,能操作任意地址码?
很多程序员都说,某类型的变量,一定不能用其他类型的指针操作。这句话其实并不严谨,例如上面举的例子中的 char 类型数组 i[8],我们完全可以使用 int* p2 指针把它当做两个 int 型变量使用。
只不过一定要小心 p2 别超过 i[8] 的范围了,p2+2 指向的就是数组 i 后的地址了。这里可能存储着非常重要的信息,如果使用 p2+2 把这部分的内容修改了,程序出现段错误退出还好,要是没有报错,却给出了错误结果就麻烦了,这种错误非常难发现,所以在开发阶段就应该小心处理。
按照上面的分析,在定义局部指针变量时,如果忘了对它初始化,根据《》一节,局部变量的值是任意的,这也就是说它可能指向任意地方,这时如果使用它,也有可能出现难以发现的错误。
这种指向不确定地址的指针,程序员习惯称为“野指针”。
为了避免出现野指针,在定义指针变量时就应该给它明确的初值,例如:
signed char * p1 = i;
或者把它初始化为NULL:
signed char * p1 = NULL;
NULL在C标准库的头文件 stddef.h 中定义:
#define NULL ((void *)0)
就是把地址0转换成指针类型,称为空指针,它的特殊之处在于,操作系统不会把任何数据保存在地址0及其附近,也不会把地址0~0xfff的页面映射到物理内存,所以任何对地址0的访问都会立刻导致段错误。* p = 0;会导致段错误,就像放在眼前的炸弹一样很容易找到,相比之下,野指针的错误就像埋下地雷一样,更难发现和排除,这次走过去没事,下次走过去就有事。
void* 指针常常被称作万能指针,限于篇幅,以后再讨论。
欢迎在评论区一起讨论,质疑。文章都是手打原创,每天最浅显的介绍C语言,喜欢我的文章就关注一波吧,可以看到最新更新和之前的文章哦。