将指针视为地址是一种近似。就像所有的近似一样,有时它足够好用,但也不是精确的,这意味着依赖它会带来麻烦。
指针类似于地址,因为它指示了对象的位置。这个类比的一个直接限制是,并不是所有的指针实际上都包含一个地址。NULL是一个不是地址的指针。指针变量的内容实际上可以是以下三种之一:
- 对象的地址,可以进行解引用(如果p包含x的地址,则表达式*p的值与x相同);
- 空指针,其中NULL是一个例子;
- 无效的内容,不指向任何对象(如果p没有保存有效值,则*p可以做任何事情(“未定义行为”),崩溃程序是相当普遍的可能性)。
此外,更准确地说,指针(如果有效且非空)
包含一个地址:指针指示了如何找到对象,但它还有更多的信息与之相关。特别地,指针具有类型。在大多数平台上,指针的类型对运行时没有影响,但它对编译时产生的影响超出了类型本身。如果
p
是指向
int
的指针(
int *p;
),那么
p + 1
指向的整数是
p
之后
sizeof(int)
字节的位置(假设
p+1
仍然是有效的指针)。如果
q
是指向与
p
相同地址的
char
的指针(
char *q = p;
),那么
q+1
的地址与
p+1
不同。如果您将指针视为地址,那么对于指向同一位置的不同指针,“下一个地址”不是很直观。
在某些环境中,可能会有多个指针值具有不同的表示方式(在内存中具有不同的位模式),它们指向内存中的同一位置。您可以将这些视为不同的指针持有相同的地址,或者将其视为相同位置的不同地址-在这种情况下,比喻并不清楚。 '=='运算符始终告诉您两个操作数是否指向同一位置,因此在这些环境中,您可以拥有“p == q”,即使'p'和'q'具有不同的位模式。
甚至有一些环境,在其中指针除了地址之外还携带其他信息,例如类型或权限信息。作为程序员,您可以轻松地度过整个生活而不遇到这些问题。
在某些环境中,不同类型的指针具有不同的表示形式。您可以将其视为具有不同表示形式的不同类型的地址。例如,某些体系结构具有字节指针和字指针,或对象指针和函数指针。
总的来说,将指针视为地址并不太糟糕,只要记住:
- 只有有效的、非空指针才是地址;
- 同一位置可以有多个地址;
- 不能对地址进行算术运算,也没有顺序;
- 指针还携带类型信息。
反过来就麻烦多了。并不是所有看起来像地址的东西都可以成为指针。在某个深层次上,任何指针都表示为可以读取为整数的位模式,你可以说这个整数是一个地址。但反过来,不是每个整数都是指针。
首先有一些众所周知的限制;例如,指定程序地址空间之外位置的整数不能成为有效指针。对于需要对齐的数据类型,未对齐的地址不构成有效指针;例如,在一个需要4字节对齐的平台上,0x7654321不能成为有效的int*
值。
然而,它不仅仅是这样,因为当你将指针转换为整数时,你会遇到一系列问题。其中很大一部分麻烦在于优化编译器比大多数程序员预期的微观优化效果要好得多,因此他们对程序工作方式的心理模型是完全错误的。仅仅因为你有相同地址的指针并不意味着它们是等价的。例如,考虑以下代码片段:
unsigned int x = 0;
unsigned short *p = (unsigned short*)&x;
p[0] = 1;
printf("%u = %u\n", x, *p);
你可能会期望在一台普通的机器上,sizeof(int)==4
和sizeof(short)==2
,这将会打印出1 = 1?
(小端)或者65536 = 1?
(大端)。但是在我的64位Linux PC上,使用GCC 4.4编译:
$ c99 -O2 -Wall a.c && ./a.out
a.c: In function ‘main’:
a.c:6: warning: dereferencing pointer ‘p’ does break strict-aliasing rules
a.c:5: note: initialized from here
0 = 1?
GCC很友好,会在这个简单的例子中
警告我们出现了什么问题——在更复杂的例子中,编译器可能不会注意到。由于
p
与
&x
具有不同的类型,改变
p
指向的内容不能影响
&x
指向的内容(除了某些明确定义的异常情况)。因此,编译器可以任意保留
x
的值在寄存器中,并且不会在
*p
更改时更新该寄存器。该程序对同一地址解引用两个指针并获得两个不同的值!
这个例子的教训是,把(非空有效)指针看作地址是可以的,只要你遵守C语言的精确规则。硬币的另一面是,C语言的规则是复杂的,难以直观地理解,除非你知道底层发生了什么。底层发生的事情是指针和地址之间的联系有些松散,既支持“异国情调”的处理器体系结构,也支持优化编译器。
所以,把指针视为地址是你理解的第一步,但不要过于追求这种直觉。