在C语言中,变量名是如何存储在内存中的?

73
在 C 语言中,假设你有一个名为variable_name的变量。我们假设它位于0xaaaaaaaa的内存地址处,而在那个内存地址上,你有整数123。换句话说,variable_name包含123。
关于“variable_name位于0xaaaaaaaa”这种措辞,我想要澄清一下。编译器如何识别该字符串“variable_name”与特定的内存地址相关联?该字符串“variable_name”是否存储在内存中?编译器是否只是在看到variable_name时将其替换为0xaaaaaaaa?如果是这样,编译器不是必须使用内存来进行替换吗?

不算是一个答案,但这可能会填补一些空白:http://www.csee.umbc.edu/~chang/cs313.s02/stack.shtml - Douglas
缺少调试信息,变量名不会存储在内存中。如果你想理解这个问题,首先需要了解机器语言和汇编语言。 - Hot Licks
5个回答

107

编译器运行后,变量名将不再存在(除了共享库或调试符号中的出口全局变量这样的特殊情况)。编译的整个过程旨在将源代码中表示的符号名称和算法转换为本机机器指令。因此,如果您有一个全局 variable_name,而编译器和链接器决定将其放置在 0xaaaaaaaa,那么无论它在代码中的使用位置如何,都将通过该地址进行访问。

因此,回答您的字面问题:

编译器如何识别字符串“variable_name”与特定内存地址相关联?

工具链(编译器和链接器)一起分配变量的内存位置。编译器的工作是跟踪所有引用,而链接器稍后会放入正确的地址。

字符串"variable_name"是否存储在内存中?

只有在编译器运行期间才存储。

编译器是否只是在看到variable_name时将其替换为0xaaaaaaaa,如果是,那么它不必使用内存来进行替换吗?

是的,这几乎就是发生的情况,但这是一个由链接器完成的两个阶段的工作。是的,它使用内存,但它是编译器的内存,而不是程序运行时的任何东西。

一个例子可能有助于您理解。让我们尝试运行此程序:

int x = 12;

int main(void)
{
    return x;
}

很直接明了,对吧?好的,让我们拿这个程序进行编译并查看反汇编代码:

$ cc -Wall -Werror -Wextra -O3    example.c   -o example
$ otool -tV example
example:
(__TEXT,__text) section
_main:
0000000100000f60    pushq   %rbp
0000000100000f61    movq    %rsp,%rbp
0000000100000f64    movl    0x00000096(%rip),%eax
0000000100000f6a    popq    %rbp
0000000100000f6b    ret

看到那行movl了吗?它正在以指令指针相对的方式获取全局变量(在本例中)。不再提到x

现在让我们让它变得更加复杂,添加一个局部变量:

int x = 12;

int main(void)
{  
    volatile int y = 4;
    return x + y;
}

这个程序的反汇编结果为:

(__TEXT,__text) section
_main:
0000000100000f60    pushq   %rbp
0000000100000f61    movq    %rsp,%rbp
0000000100000f64    movl    $0x00000004,0xfc(%rbp)
0000000100000f6b    movl    0x0000008f(%rip),%eax
0000000100000f71    addl    0xfc(%rbp),%eax
0000000100000f74    popq    %rbp
0000000100000f75    ret

现在有两个movl指令和一个addl指令。你可以看到第一个movl正在初始化y,它决定将位于堆栈(基指针-4)上。然后下一个movl将全局变量x存储在寄存器eax中,并且addly添加到该值中。但是如您所见,文本字面意义上的xy字符串不再存在了。它们只是为了方便程序员而存在,但计算机在执行时并不关心它们。


12
如何在 C 语言中实现的解释非常好。请注意,其他一些语言(尤其是现代“动态”或“脚本”语言)可能会维护数据的符号名称,并确实在运行时使用内存来保留该映射信息。 - Russell Borogove
3
这是一个很好的回答。只有一个问题:编译器在真正运行之前如何知道地址?我认为内存是动态分配的(因此不同运行中的地址可能不同)。 - Jackson Tale
2
@JacksonTale - 这在很大程度上取决于特定系统的配置,但在大多数常见系统中,虚拟内存意味着即使底层物理地址从一次运行到另一次运行发生变化,您的进程始终具有相同的内存逻辑视图。在我上面使用的示例中,变量是相对于指令指针而不是绝对地址进行寻址的。 - Carl Norum
@CarlNorum所说的是,在运行时,物理实际内存地址将被映射到虚拟内存地址吗? - Jackson Tale
1
0xfc 是表示 -4 的8位二进制补码。 - Carl Norum
显示剩余4条评论

14

一个C编译器首先创建一个符号表,它存储变量名与其在内存中的位置之间的关系。在编译时,它使用这个表来将所有变量实例替换为特定的内存位置,正如其他人已经说明的那样。你可以在维基百科页面上找到更多相关信息。


10

所有变量都由编译器替换。首先它们被替换为引用,然后链接器将地址放置在引用的位置。

换句话说,一旦编译器运行完毕,变量名将不再可用。


1
编译多个文件并稍后链接时,了解“先引用..再寻址”是非常重要的。这点很重要。 - P.P

6
这被称为“实现细节”。虽然你所描述的情况在我使用过的所有编译器中都是这样,但并不要求这样。C编译器可以将每个变量放入哈希表中,并在运行时查找它们(或类似的操作),实际上早期的JavaScript解释器就是这样做的(现在,它们进行即时编译,结果更加原始)。
对于像VC++、GCC和LLVM这样的常见编译器,编译器通常会为变量分配一个内存位置。具有全局或静态作用域的变量获得一个固定地址,在程序运行时不会改变,而函数内部的变量则获得一个“堆栈”地址-即相对于当前堆栈指针的地址,每次调用函数时都会改变。(这是一种简化。)堆栈地址在函数返回后立即失效,但使用起来几乎没有开销。
一旦为变量分配了地址,就不再需要变量名了,因此被丢弃。根据名称的类型,名称可能会在预处理时(用于宏名称)、编译时(用于静态和局部变量/函数)和链接时(用于全局变量/函数)被丢弃。如果导出符号(使其可见于其他程序以便访问),则名称通常仍会保留在“符号表”中,这需要占用一些内存和磁盘空间。

4

编译器是否在看到0xaaaaaaaa时只是将其替换为variable_name?

是的。

如果是这样,它不会使用内存来进行替换吗?

是的。但这是编译器,在编译您的代码后,为什么要关心内存呢?


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