今天刷知乎的时候看到有这么个问题:“既然指针的本质是地址,那为啥还需要指定数据类型呢?地址不就是一串 0x 数字吗?”
挺有意思的,我们一起来探讨探讨这个问题。
首先,指针的本质是不是地址?
毫无疑问,指针中的数据就是地址。严格意义来讲,指针是包含一个变量地址的变量:
上图是《The C programming Language》 中对指针的定义,即指针是变量,这个变量里面的数据是地址。所以简化来讲,指针确实是地址!
那回到上面的问题,既然指针是地址,那为什么还要指定数据类型,地址本质不就是一串数字吗?
要回答这个问题,先要问一个问题:指针是不是仅仅就只是地址?
根据上面的定义就可以发现,“指针是地址” 只是一个简化的说法,实际上指针首先是个变量,这个变量的值是另一个变量的地址。
可以看到,上面这句话的核心是变量,而在 C 语言中,变量是要用类型来定义的,例如 int a 或 float a,有了类型,编译器才知道如何去解释这个变量,比如是作为一个整数来使用,还是浮点数,从而选择正确的底层取值指令以及计算指令。
我们在使用指针的时候一定是想要获取其内部的值,或者用它来做各种计算。这时候如果不指定其类型,那就无法对其进行取值和计算操作。接下来我们结合代码深入理解一下:
#include
int main() {
char buffer[10];
int *i_ptr = (int *)buffer;
char *c_ptr = (char *)buffer;
for (int i = 0; i < sizeof(buffer); i++) {
buffer[i] = i;
}
printf("i_ptr = %p *i_ptr = 0x%08x\n", i_ptr, *i_ptr);
printf("c_ptr = %p *c_ptr = 0x%08x\n", c_ptr, *c_ptr);
return 0;
}
首先定义了一个数组,然后按顺序进行了赋值。接着定义了两个指针,分别为 int 类型和 char 类型。最后打印这两个指针的值及对这两个指针解引用后的值。我们来看下运行结果:
jay@jaylinuxlenovo:~/test$ ./test
i_ptr = 0x7ffe1ce03a0e *i_ptr = 0x03020100
c_ptr = 0x7ffe1ce03a0e *c_ptr = 0x00000000
可以发现,这两个指针本身的值是完全一样的,这也容易理解,因为这两个指针都是取的数组 buffer 的首地址,这个地址是不会变的。然而对这两个指针解引用得到的值却完全不一样,这正是其类型在起作用!
在我的编译环境下,int 类型的大小是四字节,char 类型的大小是一字节。因此在对一个 int 类型的指针解引用时会取四个字节的数据,而在对 char 类型的指针解引用时只会取一个字节的数据。内存结构图如下:
除了对指针的解引用,我们再看看指针本身的计算是什么样的:
#include
int main() {
char buffer[10];
int *i_ptr = (int *)buffer;
char *c_ptr = (char *)buffer;
int *i_ptr2;
char *c_ptr2;
i_ptr2 = i_ptr + 1;
c_ptr2 = c_ptr + 1;
printf("i_ptr:%p, i_ptr2: %p, i_ptr2 - i_ptr = %ld\n", i_ptr, i_ptr2, (long int)i_ptr2 - (long int)i_ptr);
printf("c_ptr:%p, c_ptr2: %p, c_ptr2 - c_ptr = %ld\n", c_ptr, c_ptr2, (long int)c_ptr2 - (long int)c_ptr);
return 0;
}
这里我们额外定义了两个指针 i_ptr2 和 c_ptr2,其分别为 i_ptr + 1 以及 c_ptr + 1 计算后的值,并将这些值及其与原始值的差值打印出来。根据上一个示例我们知道 i_ptr 和 c_ptr 其本身由于取得是同一个地址,所以值是一样的,而这里又都是加 1,完全一样数值加上完全一样的数值,根据幼儿园数学我们可知,最终的结果肯定也是完全一样的,那实际是不是这样呢?我们来运行一下:
jay@jaylinuxlenovo:~/test$ ./test
i_ptr:0x7ffd55017c9e, i_ptr2: 0x7ffd55017ca2, i_ptr2 - i_ptr = 4
c_ptr:0x7ffd55017c9e, c_ptr2: 0x7ffd55017c9f, c_ptr2 - c_ptr = 1
不对劲,我们加两行打印把计算过程打出来:
printf("i_ptr + 1 = %ld + 1 = %ld\n", i_ptr, i_ptr + 1);
printf("c_ptr + 1 = %ld + 1 = %ld\n", c_ptr, c_ptr + 1);
运行后结果如下:
jay@jaylinuxlenovo:~/test$ ./test
i_ptr + 1 = 140722462817166 + 1 = 140722462817170
c_ptr + 1 = 140722462817166 + 1 = 140722462817167
i_ptr:0x7ffc80687b8e, i_ptr2: 0x7ffc80687b92, i_ptr2 - i_ptr = 4
c_ptr:0x7ffc80687b8e, c_ptr2: 0x7ffc80687b8f, c_ptr2 - c_ptr = 1
见鬼了!两个完全一样的值都加 1 ,得出的结果却不一样!我们试下不用指针,直接给一个固定的数值再看看结果:
printf("i_ptr + 1 = %ld + 1 = %ld\n", 140722462817166, 140722462817166 + 1);
printf("c_ptr + 1 = %ld + 1 = %ld\n", 140722462817166, 140722462817166 + 1);
运行结果如下:
jay@jaylinuxlenovo:~/test$ ./test
i_ptr + 1 = 140722462817166 + 1 = 140722462817167
c_ptr + 1 = 140722462817166 + 1 = 140722462817167
直接用数值进行计算就是一样的,用指针计算就不一样,这一点就更加证实了指针并不是完全等于一串地址数字!而造成这种区别的,也正是问题中提到的 类型!
指针的计算会根据指针的类型调整步长,可以理解为一个 int 型的指针在执行加一的操作后是指向了连续内存中的下一个 int 型数据地址;自然 char 型指针的加一就是指向了下一个 char 型数据的地址。这也解释了为什么在上面的计算结果中,i_ptr2 - i_ptr 等于 4,而 c_ptr2 - c_ptr 等于 1,这里的 4 和 1 就是 int 和 char 类型数据的大小。这些指针在内存地址中的含义可以参考下图:
至此,我们可以发现,指针虽然表示的是地址,但其不仅仅是地址的数值,还包含有这个数值对应的类型信息。如果我们要对这个指针进行具体的操作,如解引用,做计算,那么必须要知道其类型。
不过在某种情况下我们只想要这个地址本身,比如在中间层进行指针传递的时候,是完全可以不指定数据类型的(实际上是指定了空指针类型 void *),此时这个指针就可以认为仅仅是代表地址的一串数值。但话又说回来,传递的目的还是要在某个地方来使用,也就必然会涉及到计算或者解引用,因此最终还是逃不过类型的指定。