如果getc()通过SIGINT退出,则可能在getc()之后的未定义行为会改变程序行为

在“未定义行为”的现代解释下,编译器有权假设不会发生导致未定义行为“不可避免”的事件链,并且可以消除仅在代码将要执行的情况下适用的代码。未定义的行为; 这可能会导致未定义行为的影响及时向后工作,并使原本可以观察到的行为无效。 另一方面,如果除非程序终止,否则未定义行为将是不可避免的,但程序在调用未定义行为之前可以并且确实终止,程序的行为将保持完全定义。

在做出此决定时,需要考虑编译器的原因是什么? 举几个例子:

在许多平台上,对“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; } } 

如果在argc等于3的情况下调用程序,则会定义行为,但是SIGINT阻止了getc()调用的返回? 当然,如果getc()可以返回哪个值会导致定义的行为,那么在编译器可以确定不会接收到这样的输入之前,不会发生未定义的行为。 如果没有值getc()可以返回,这将避免未定义的行为,但是,如果阻止getc()返回任何值,整个程序是否仍然定义? getc()的返回值与调用未定义行为的操作之间是否存在因果关系会影响事物(在上面的示例中,编译器无法知道任何特定forms的未定义行为会在不知道输入的字符的情况下发生,但任何可能的输入都会触发某种forms)。

同样,如果在平台上存在地址,如果读取,则指定导致程序立即终止,编译器指定易失性读取将触发硬件读取请求,并且该平台上的某些外部库指定它将返回指针对于这样一个地址,这些因素是否意味着在这个单独的例子中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); } 

将被定义(作为终止而没有打印任何东西)如果试图读取* p实际上会立即终止程序执行而不产生值? 在返回值之前,除法不能进行; 因此,如果没有返回任何值,则不会除以零。

通过什么方式,C编译器允许确定给定的操作是否可能导致程序执行以其不知道的方式终止,以及在什么情况下允许在此类操作之前重新安排未定义的行为?

这在[intro.execution]下的C ++中有详细描述:

5 – 执行格式良好的程序的符合实现应产生与具有相同程序和相同输入的抽象机的相应实例的可能执行之一相同的可观察行为。 但是,如果任何此类执行包含未定义的操作,则此国际标准不要求使用该输入执行该程序的实现(甚至不考虑第一个未定义操作之前的操作)。

通常认为C具有相同的特性,因此C编译器可以在存在未定义的行为的情况下类似地执行“时间旅行” 。

重要的是,请注意问题是是否存在表现出不确定行为的抽象机器实例 ; 通过首先终止程序执行,您可以安排防止机器上的未定义行为并不重要。

如果导致程序以抽象机无法摆脱的完全定义的方式终止自身,则可以防止未定义的行为(以及产生的时间旅行)。 例如,在第二个示例中,如果使用(exit(0), 0)替换对*p访问(exit(0), 0)则不会发生未定义的行为,因为没有可能执行抽象返回其调用者的抽象机器。 但无论您的平台有什么特性,抽象机器都不必在访问insta-kill地址时终止您的程序(事实上,抽象机器没有任何insta-kill地址)。