如果C指针不是内存地址,那它到底是什么?

225
在一本关于C语言的可靠资料中,在讨论完&运算符后,给出了以下信息:

...不幸的是,“[地址]”这个术语仍然存在,因为它会让那些不了解地址的人感到困惑,并误导那些了解地址的人:把指针看作地址通常会导致悲剧...

我读过其他材料(同样来自可靠的来源,我想说),它们总是毫不掩饰地将指针和&运算符称为内存地址。现在我有点困惑——如果指针不是内存地址,那么它到底是什么?我很想继续寻找问题的真相,但当可靠的来源“有点”意见不一致时,这有点困难。

现在我有点困惑——如果指针不是内存地址,那么它到底是什么?

P.S.

作者后来说:...虽然我将继续使用“地址”的术语,但发明一个不同的术语会更糟糕。


127
指针是一个变量,它保存一个地址,并且具有它自己的地址。这是指针和数组之间的根本区别。数组实际上就是一个地址(暗示着它的地址是它本身)。 - WhozCraig
8
你引用的这句话是从哪个“可靠的来源”得来的? - Cornstalks
23
最权威的来源是语言标准而非从标准中半推半就、半自创的书籍。我曾经吃过亏,犯了几乎所有可能的错误,然后慢慢建立了一个接近于标准所描述的C语言的心智模型,最终用标准的模型来替换它。 - Alexey Frunze
9
人们认为指针=整数,原因是它通常如此(x86 Linux和Windows “教”我们这样做),人们喜欢概括,人们不熟悉语言标准,并且他们在处理完全不同的平台时缺乏经验。那些同样的人可能会假定数据指针和函数指针可以相互转换,数据可以作为代码执行,代码可以被视为数据访问。虽然这在冯·诺依曼结构中可能是正确的(具有1个地址空间),但在哈佛结构中不一定正确(带有代码和数据空间)。 - Alexey Frunze
6
标准不是给新手(尤其是完整的标准)的。它们不应该提供温和的介绍和大量的示例。它们正式地定义了某个东西,以便专业人士可以正确实现它。 - Alexey Frunze
显示剩余26条评论
24个回答

160
C标准没有定义指针的内部结构和工作原理,这是有意为之的,以免限制可以将C实现为编译或解释语言的平台数量。
指针值可以是某种ID或句柄,也可以是几个ID的组合(例如x86段和偏移量),而不一定是真正的内存地址。这个ID可以是任何东西,甚至是固定大小的文本字符串。非地址表示对于C解释器可能特别有用。

36
没什么好解释的。每个变量在内存中都有地址。但是你不必把它们的地址存储在指向它们的指针中。相反,你可以给你的变量从1到任何数字编号,并将该数字存储在指针中。只要实现知道如何将这些数字转换为地址并且知道如何使用这些数字进行指针算术运算以及其他符合标准的操作,那就是完全合法的。 - Alexey Frunze
4
我想补充一下,在x86架构中,一个内存地址由段选择器和偏移量组成,因此将指针表示为段:偏移仍然使用内存地址。 - thang
7
当我了解我的平台和编译器时,我没有忽略标准的通用性或不适用性的问题。然而,由于原始问题是通用的,因此在回答它时不能忽略标准。 - Alexey Frunze
9
@Lundin,你不需要成为革命家或科学家。假设你想在一台物理的16位机器上仿真32位机器,并通过使用磁盘存储将你的64KB RAM 扩展到最多4GB,然后将32位指针实现为对一个巨大文件的偏移量。这些指针不是真正的内存地址。 - Alexey Frunze
6
我所见过的最好例子是Symbolics Lisp机器(约1990年)上的C语言实现。每个C对象都被实现为Lisp数组,指针则被实现为数组和索引的一对。由于Lisp的数组边界检查,你永远不会从一个对象溢出到另一个对象。 - Barmar
显示剩余30条评论

64
我不确定您的来源,但是您描述的语言类型来自C标准:
6.5.3.2地址和间接运算符 [...] 3. 一元&运算符产生其操作数的地址。[...]
所以...是的,指针指向内存地址。至少C标准建议这样理解。
更明确地说,指针是一个保存某个地址值的变量。对象的地址(可能存储在指针中)可以使用一元&运算符返回。
我可以将地址“42 Wallaby Way,悉尼”存储在一个变量中(该变量类似于“指针”,但由于它不是内存地址,因此我们不会正确地称之为“指针”)。您的计算机有其内存桶的地址。指针存储地址的值(即指针存储值“42 Wallaby Way,悉尼”,这是一个地址)。
编辑:我想扩展Alexey Frunze的评论。

指针到底是什么?让我们看一下C标准:

6.2.5 类型
[...]
20. [...]
指针类型可以来源于函数类型或对象类型,称为引用类型。指针类型描述了一个对象,其值提供对引用类型实体的引用。从引用类型T派生的指针类型有时称为“指向T的指针”。从引用类型构造指针类型称为“指针类型推导”。指针类型是完整的对象类型。

基本上,指针存储一个值,该值提供对某个对象或函数的引用。但有点区别。指针旨在存储一个值,该值提供对某个对象或函数的引用,但这并不总是如此:

6.3.2.3 指针
[...]
5. 整数可以转换为任何指针类型。除非另有规定,否则结果是实现定义的,可能不正确对齐,可能不指向引用类型的实体,并且可能是陷阱表示。

上面的引用说我们可以将整数转换为指针。如果我们这样做(也就是说,如果我们把一个整数值放入指针中而不是特定对象或函数的引用),那么指针“可能不指向引用类型实体”(即它可能不提供对象或函数的引用)。它可能为我们提供其他东西。这是您可以在指针中插入某种句柄或ID的一个地方(即指针不指向对象;它存储表示某个值的值,但该值可能不是地址)。
所以,正如Alexey Frunze所说,指针可能不存储对象或函数的地址。指针可能代替存储某种“句柄”或ID,您可以通过将一些任意整数值分配给指针来实现这一点。此句柄或ID表示取决于系统/环境/上下文。只要您的系统/实现可以理解该值,您就处于良好状态(但这取决于具体值和具体系统/实现)。
通常情况下,指针会存储一个对象或函数的地址。如果它没有存储实际的地址(指向对象或函数),则其结果是由实现定义的(这意味着发生了什么以及指针现在代表什么取决于您的系统和实现,所以它可能是某个特定系统上的句柄或ID,但在另一个系统上使用相同的代码/值可能会导致程序崩溃)。

4
在C解释器中,指针可以存储非地址的ID、句柄等。 - Alexey Frunze
5
标准并不仅限于编译后的C语言。 - Alexey Frunze
8
@Lundin 太棒了!让我们再次无视标准吧!仿佛我们还没有因此而忽略它并且没有因此制造出有缺陷和不易移植的软件。此外,请注意原始问题是通用的,因此需要通用的答案。 - Alexey Frunze
3
当他人说指针可能是句柄或其他不一定是地址的内容时,他们并不仅仅是表示你可以通过将整数强制转换为指针来将数据转换为指针。他们的意思是编译器可能使用除内存地址之外的其他机制来实现指针。例如,在Alpha处理器上使用DEC的ABI时,函数指针不是函数的地址,而是一个描述符的地址,该描述符包含有关函数参数的数据以及函数的地址。重点在于C标准非常灵活。 - Eric Postpischil
5
@Lundin:在现实世界中,指针被实现为整数地址的说法是错误的。存在使用字寻址和段偏移寻址的计算机。编译器仍然支持近指针和远指针。PDP-11计算机存在,其中RSX-11和Task Builder及其覆盖层需要指针标识从磁盘加载函数所需的信息。如果对象不在内存中,指针不能具有对象的内存地址! - Eric Postpischil
显示剩余11条评论

38
将指针视为地址是一种近似。就像所有的近似一样,有时它足够好用,但也不是精确的,这意味着依赖它会带来麻烦。
指针类似于地址,因为它指示了对象的位置。这个类比的一个直接限制是,并不是所有的指针实际上都包含一个地址。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)==4sizeof(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语言的规则是复杂的,难以直观地理解,除非你知道底层发生了什么。底层发生的事情是指针和地址之间的联系有些松散,既支持“异国情调”的处理器体系结构,也支持优化编译器。
所以,把指针视为地址是你理解的第一步,但不要过于追求这种直觉。

5
其他答案似乎忽略了指针携带类型信息的事实,这比地址/ID等讨论更为重要。 - undur_gongor
@LarsH 你是对的,谢谢,我怎么写成那样了呢?我已经用一个例子代替了它,甚至演示了在我的电脑上出现的令人惊讶的行为。 - Gilles 'SO- stop being evil'
1
NULL是((void *)0) .. ? - Aniket Inge
1
@gnasher729 空指针就是一个指针。NULL不是,但对于此处所需的详细级别来说,这是一个无关紧要的干扰。即使在日常编程中,NULL可能被实现为某些不表示“指针”的东西,也很少出现(主要是将NULL传递给可变参数函数 - 但即使在那里,如果您没有进行强制转换,则已经假定所有指针类型具有相同的表示形式)。 - Gilles 'SO- stop being evil'
NULL和0相等。这与(void*)0是不同的。 - Jeff Hammond
显示剩余3条评论

37

Pointer vs Variable

在这张图片中,pointer_p是一个指针,位于0x12345处,指向位于0x34567的变量variable_v。

20
这不仅没有涉及地址与指针的概念区别,而且它本质上忽略了一个地址不仅仅是一个整数的要点。 - Gilles 'SO- stop being evil'
21
-1,这只是解释了指针的含义。那不是问题所在——你忽略了问题本身的复杂性。 - alexis

19
指针是一个保存内存地址的变量,而不是地址本身。但是,您可以对指针进行解引用,并访问内存位置。
例如:
int q = 10; /*say q is at address 0x10203040*/
int *p = &q; /*means let p contain the address of q, which is 0x10203040*/
*p = 20; /*set whatever is at the address pointed by "p" as 20*/

就是这样。很简单。

enter image description here

这是一个演示我所说内容的程序,其输出在此处:

http://ideone.com/rcSUsb

这个程序:

#include <stdio.h>

int main(int argc, char *argv[])
{
  /* POINTER AS AN ADDRESS */
  int q = 10;
  int *p = &q;

  printf("address of q is %p\n", (void *)&q);
  printf("p contains %p\n", (void *)p);

  p = NULL;
  printf("NULL p now contains %p\n", (void *)p);
  return 0;
}

5
它甚至可能会更加混淆。爱丽丝,你能看到一只猫吗?不,我只能看到一只猫的微笑。因此,说指针是一个地址或指针是保存地址的变量,或者说指针是一个概念的名称,它指的是地址的想法,书籍作者在混淆新手方面可以走多远? - exebook
对于那些熟悉指针的人来说,这很简单。也许一张图片会有所帮助? - Aniket Inge
5
一个指针不一定持有一个地址。在C解释器中,它可能是其他东西,比如某种ID/句柄。 - Alexey Frunze
“标签”或变量名是编译器/汇编器的概念,在机器级别上并不存在,因此我认为它不应该出现在内存中。 - Ben
1
@Aniket 指针变量可以包含指针值。只有在需要多次使用它时(对于 fopen 来说,几乎所有情况都是如此),才需要将 fopen 的结果存储到变量中。 - Gilles 'SO- stop being evil'
显示剩余4条评论

16
好的,指针是表示内存位置的一个抽象概念。请注意,引用并没有说将指针看作内存地址是错误的,只是说这通常会导致困难。换句话说,它会让你期望出现错误。 最可能导致困难的来源当然是指针算术运算,这实际上是 C 语言的优势之一。如果指针是一个地址,你会期望指针算术运算是地址算术运算;但事实并非如此。例如,将 10 添加到地址应该给出一个比原来大 10 个寻址单元的地址;但将 10 添加到指针会使其增加指向对象类型的大小的 10 倍(甚至不是实际大小,而是舍入到对齐边界)。在具有 32 位整数的普通架构上使用 int * ,将其增加 10 将使其增加 40 个寻址单元(字节)。经验丰富的 C 程序员都知道这一点,并将其用于各种良好的用途,但你的作者显然不喜欢含糊的比喻。

还有一个额外的问题,即指针内容如何表示内存位置:正如许多答案所解释的那样,地址并不总是int(或long)。在某些架构中,地址是“段”加上偏移量。指针甚至可能只包含当前段的偏移量(“近”指针),它本身不是唯一的内存地址。而指针内容可能仅与硬件理解的内存地址存在间接关系。但是引用的作者甚至没有提到表示,因此我认为他们考虑的是概念上的等价性,而不是表示。


16
“很难准确地说出那些书的作者到底是什么意思。指针是否包含地址取决于您如何定义地址和指针。”
“从所有已编写的答案中判断,有些人认为(1)地址必须是整数,(2)由于规范中没有明确说明,指针不需要这样做。基于这些假设,那么显然指针不一定包含地址。”
“然而,我们看到,虽然(2)可能是真的,但(1)可能不必是真的。而且,@CornStalks的答案中‘&’被称为‘地址运算符’,这是什么意思?这是否意味着规范的作者打算让指针包含一个地址?”
“因此,我们能否说,指针包含一个地址,但地址不一定是整数?也许可以。”
“我认为所有这些都是无用的学究式语言。就实际意义而言,您能想到哪个编译器会以指针的值不是地址的方式生成代码吗?如果有,那是什么?我想就是这样...”
我认为书中作者(第一段声称指针不仅仅是地址的摘录)可能指的是指针本身带有固有类型信息这一事实。
例如,
 int x;
 int* y = &x;
 char* z = &x;

变量y和z都是指针,但是y+1和z+1却不同。如果它们是内存地址,那么这些表达式不是应该给出相同的值吗?

这就是为什么把指针看作地址通常会导致错误。由于人们把指针看作地址,所以可能会写出错误的代码,这通常会导致错误

55555很可能不是指针,虽然它可能是一个地址,但(int*)55555就是一个指针。55555+1=55556,但是(int*)55555+1是55559(这取决于sizeof(int)的+/差异)。


1
+1 是指出指针算术与地址算术不同。 - kutschkem
在16位8086的情况下,内存地址由段基址+偏移量来描述,它们都是16位。有许多段基址和偏移量的组合可以给出相同的内存地址。这个“远指针”不仅仅是一个整数。 - vonbrand
@vonbrand,我不明白你为什么发表了那个评论。该问题已经在其他答案的评论中讨论过了。几乎每一个其他的答案都假设地址=整数,而任何不是整数的东西都不是地址。我只是指出这一点,并指出它可能正确也可能不正确。我的整个回答的重点是它并不相关。这些都只是纠结细节,而主要问题并没有得到解决。 - thang
@tang,关于“指针==地址”的想法是错误的。无论每个人和他们最喜欢的阿姨都继续这样说,也不能使它变得正确。 - vonbrand
@vonbrand,你在我的帖子下发表了什么意见?我并没有说它是对还是错。事实上,在某些情况/假设下是正确的,但并非始终如此。让我再次概括一下帖子的要点(第二次)。我的整个答案的重点是它不相关。这只是纠结细节问题,而主要问题在其他答案中未得到解决。更合适的做法是评论那些声称pointer==address或address==integer的答案。请参见我在Alexey帖子下关于segment:offset的评论。 - thang
顺便说一句,那里有很多答案大多数都是错误的、不准确的、不完整的,而且完全错过了重点,这是我无法控制的。而且,它们被投票赞成也是我无法控制的。在撰写本答案之前,我已经对它们进行了评论以澄清问题。 - thang

12

以下是我之前向一些困惑的人解释过指针的方式: 指针有两个属性影响它的行为。它有一个,通常是一个内存地址,还有一个类型,告诉你它所指向的对象的类型和大小。

例如,给定:

union {
    int i;
    char c;
} u;

您可以拥有三个不同的指针,都指向同一个对象:

void *v = &u;
int *i = &u.i;
char *c = &u.c;

如果您比较这些指针的值,它们都是相等的:

v==i && i==c

然而,如果你递增每个指针,你会发现它们所指向的类型变得相关。
i++;
c++;
// You can't perform arithmetic on a void pointer, so no v++
i != c

变量ic此时将具有不同的值,因为i++使i包含下一个可访问整数的地址,而c++使c指向下一个可寻址字符。通常,整数占用的内存比字符多,所以在它们都被递增后,i的值将比c大。

2
+1 谢谢。在指针中,值和类型就像人的身体和灵魂一样不可分割。 - Aki Suihkonen
i == c 是不合法的(只有在一个指针可以隐式转换为另一个指针时,才能比较不同类型的指针)。此外,使用强制类型转换来修复这个问题意味着你已经应用了一次转换,那么这个转换是否改变了值就是有争议的了。(你可以断言它没有改变,但这只是在断言你试图用这个例子证明的同样的事情)。 - M.M

9
你是正确的,也很明智。通常,指针只是一个地址,因此可以将其转换为整数并执行任何算术运算。但有时,指针只是地址的一部分。在某些架构中,指针通过加上基址或使用另一个CPU寄存器来转换为地址。但如今,在PC和ARM架构上,采用平面内存模型和原生编译C语言,认为指针是指向可寻址RAM中某个位置的整数地址就可以了。

PC是什么?扁平内存模型是什么?选择器又是什么? - thang
好的。当下一次架构变化到来时,也许会有单独的代码和数据空间,或者有人回到古老的段式架构(对于安全性来说非常有意义,甚至可以添加一些关键字到段号+偏移量以检查权限),你可爱的“指针只是整数”就会崩溃。 - vonbrand

8

Mark Bessey已经说过了,但这需要再次强调直到被理解。

指针与变量以及字面值3同样相关。

指针是一个值(地址)和类型(带有附加属性,例如只读)的元组。类型(以及任何其他参数)可以进一步定义或限制上下文;例如,__far ptr、__near ptr:地址的上下文是什么:堆栈、堆、线性地址、某个偏移量、物理内存或其他。

正是类型的属性使指针算术与整数算术有所不同。

指针不是变量的反例太多了

  • fopen返回一个FILE指针。(变量在哪里)

  • 堆栈指针或帧指针通常是不可寻址的寄存器

    *(int *)0x1231330 = 13; -- 将任意整数值转换为指向整数类型的指针,并在不引入变量的情况下写入/读取整数

在C程序的生命周期中,将有许多其他临时指针没有地址--因此它们不是变量,而是具有编译时关联类型的表达式/值。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接