int c=0; cout<<c++<<c; 的结果是什么?

5
我认为应该是01,但有人说是“未定义”,这是什么原因?
5个回答

8

c++既是一个增量,也是一个赋值。编译器决定赋值何时发生(在该行其他代码之前或之后)。它可以在cout <<之后或之前发生。

这可以在C99标准中找到http://www.open-std.org/JTC1/SC22/wg14/www/docs/n1124.pdf。在pdf中的第28页或5.1.2.3节可以找到。

p的实际增加可以在前一个序列点和下一个序列点之间的任何时间发生。

由于有人要求C++标准(因为这是一个C++问题),所以可以在第1.9.15节第10页(或pdf格式的第24页)中找到。

单个运算符的操作数和单个表达式的子表达式的评估是无序的。

它还包括以下代码块:

i = v[i++]; // the behavior is undefined
i = 7, i++, i++; // i becomes 9
i = i++ + 1; // the behavior is undefined

我觉得C99标准的解释更清晰,但两种语言都是正确的。


6
是我自己的问题,还是引用C99标准不是对C++问题的很好回答? - anon
1
序列点不一定是分号。函数调用也是一个序列点(请参见标准的附录C)。问题在于参数“c++”和“c”的评估之间没有序列点。 - tiftik
@Neil 我添加了C++标准,即ISO/IEC 14882 @tiftik 在示例中使用的下一个序列点恰好是分号,这就是引文所指的,由于我删除了上下文,可能会看起来有歧义,但语法上是准确的。我将删除圆括号声明。 - tzenes

4
如果您在没有中间序列点的情况下修改值并读取它(或尝试再次修改),则行为未定义。 在C++中,序列点的概念有点技术性(您可以在这里了解一些信息),但重要的是流插入操作符(<<)不是序列点。
这种行为未定义的原因是,在没有序列点的情况下,编译器可以以任何方式重新排序操作。也就是说,它可以检索的值(并保存第二次插入时的值),然后执行c ++来获取第一次插入的值。因此,您无法确定增量是在还是在第二个插入的的值之前发生的。

1
请记住,在序列点之间两次更改变量的值,或在序列点之间以任何与更改值无直接关联的方式更改值并访问该值,都是未定义的。根据标准,任何事情都可能发生。 - David Thornley

1

该行为已定义但未指定。表达式中评估“c”两次使用的相对顺序未指定。但是,如果将其转换为函数表示法,则如下所示:

cout.operator<<(c++).operator<<(c);

在评估函数参数和执行函数体之间存在序列点,并且函数体不是交错的,因此结果只是未指定的行为,而不是未定义的行为。

如果您没有重载运算符:

int c=0;
int a = c++ << c;

那么行为将是未定义的,因为在没有中间序列点的情况下修改和使用c的值。

编辑:序列litb提出的是错误的。标准规定(§1.9/17):“调用函数时(无论函数是否内联),在评估所有函数参数(如果有)之后,执行函数体中的任何表达式或语句之前都有一个序列点。”

这明确地写明了参数被评估,然后(紧接着)执行函数体。他建议的顺序,其中对一个函数的参数进行评估,然后对另一个函数的参数进行评估,然后执行两个函数体,似乎并不是预期的,但也不被禁止。然而,这并不会改变要求:“...在评估所有函数参数(如果有)之后,存在一个序列点...”

关于函数体执行的后续语言并不会在评估所有函数参数后删除对序列点的要求。所有其他评估,无论是函数体还是其他函数参数,都遵循该序列点。我可以像任何人一样苛刻和扭曲地误读明显意图(但未完全说明)-但我无法想象“在评估所有函数参数后存在一个序列点”如何被解读为意味着“在评估所有函数参数后不存在序列点。”

尼尔的观点当然是正确的:我上面使用的语法是针对成员函数的。对于非成员重载,语法更像是:

operator<<(operator<<(cout,c++), c);

这并不消除对序列点的要求。

就其未指定性而言,它真的很简单:在评估所有函数参数之后,存在一个序列点,因此一个函数调用的所有参数必须完全评估(包括所有副作用),然后可以评估另一个函数调用的参数(考虑来自其他函数调用的任何副作用)-- 但是没有要求哪个函数调用的参数必须先评估还是后评估,所以它可以是 c,然后是 c++,或者它可以是 c++,然后是 c -- 但必须是其中之一,不能是交错的。


2
代码的行为未定义。函数进入序列点必须在进入函数之前和评估其参数之后发生。一个编译器如果在没有中间序列点的情况下同时评估c++c是完全符合规范的。 - Johannes Schaub - litb
1
@Jerry 在 operator<<(operator<<(cout,c++), c)(或者在你的原始代码中)中,究竟是哪个序列点防止了这种情况成为未定义行为? - anon
1
好的,所以外部op<<有两个参数。按从右到左的顺序(作为一种选项),我首先评估c,然后继续评估op<<(cout, c++)。好的,当我转移到op<<(cout, c++)的参数时,我还没有到达任何一个函数序列点。从右到左,我评估c++,但我仍然没有到达函数调用隐含的任何序列点。这肯定是未定义行为吧? - CB Bailey
4
我不确定是否需要参与这个讨论,但我猜我会。在 f(g(a,b),c) 中,标准规定必须在计算 g 之前计算 ab ,并且必须在计算 f 之前计算 gc。但这只是部分顺序。计算顺序可以是:cbagf,或者 bcagf,或者 bag,如此类推。现在,如果您将 f == g == operator<<a == coutb == c++c == c(最后一个很容易!),那么就有不同的组合方式允许在执行 c 之前或之后执行 c++ - David Rodríguez - dribeas
1
@litb:我开始理解你的立场,但对我来说,它似乎仍然需要违反另一个条款:§1.9/8,该条款要求:“一旦函数的执行开始,直到被调用的函数执行完成之前,不会评估来自调用函数的任何表达式。”为了得到你所说的顺序,我们必须将对函数参数的求值视为不属于“函数的执行”。我想这可能勉强可行,但似乎有些牵强。 - Jerry Coffin
显示剩余27条评论

1

它未定义的原因是编译器可以按任意顺序计算函数参数。考虑一下如果你正在调用一个函数(因为你正在这样做,但在函数语法中更容易设想):


  cout.output(c++).output(c);

编译器可能会以相反顺序、正向顺序或任何顺序来处理参数。它可能会在计算第二个输出的参数之前先调用第一个输出,也可能两者都执行后再调用。

0

在我看来, f(c++); 等同于: f(c); c += 1;

而 f(c++,c++); 等同于: f(c,c); c += 1; c += 1;

但也有可能 f(c++,c++); 变成了 f(c,c+1); c+= 2;

使用gcc和clang进行的实验,首先是C语言

#include <stdio.h>

void f(int a, int b) {
    printf("%d %d\n",a,b);
}

int main(int argc, char **argv) {
    int c = 0;
    f(c++,c++);

    return 0;
}

而且是在C++中

#include <iostream>

int main(int argc, char **argv) {
    int c = 0;
    std::cout << c++ << " " << c++ << std::endl;
    return 0;
}

有趣的是,使用gcc和g++编译的结果为1 0,而使用clang编译的结果为0 1。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接