在C99中,f()+ g()是未定义的还是仅仅是未指定的?

我曾经认为在C99中,即使函数fg的副作用受到干扰,尽pipeexpression式f() + g()不包含序列点,但fg将包含一些,所以行为会未指定:f()将在g()之前调用,或者在f()之前调用g()。

我不再那么肯定。 如果编译器内联函数(编译器可能决定执行哪个函数(即使这些函数没有声明为inline函数),然后重新排列指令呢? 可能有人得到上述两个不同的结果吗? 换句话说,这是不确定的行为?

这不是因为我打算写这样的东西,这是在静态分析器中为这样的陈述select最好的标签。

expression式f() + g()包含最less4个序列点; 一个在调用f() (在它的参数全部为零之后); 一个在调用g() (在它的参数的所有零之后被评估); 一个调用f()返回; 和一个作为调用g()返回。 此外,与f()相关的两个序列点或者在与g()相关联的两个序列点之前或之后。 你不知道序列点将出现在哪一个顺序 – 无论f点发生在g点之前,反之亦然。

即使编译器内嵌了代码,它也必须遵守'as if'规则 – 代码的行为必须与函数未交错的行为相同。 这限制了损害的范围(假设编译器没有问题)。

所以f()g()的评估顺序是不确定的。 但其他一切都很干净。


在评论中, 超级猫问:

我希望源代码中的函数调用仍然作为顺序点,即使编译器自行决定内联它们。 这是否仍然是真正的函数声明“内联”,还是编译器获得额外的纬度?

我相信'如果'规则适用,并且编译器没有得到额外的纬度来省略序列点,因为它使用了明确的inline函数。 认为(懒得在标准中查找确切的字眼)的主要原因是编译器可以根据规则内联或不内联一个函数,但是程序的行为不应该改变(除了性能)。

另外,关于(a(),b()) + (c(),d())的sorting可以说什么呢? c()和/或d()a()b()之间执行,还是在c()d()之间执行a()b() d()

  • 显然,a在b之前执行,c在d之前执行。 我相信c和d是可以在a和b之间执行的,尽pipe编译器不太可能像这样生成代码。 同样,a和b可以在c和d之间执行。 虽然我在'c和d'中使用了'和',但这可能是一个'或' – 也就是说,这些操作序列中的任何一个都可以满足约束:

    • 绝对允许
    • A B C D
    • CDAB
    • 可能允许(保留≺b,c≺d顺序)
    • ACBD
    • ACDB
    • 农发行
    • CABD

    我相信这涵盖了所有可能的序列。 另请参阅Jonathan Leffler和AnArrayOfFunctions之间的交谈 – 要点是AnArrayOfFunctions并不认为“可能允许”的序列是完全不允许的。

如果可能的话,这将意味着内联函数和macros之间的显着差异。

内联函数和macros之间有显着的区别,但我不认为expression式中的顺序是其中之一。 也就是说,函数a,b,c或d中的任何一个都可以用一个macros来代替,并且可以出现macros体的相同sorting。 主要区别在于,对于内联函数,在函数调用时(如主要答案中概述的)以及逗号操作符都有保证的顺序点。 使用macros,会丢失与function相关的顺序点。 (所以,也许这是一个很大的区别…)然而,在很多方面,这个问题就像是关于有多less天使可以跳舞的问题 – 这在实践中并不重要。 如果有人在代码审查中给我提供了expression式(a(),b()) + (c(),d()) ,我会告诉他们重写代码以使其清楚:

 a(); c(); x = b() + d(); 

而且这假定b()d()没有关键的sorting要求。

参见附录C的序列点列表。 函数调用(被评估的所有参数和传递给函数的执行之间的点)是序列点。 正如你所说,没有指定哪个函数首先被调用,但是这两个函数中的每一个都将看到另一个函数的所有副作用,或者根本没有。

@dmckee

那么,这不适合评论,但这是事情:

首先,你写一个正确的静态分析器。 在这种情况下,“正确”意味着,如果对分析的代码有任何疑问,它就不会保持沉默,所以在这个阶段,您可以愉快地混淆未定义和未定义的行为。 在关键的代码中,它们既不好也不可接受,而且你们都正确地警告这两者。

但是,您只想警告一次可能的错误,并且您还知道,与其他可能不正确的分析仪相比,您的分析仪在“精度”和“回忆”警告两个同样的问题…是一个真正的或虚假的警报(你不知道哪个,你永远不知道哪个,否则它会太容易)。

所以你想发出一个单一的警告

 *p = x; y = *p; 

因为只要p是第一个语句的有效指针,就可以假定它是第二个语句的有效指针。 而不是推断这会降低精度指标上的分数。

所以,当你在上面的代码中第一次提醒你时,你教你的分析器假设p是一个有效的指针,所以你不要再次提醒它。 更一般地说,你学习忽略与你已经警告过的东西相对应的值(和执行path)。

然后,您意识到没有多less人正在编写关键代码,因此您根据最初的正确分析的结果为其余的人做了其他轻量级的分析。 说一个C程序切片机。

你告诉“他们”:你不必检查第一次分析发出的所有(可能是错误的)警报。 切片程序的行为与原始程序相同,只要没有任何一个被触发。 切片机产生与“定义的”执行path的切片标准等效的程序。

用户快乐地忽略警报并使用切片机。

然后你意识到,也许有一个误解。 例如, memmove大多数实现(你知道,处理重叠块的实现)实际上在用指向同一个块的指针(比较不指向同一个块的地址)调用时调用未指定的行为。 你的分析器忽略了两个执行path,因为两者都没有说明,但实际上两个执行path是相等的,一切都很好。

所以对警报的含义不应该有任何误解,如果有意不去理会,只会排除明显的不明确的行为。

这就是你如何最终区分未定义的行为和未定义的行为。 没有人可以责怪你忽视后者。 但是程序员会在没有考虑的情况下写前者,而当你说你的切片器排除了程序的“错误行为”时,他们就不会感到担心了。

这是一个绝对不适合评论的故事的结尾。 向那些阅读过的人道歉。