sup*_*cat 5 c undefined-behavior language-lawyer
在"未定义行为"的现代解释下,编译器有权假设不会发生导致未定义行为"不可避免"的事件链,并且可以消除仅在代码将要执行的情况下适用的代码.未定义的行为; 这可能会导致未定义行为的影响及时向后工作,并使原本可以观察到的行为无效.另一方面,如果除非程序终止,但是在程序可以并且在调用未定义行为之前终止的情况下,未定义行为将是不可避免的,程序的行为将保持完全定义.
在做出此决定时,需要考虑编译器的原因是什么?举几个例子:
在许多平台上,对"getc"之类的函数的调用通常会返回(至少最终),但在某些情况下,编译器控制之外的情况不会.如果有一个程序,如:
int main(int argc, char *argv[])
{
if (argc != 3)
{
printf("Foo\n");
return 0;
}
else
{
int ch;
printf("You'd better type control-C!\n");
int ch = getc();
if (ch < 65)
return (ch-33) / (argc-3);
else
return INT_MAX + ch;
}
}
Run Code Online (Sandbox Code Playgroud)
如果用argc等于3 调用程序,将会定义行为,但是SIGINT阻止getc()调用返回?当然,如果有任何值,即getc()可以返回,这将导致定义的行为,可能不会出现未定义行为,直到编译器可以肯定的是,这样的输入也不会接收.如果没有值getc()可以返回,这将避免未定义的行为,但是,如果getc()阻止任何值返回任何值,整个程序是否仍然定义?返回值getc()与调用未定义行为的操作之间是否存在因果关系是否会影响事物(在上面的示例中,编译器无法知道任何特定形式的未定义行为会在不知道输入什么字符的情况下发生,但任何可能的输入会触发某种形式).
同样,如果在平台上存在地址,如果读取,则指定导致程序立即终止,编译器指定易失性读取将触发硬件读取请求,并且该平台上的某些外部库指定它将返回指针对于这样一个地址,这些因素是否意味着bar在这个单独的例子中的行为:
int foo(int x)
{
char volatile * p = get_instant_quit_address();
if (x)
{ printf("Hey"); fflush(stdout); }
return *p / x; // Will cause UB if *p yields a value and x is zero
}
int bar(void)
{
return foo(0);
}
Run Code Online (Sandbox Code Playgroud)
将被定义(作为终止而没有打印任何东西)如果试图读取*p实际上会立即终止程序执行而不产生值?在返回值之前,除法不能进行; 因此,如果没有返回任何值,则不会除以零.
通过什么方式,C编译器允许确定给定的操作是否可能导致程序执行以其不知道的方式终止,以及在什么情况下允许在此类操作之前重新安排未定义的行为?
这在 C++ 的[intro.execution]下有很好的描述:
5 - 执行格式良好的程序的一致实现应产生与具有相同程序和相同输入的抽象机的相应实例的可能执行之一相同的可观察行为。然而,如果任何此类执行包含未定义的操作,则本国际标准对使用该输入执行该程序的实现没有任何要求(甚至不考虑第一个未定义操作之前的操作)。
人们普遍认为 C 具有相同的特性,因此 C 编译器可以在存在未定义行为的情况下类似地执行“时间旅行” 。
重要的是,请注意问题是是否存在表现出未定义行为的抽象机实例;您可以通过首先终止程序执行来防止计算机上的未定义行为,但这并不重要。
如果您使程序以抽象机无法摆脱的完全定义的方式自行终止,则可以防止未定义的行为(以及由此产生的时间旅行)。例如,在第二个示例中,如果将访问替换为*p,(exit(0), 0)则不会发生未定义的行为,因为不可能执行exit返回到其调用者的抽象机。但无论您的平台的特性如何,抽象机都不必在访问 insta-kill 地址时终止您的程序(实际上,抽象机没有任何 insta-kill 地址)。