指针

指针就像陷阱,你得主动跳进去

指针运算

转换

由于指针的位数相同,在64位机器下均为8 byte,所以转换后的指针的值也是相同的。

1
2
int a[4];              // a: 0x7ffc2a11b080
char *c = (char *) a; // c: 0x7ffc2a11b080

但是需要注意的是,不同类型的指针做偏移运算时,偏移的大小是不同的,一个int指针的偏移量为4byte,但是一个char指针偏移量为1byte。

1
2
3
4
int *d = a;

// d+1 = 0x7ffc2a11b080+0x4 = 0x7ffc2a11b084
// c+1 = 0x7ffc2a11b080+0x1 = 0x7ffc2a11b081

数学运算

注意

指针不能想加,但能相减

两个指针相加是没有任何意义的,一个指针和一个偏移量相加,可以代表将这个指针偏移某个位置,但是两个指针相加能代表什么呢?nothing。但是两个指针相减是有意义的,代表两个指针之间的距离,例如:

1
2
3
4
5
6
7
int a = 1;
int b = 2;

int *p1 = &a;
int *p2 = &b;

int distance = p1 - p2;

如果打印的distace值为5,代表p1和p2之间间隔着能够容纳5个int的空间,我们可以借此得到两个指针中间的位置。

1
2
mid = (low+high)/2;     //Error
mid = low+(high-low)/2 //OK

需要注意的是,指针相减必须是相同类型的。

地址表示

在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=400a[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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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'-------->c
0 1 2 3 4 5 6 7
+----------+----------+----------+----------+----------+----------+----------+----------+
|1100 1000 | 00000000 | 00000000 | 00000000 | 10010000 | 00000001 | 00000000 | 00000000 |
+----------+----------+----------+----------+----------+----------+----------+----------+
a[0] a[1]

8 9 10 11 12 13 14 15
+----------+----------+----------+----------+----------+----------+----------+----------+
| | | | | | | | |
+----------+----------+----------+----------+----------+----------+----------+----------+
a[2] a[3]
*****************************************************************************************/

*c = 500;
/***************************************************************************************
c
0 1 2 3 4 5 6 7
+----------+----------+----------+----------+----------+----------+----------+----------+
|1100 1000 | 00000000 | 00000000 | 00000000 | 10010000 | 11110100 | 00000001 | 00000000 |
+----------+----------+----------+----------+----------+----------+----------+----------+
a[0] a[1]

8 9 10 11 12 13 14 15
+----------+----------+----------+----------+----------+----------+----------+----------+
| 00000000 | | | | | | | |
+----------+----------+----------+----------+----------+----------+----------+----------+
a[2] a[3]
*****************************************************************************************/

从上面的分析过程可以看出,最终,*c = 500;会使5~8的内存被修改,而此时计算a[1]得到的结果即为128144:

1
(00000000 00000001 11110100 10010000)BIN = (128144)DEC

这个例子告诉我们,尽量不要进行指针类型转换,如果转换,也要尽可能保证转换的指针所指向的类型占用空间一致。否则可能会出现非常奇怪的问题。

指针使用Checklist

  • [ ] 检查指针是否为空:为空一般说明内存分配不正确
  • [ ] 检查指针是否指向明确地址空间:是否在该用指针的场合错误使用了对象
  • [ ] 检查是否需要对指针解引用:该用对象的场合错误使用了指针

如何正确理解指针

指针是非常难以驾驭的存在,即使是有经验的程序员,也会陷入指针的陷阱当中,所以我们不妨在这里做一个抽象:

  • 指针本身是一张表,用于分配空间,我们说指针的时候,实际上是在讨论这段空间

  • 指针内容是这张表中的具体项目,我们使用指针解引用后的对象,就是在使用这个具体的项目

在使用指针过程中,一定要注意,是在处理空间,还是在处理对象。

参考文献

0%