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


206

在有关C的著名来源中,在讨论完&运算符后给出了以下信息:

...有点遗憾,因为[地址]仍然存在,因为它使那些不知道地址的人感到困惑,并误导了那些人:像指针一样思考指针通常会导致悲伤。 。

我读过的其他材料(我想从同样享有声誉的资料中)总是毫不掩饰地将指针和&运算符称为提供内存地址。我很乐意继续寻找问题的真实性,但是当有信誉的消息来源(KIND OF)不同意时,这会有些困难。

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

聚苯乙烯

作者后来说:...我将继续使用“住所”一词,因为发明一个不同的术语会更糟。


118
指针是一个可变的是保持一个地址。它也有自己的地址。这是指针和数组之间的根本区别。数组实际上一个地址(暗示来说,它的地址本身就是)。
WhozCraig

7
您的报价的“可靠来源”是什么?
13年

22
最终的信誉良好的来源是语言标准,而不是源于该标准的半书,也不是从作者的屁股上半拉出来的书。我很难学到它,几乎犯了我所有的错误,然后慢慢地建立了C语言的心理模型,使其在某种程度上接近于标准所描述的思维模型,然后最终用标准模型代替了该模型。
Alexey Frunze

9
@thang人们之所以认为指针=整数,是因为它经常是如此(x86 Linux和Windows向我们“传授”该信息),因为人们喜欢泛化,因为人们不太了解语言标准,并且因为他们很少有根本不同的经验平台。那些人可能会假设可以将指向数据的指针和指向函数的指针相互转换,并且可以将数据作为代码执行,并将代码作为数据进行访问。虽然这在冯·诺依曼架构(具有1个地址空间)上可能是正确的,但在哈佛架构(带有代码和数据空间)上却不一定是正确的。
Alexey Frunze

6
@exebook标准不适用于新手(尤其是完整的新手)。他们不应该提供温和的介绍和大量示例。他们正式定义了某些内容,因此专业人员可以正确实施。
Alexey Frunze

Answers:


148

C标准没有定义内部指针和内部指针的工作方式。这是有意的,以便不限制可以将C实现为编译或解释语言的平台的数量。

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


34
没有太多解释。每个变量在内存中都有其地址。但是您不必将其地址存储在指向它们的指针中。相反,您可以将变量从1编号到任何数字,并将该数字存储在指针中。只要实现知道如何将这些数字转换为地址,以及如何使用这些数字以及该标准要求的所有其他操作,这对于语言标准是完全合法的。
Alexey Frunze

4
我想补充一点,在x86上,内存地址由段选择器和偏移量组成,因此将指针表示为segment:offset仍在使用内存地址。

6
@Lundin我知道我的平台和编译器时,可以毫无疑问地忽略标准的通用性和不适用性。但是,最初的问题是通用的,因此您在回答标准时不能忽略它。
Alexey Frunze

8
@Lundin您不需要是革命者或科学家。假设您要在物理16位计算机上模拟32位计算机,并通过使用磁盘存储将64KB的RAM扩展到4GB,并将32位指针作为偏移量实现为一个大文件。这些指针不是真实的内存地址。
Alexey Frunze

6
我见过的最好的例子是Symbolics Lisp Machines的C实现(1990年左右)。每个C对象都实现为Lisp数组,而指针则实现为一对数组和一个索引。由于Lisp进行了数组边界检查,因此您永远不会从一个对象溢出到另一个对象。
Barmar

62

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

6.5.3.2地址和间接运算符
[...]
3. 一元&运算符产生其操作数的地址。[...]

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

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

我可以将地址“ 42 Wallaby Way,Sydney”存储在一个变量中(该变量将是某种“指针”,但由于这不是内存地址,因此我们不宜将其称为“指针”)。您的计算机具有其存储分区的地址。指针存储地址的值(即,指针存储值“ 42 Wallaby Way,Sydney”,即地址)。

编辑:我想扩展Alexey Frunze的评论。

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

6.2.5类型
[...]
20 [...]
指针类型可以由函数类型或对象类型,称为派生引用的类型。指针类型描述了一个对象,该对象的值提供对所引用类型的实体的引用。从引用的类型T派生的指针类型有时称为``T的指针''。从引用类型构造指针类型的过程称为“指针类型派生”。指针类型是完整的对象类型。

本质上,指针存储一个提供对某些对象或功能的引用的值。有点儿。指针旨在存储提供对某些对象或函数的引用的值,但并非总是如此:

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

上面的引用说,我们可以将整数变成指针。如果这样做(也就是说,如果将整数值填充到指针中,而不是对对象或函数的特定引用),则指针“可能不指向引用类型的实体”(即,它可能不提供引用类型的实体)。对对象或功能的引用)。它可能为我们提供其他一些东西。这是一个您可能在指针中粘贴某种句柄或ID的地方(即,指针未指向对象;它存储的是表示某物的值,但该值可能不是地址)。

因此,是的,正如Alexey Frunze所说,指针可能没有存储对象或函数的地址。指针可能存储了某种“句柄”或ID,而您可以通过为指针分配任意整数值来实现。该句柄或ID表示什么取决于系统/环境/上下文。只要您的系统/实现可以理解价值,您就处于良好状态(但这取决于特定的价值和特定的系统/实现)。

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

结果比我想象的要长。


3
在C解释器中,指针可以保存一个非地址ID /句柄/等。
Alexey Frunze 2013年

4
@exebook标准不无论如何不限于编译C.
阿列克谢伏龙芝

7
@Lundin Bravo!让我们更多地忽略标准!好像我们还没有足够地忽略它,也没有因此而生产出错误且便携性差的软件。另外,请不要以为原始问题是通用的,因此需要通用的答案。
Alexey Frunze

3
当其他人说指针可能是句柄或地址以外的其他内容时,它们不仅意味着您可以通过将整数转换为指针来将数据强制为指针。它们意味着编译器可能正在使用内存地址以外的其他方式来实现指针。在具有DEC的ABI的Alpha处理器上,函数指针不是函数的地址,而是函数的描述符的地址,并且描述符包含函数的地址和有关函数参数的一些数据。关键是C标准非常灵活。
Eric Postpischil 2013年

5
@Lundin:关于在现实世界中100%的现有计算机系统上将指针实现为整数地址的说法是错误的。存在具有字寻址和段偏移量寻址的计算机。编译器仍然存在,并且支持近和远指针。存在带有RSX-11和任务生成器及其覆盖图的PDP-11计算机,其中的指针必须标识从磁盘加载功能所需的信息。如果对象不在内存中,则指针不能具有对象的内存地址!
Eric Postpischil 2013年

39

指针与变量

在这幅图片中,

pointer_p是位于0x12345的指针,指向0x34567处的变量variable_v。


16
这不仅没有解决与指针相反的地址概念,而且整体上错过了一个地址不仅仅是整数的观点。
吉尔(Gilles)'所以

19
-1,这仅说明了指针是什么。这不是问题所在,而您正在抛开该问题涉及的所有复杂性。
亚历克西斯

34

将指针视为地址是一种近似。像所有近似值一样,它有时足以有用,但它也不精确,这意味着依靠它会带来麻烦。

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

  • 可以取消引用的对象的地址(如果p包含的地址,x则表达式*p的值与相同x);
  • 一个空指针,其中NULL是一个例子;
  • 无效的内容,该内容不指向对象(如果p不持有有效值,则*p可以执行任何操作(“未定义的行为”),并且很可能使程序崩溃。

此外,更准确地说一个指针(如果有效且非空)包含一个地址:指针指示在哪里可以找到对象,但是有更多的信息与之相关。

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

在某些环境中,可能有多个指针值具有不同的表示形式(内存中的不同位模式),它们指向内存中的相同位置。您可以将它们视为持有相同地址的不同指针,或位于同一位置的不同地址-在这种情况下隐喻并不明确。该==运营商总是告诉你两个操作数是否都指向同一位置,所以在这些环境中,你可以有p == q,即使pq有不同的位模式。

甚至在环境中,指针还携带地址之外的其他信息,例如类型或权限信息。您可以轻松地过着程序员的生活,而不会遇到这些。

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

总而言之,只要您记住以下几点,就可以将指针视为地址并不算太糟糕

  • 它只是地址的有效非空指针;
  • 同一地点可以有多个地址;
  • 您不能对地址进行算术运算,并且它们没有顺序;
  • 指针还携带类型信息。

相反,要麻烦得多。并非所有看起来像地址的东西都可以成为指针。在深处的某个地方,任何指针都表示为可以读取为整数的位模式,您可以说该整数是一个地址。但是换句话说,并不是每个整数都是一个指针。

首先有一些众所周知的局限性。例如,指定程序地址空间之外的位置的整数不能成为有效的指针。地址未对齐不会为需要对齐的数据类型提供有效的指针;例如,在int需要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?(大端)。但是在装有GCC 4.4的64位Linux PC上:

$ 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
+1。其他答案似乎错过了指针附带类型信息。这远比地址/ ID /任何讨论都重要。
undur_gongor

+1关于类型信息的出色点。我不确定编译器示例是否正确,例如...似乎不太可能*p = 3在未初始化p时保证成功。
LarsH

@LarsH您说得对,谢谢,我是怎么写的?我用一个示例代替了它,该示例甚至演示了PC上令人惊讶的行为。
吉尔(Gilles)“所以,别再邪恶了”

1
嗯,NULL是((void *)0)..?
Aniket Inge

1
@ gnasher729空指针一个指针。NULL不是,但对于此处所需的详细程度,这是无关紧要的。即使对于日常编程,NULL也不会经常出现这样的事实:它不会说“指针”(主要是传递NULL给可变参数函数,但即使在这里,如果您不进行强制转换) ,您已经在假设所有指针类型都具有相同的表示形式)。
吉尔(Gilles)'所以

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*/

而已。就这么简单。

在此处输入图片说明

一个演示我在说什么的程序及其输出在这里:

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

对于那些经验丰富的指针的@exebook,这非常简单。也许图片会有所帮助?
Aniket Inge 2013年

5
指针不一定包含地址。在C解释器中,可能是其他的东西,例如某种ID /句柄。
Alexey Frunze'3

“标签”或变量名是编译器/汇编器,在计算机级别不存在,因此我认为它不应该出现在内存中。

1
@Aniket指针变量可以包含一个指针值。fopen如果您需要多次使用它,则只需要将结果存储到一个变量中(对于而言fopen,几乎一直如此)。
吉尔斯(Gilles)'所以

16

很难确切地说出这些书的作者是什么意思。指针是否包含地址取决于您如何定义地址以及如何定义指针。

从所写的所有答案来看,有些人认为(1)地址必须是整数,并且(2)指针实际上不需要在规范中这么说。通过这些假设,显然指针不一定包含地址。

但是,我们看到虽然(2)可能是正确的,但(1)可能不一定是正确的。而根据@CornStalks的答案,将&称为运算符的地址又该怎么办呢?这是否意味着规范的作者打算将指针包含地址?

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

我认为所有这些都是胡言乱语的学问语义。实际上,这毫无价值。您能想到一个编译器以一种指针值不是地址的方式生成代码吗?如果是这样,该怎么办?我也这么想...

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

例如,

 int x;
 int* y = &x;
 char* z = &x;

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

关于指针思考就在这里,就好像它们通常是地址一样导致悲伤。编写bug的原因是人们认为指针就像是地址常常导致悲伤

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


1
指出指针算术的+1与地址算术不同。
kutschkem 2013年

在16位8086的情况下,存储器地址由段基数+偏移量(均为16位)描述。段基+偏移量有许多组合,它们在内存中提供相同的地址。该far指针不仅是“整数”。
vonbrand 2013年

@ vonbrand我不明白你为什么发表这个评论。该问题已作为其他答案的注释进行了讨论。几乎所有其他答案都假定address =整数,任何非整数都不是address。我只是指出这一点,并注意它可能正确或不正确。我的全部意思是,这是不相关的。都只是花哨的,而其他答案并没有解决主要问题。

@tang,“指针==地址”的想法是错误的。每个人和他们最喜欢的阿姨继续说这是不对的。
vonbrand 2013年

@vonbrand,您为什么在我的帖子下发表评论?我没有说对与错。实际上,在某些情况/假设中这是正确的,但并非总是如此。让我再次总结一下帖子的要点(第二次)。 我的全部意思是,这是不相关的。都只是花哨的,而其他答案并没有解决主要问题。 最好对确实声称“指针==地址或地址==整数”的答案进行评论。请参阅我在Alexey帖子下关于segment:offset的评论。
thang 2013年

15

好吧,指针是代表内存位置的抽象。请注意,引号并不是说将指针视为内存地址是错误的,只是说它“通常会导致悲伤”。换句话说,它会导致您有错误的期望。

悲伤的最可能根源当然是指针算术,这实际上是C的强项之一。如果指针是地址,则您希望指针算术是地址算术。但事实并非如此。例如,将10加到一个地址应该给您一个比10个寻址单元大的地址。但在指针上加上10会使它增加其所指向对象的大小的10倍(甚至不是实际大小,而是四舍五入到对齐边界)。对于int *具有32位整数的普通体系结构,将其添加10将使其增加40个寻址单元(字节)。经验丰富的C程序员意识到这一点并接受了它,但是您的作者显然不喜欢草率的隐喻。

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


12

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

例如,给定:

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

变量i和此时c将具有不同的值,因为i++导致i包含下一个可访问整数的地址,并c++导致c指向下一个可寻址字符。通常,整数占用的内存要比字符占用的内存多,因此i最终的值将大于c它们都递增后的值。


2
+1谢谢。使用指针,价值和类型密不可分,就像人的身体与灵魂分离一样。
Aki Suihkonen

i == c格式不正确(如果存在从一个到另一个的隐式转换,则只能比较指向不同类型的指针)。此外,使用强制转换解决此问题意味着您已经应用了转换,然后转换是否更改该值是否值得商bat。(您可以断言它不是,但这只是断言您试图通过此示例证明的同一件事)。
MM

8

马克·贝西(Mark Bessey)已经说过,但是在理解之前需要重新强调。

指针与变量的关系远比文字3大。

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

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

指针不是变量的计数器示例太多了,不容忽视

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

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

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

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


8

你是对的,理智的。通常,指针只是一个地址,因此您可以将其转换为整数并进行任何算术运算。

但是有时指针只是地址的一部分。在某些架构上,将指针添加到基址后转换为地址,或者使用另一个CPU寄存器。

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


PC ...平面内存型号?什么是选择器?
比唐格2013年

赖特 当下一个体系结构发生变化时,也许使用单独的代码和数据空间,或者有人回到了古老的段体系结构(这对安全性意义非凡,甚至可能在段号上添加一些键+偏移量来检查权限),可爱的“指针只是整数”崩溃了。
vonbrand

7

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

许多编译器通过使用其位存储实际的机器地址来实现指针,但这不是唯一可能的实现。一个实现可以保留一个数组(用户代码不可访问)列出程序正在使用的所有存储对象(变量集)的硬件地址和分配的大小,并使每个指针包含一个指向数组的索引以及与该索引的偏移量。这样的设计将使系统不仅可以限制代码仅在其拥有的内存上运行,而且还可以确保指向一个存储项的指针不会被意外地转换为指向另一个存储项的指针(在使用硬件的系统中)地址,如果foobar是连续存储在内存中的10个项目的数组,则是指向第11个项目的指针foo可能改为指向的第一项bar,但在每个“指针”都是对象ID和偏移量的系统中,如果代码试图将指针索引到foo其分配的范围之外,则系统可能会捕获该错误)。由于与任何指针相关联的物理地址都可以移动,因此这种系统也有可能消除存储器碎片问题。

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


以巨大的开销,也许您可​​以检测到可能会“泄漏”其数值的指针值的任何使用,并固定分配,以使垃圾收集器不会收集或重定位它(free当然,除非明确地进行了调用)。产生的实现是否有用将是另一回事,因为它的收集能力可能太有限了,但是您至少可以将其称为垃圾收集器:-)指针分配和算术运算不会“泄漏”该值,但是char*必须检查对来源不明的任何访问。
史蒂夫·杰索普

@SteveJessop:我认为这样的设计比没有用的要糟,因为代码不可能知道需要释放哪些指针。假定任何看起来像指针的东西都是垃圾收集器可能过于保守,但是通常看起来(但并非如此)的指针有可能发生变化,从而避免了“永久性”内存泄漏。采取任何看起来像将指针分解为字节的操作一样永久冻结该指针的方法,可以确保内存泄漏。
2015年

我认为由于性能原因,它还是会失败的-如果您希望代码运行缓慢,因为每次访问都被检查,则不要用C编写它;-)我对C程序员的独创性寄予厚望,因为我认为尽管不方便,但避免不必要地固定分配也不是不可能的。无论如何,C ++正是为了处理此问题而精确地定义了“安全派生的指针”,因此我们知道如果想将C指针的抽象性提高到它们支持合理有效垃圾回收的水平,该怎么做。
史蒂夫·杰索普

@SteveJessop:为了使GC系统有用,它应该能够可靠地释放free尚未调用的内存,或者应防止对释放对象的任何引用成为对活动对象的引用(即使在使用需要资源的资源时也是如此)明确的生命周期管理,GC仍然可以有效地执行后者的功能];如果N个物体变大时,N个物体同时被不必要地钉住的可能性接近零,则有时会错误地认为物体具有对它们的实时引用的GC系统可以使用。除非有人愿意举报编译器错误...
supercat

...对于有效的C ++代码,但对于编译器将无法证明其指针永远无法转换为无法识别的形式的代码,我看不出如何避免这样的风险:使用指针,因为整数可能被错误地认为是这样做的。
超级猫

6

指针是C / C ++本机可用的变量类型,它包含一个内存地址。像其他任何变量一样,它具有自己的地址并占用内存(具体数量取决于平台)。

由于混淆而导致的一个问题是,您试图通过简单地按值传递指针来更改函数中的引用对象。这将在功能范围内创建指针的副本,并且对该新指针“指向”的位置所做的任何更改都不会更改在调用函数的范围内指针的引用。为了修改一个函数中的实际指针,通常会将一个指针传递给一个指针。


1
通常,它是一个句柄/ ID。通常,这是一个简单的地址。
Alexey Frunze

我将答案调整为更多PC,以适应维基百科中Handle的定义。我喜欢将指针称为句柄的特定实例,因为句柄可能只是对指针的引用。
马修·桑德斯

6

概要 (我还将放在顶部):

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

(1)但是,在许多(也许是大多数)编译器中,指向函数的指针不是地址,而是大于地址(通常为2x,有时更大),或者实际上是指向内存中结构的指针,而不是包含函数和诸如此类的东西的地址一个恒定的池。

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

(3)具有FAR和NEAR指针问题的旧版x86代码

(4)带有安全“胖指针”的几个示例,最著名的是IBM AS / 400。

我相信您可以找到更多。

详情:

嗯!!!!!! 到目前为止,许多答案都是相当典型的“程序员中间人”答案,但不是编译器中间人或硬件中间人。由于我假装自己是一个硬件weenie,并且经常与编译器weenies一起工作,所以让我投入两分钱:

在许多(可能是大多数)C编译器上,指向类型数据的指针T实际上是的地址T

精细。

但是,即使在许多这样的编译器上,某些指针也不是地址。您可以通过查看来告诉我们sizeof(ThePointer)

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

我们中的许多人对NEAR POINTER和FAR POINTERS都怀念旧的Intel x86分段架构。幸运的是,现在这些几乎已经绝迹了,因此仅作一个简短的总结:在16位实模式下,实际的线性地址为

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

而在保护模式下,可能是

LinearAddress = SegmentRegister[SegNum].base + offset

根据段中设置的限制检查结果地址。有些程序使用的并不是真正的标准C / C ++ FAR和NEAR指针声明,但是许多程序只是说*T---但是有编译器和链接器开关,因此,例如,代码指针可能位于指针附近,仅比指针中的32位偏移。 CS(代码段)寄存器,而数据指针可能是FAR指针,同时为16位段号和32位偏移量指定48位值。现在,这两个数量肯定与地址有关,但是由于它们的大小不同,因此哪个是地址?此外,除了与实际地址有关的内容外,这些网段还具有权限-只读,读写,可执行。

一个更有趣的例子,恕我直言,是(或者也许是)IBM AS / 400系列。这台计算机是最早用C ++实现操作系统的计算机之一。这个指针上的指针通常是实际地址大小的2倍-例如,此演示文稿例如128位指针,但实际地址为48-64位,此外,还提供了一些额外的信息,即所谓的功能,该功能提供了诸如读取,写入之类的权限以及防止缓冲区溢出的限制。是的:您可以与C / C ++兼容地执行此操作-并且如果这无所不在,那么中国的PLA和斯拉夫黑手党就不会侵入如此众多的西方计算机系统。但是历史上大多数C / C ++编程都忽略了性能的安全性。最有趣的是,AS400系列允许操作系统创建安全的指针,该指针可以提供给未特权的代码,但未特权的代码无法伪造或篡改。同样,安全性虽然符合标准,但草率的非符合标准的C / C ++代码在这样的安全系统中将无法工作。同样,有官方标准,

现在,我将离开安全性提示框,并提到通常不能真正解决(各种类型的)指针的其他一些方式:指向数据成员的指针,成员函数方法的指针以及它们的静态版本大于普通地址。正如这篇文章 所说:

解决这个问题的方法有很多种[与单人或多人继承和虚拟继承有关的问题]。这是Visual Studio编译器决定处理它的方式:指向多重继承类的成员函数的指针实际上是一个结构。”然后他们继续说:“投射函数指针可以改变其大小!”。

从对安全性的高度评价中可以猜到,我参与了C / C ++硬件/软件项目,在该项目中,指针被视为功能而非原始地址。

我可以继续,但希望您能明白。

概要 (我还将放在顶部):

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

(1)但是,在许多(也许是大多数)编译器中,指向函数的指针不是地址,而是大于地址(通常为2X,有时更大),或者实际上是指向内存中的结构的指针,而不包含函数和诸如此类的东西一个恒定的池。

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

(3)具有FAR和NEAR指针问题的旧版x86代码

(4)带有安全“胖指针”的几个示例,最著名的是IBM AS / 400。

我相信您可以找到更多。


在16位实模式下LinearAddress = SegmentRegister.Selector * 16 + Offset(请注意时间16,而不是移位16)。在保护模式下LinearAddress = SegmentRegister.base + offset(不进行任何形式的乘法;段基存储在GDT / LDT中,并按原样缓存在段寄存器)。
Alexey Frunze 2013年

您对细分受众群的判断也是正确的。我记错了。段限制可以选择乘以4K。当将段描述符从内存中加载到段寄存器中时,硬件只需要对段基进行解密即可。
Krazy Glew

4

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


那么,指针对象实际上是一个内存地址吗?您不同意作者吗?只是想了解。
d0rmLife 2013年

指针的主要功能是指向某物。还不确定如何精确地实现以及是否有真实地址。指针可能只是一个ID /句柄,而不是真实地址。
Alexey Frunze 2013年

4

您可以通过这种方式看到它。指针是代表可寻址存储空间中地址的值。


2
指针不一定必须在其中保存实存储器地址。请参阅我的答案及其下的评论。
Alexey Frunze

什么...指向堆栈上第一个变量的指针不会显示0。它会根据实现的方式打印堆栈帧的顶部(或底部)。
比唐格2013年

@thang对于第一个变量,顶部和底部相同。在这种情况下,顶部或底部的地址是什么?
Valentin Radu

@ValentinRadu,为什么不尝试呢。显然您还没有尝试过。
比唐格2013年

2
@thang你是对的,我做了一些非常糟糕的假设,我的辩护是在凌晨5点。
Valentin Radu

3

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


1
不一定是地址。顺便说一句,在发布答案之前,您是否已阅读现有答案和评论?
Alexey Frunze

3

AC指针与存储器地址非常相似,但是抽象了与机器相关的细节,以及一些在较低级指令集中找不到的功能。

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

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

有一个特殊的“空指针”值可在源代码级别移植,但其表示形式可能有所不同。如果为指针分配一个值为零的整数常量,则该指针将采用空指针值。如果您以这种方式初始化指针,则同上。

指针可以用作布尔变量:如果它不是null,则测试为true,否则为false。

在机器语言中,如果空指针是一个有趣的地址(如0xFFFFFFFF),则可能必须对该值进行显式测试。C对您隐藏了这一点。即使空指针是0xFFFFFFFF,也可以使用进行测试if (ptr != 0) { /* not null! */}

使用会破坏类型系统的指针会导致未定义的行为,而使用机器语言编写的相似代码可能会得到很好的定义。汇编器将汇编您编写的指令,但是C编译器将基于您没有做错任何事情的假设进行优化。如果float *p指针指向long n变量并被*p = 0.0执行,则不需要编译器来处理。后续的使用n将不必读取浮点值的位模式,但也许这将是一个优化的访问,它基于n尚未被触及的“严格别名”假设!也就是说,该程序的行为良好,因此p不应指向n

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

因此,您可以看到,机器地址和C指针之间在语义上存在许多差异。


NULL指针不能在您认为的所有平台上都可以正常工作-请参阅上面我对CiscoIPPhone的答复。NULL == 0是仅在基于x86的平台上适用的假设。Convention表示,新平台应与x86相匹配,但是特别是在嵌入式世界中,事实并非如此。编辑:此外,C并没有做任何事情来从硬件中提取指针方式的值-“ ptr!= 0”将无法在NULL!= 0的平台上作为NULL测试
。– DX-MON

1
DX-MON,这对于标准C是完全错误的。NULL被定义为0,并且它们可以在语句中互换使用。硬件中的NULL指针表示是否为全0位与源代码中的表示方式无关。
Mark Bessey

@ DX-MON恐怕您使用的不是正确的事实。在C语言中,无论空指针是否为空地址,整数常量表达式均用作空指针常量。如果您知道ptr != 0不是空测试的C编译器,请透露其身份(但在这样做之前,请将错误报告发送给供应商)。
卡兹(Kaz)2013年

我明白了您的意思,但是您对空指针的评论不连贯,因为您将指针和内存地址弄混了 —正是该问题中引用的引语建议避免使用!正确的语句:C将空指针定义为零,而不管偏移量为零的内存地址是否合法。
Alexis

1
请@alexis章节和经文。C没有将空指针定义为零。C将零(或任何值为零的整数常量表达式)定义为表示空指针常量的语法faqs.org/faqs/C-faq/faq(第5节)。
卡兹(Kaz)2013年

3

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

+ : 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`

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

任何对象都支持操作&(的地址),该操作检索对象的位置说明符(地址)作为指针对象类型。这应该减轻围绕术语的混淆,因为将其称为&对象的操作而不是其结果类型是对象类型的指针的指针是有意义的。

注意在整个说明中,我没有提到内存的概念。


我喜欢您对通用系统中通用指针的抽象现实的解释。但是,也许讨论记忆会有所帮助。实际上,为我自己说话,我知道会...!我认为讨论连接对于理解全局很有帮助。无论如何+1 :)
d0rmLife

@ d0rmLife:您在其他答案中有足够的解释,涵盖了更大的图景。我只是想给出一个数学抽象的解释,作为另一种观点。同样是恕我直言,它将在调用&“地址”时产生较少的混乱,因为它与对象而不是指针本身紧密联系在一起
Abhijit

没有冒犯,但我会自己决定什么足够的解释。一本教科书不足以全面说明数据结构和内存分配。;)....无论如何,您的回答仍然很有帮助即使它不是很新颖。
d0rmLife 2013年

没有内存的概念处理指针是没有意义的。如果对象存在而没有内存,则它必须位于没有地址的地方,例如在寄存器中。能够使用“&”作为前提。
Aki Suihkonen

3

地址用于将一块固定大小的存储区(通常对于每个字节)标识为整数。精确地将其称为字节地址,ISO C也使用该地址。可以存在一些其他方法来构造地址,例如,针对每个位。但是,仅经常使用字节地址,因此我们通常忽略“字节”。

从技术上讲,地址绝不是C中的值,因为(ISO)C中术语“值”的定义是:

当解释为具有特定类型时,对象内容的确切含义

(由我强调。)但是,在C中没有这样的“地址类型”。

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

C中的值可以是指针类型。这称为指针值。需要明确的是,指针值不是C语言中的指针。但是我们习惯于将它们混合在一起,因为在C中不太可能是模棱两可的:如果我们将表达式p称为“指针”,则它只是一个指针值而不是类型,因为C中的命名类型不是用表达式表示,但用type-nametypedef-name表示

其他一些事情是微妙的。作为C用户,首先,应该知道什么object意思:

执行环境中数据存储的区域,其内容可以表示值

对象是表示特定类型的值的实体。指针是一种对象类型。因此,如果声明int* p;,则p表示“指针类型的对象”或“指针对象”。

请注意,该标准没有 “变量”的规范定义(实际上,ISO C在规范文本中从未将其用作名词)。但是,非正式地,我们将对象称为变量,就像其他语言一样。(但仍然不是那么精确,例如,在C ++中,变量可以是标准引用类型,而不是对象。)有时将短语“指针对象”或“指针变量”与上述“指针值”一样对待,并带有一个可能略有差异。(另一组示例是“数组”。)

由于指针是一种类型,并且地址在C中实际上是“无类型的”,因此指针值大致“包含”一个地址。指针类型的表达式可以产生一个地址,例如

ISO C11 6.5.2.3

3一元运算&符产生其操作数的地址。

请注意,此措辞是由WG14 / N1256引入的,即ISO C99:TC3。在C99中有

3一元运算&符返回其操作数的地址。

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

尽管上面有这些措辞,但即使在标准中仍然有些混乱。

ISO C11 6.6

9 地址常量是一个空指针,一个指向左值的指针,该左值指示一个静态存储持续时间的对象,或者一个指向函数指示符的指针

ISO C ++ 11 5.19

3 ... 地址常量表达式是指针类型的prvalue核心常量表达式,其值为静态存储持续时间的对象的地址,函数的地址或空指针值或prvalue核心常量表达式的类型std::nullptr_t。...

(最新的C ++标准草案使用了另一种措辞,因此没有此问题。)

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

内置的一元运算&符在C和C ++中称为“地址”;类似地,std::addressof在C ++ 11中引入。

这些命名可能会带来误解。将得到的表达是指针类型的,所以他们会被解释为:结果包含/产生一个地址,而不是一个地址。


2

它说:“因为它使那些不知道地址有关的人感到困惑”-的确如此:如果您了解地址是关于什么的,就不会感到困惑。从理论上讲,指针是一个指向另一个变量,实际上保存着一个地址,即它指向的变量的地址。我不知道为什么要隐藏这个事实,这不是火箭科学。如果您了解了指针,您将更进一步了解计算机的工作方式。前进!


2

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


指针保存一个地址(如果为空,则不保存)。但是,这是由它相距甚远一个地址:例如,两个指针到同一个地址,但使用不同类型不是在许多情况下等同。
吉尔(Gilles)'“ SO-别邪恶”,

@Gilles如果看到“存在”,如int i=5-> i 5,则指针为地址yes。同样,null也有一个地址。通常是无效的写地址(但不一定是x86实模式),但地址仍然如此。实际上,对于null仅有2个要求:保证比较不等于指向实际对象的指针,并且任意两个null指针将比较相等。
Valentin Radu

相反,保证空指针不等于任何对象的地址。取消引用空指针是未定义的行为。说“指针就是地址”的一个大问题是它们的工作方式不同。如果p是一个指针,p+1并不总是按1递增地址
吉尔“SO-停止作恶”

请再次阅读评论it's guaranteed to compare unequal to a pointer to an actual object。至于指针算法,我看不到重点,指针的值仍然是地址,即使“ +”操作不一定会向其添加一个字节。
Valentin Radu

1

由于我在其他答案中没有看到的指针类型不同,因此C或C ++指针与简单内存地址不同的另一种方式(尽管鉴于它们的总大小,我可能忽略了它)。但这可能是最重要的一个,因为即使是经验丰富的C / C ++程序员也可以克服它:

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

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*可以使用来操纵值(尽管不是很方便)。


0

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

不合格的词“指针”是不明确的。C具有指针对象(变量),指针类型,指针表达式和指针

使用“指针”一词来表示“指针对象”是很常见的,这可能会引起一些混淆-这就是为什么我尝试将“指针”用作形容词而不是名词。

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

那么C语言中的地址是什么?它是一个指针值,即某种特定指针类型的值。(除了空指针值不一定是“地址”,因为它不是任何东西的地址)。

一元标准的描述 &符说,它“产生其操作数的地址”。在C标准之外,“地址”一词通常用于指代(物理或虚拟)内存地址,通常是一个大小的单词(无论“给定”系统上有什么“单词”)。

AC“地址”通常实现为机器地址-就像C int值通常实现为机器字一样。但是,C地址(指针值)不仅仅是机器地址。这是一个通常表示为机器地址的值,并且是具有某些特定类型的值。


0

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

但是,该标准在特定的抽象级别上进行讨论。作者谈论的那些人“谁知道什么地址”,但是谁是C的新手,他们必须以不同的抽象级别学习地址-可能是通过汇编语言编程的。这些人已经知道,不能保证C实现使用与CPU操作码使用的地址相同的表示形式(在此段落中称为“存储地址”)。

他继续谈论“完全合理的地址操纵”。就C标准而言,基本上没有“完全合理的地址操纵”之类的东西。加法是在指针上定义的,基本上就是这样。当然,您可以将指针转换为整数,进行按位或算术运算,然后再将其转换回去。这不能保证按标准工作,因此在编写该代码之前,您最好了解特定的C实现如何表示指针并执行该转换。它可能使用了您期望的地址表示形式,但这并不是您的错,因为您没有阅读手册。这不是混乱,它是错误的编程过程;-)

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

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

就个人而言,除了向汇编程序员介绍C之外,我认为作者的言论并没有帮助。对于那些来自高级语言的人来说,说指针值不是地址肯定是没有帮助的。承认这种复杂性要比说CPU在说一个地址是什么,从而使C指针值“不是”地址上具有垄断性要好得多。它们是地址,但它们的写法可能不同于他所指的地址。我认为,在C上下文中将“地址”和“商店地址”区分为两件事就足够了。


0

简而言之,指针实际上是分段机制的偏移部分,分段后它转换为线性地址,然后在分页后转换为物理地址。实际地址实际上是从您的ram寻址的。

       Selector  +--------------+         +-----------+
      ---------->|              |         |           |
                 | Segmentation | ------->|  Paging   |
        Offset   |  Mechanism   |         | Mechanism |
      ---------->|              |         |           |
                 +--------------+         +-----------+
        Virtual                   Linear                Physical
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.