如果不是内存地址,究竟什么是C指针?

What exactly is a C pointer if not a memory address?

在一个关于c的著名来源中,在讨论&运算符后给出了以下信息:

... It's a bit unfortunate that the terminology [address of] remains, because it confuses those who don't know what addresses are about, and misleads those who do: thinking about pointers as if they were addresses usually leads to grief...

我读过的其他材料(我会说,来自同样有声望的来源)总是毫不掩饰地提到指针和&操作符作为内存地址。我很想继续调查这件事的实际情况,但当有信誉的消息来源不同意时,这有点困难。

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

附笔。

作者后来说:·········································


C标准没有定义指针在内部是什么以及如何在内部工作。这是为了不限制平台的数量,C可以作为编译或解释语言实现。

指针值可以是某种ID或句柄,也可以是多个ID的组合(对x86段和偏移量说你好),不一定是实际的内存地址。这个ID可以是任何东西,甚至是一个固定大小的文本字符串。非地址表示对于C解释器可能特别有用。


我不确定你的来源,但是你描述的语言类型来自C标准:

6.5.3.2 Address and indirection operators
[...]
3. The unary & operator yields the address of its operand. [...]

所以…是的,指针指向内存地址。至少这就是C标准所暗示的意思。

更清楚地说,指针是一个保存某个地址值的变量。对象的地址(可以存储在指针中)由一元&运算符返回。

我可以将地址"42 Wallaby Way,Sydney"存储在一个变量中(该变量可能是某种"指针",但因为它不是内存地址,所以我们不能正确地称之为"指针")。你的计算机有存储桶的地址。指针存储地址值(即指针存储地址值"42 Wallaby Way,Sydney")。

编辑:我想扩展亚历克赛·弗伦兹的评论。

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

6.2.5 Types
[...]
20. [...]
A pointer type may be derived from a function type or an object type, called the referenced type. A pointer type describes an object whose value provides a reference to an entity of the referenced type. A pointer type derived from the referenced type T is sometimes called ‘‘pointer to T’’. The construction of a pointer type from a referenced type is called ‘‘pointer type derivation’’. A pointer type is a complete object type.

从本质上讲,指针存储的值提供了对某个对象或函数的引用。有点。指针用于存储提供对某些对象或函数的引用的值,但情况并非总是这样:

6.3.2.3 Pointers
[...]
5. An integer may be converted to any pointer type. Except as previously specified, the result is implementation-defined, might not be correctly aligned, might not point to an entity of the referenced type, and might be a trap representation.

上面的引号说我们可以把一个整数变成一个指针。如果我们这样做(也就是说,如果我们将一个整数值填充到一个指针中而不是对一个对象或函数的特定引用),那么指针"可能不会指向一个引用类型的实体"(也就是说,它可能不会提供对一个对象或函数的引用)。它可能会给我们提供其他的东西。这是一个你可以在指针中插入某种句柄或ID的地方(也就是说,指针不是指向一个对象;它存储了一个表示某个东西的值,但这个值可能不是地址)。

是的,正如AlexeyFrunze所说,指针可能没有存储对象或函数的地址。指针可能存储了某种"句柄"或ID,您可以通过为指针分配任意整数值来实现这一点。此句柄或ID表示的内容取决于系统/环境/上下文。只要您的系统/实现能够理解价值,您就处于良好的状态(但这取决于具体的价值和具体的系统/实现)。

通常,指针存储对象或函数的地址。如果它没有存储一个实际地址(对象或函数),结果是由实现定义的(也就是说,指针现在所表示的内容和发生的情况取决于您的系统和实现,因此它可能是特定系统上的句柄或ID,但在另一个系统上使用相同的代码/值可能会使您的程序崩溃)。

结果比我想象的要长…


Pointer vs Variable

在这张照片中,

指针_p是一个指针,位于0x12345,指向变量_v,位于0x34567。


把指针看作地址是一种近似值。像所有的近似法一样,它有时足够有用,但也不精确,这意味着依赖它会导致麻烦。好的。

指针就像一个地址,它指示在哪里找到一个对象。这种类比的一个直接限制是并非所有指针都实际包含地址。NULL是一个指针,它不是地址。指针变量的内容实际上可以是以下三种类型之一:好的。

  • 可以取消引用的对象的地址(如果p包含x的地址,则表达式*px的值相同);
  • 空指针,以NULL为例;
  • 无效内容,它不指向对象(如果p不包含有效值,那么*p可以做任何事情("未定义的行为"),使程序崩溃是很常见的可能性)。

此外,更准确的说法是指针(如果有效和非空)包含一个地址:指针指示在何处查找对象,但有更多的信息绑定到它。好的。

特别是指针有一个类型。在大多数平台上,指针的类型在运行时没有影响,但它的影响在编译时超出了类型。如果p是指向int(int *p;)的指针,那么p + 1指向一个整数,该整数是p之后的sizeof(int)字节(假设p + 1仍然是有效指针)。如果q是指向与p相同地址(char *q = p;char的指针,那么q + 1p + 1不是同一地址。如果您将指针视为地址,那么对于同一位置的不同指针,"下一个地址"是不同的,这是不太直观的。好的。

在某些环境中,可能有多个指针值具有不同的表示形式(内存中的不同位模式),它们指向内存中的相同位置。你可以把它们看作是持有相同地址的不同指针,或者是同一位置的不同地址——在这种情况下,这个比喻并不清楚。==运算符总是告诉您两个操作数是否指向同一位置,因此在这些环境中,即使pq具有不同的位模式,也可以使用p == q。好的。

甚至在有些环境中,指针携带地址之外的其他信息,例如类型或权限信息。作为一个程序员,你可以很容易地度过你的一生而不必遇到这些。好的。

在某些环境中,不同类型的指针具有不同的表示形式。您可以将其视为具有不同表示形式的不同类型的地址。例如,一些架构有字节指针和字指针,或者对象指针和函数指针。好的。

总之,只要你记住了,把指针看作地址就不会太糟糕了。好的。

  • 它是唯一有效的非空指针,即地址;
  • 同一地点可以有多个地址;
  • 你不能在地址上做算术,而且地址上没有顺序;
  • 指针还包含类型信息。

换个方向走更麻烦。不是所有看起来像地址的东西都可以是指针。在某个较深的地方,任何指针都表示为一个位模式,可以作为一个整数读取,您可以说这个整数是一个地址。但反过来说,不是每个整数都是指针。好的。

首先有一些众所周知的限制;例如,指定程序地址空间之外位置的整数不能是有效的指针。未对齐的地址不会为需要对齐的数据类型生成有效指针;例如,在int需要4字节对齐的平台上,0x7654321不能是有效的int*值。好的。

然而,它远远超出了这个范围,因为当你把一个指针变成一个整数时,你就陷入了一个麻烦的世界。这一问题的很大一部分是,优化编译器在微优化方面比大多数程序员所期望的要好得多,因此他们关于程序如何工作的心理模型是完全错误的。仅仅因为你有相同地址的指针并不意味着它们是等价的。例如,考虑以下代码段:好的。

1
2
3
4
5
unsigned int x = 0;
unsigned short *p = (unsigned short*)&x;
p[0] = 1;
printf("%u = %u
"
, x, *p);

在运行sizeof(int)==4sizeof(short)==2的铣床时,您可能会期望打印EDOCX1(小端)或EDOCX1(大端)。但在我的64位Linux PC上,使用GCC4.4:好的。

1
2
3
4
5
$ 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语言的规则是复杂的,除非你知道引擎盖下发生了什么,否则很难获得直观的感觉。在这种情况下,指针和地址之间的联系有些松散,既支持"异域"处理器架构,也支持优化编译器。好的。

所以,把指点作为你理解的第一步,但不要太过依赖直觉。好的。好啊。


指针是保存内存地址的变量,而不是地址本身。但是,您可以取消对指针的引用,并访问内存位置。

例如:

1
2
3
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

程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#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
"
, (void *)&q);
  printf("p contains %p
"
, (void *)p);

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


很难说清楚那些书的作者到底是什么意思。指针是否包含地址取决于如何定义地址以及如何定义指针。

从所有的答案来看,有些人认为(1)一个地址必须是一个整数,(2)指针不需要虚拟的,不需要在规范中这样说。有了这些假设,显然指针不一定包含地址。

然而,我们看到,虽然(2)可能是真的,(1)可能不一定是真的。那么,根据@cornstaks的答案,将&;称为操作员地址这一事实怎么解释呢?这是否意味着规范的作者希望指针包含地址?

所以我们可以说,指针包含一个地址,但地址不必是整数吗?也许吧。

我认为这一切都是在回避学究式的语义谈话。实际上,这是毫无价值的。你能想到一个编译器,它以一种指针值不是地址的方式生成代码吗?如果是这样,什么?我就是这么想的…

我认为这本书的作者(声称指针不一定只是地址的第一个摘录)可能指的是指针带有固有类型信息的事实。

例如,

1
2
3
 int x;
 int* y = &x;
 char* z = &x;

Y和Z都是指针,但Y+1和Z+1是不同的。如果它们是内存地址,这些表达式是否会给您相同的值?

而在这里,人们对指针的思考,就好像它们是地址,通常会导致悲伤。写错误是因为人们把指针当作地址来考虑,这通常会导致悲伤。

55555可能不是指针,尽管它可能是地址,但(int*)55555是指针。55555+1=55556,但(int*)55555+1为55559(+/-尺寸差(int))。


指针是表示内存位置的抽象。注意,这句话并没有说把指针当作记忆地址是错误的,它只是说它"通常会导致悲伤"。换句话说,它会导致你有错误的期望。

最可能的悲伤来源当然是指针算术,这实际上是C的优势之一。如果指针是一个地址,您会期望指针算术是地址算术,但它不是。例如,向一个地址添加10应该会给您一个更大10个寻址单元的地址;但是向指针添加10会使它增加10倍于它指向的对象的大小(甚至不是实际大小,而是四舍五入到对齐边界)。在32位整数的普通体系结构上使用int *时,向其添加10将使其增加40个寻址单元(字节)。经验丰富的C程序员知道这一点并与之共存,但您的作者显然不喜欢草率的隐喻。

还有一个关于指针内容如何表示内存位置的附加问题:正如许多答案所解释的那样,地址并不总是int(或long)。在某些体系结构中,地址是"段"加上偏移量。指针甚至可能只包含当前段("near"指针)的偏移量,该段本身不是唯一的内存地址。而指针内容可能与硬件理解的内存地址只有间接关系。但引用的这段话的作者甚至没有提到表象,所以我认为这是概念上的对等,而不是他们心目中的表象。


以下是我过去对一些困惑的人的解释:指针有两个影响其行为的属性。它有一个值(在典型环境中)是内存地址,还有一个类型,它告诉您它指向的对象的类型和大小。

例如,给定:

1
2
3
4
union {
    int i;
    char c;
} u;

可以有三个不同的指针指向同一对象:

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

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

1
v==i && i==c

但是,如果您增加每个指针,您将看到它们指向的类型变得相关。

1
2
3
4
i++;
c++;
// You can't perform arithmetic on a void pointer, so no v++
i != c

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


你是对的,神志清醒。通常,指针只是一个地址,所以您可以将其转换为整数并进行任何运算。

但有时指针只是地址的一部分。在某些体系结构中,指针被转换成一个地址,加上基地址或使用另一个CPU寄存器。

但是现在,在带有平面内存模型和C语言本机编译的PC和ARM体系结构中,可以认为指针是指向一维可寻址RAM中某个位置的整数地址。


马克·贝西已经说过了,但这需要重新强调,直到理解。

指针与变量的关系与文本3无关。

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

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

非变量指针的计数器示例太多,无法忽略

  • fopen返回文件指针。(变量在哪里)
  • 堆栈指针或帧指针通常是不可寻址的寄存器

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

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


指针与C中的任何其他变量一样,从根本上来说是一组位,可以由一个或多个连接的unsigned char值表示(与任何其他类型的cariable一样,sizeof(some_variable)将指示unsigned char值的数目)。使指针与其他变量不同的是,C编译器将解释指针中的位,以某种方式标识变量可能存储的位置。在C语言中,与其他语言不同,它可以为多个变量请求空间,然后将指向该集中任何值的指针转换为指向该集中任何其他变量的指针。

许多编译器通过使用其位存储实际的机器地址来实现指针,但这并不是唯一可能的实现。一个实现可以让一个数组(用户代码无法访问)列出程序正在使用的所有内存对象(变量集)的硬件地址和分配的大小,并让每个指针在数组中包含一个索引以及该索引的偏移量。这样的设计不仅允许系统将代码限制为仅在其拥有的内存上运行,还可以确保指向一个内存项的指针不会意外转换为指向另一个内存项的指针(在使用硬件地址的系统中,如果foobar是由10个存储项组成的数组,那么实际上,在内存中,指向foo的"第十一"项的指针可能会指向bar的第一项,但在每个"指针"都是对象ID和偏移量的系统中,如果代码试图索引超出其分配范围的指向foo的指针,则系统可能会陷阱。这样的系统也可以消除内存碎片问题,因为与任何指针关联的物理地址都可以移动。

注意,虽然指针有些抽象,但它们不够抽象,不足以允许完全符合标准的C编译器实现垃圾收集器。C编译器指定每个变量(包括指针)都表示为一个unsigned char值的序列。对于任何变量,都可以将其分解为数字序列,然后将该数字序列转换回原始类型的变量。因此,一个程序可以存储一些存储器(接收指向它的指针),在那里存储一些东西,将指针分解成一系列字节,在屏幕上显示这些字节,然后删除对它们的所有引用。如果程序随后接受键盘上的一些数字,将这些数字重新组合成一个指针,然后尝试从该指针读取数据,如果用户输入的数字与程序先前显示的数字相同,程序将需要输出存储在calloc内存中的数据。由于计算机不可能知道用户是否复制了显示的数字,因此计算机不可能知道将来是否可以访问上述内存。


小结(我也会放在上面):好的。

(0)将指针视为地址通常是一个很好的学习工具,并且通常是指向普通数据类型的指针的实际实现。好的。

(1)但在许多情况下,也许大多数情况下,编译器指向函数的指针不是地址,而是大于地址(通常为2x,有时更大),或者实际上是指向内存中结构的指针,而不是包含函数地址和常量池之类的内容的指针。好的。

(2)指向数据成员的指针和指向方法的指针通常更为陌生。好的。

(3)具有远指针和近指针问题的传统x86代码好的。

(4)几个例子,最显著的是IBMAS/400,带有安全的"胖指针"。好的。

我相信你能找到更多。好的。

细节:好的。

啊!!!!!!!到目前为止,许多答案都是典型的"程序员weenie"答案,但不是编译器weenie或硬件weenie。因为我假装是一个硬件工程师,而且经常和编译器工程师一起工作,所以让我把我的两分钱投进去:好的。

在许多C编译器上,指向T类型数据的指针实际上是T的地址。好的。

好的。好的。

但是,即使在许多编译器上,某些指针也不是地址。你可以通过观察sizeof(ThePointer)来判断。好的。

例如,指向函数的指针有时比普通地址大得多。或者,他们可能涉及一个间接的层面。本文提供了一个涉及IntelItanium处理器的描述,但我见过其他的描述。通常,要调用一个函数,不仅必须知道函数代码的地址,还必须知道函数常量池的地址——一个内存区域,从中用一条加载指令加载常量,而不是编译器必须从几个立即加载、移位和或指令中生成64位常量。因此,您需要2个64位地址,而不是单个64位地址。一些ABI(应用程序二进制接口)将其移动为128位,而另一些则使用间接级别,函数指针实际上是包含刚才提到的2个实际地址的函数描述符的地址。哪个更好?取决于您的观点:性能、代码大小和一些兼容性问题-通常,代码假定指针可以转换为长或长,但也可以假定长正好是64位。这种代码可能不符合标准,但客户可能希望它工作。好的。

我们中的许多人对旧的Intelx86分段体系结构有着痛苦的记忆,它们有近指针和远指针。值得庆幸的是,到目前为止,这些都几乎灭绝了,所以只是一个简短的总结:在16位实模式中,实际的线性地址是好的。

1
LinearAddress = SegmentRegister[SegNum].base << 4 + Offset

而在保护模式下,它可能是好的。

1
LinearAddress = SegmentRegister[SegNum].base + offset

根据段中设置的限制检查结果地址。有些程序并不是真正的标准C/C++ +远近指针声明,但很多人只是说EDCOX1〔0〕-但是有编译器和链接器开关,例如,代码指针可能在指针附近,只对CS(代码段)寄存器中的任何一个32位偏移,而数据指针可能是远指针,指定一个16位的段号和一个48位值的32位偏移量。现在,这两个数量肯定与地址有关,但是由于它们的大小不同,地址是哪个?此外,除了与实际地址相关的内容外,这些段还具有只读、读写、可执行等权限。好的。

一个更有趣的例子,imho,是(或者可能是)IBMAS/400系列。这台计算机是第一个在C++中实现操作系统的计算机之一。这个机器上的指针通常是实际地址大小的2倍——例如,正如本演示所说,128位指针,但实际地址是48-64位,而且,还有一些额外的信息,即所谓的功能,提供了读、写等权限以及防止缓冲区溢出的限制。是的:你可以兼容C/C++做这件事,如果这是无处不在的,中国解放军和斯拉夫黑手党不会侵入这么多的西方计算机系统。但是历史上大多数C/C++编程忽略了性能的安全性。最有趣的是,AS400系列允许操作系统创建安全指针,该指针可以提供给非特权代码,但非特权代码不能伪造或篡改。同样,安全性,同时符合标准,许多草率的非标准兼容C/C++代码在这样的安全系统中是行不通的。同样,有官方标准,也有事实上的标准。好的。

现在,我将不再讨论安全性的Soapbox,并介绍一些指针(各种类型)通常不是真正的地址的其他方法:指向数据成员的指针、指向成员函数方法的指针,以及它们的静态版本比普通地址大。正如这篇文章所说:好的。

There are many ways of solving this [problems related to single versus multiple inheitance, and virtual inheritance]. Here's how the Visual Studio compiler decides to handle it: A pointer to a member function of a multiply-inherited class is really a structure."
And they go on to say"Casting a function pointer can change its size!".

Ok.

正如你可以猜到的那样,我参与了C/C++硬件/软件项目,其中一个指针被处理得更像是一个能力而不是原始地址。好的。

我可以继续说,但我希望你能明白。好的。

小结(我也会放在上面):好的。

(0)将指针视为地址通常是一个很好的学习工具,并且通常是指向普通数据类型的指针的实际实现。好的。

(1)但在许多情况下,也许大多数情况下,编译器指向函数的指针不是地址,而是大于地址(通常为2x,有时更大),或者实际上是指向内存中结构的指针,而不是包含函数地址和常量池之类的内容的指针。好的。

(2)指向数据成员的指针和指向方法的指针通常更为陌生。好的。

(3)具有远指针和近指针问题的传统x86代码好的。

(4)几个例子,最显著的是IBMAS/400,带有安全的"胖指针"。好的。

我相信你能找到更多。好的。好啊。


指针是一种变量类型,它在C/C++中是可用的,并且包含内存地址。与任何其他变量一样,它有自己的地址并占用内存(数量是平台特定的)。

由于混淆,您将看到的一个问题是,试图通过简单地按值传递指针来更改函数中的引用。这将复制函数作用域中的指针,对新指针"points"的任何更改都不会更改调用函数作用域中指针的引用。为了修改函数中的实际指针,通常会将指针传递给指针。


指针只是另一个变量,用于保存内存位置的地址(通常是另一个变量的内存地址)。


你可以这样看。指针是表示可寻址内存空间中地址的值。


地址用于将固定大小的存储器(通常是每个字节)标识为整数。这被精确地称为字节地址,ISO C也使用它。可以有一些其他的方法来构造地址,例如每个位的地址。然而,通常只使用字节地址,我们通常省略"byte"。

从技术上讲,地址永远不是C中的值,因为(iso)C中术语"值"的定义是:

precise meaning of the contents of an object when interpreted as having a specific type

(我强调)但是,在C语言中没有这样的"地址类型"。

指针不相同。指针是C语言中的一种类型。有几种不同的指针类型。它们不一定遵守相同的语言规则,例如,++int*char*类型值的影响。

C中的值可以是指针类型。这称为指针值。显然,指针值不是C语言中的指针。但我们习惯于将它们混合在一起,因为在C中不太可能是含糊不清的:如果我们将表达式p称为"指针",它只是一个指针值,而不是类型,因为C中的命名类型不是由表达式表示的,而是由类型名或typedef名称表示的。

还有一些事情很微妙。作为C用户,首先应该知道object的含义:

region of data storage in the execution environment, the contents of which can represent
values

对象是表示特定类型的值的实体。指针是对象类型。因此,如果我们声明int* p;,那么p表示"指针类型的对象",或"指针对象"。

注:本标准没有规范定义的"变量"(事实上,ISO C在规范性文本中从未将其用作名词)。然而,非正式地,我们称对象为变量,就像其他语言一样。(但仍然不是那么精确,例如在C++中,变量可以是标准的引用类型,而不是对象。)短语"指针对象"或"指针变量"有时被视为"指针值",上面可能有微小的差别。(还有一组示例是"array"。)

由于指针是一种类型,地址在C中实际上是"无类型的",所以指针值大致"包含"一个地址。指针类型的表达式可以生成一个地址,例如

ISO C115.5.2.3

3 The unary & operator yields the address of its operand.

注:该措词由WG14/N1256引入,即ISO C99:TC3。在C99中有

3 The unary & operator returns the address of its operand.

它反映了委员会的意见:地址不是一元&运算符返回的指针值。

尽管有上述措辞,但即使在标准中也存在一些混乱。

ISO C11 6.6

9 An address constant is a null pointer, a pointer to an lvalue designating an object of static
storage duration, or a pointer to a function designator

ISO C++ 11 5.19

3 ... An address
constant expression is a prvalue core constant expression of pointer type that evaluates to the address of an
object with static storage duration, to the address of a function, or to a null pointer value, or a prvalue core
constant expression of type std::nullptr_t. ...

(最近的C++标准草案使用了另一个措辞,所以没有这个问题。)

实际上,C中的"地址常量"和C++中的"地址常量表达式"都是指针类型的常量表达式(或者至少是C++ 11中的"指针类型")。

在C和C++中,构造一元EDCOX1〔7〕算子称为"地址";同样地,在C++ 11中引入EDCOX1〔9〕。

这些命名可能会带来误解。结果表达式是指针类型的,因此可以解释为:结果包含/生成地址,而不是地址。


在理解指针之前,我们需要理解对象。对象是存在的实体,具有一个名为地址的位置说明符。指针只是一个变量,与C中其他任何变量一样,它的类型为pointer,其内容被解释为支持以下操作的对象的地址。

1
2
3
4
5
6
+ : A variable of type integer (usually called offset) can be added to yield a new pointer
- : A variable of type integer (usually called offset) can be subtracted to yield a new pointer
  : A variable of type pointer can be subtracted to yield an integer (usually called offset)
* : De-referencing. Retrieve the value of the variable (called address) and map to the object the address refers to.
++: It's just `+= 1`
--: It'
s just `-= 1`

指针根据其当前引用的对象类型进行分类。唯一重要的信息是对象的大小。

任何对象都支持一个操作,即&(address of),它将对象的位置说明符(address)检索为指针对象类型。这应该减少围绕术语的混淆,因为调用&作为对象的操作而不是其结果类型是对象类型指针的指针是有意义的。

注:在这个解释中,我遗漏了记忆的概念。


指针只是另一个变量,通常可以包含另一个变量的内存地址。指针也是一个变量,它也有一个内存地址。


C指针与内存地址非常相似,但是它抽象出了与机器相关的细节,以及一些在低级指令集中找不到的特性。

例如,C指针的类型相对丰富。如果您通过一个结构数组增加一个指针,它会很好地从一个结构跳到另一个结构。

指针受转换规则的约束,并提供编译时类型检查。

有一个特殊的"空指针"值,在源代码级别是可移植的,但其表示形式可能不同。如果将值为零的整型常量赋给指针,则该指针将接受空指针值。如果用这种方式初始化一个指针,也是一样。

指针可以用作布尔变量:如果它不是空值,则测试为真;如果它是空值,则测试为假。

在机器语言中,如果空指针是一个有趣的地址,比如0xffffffff,那么您可能需要对该值进行显式测试。C对你隐瞒。即使空指针是0xffffffff,也可以使用if (ptr != 0) { /* not null! */}测试它。

使用破坏类型系统的指针会导致未定义的行为,而机器语言中的类似代码可能定义得很好。汇编程序将汇编您编写的指令,但C编译器将根据您没有做错任何事情的假设进行优化。如果一个float *p指针指向一个long n变量,并且执行*p = 0.0时,编译器不需要处理这个问题。随后使用n将不需要读取浮点值的位模式,但可能是一种基于"严格混叠"假设的优化访问,即n未被触摸!也就是说,假设程序运行良好,因此p不应指向n

在C语言中,指向代码的指针和指向数据的指针是不同的,但是在许多体系结构中,地址是相同的。可以开发具有"fat"指针的C编译器,即使目标体系结构没有。胖指针意味着指针不仅仅是机器地址,还包含其他信息,例如关于被指向对象大小的信息,用于边界检查。可移植的程序将很容易移植到这样的编译器。

所以你可以看到,机器地址和C指针之间有很多语义上的区别。


它说"因为它混淆了那些不知道地址是关于什么的人"——而且,它是真的:如果你知道地址是关于什么的,你就不会混淆。从理论上讲,指针是指向另一个变量的变量,实际上拥有一个地址,即它指向的变量的地址。我不知道为什么要隐瞒这个事实,这不是火箭科学。如果你理解指针,你将更进一步了解计算机是如何工作的。前进!


来想想,我认为这是语义学的问题。我不认为作者是对的,因为C标准将指针引用为保存引用对象的地址,正如其他人在这里已经提到的那样。但是,地址!=内存地址。根据C标准,地址实际上可以是任何东西,尽管它最终会导致一个内存地址,指针本身可以是一个ID,一个偏移量+选择器(x86),只要它能够描述(映射之后)可寻址空间中的任何内存地址,就可以是任何东西。


另一种方法是C或C++指针与简单的内存地址不同,这是因为我在其他答案中没有看到不同的指针类型(AltHurf,考虑到它们的总大小,我可能忽略了它)。但它可能是最重要的一个,因为即使是有经验的C/C++程序员也可以跳过它:

编译器可能会假定不兼容类型的指针不会指向同一个地址,即使它们确实指向同一个地址,这可能会给出简单的指针==地址模型无法实现的行为。考虑以下代码(假设sizeof(int) = 2*sizeof(short)):

1
2
3
4
5
6
7
8
9
10
11
12
13
unsigned int i = 0;
unsigned short* p = (unsigned short*)&i;
p[0]=p[1]=1;

if (i == 2 + (unsigned short)(-1))
{
  // you'd expect this to execute, but it need not
}

if (i == 0)
{
  // you'd expect this not to execute, but it actually may do so
}

注意,char*有一个例外,因此使用char*操纵值是可能的(尽管不是非常可移植的)。


快速摘要:C地址是一个值,通常表示为具有特定类型的机器级内存地址。

非限定词"指针"不明确。C有指针对象(变量)、指针类型、指针表达式和指针值。

用"指针"这个词来表示"指针对象"是很常见的,这会导致一些混乱——这就是为什么我试图用"指针"作为形容词而不是名词的原因。

至少在某些情况下,C标准使用"指针"一词来表示"指针值"。例如,malloc的描述说它"返回一个空指针或一个指向分配空间的指针"。

那么C中的地址是什么?它是一个指针值,即某个特定指针类型的值。(除了空指针值不一定被称为"地址",因为它不是任何内容的地址)。

标准对一元&运算符的描述是"生成操作数的地址"。在C标准之外,"地址"一词通常用于指(物理或虚拟)内存地址,通常是一个大小的字(无论给定系统上的"字"是什么)。

C"地址"通常作为机器地址实现——就像C int值通常作为机器字实现一样。但是C地址(指针值)不仅仅是一个机器地址。它是一个通常表示为机器地址的值,它是一个具有特定类型的值。


指针值是地址。指针变量是可以存储地址的对象。这是真的,因为这就是标准定义的指针。告诉C新手很重要,因为C新手通常不清楚指针和它指向的东西之间的区别(也就是说,他们不知道信封和建筑物之间的区别)。地址的概念(每个对象都有一个地址,这就是指针存储的内容)很重要,因为它对地址进行排序。

然而,标准在特定的抽象层次上进行讨论。作者谈到的那些人"知道地址是关于什么的",但是对于C来说是新手,他们一定是在不同的抽象层次上学习了地址——也许是通过编程汇编语言。不保证C实现使用与CPU操作码使用相同的地址表示(在本文中称为"存储地址"),这些人已经知道。

他接着谈到"完全合理的地址操纵"。就C标准而言,基本上没有"完全合理的地址操作"。加法是在指针上定义的,基本上就是它。当然,您可以将指针转换为整数,执行一些按位或算术运算,然后再将其转换回来。标准并不能保证这一点,所以在编写代码之前,您最好了解特定的C实现如何表示指针并执行转换。它可能使用了您期望的地址表示,但这不是您的错,因为您没有阅读手册。这不是混淆,这是不正确的编程过程;-)

简而言之,C使用的地址概念比作者更抽象。

作者的地址概念当然也不是这个问题的最低级词汇。对于跨多个芯片的虚拟内存映射和物理RAM寻址,您告诉CPU的数字是您想要访问的"存储地址",基本上与您想要的数据在硬件中的实际位置无关。这是所有间接和代表层,但作者选择了一个特权。如果你要在谈论C的时候这样做,选择C级别作为特权!

就我个人而言,我认为作者的评论没有那么有帮助,除了在将C介绍给汇编程序员的上下文中。对于那些来自更高级别语言的人来说,指针值不是地址当然没有帮助。承认这种复杂性要比说CPU在说地址是什么方面具有垄断性要好得多,因此C指针值"不是"地址。它们是地址,但它们可以用不同于他所指的地址的语言书写。我认为,在C的上下文中将这两个词区分为"地址"和"商店地址"就足够了。


简单地说,指针实际上是分割机制的偏移部分,分割后转换为线性地址,然后在分页后转换为物理地址。物理地址实际上是从RAM中寻址的。

1
2
3
4
5
6
7
       Selector  +--------------+         +-----------+
      ---------->|              |         |           |
                 | Segmentation | ------->|  Paging   |
        Offset   |  Mechanism   |         | Mechanism |
      ---------->|              |         |           |
                 +--------------+         +-----------+
        Virtual                   Linear                Physical