我周末很慢,所以出于兴趣,我开始在K.N.工作。金(King)的《 C程序设计:一种现代方法》今天出版,并在第二章开始着手进行练习。练习之一是这样:
编写一个程序,该程序声明多个int和float变量-而不对其进行初始化-然后打印其值。
我下面的解决方案包括输出。其实这并不是真正的问题,我只是很好奇它为什么会这样做,特别是因为我对底层语言的了解不多。
我在GitHub上快速寻找了一些其他预制的解决方案,希望可以对其进行评论或其他操作,但是它是如此简单的问题,实际上没有任何问题。肯尼迪金的自己的网站建议,输出的模式取决于(引用)“许多因素”,但不再泄漏。这反映在我的输出与King的输出不同。
#include <stdio.h>
int main()
{
int num1, num2, num3;
float flo1, flo2, flo3;
printf("Our integers are %d, %d, %d\n", num1, num2, num3);
printf("Our floats are %g, %g, %g\n", flo1, flo2, flo3);
return 0;
}
输出如下:
C:\C\Intro\exercises>a
Our integers are 0, 16, 0
Our floats are 2.8026e-045, 0, 1.73639e-038
同样,不是什么大问题,只是很好奇它在做什么,可能是在硬件级别。
答案 0 :(得分:2)
严格来说,您的代码具有undefined behaviour,这意味着它可以做很多事。
实际上,您的变量存在于堆栈中,但没有初始化。这可能意味着它们在编译器将变量放置到的位置获取堆栈恰好包含的任何值。这些值很可能是在流程生命周期中较早的例程(即在其启动期间)中遗留下来的。
答案 1 :(得分:1)
首先,让我们考虑一个非常简单的编译器如何处理此代码。当在函数内看到int num1, num2, num3;
时,可能会在堆栈上为这些留出空间。堆栈通常是编译器如何实现具有自动存储持续时间的对象(特别是在函数内部定义的变量,这些变量不是static
或线程本地的)。每当调用新函数时,编译器都会编写代码以在堆栈上为其局部变量和其他信息腾出空间。同样,也为float flo1, flo2, flo3;
分配了空间。
然后,当编译器看到printf("Our integers are %d, %d, %d\n", num1, num2, num3);
时,它将生成代码以加载num1
,num2
和num3
的值并将其传递给{{1} }。这些值是从为这些对象分配的内存中加载的。那记忆里有什么?嗯,此源代码没有为这些对象分配任何值,因此该内存中的数据与printf
例程启动时存在的数据相同。
那是什么记忆?通常,当操作系统为进程提供通用内存时,它会清除内存(将其中的所有字节设置为零),以便它不会透露以前使用该内存的任何程序的任何数据。那么为什么main
语句不打印零?
printf
实际上不是程序的开始。在执行main
之前,必须先设置C环境。运行C程序需要初始化您可能调用的库例程使用的所有数据(例如main
)。同样,当printf
例程返回时,它必须要返回一些东西,该东西将采用返回值并将其作为进程退出状态传递给系统。该代码还负责关闭打开的文件并执行其他一些清理工作。通常,当您链接C程序时,一个额外的“启动”例程会链接到您的可执行文件中。操作系统启动您的程序时,它将首先调用此“启动”例程,然后启动例程会设置C环境,然后调用main
。
因此,当您打印main
,num1
,num2
,num3
,flo1
和flo2
时,为其分配的内存已被“开始”例程使用,并且包含“开始”例程碰巧留下的所有数据。
这是为什么您看到此源代码打印的各种值的一种解释。
另一方面,让我们考虑一个更复杂的编译器。一个更复杂的编译器分析代码,可以看到未初始化就使用了变量。它将向用户发出警告,并且它也知道这违反了C中的各种规则。特别是,当您使用既没有初始化也没有自动存储期限的对象(对于技术/深奥的原因)的地址。
为协助优化,复杂的编译器提供了处理未定义行为的特殊方法。例如,如果编译器看到如下代码:
flo3
编译器可以通过“选择”如何定义未定义的行为来优化此设置。它可以定义更改程序的行为,就像编写程序一样:
if (some test)
FunctionA();
else
{
Some undefined behavior here…
FunctionB();
}
因为这是未定义行为的有效实例。然后可以进行优化以简化为:
if (some test)
FunctionA();
else
{
FunctionA();
}
有时在代码中会出现这种情况,因为程序员正在为可移植到各种环境而进行编写,并且碰巧FunctionA();
在特定的编译器中确实不能为假,并且这种优化产生了正确且简单的代码。类似的情况也可能出现,其中编译器已经以其他方式转换代码,而上面的代码并不是因为它是在源代码中按原样编写的,而是由编译器在其内部转换期间生成的。例如,对于第一个迭代,一般的中间迭代和最后一个迭代,编译器可能会将循环拆分为单独的代码,并且some test
在最后一个迭代中可能始终为true,即使在以下情况中并非始终如此:程序员编写它的上下文。
这意味着,当您使用未定义的行为(不仅根据C标准未定义,而且未由C实现定义)时,它可能会以您不希望的方式转换。
我使用LLVM和Clang版本测试了此代码,并且编译器通过不为变量分配任何内存并且不将其从内存加载以传递给some test
来对其进行了优化。相反,它只是调用printf
,而没有为这些参数做任何准备。在我使用的平台中,这些参数在寄存器中传递。因此,结果是printf
打印出那些寄存器中的任何值。与内存一样,这将是早期软件恰好保留在该内存中的所有数据。