指针访问变量的地址

25

考虑以下C代码

int x=100;
int*addr=&x;
我知道addr会存储x的地址。一直在我的脑海中反复出现的问题是,addr指针将有其自己的地址,这可以再次使用&运算符访问,并且也将被存储在某个地方,因此我们在地址上有无限递归,那么这会在哪里结束呢?

3
这并不是结束……你可能会有一个 int**************************。另外,除非你创建指针,否则地址不会被存储在任何地方。 - StoryTeller - Unslander Monica
4
你可以在一张纸上写下一所房子的地址,然后将它放在另一所房子里。然后你可以在另一张纸上写下那所房子的地址,再把它放在另一所房子里。接着,你可以将第三所房子的地址写在第三张纸上,将其放在第四所房子里。因此,在地址上出现了无限递归,那么这个过程何时结束呢? - Shahbaz
1
当您停止向类型添加 * 时,它就结束了。可以这样想,所有像 int *int ** 等的类型在编译时都是已知的。如果有一个实现定义的间接限制,您将在编译时得知,因此从循环或递归函数调用的角度来看,它并不是无限的。 - Brandin
11个回答

25
< p > addr 的地址没有明确地“存储”在任何地方,它只是存在。如果您声明第二个变量并使用它来存储该地址,那么它肯定会占用空间:

int **addr2 = &addr;

您可以将内存视为一系列盒子。 假设使用4字节的 int 和指针,并采用小端字节顺序,则您的情况可能如下所示:
+----------+--+--+--+--+
| Address  |Data bytes |
+----------+--+--+--+--+
|0x00000000|64|00|00|00|
+----------+--+--+--+--+
|0x00000004|00|00|00|00|
+----------+--+--+--+--+

左侧显示地址,右侧显示该位置包含的字节。您可以看到第一个四字节中存储的值为100(十进制下的100在十六进制下为0x64)。第二个四字节位置保存了值0x00000000,这是x的地址。地址0x00000004没有存储在任何地方。
现在,如果我们添加第二个指针,我们将使用更多内存:
+----------+--+--+--+--+
|0x00000008|04|00|00|00|
+----------+--+--+--+--+

那个内存用于表示指针addr2,您可以看到它包含addr的地址,即0x00000004。使用表达式*addr2addr2进行解引用将产生地址0x00000004处的值,即带有类型int *的0x00000000。再次解引用会得到该地址处的int,即0x00000064。
由于这种内存布局是编译器选择的,因此它“知道”所涉及的地址,并且可以替换,因此当代码引用addr2时,它会生成操作地址0x00000008等的指令。在实际代码中,这可能全部发生在堆栈上,但原理是相同的。
最后提示:请听从@Phil Perry的建议;上述内容简化了许多本应抽象的概念并将其具体化。在当前许多架构中,它的确是如此工作的,但其中许多提到的事情并不是C语言所保证的,因此你不能完全依赖它们始终成立。我想通过上述内容来说明(希望)使概念稍微明确一些。

特别感谢您的可视化,它很有帮助。 - Naseer
1
你忘记了一个星号:int ** addr2 =&addr - marczellm
@marczellm 谢谢,已修复! - unwind
3
仅仅需要提醒一下:不要将指针视为某种整数。不能保证指针占用特定字节数,甚至可能不是单个数字(请想想旧的Intel 808x分段架构)。在处理指针时一定要非常小心! - Phil Perry
@PhilPerry 好观点,我加入了一些相关的单词。我认为sizeof(T)可以对每种指针类型T进行评估,你不这么认为吗? - unwind
sizeof和从指针中添加/减去元素应始终定义,也许还应该区分两个指针之间的差异(至少是相同类型的指针)。我只是在警告程序员倾向于将指针视为另一种整数,并执行不当操作,例如将它们强制转换为字int。 - Phil Perry

13

如果您想要存储一个地址,自然需要有一个地方来存储它(即另一个地址),这样您可以一直进行下去。这将在您希望结束时精确结束。

int         a = 1;
int       *pa = &a;
int     **ppa = &pa;
int   ***pppa = &ppa;
int ****ppppa = &pppa;
...

这里有一个简单的比喻:假设你有一本有编号和行数的笔记本。假设你在第3页、第8行上做了个笔记,现在你想从其他地方引用它。也许你可以在第7页、第20行上写下一个“见第3页第8行的笔记”的引用。现在这个引用有了自己的“地址”,也就是第7页第20行。你也可以引用它 - 在第8页第1行上,你可以写下“见第7页第20行的笔记”。你可以无限延伸这个引用链。


1
还要考虑到你不能执行 &&a。为什么呢?因为 &a 在存储之前没有地址。 - Claudiu

7
C实现了所谓的 von Neumann 机器。如果你能让计算机像 von Neumann 机器那样工作,那么你就可以在其他类型的计算机上编译C。
那什么是 von Neumann 机器呢?尽管人们普遍认为程序在内存中有着严格和独立的代码和数据概念,但实际上——从一个统一的 von Neumann 视角来看——程序的内存空间更像是一个充满了“wibbly-wobbly, numbery-wumbery”东西的大球体。你可以像访问数据一样访问甚至改变它,或者像运行代码一样运行它。但是如果你不确定它是否是可靠的代码,这样做不是个好主意,这也是人们不再经常使用汇编语言的原因之一。高级编程语言会将数字仔细组织成一些(理想情况下)可行的东西:它们并不完美,在设计上C比大多数语言都不够完美,但它们通常能胜任工作。
现在,这与你的问题有什么关系呢?正如你所说,每个变量都必须有一个地址,而C必须知道这些地址才能使用它们。具体的实现细节可能因编译器而异,有时甚至在编译器的不同设置中也可能会有所不同,但在某些情况下,程序在机器代码层面甚至已经不再知道变量名字。地址是唯一存在的,并且它们才是真正被使用的。这些地址由编译器决定,最终成为代码的一部分。它们保留在通常情况下不能访问但确实可以访问的区域,这些区域基本上是递归停止的地方。

3

您没有无限递归。每个变量都有它的地址,就这样。如果您定义一个变量等于另一个变量的地址,则该地址变量本身将被存储在某个地方,并且将具有一个地址。一个地址本身没有一个地址存储它的变量有。


3

概念上,只有在存储地址时才会为指针分配存储空间:

1)在您的情况下:int* addr=&x;addr将占用sizeof(int*)字节的内存。(但编译器保留不将指针存储在内存中的权利:它可能将其保留在CPU寄存器中)。

2)&x没有显式分配任何存储空间。

3)&&x&&&x等在语法上无效,因此您不需要担心潜在的递归问题。

&&曾经是非法语法,这使得C++11可以采用它来表示r-value引用


2
这里已经有很多好的答案了,但我想再补充一个。我想澄清Bathsheba提出的一个微妙的观点。你说过:
“addr指针将有自己的地址,也可以使用&运算符访问该地址,这个地址也将被存储在某个地方。”
那句话有一个微妙的错误。以下是正确的重新表述:
“如果在包含指针值的存储位置上使用取地址符,则该存储位置将具有自己的地址......”
指针是一个值。所有的值都存储在某个地方。但并非所有的存储位置都有地址!有些存储位置没有地址——它们被称为寄存器——编译器可以自由地为任何存储位置使用寄存器,只要不取该存储位置的地址。
所以当你说:
int x = 100;
int*addr = &x;

那么与变量 x 相关联的存储位置必须具有地址,因为它的地址被 取出来了。但是,与 addr 相关联的存储位置没有要求必须具有地址,因为它的地址从未被 取出来;因此,它可以被 寄存器化。它不是必须被寄存器化,但是可以这样做。

有意义吗?


2
@unwind的回答很好,但有一个巨大的警告:C语言允许你创建指向任何东西的指针,这可能会给你带来麻烦。考虑下面这个简单的C程序:
/*
 * show that a variable allocated on the heap sticks around, while a
 * variable allocated on the stack vanishes after its enclosing
 * function returns.  Holding a pointer to the latter can lead to 
 * unexpected results.
 */
#include <stdio.h>

int heapvar = 20;

int *get_heapvar_ptr() {
  return &heapvar;
}

int *get_stackvar_ptr() {
  int stackvar = 30;
  return &stackvar;
}

int main() {
  int *heap_ptr = get_heapvar_ptr();
  int *stack_ptr = get_stackvar_ptr();

  /* should print value = 20 */
  printf("heapvar address = %#x, heapvar value = %d\n", heap_ptr, *heap_ptr);
  /* you might expect this to print value = 30, but it (probably) doesn't */
  printf("stackvar address = %#x, stackvar value = %d\n", stack_ptr, *stack_ptr);
}

在我的环境中运行时,我得到了以下结果:
heapvar address = 0x22a8018, heapvar value = 20
stackvar address = 0x5d957fac, stackvar value = 0

请注意,尽管stackvar的值曾经被设置为20,但它现在的“值”为零。这是因为stack_ptr指向栈上已被其他函数覆盖的位置。
值得一提的是,编译器警告了我“警告:与局部变量相关联的栈内存地址”,但故事的寓意是,在C语言中,指针算术非常强大,因此也相应地具有危险性!

2

当然,地址指针将有其自己的地址,但除非您专门访问它所持有的地址,否则它不会被使用,因此对于这个没有递归...


2
让我们这样看待。我们有有限的内存。在这个整个内存中,只有有限的内存被分配给编译器来存储它们的变量。你可以声明尽可能多的变量,但是总有一个时刻,你无法再声明更多的变量,因为分配的内存空间将用完。所以,对于你的问题,答案是否定的。由于有限的分配空间来存储变量,不存在无限递归。

2
指针就像一个房子和它在社区中的地址。房子是真实存在的,而它的地址是找到房子的方法。假设有两个房子,X和ptr,我需要将一个房子(比如说X)的地址存储在另一个房子(比如说ptr)中(可能会在纸上写下来)。通过这种安排,如果我需要知道X的地址,我可以查看ptr,但我需要亲自访问房子X才能知道里面有什么。但目前我并不感兴趣知道ptr的地址。
为了更深入地了解它:
ptr and X are two houses.

//lets say &X is a sheet of paper containing the address of house X. I am going to keep this address in the house ptr. So the address of X is stored in ptr.

ptr = &X;

//Now what about ptr. Only if I need to know the address, I need to write the address in some paper and store it again in some other house. Else I am happy to know the house exits. 

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