c语言入门21,一文弄懂指针为何要有类型,1+1不等于2是对的

上一节介绍了 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语言,喜欢我的文章就关注一波吧,可以看到最新更新和之前的文章哦。

阅读更多:   C语言
添加新评论

icon_redface.gificon_idea.gificon_cool.gif2016kuk.gificon_mrgreen.gif2016shuai.gif2016tp.gif2016db.gif2016ch.gificon_razz.gif2016zj.gificon_sad.gificon_cry.gif2016zhh.gificon_question.gif2016jk.gif2016bs.gificon_lol.gif2016qiao.gificon_surprised.gif2016fendou.gif2016ll.gif