指针就像陷阱,你得主动跳进去
指针运算
转换
由于指针的位数相同,在64位机器下均为8 byte,所以转换后的指针的值也是相同的。
1 | int a[4]; // a: 0x7ffc2a11b080 |
但是需要注意的是,不同类型的指针做偏移运算时,偏移的大小是不同的,一个int指针的偏移量为4byte,但是一个char指针偏移量为1byte。
1 | int *d = a; |
数学运算
注意
指针不能想加,但能相减
两个指针相加是没有任何意义的,一个指针和一个偏移量相加,可以代表将这个指针偏移某个位置,但是两个指针相加能代表什么呢?nothing。但是两个指针相减是有意义的,代表两个指针之间的距离,例如:
1 | int a = 1; |
如果打印的distace值为5,代表p1和p2之间间隔着能够容纳5个int的空间,我们可以借此得到两个指针中间的位置。
1 | mid = (low+high)/2; //Error |
需要注意的是,指针相减必须是相同类型的。
地址表示
在64位的PC上,指针所占的长度为8个字节,但是如果实际打印会发现输出只有6个字节,这是因为6字节寻址范围已经非常大了,基本上没有什么软件能够使用6个字节寻址的空间。
不同类型在内存中的表示
整型
整型的表示方式为:先计算整型的补码,然后按照低地址放低位,高地址放高位的方式,将整型进行存储,以int类型为例,该类型占据4个byte。
例题
有一段代码如下:
1
2
3
4
5
6
7
8
9 > int a[4];
> int *c = a;
> a[0] = 200, a[1] = 300, a[2] = 301, a[3] = 302;
> c = c + 1;
> *c = 400;
>
> c = (int *)(((char *) a) + 1 );
> *c = 500;
>
>
请问上述代码执行完后,a[0]~a[3]的值各为多少?
这里实际考察了不同类型指针的计算以及整数在内存中的存储,首先,c=c+1
执行完后,指针向后偏移一个int类型的大小,那么此时c指向a[1],*c=400
即a[1] = 400
,现在a[0]~a[3]的值为:
1 | a[0] = 200, a[1] = 400, a[2] = 301, a[3] = 302; |
而下一句语句执行了指针类型转换,将int类型转换为了char类型,char类型指针+1向后偏移一个char类型的大小,即1byte,此时需要将内存画出具体进行判断。过程如下:
- (200)DEC = (11001000)BIN,所以200在a[0]中排布如下,(400)DEC=(000110010000)BIN同理
- c在转换成为char *类型后,+1偏移1byte,指向内存序号5所在的byte
- 随即c又被转换为int类型,那么现在c指向的就是从5开始到8结束这4byte,对其赋值会导致5~8所在内存被覆盖掉
1 | int a[4]; |
从上面的分析过程可以看出,最终,*c = 500;
会使5~8的内存被修改,而此时计算a[1]得到的结果即为128144:
1 | (00000000 00000001 11110100 10010000)BIN = (128144)DEC |
这个例子告诉我们,尽量不要进行指针类型转换,如果转换,也要尽可能保证转换的指针所指向的类型占用空间一致。否则可能会出现非常奇怪的问题。
指针使用Checklist
- [ ] 检查指针是否为空:为空一般说明内存分配不正确
- [ ] 检查指针是否指向明确地址空间:是否在该用指针的场合错误使用了对象
- [ ] 检查是否需要对指针解引用:该用对象的场合错误使用了指针
如何正确理解指针
指针是非常难以驾驭的存在,即使是有经验的程序员,也会陷入指针的陷阱当中,所以我们不妨在这里做一个抽象:
指针本身是一张表,用于分配空间,我们说指针的时候,实际上是在讨论这段空间
指针内容是这张表中的具体项目,我们使用指针解引用后的对象,就是在使用这个具体的项目
在使用指针过程中,一定要注意,是在处理空间,还是在处理对象。
参考文献
- 1.一个关于指针实验的代码 ↩