C语言中常量是如何存储在内存中的?

3

我正在开发一门自己的编程语言,它支持指针和常量。我想知道在C语言等其他语言中,常量是如何存储在内存中的?我在StackOverflow上读到过,运行时它们被存储在只读内存中,但我不明白这是如何可能的,因为下面的代码可以编译并成功执行:

#include <stdio.h>

int main (int argc, char ** argv) {
  const int x = 1;

  int *y;
  y = &x;

  *y += 1;

  printf("x = %d\n", x); // Prints: 2
  printf("y = %d\n", *y); // Prints: 2

  return 0;
}

在这里,我定义了一个名为x的常量,并从中创建一个指针,以便我可以修改其值。这意味着x不能存储在只读内存中。
因此,我真的很想知道常量在运行时是如何存储的?

4
它们存储在RAM中。在PC上,它不会将其存储在闪存或类似设备中。 "const"告诉编译器您不打算更改它。但是,如果您“欺骗”并使用指针,则可以更改它,但我打赌您在编译时看到了相关警告。使用"gcc"编译时收到的警告是:“warning: assignment discards 'const' qualifier from pointer target type [-Wdiscarded-qualifiers] y = &x;”,因此在这种情况下忽略了"const"。 - lurker
1
你展示了一个程序的源代码,但这不能证明什么。你有编译并运行它吗?如果是,而且它运行了,那只能证明你所编译和运行的特定实现这种情况下不会将x存储在只读内存中(并对C计算模型的实现方式做出某些假设)。 - Eric Postpischil
1
关键字 const 并不意味着常量,而是只读的... 是的,好吧,这很误导人。当我编译你的代码时,会出现 warning: assigning to 'int *' from 'const int *' discards qualifiers [-Wincompatible-pointer-types-discards-qualifiers] 的警告... 标准并没有描述这种规范,编译器可以自由地做他们喜欢的事情。 - Stargateur
1
取决于优化级别。使用 -O0 时,gcc 将其放在堆栈上。 - Serge
1
这不是一个有效的C程序,而且根据“好”的合理定义,它无法编译成功 - n. m.
显示剩余3条评论
4个回答

5
一般来说,特别是在启用优化时,编译器会尽可能地使常量更加高效。例如,如果您写了 x = 3*4 + 5,编译器会在编译时将其减少为17,而不是将3、4和5放入编译后的程序中。
直接使用的小常量通常被编码到指令的立即字段中。
如果编译器无法将常量放入指令的立即字段中,它通常会尝试将其存储在只读内存中。
然而,您给出的示例使得这对编译器很困难。您示例中的代码:
  • 在例程内定义了一个const对象(而不是在文件作用域)。
  • 获取了该对象的地址。
如果您仅仅定义了一个const对象并且从未获取过它的地址,那么编译器可以将常量存储在只读数据段中。
但是,由于您获取了该对象的地址,所以存在问题。例程可以被递归调用。(虽然您的程序没有递归调用main,但编译器设计支持递归调用,因此这里讨论的问题适用于它的设计。)每当例程被递归调用时,必须创建该例程中定义的对象的新实例(在C计算模型中)。如果const对象的地址没有被获取,那么编译器可以通过使用相同的只读内存来优化这个过程,用于所有对象的实例 - 因为它们的值永远不会改变,所以没人能告诉它们是多个副本而不是一个实例。
然而,不同的对象实例可以通过它们的地址来区分。由于您获取了该对象的地址,编译器希望实际地创建不同的对象实例。在只读内存中做到这一点很困难。程序通常不维护只读内存的堆栈,因此编译器没有方便的方法来跟踪必须为只读内存中的对象创建的多个实例。 (维护只读内存的堆栈将是困难的。如果堆栈上的内容在不同时间可能不同,则该堆栈的内存必须更改。因此,即使只有只读对象在堆栈上,堆栈本身也不能是只读的。)
因此,在这种情况下,编译器将您的const对象放置在常规堆栈上。
当然,这并不是您可以依赖的行为。试图更改定义为const的对象的值具有未被C标准定义的行为。即使在本例中似乎“起作用”,但在更复杂的程序中,编译器可能会通过各种优化来转换您的程序,结果当尝试修改像这样的const对象时,您的程序可能会失败。例如,printf(“%d”,* y)可能会打印“2”,因为y指向的内存已更改为2,而printf(“%d”,x)可能会打印“1”,因为x 在计算机C模型中已知为常数1。

2
这是一个非常好的关于const注意事项的讨论,以及为什么你不应该作弊。 - David C. Rankin

2

你发现了C语言中关于未定义行为的一个令人沮丧的事实。

"编译并执行良好"并不证明代码是合法和正确的(注意不要重复否定)!

如果你作弊,并尝试写入一个const限定的位置,任何事情都可能发生:你可能会得到一个错误信息,它似乎可以工作,或者悄悄地做一些几乎或完全不像你预期的事情。

当你说:

y = &x;

您的编译器应该会发出警告,类似于“警告:将'const int *'分配给'int *'会丢弃限定符”。(这正是我的编译器所说的。)

因此,编译器可完全有权在文本段或其他只读内存段中存储不可修改的常量。如果这意味着像您的程序这样的程序失败,那就没问题。

但还有一点要注意。您声明的“常量”const int x = 1是一个局部变量。因此,每次调用函数时都需要一个新实例,这意味着它很可能存储在某种堆栈上。所以编译器可能不会将x放入只读内存中,因为我从未听说过在堆栈上的只读内存。

如果您将x变成全局变量,即在main()之外声明它,或者在其前面加上static,那么它就不会存储在任何堆栈上,而且更有可能存储在只读内存中。事实上,当我对您的代码进行以上两个更改时,不仅在编译它时会得到相同的警告,而且在运行程序时,它会崩溃并显示“总线错误”。


1
据我所知,它们的存储方式取决于编译器的实现细节。但是,通常它们存储在文本段中。
顺便说一下,“const”只是让编译器警告您如果想要更改“const”变量。

这是否意味着我编写的代码可能会因平台和/或编译器而崩溃? - ClementNerma
2
@ClementNerma:当然。 - Eric Postpischil
1
如果您在嵌入式系统上编写此代码,取决于工具,编译器的假设可能会认为该值在ROM中。您的程序可能会崩溃,或者由于它没有执行您认为已经告诉它要执行的操作而导致简单故障。 - lurker
澄清一下,“文本段”是可执行代码所在的地方。这种使用“文本”的方式根源于很久以前的计算机科学。 - nicomp

0
在 C 语言中,const 对象是一种编译器不允许赋值的变量。因此,可以使用地址运算符获取其地址,或者将其用作变量(甚至是左值-当然是 const 类型),但是您无法修改它(至少不能使用普通赋值程序)。对于静态的 const,编译器允许将它们存储在只读内存中,但这并不是标准强制执行的。这就是快速答案。如需更详细的信息,请参阅标准文档。
在您提供的情况下,当您使用 &x 表达式时,将获得一个 const int* 值,该值会自动转换为 int * 指针(可能会收到一个警告,但该警告被忽略了)。
$ make pru
cc -O -pipe  pru.c  -o pru
pru.c:7:5: warning: assigning to 'int *' from 'const int *' discards qualifiers [-Wincompatible-pointer-types-discards-qualifiers]
  y = &x;
    ^ ~~
1 warning generated.

由于遗留代码兼容性原因,这被视为警告而不是错误,因此您可以自由地采取相应措施(在这种情况下,您还没有这样做 :))。

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