指针与地址
有一些书和视频中会提到这样一件事情:指针就是地址。这中说法貌似很盛行,尤其是在不会熟练使用指针,甚至搞不清楚指针的本质的人群被奉为真理。笔者的观点是:虽然在代码过程中可以认为指针就是地址,但如果细究,它们是有区别的。
为了论证上述观点,我们必须从头开始,首先讲述类型与变量。
在C语言中,我们若想使用某个变量,就必须提前声明。想要使用short类型的变量,就需要先声明;想要使用int类型的变量,也要先声明。其实我 们声明变量其实是告诉编译器,我们需要一块内存空间,具体空间大小由变量类型决定。short类型的变量需要2字节,int类型的变量需要4字节。
类型是一个抽象概念,它描述了其变量所占用的内存空间大小、如何解析其值在内存中的表示以及在其变量上可以发生的操作。
int类型的变量可以进行四则运算,但指针类型变量却只能进行加运算,而且指针类型变量的加运算不是数学意义上的加运算。
变量是类型的具体事物,它的属性包括所属类型、地址以及值。变量所占的内存空间大小和可发生的操作都是有其所属类型决定。
某些书籍教程中提到,变量的值就是其内存单元中保存的数据。这是一种错误的观点。内存单元中保存的数据仅仅变量值的一种表达,而非变量值本身。举个 例子来说,int类型的变量a,我们给其赋值为-1,但内存单元中保存的却是FFFFFFFF(十进制值为4294967295);另有一个 unsigned int类型的变量b,我们给其赋值为0xFFFFFFFF。两个变量在内存中存储的数据完全一样,但两者所表达的值却完全不一样。看似无法区分-1和 4294967295,但编译器总是不会发生错误。因为编译器知道变量的类型不同,依据类型完全可以区分出来。
有两个类型的变量:int c = 0x1234;,short d = 0x1234;。两个变量虽然其值在内存中的表示完全一样,但其类型不同,访问的内存空间大小也不一样。变量c访问了4个字节,而变量d只访问了2个字节。
我们说整型变量c和d的值是一样的,其实并非说内存空间中存储的位组合完全一样。值是变量的一种属性,但内存空间中存储的位组合只是变量的值的一种表达,区分两者是非常重要的。
下面我们看看指针是怎样的。指针其实不是C语言独有的,汇编语言其实也是有指针,只是被称为间接寻址之类的。我们对变量的引用本质上是对变量的地址 的引用。我们知道某个变量的地址是无法访问这个变量,因为CPU不知道这个变量所占的内存空间是1个字节还是2个字节还是4个自己,甚至更多。只有知道变 量的地址和它所占用内存空间的大小才能正确引用变量,而指针正是表述地址和空间大小的事物。CPU访问变量依据的正是指针,但编译之后的程序中无法直接表 达指针。对于基本类型变量的访问,CPU是通过一次访问1、2或4字节内存空间来实现,但对于复杂的数据类型并非如此。可以说,对于基本类型,指针存在于 编译后的二进制代码中;但对于复杂类型,指针只存在于编译器中,编译器把对复杂类型变量的访问拆分成对简单类型变量的访问。
我们说0x1234的值是就是0x1234,这中说法确实也没问题,因为整数是个标量。但我们能否直接说指针的值是0x1234?答案是错误的,因为指针并非一个标量。我们只能这样正确表达一个指针:地址是0x1234并且大小为4个字节的指针。是的,这才是指针的值!
搞清楚了指针,我们再来看看指针类型。首先必须澄清的一种观点是,指针类型不是一种类型,而是间接引用其他类型(包括指针类型)的一类类型。我们要 提一种指针类型,必须说清楚它所引用的是哪种类型。比如:指向int类型的指针类型,或简单地说是Int指针类型。同样的,我们只能说指向int类型的指 针变量,或简单地说int指针变量,但不能简单地说指针变量。
指针变量也是有值的。int e = 5; int * p = &c;,其中变量e的地址是 0x9000000, 那p的值是什么呢?有人说p的值就是变量e的地址,也就是0x9000000。这种说法是错误的。变量p在内存空间中的存储位的组合确实是 0x9000000,但指针变量的值是指针,而非指针在内存空间中的表示。正确的说法是:地址为0x9000000,占用4个字节的指针。
我们看看下面的代码片段:
int e;
int * p;
e = 5;
p = &e;
那么p = &e;这一句到底是怎么回事呢?或者说,指针变量的值确实如我前面所说是指针呢?&我们称为取值符号,如果&符号得到的是地址,那么下面的代码是正确的:
unsigned short * p;
unsigned short a = 0x3412;
unsigned int b = 0x7856;
p = &a;
p1 = &b;
但实际情况是,对于p1 = &b;这一句,编译器无法通过,而且是个错误。看来&符号得到的并不是地址,因为如果是地址,那么就说明是同一种事物,可以直接赋值。
我们再看看下面的代码:
unsigned short * p;
unsigned short a = 0x3412;
unsigned int b = 0x7856;
p = &a;
p = (unsigned short *)&b;
这两个代码段的区别是在&b前面多了(unsigned short *)。实际上&符号取到的是指针而非地址。&b取到的是指针的值是:地址为变量b的地址,占用空间是4个字节。但p所指向的变量所占用的 空间是2个字节,所以需要进行强转。注意,这里转换的是指针而非变量。
另外,我们再简单谈谈*符号:*符号取得的是指针变量指向的变量,而而非指针变量指向的变量的值。在表达式中对变量的引用其实是取变量的值,这一步操作是隐形的。
有了上述,我们看下面的代码就比较容易了:
unsigned int a = 0x12345678;
unsigned int * p1 = &a;
unsigned char * p2 = (unsigned char *)&a;
printf("a : %x\n", *p1);
printf("a的第一个字节 : %x\n", *p2++);
printf("a的第二个字节 : %x\n", *p2++);
printf("a的第三个字节 : %x\n", *p2++);
printf("a的第四个字节 : %x\n", *p2++);
它的输出是:
a : 12345678
a的第一个字节 : 78
a的第二个字节 : 56
a的第三个字节 : 34
a的第四个字节 : 12
通常我们不会把一个整型变量的值赋值给一个浮点型变量,因为我们直到两者的值是不同的。
但是,我们经常错误地把int指针类型变量的值赋值给short指针类型变量,因为我们想当然地认为都是指针类型,可以相互赋值。不同类型的指针类型之间 相互赋值(准确的说是把一种类型的指针变量的值赋值给另一种类型的指针变量),这需要进行强转,而且程序员自己承担这种强转带来的风险。当我们把int指 针类型变量的值赋值给unsigned int指针类型的变量时,编译器会给出一个警告。
总结一下:
1、指针变量的值是指针,而不是地址。
2、指针类型不是一种类型,而是一类类型的总称。
3、如果指向的变量类型不同,那么两种指针类型就属于不同的类型。
4、强转指针有风险,如果需要这么做,就要有充足的理由。