变量初始化

问题描述 投票:1回答:3

在下面的代码中,变量x,y,z是在运行时分配给它们的内存还是在编译时初始化的?

int main(void)
{
    int x = 5;
    static int y=5;
    int z[] ={1,2,3,4};
    return 0;
}
c
3个回答
4
投票

正如注释所述,未定义变量是否在运行时初始化,即使您在注释中提供的信息是在函数范围内。只有行为就像虚拟机一次或多次进行初始化。

但是可以确保它们在编译时没有初始化。 无论是否标准,任何真正的机器都无法做到这一点。 (我很乐意编辑我的答案并删除此声明,如果有人提出一个如何实现这一点的例子。软件模拟器和任何不基于编译器的东西,如C语言解释器,都不算数。) 在编译时,只能确定将初始化这些变量的值。对于您的示例中确实发生值确定的情况,因为除了程序文本中的数字之外的其他任何位置都不能使用该值。

初始化(即由标识符表示的存储器片段中的那些值的“到达”)可以在不同的时间发生,但从不在编译时发生。因为在编译时还没有选择这样的存储器片。在交叉编译的情况下,它甚至可能还不存在(运行它的芯片还没有生产)。

以下初始化时间是可能的 (不是说所有人都“通常已经完成”......):

  • 运行 对于第二次执行并在第一次运行中更改了非静态变量的函数的局部范围内的非静态变量所必需的
  • 加载时间(在动态执行上下文的情况下,例如Windows PC等)或 内存设置时间(在静态执行上下文的情况下,例如嵌入式就地执行环境) 对于全局变量,静态局部变量,非静态const局部变量(可能但不太可能使用概念的堆栈),非静态非const局部变量碰巧永远不会改变(堆栈也不太可能)
  • 决不 对于任何完全被优化而不被使用的东西
  • 解释时间 从Antti Haapala中获取有效的输入,可以解释C. (但这仍然不是编译时间,至少如果你允许我在那里有所作为并考虑编译器这个问题的适用范围;-)

对于您的示例,在main()的范围内,这些选项中的任何一个都可能适用。 所以,它还不知道。

笔记:

我在这里加载时间和内存设置时间之间存在差异,因为内存设置时间通常与上电或复位情况有关,这有些例外,而加载程序执行被认为是正常情况。 然而,从正在执行的C程序的角度来看,两者都可以被视为同样的事情,因为它是“在main()之前”。

我还在运行时(由您编写的C代码定义的程序控制)和加载/设置时间之间做出了区别,它们是C运行时环境准备代码的一部分。可以通过OS或(例如,对于嵌入式环境)通过从复位向量链接的机器代码(不基于C)来准备C运行时。


2
投票

这个问题没有明确的答案。初始化是在C标准用于描述程序必须如何表现的抽象机器中发生的事情,并且实现该机器的唯一要求是必须生成标准描述的可观察行为。对象的初始值可以在编译时生成(即使计算初始值需要复杂的计算)或在运行时(即使初始值是简单常量)或根本不生成(只要可观察的行为)该计划以某种方式产生)。此外,初始值可能出现在编码到指令中的立即值中,在目标文件描述的存储器中,在程序启动期间写入存储器时,在执行函数时写入存储器,寄存器或其他地方。对象可能在不同的时间保存在不同的位置,并且它可能与其他对象共享存储或与其自身的其他实例共享(如同函数递归执行时),甚至同时共享。

一个更有用的问题是:各种对象初始化的成本和效果是什么?同样,这不是由C标准定义的,但它具有现实世界的后果。许多对象或大型对象的初始化会对性能产生明显的拖累,并可能影响产品质量。

本答案的其余部分假定一个人使用的是高质量的编译器并且正在请求优化(与使用Clang或GCC的-O3-Os开关一样)。

如果需要的话,块范围内的int x = 5声明可能会导致5被硬编码到指令中。可能不需要初始值,因为编译器优化了x的进一步工作,以便将初始值合并到使用x的第一个表达式中。 (在问题中显示的代码中,根本不需要x,因为它从未使用过,所以编译器会将它从程序中完全删除。)

诸如static int y = 5之类的声明可能导致将5写入目标文件中的数据部分(并且,当链接目标文件以生成可执行文件时,数据部分将被复制或合并到可执行文件中)。同样,这可以进行优化。

如果在文件范围声明了int x = 5,那么它是外部定义,并且编译器必须使其可用于其他翻译单元(其他源文件的编译),因此必须将其存储在内存中(除非编译器和其他开发人员工具是能够跨翻译单位进行优化)。在这种情况下,编译器可能会将5写入目标文件的数据部分。

对于小对象和简单的初始值(例如这些),几乎没有任何理由关注它们何时或如何初始化,因为成本非常低廉并且因为没有可用的改进。

在块范围内使用int z[] = { 1, 2, 3, 4 };时,数组足够小,编译器可能会像上面的单个int x一样处理它,具体取决于具体情况。在数组较大的情况下,可能会处理以下情况:

  • 值1,2,3和4被写入目标文件中的常量数据部分。
  • 当执行有效地达到声明时,编译器为z分配存储,可能在堆栈上,并将数据从常量数据部分复制到z。 (可能很难说执行何时到达声明,因为抽象机器模型意味着C实现可以自由地重新安排许多事情。声明之前和之后的语句执行可以重新排列并混合在一起。)

但是,假设您有这样的代码:

int z[] = { 1, 2, 3, 4 };

for (int i = 0; i < 4; ++i)
    z[i] = f(z[i]);

在这种情况下,编译器可以看到z的成员在其初始值传递给f之前没有更改。实现此操作时,编译器可能会跳过将1,2,3和4复制到为z分配的存储中的上述步骤。相反,它可以通过读取常量数据,将每个值传递给f,并将结果写入为z分配的存储来实现循环。在这种情况下,z曾经初始化?该程序将初始值存储在常量数据部分中,但它从未将它们写入特别为z分配的存储中。它稍后才会写出派生值。

然而,真正的问题是,这个成本是多少?我们必须将数据存储在常量数据部分,我们不得不重复调用f。假设z有数千个元素而不是4个元素。我们能减少这些费用吗?

如果f是纯函数(仅取决于它的参数,而不是任何全局状态),那么存储1,2,3和4是没有意义的。我们应该存储f(1)f(2)f(3)f(4)并跳过在运行时执行f。如果您有一个好的编译器并且在编译器编译此代码时可以看到f的源代码,那么编译器可能会这样做 - 它可能存储调用f的结果而不仅仅是显式初始值。 (即使f不可见,编译器也可能针对已知函数执行此操作,例如coslog。)

如果您正在开发一个重要的项目,您应该了解您的编译器以及它对这样的事情的处理方式。没有统一的答案,特别是随着技术的不断发展。

如果这样的初始化对您的应用程序很重要,另一种方法是编写一个计算初始值(f(1)f(2)等)的程序,并将它们写入新的源文件中。在编译时,您将执行该程序,然后编译生成的源代码。

在C中,初始化成本通常不是问题,除非您有大量数据可以使用如上所述的辅助程序进行预处理。它在C ++中更是一个问题,其中类的构造函数可以非常广泛。


1
投票

编译器的工作就是按照它所说的去做。你已经发布了该程序

int main(void)
{
    int x = 5;
    static int y=5;
    int z[] ={1,2,3,4};
    return 0;
}

C标准允许编译器生成编译代码,该代码完全复制程序的可观察行为。

为此,任何具有优化性能的C编译器都会产生最大值,这将产生相当于的编译代码

int main()
{
}

如果您想知道特定编译器如何处理源代码,请检查生成的输出。

© www.soinside.com 2019 - 2024. All rights reserved.