8wDlpd.png
8wDFp9.png
8wDEOx.png
8wDMfH.png
8wDKte.png

为什么这些构造使用前置和后置增量未定义的行为?

novski 2月前

202 0

#包括int main(void){ int i = 0; i = i++ + ++i; printf(\'%d\n\', i); // 3 i = 1; i = (i++); printf(\'%d\n\', i); // 2 应该是 1,不是吗? volatile int u = 0; ...

#include <stdio.h>

int main(void)
{
   int i = 0;
   i = i++ + ++i;
   printf("%d\n", i); // 3

   i = 1;
   i = (i++);
   printf("%d\n", i); // 2 Should be 1, no ?

   volatile int u = 0;
   u = u++ + ++u;
   printf("%d\n", u); // 1

   u = 1;
   u = (u++);
   printf("%d\n", u); // 2 Should also be one, no ?

   register int v = 0;
   v = v++ + ++v;
   printf("%d\n", v); // 3 (Should be the same as u ?)

   int w = 0;
   printf("%d %d\n", ++w, w); // shouldn't this print 1 1

   int x[2] = { 5, 8 }, y = 0;
   x[y] = y ++;
   printf("%d %d\n", x[0], x[1]); // shouldn't this print 0 8? or 5 0?
}
帖子版权声明 1、本帖标题:为什么这些构造使用前置和后置增量未定义的行为?
    本站网址:http://xjnalaquan.com/
2、本网站的资源部分来源于网络,如有侵权,请联系站长进行删除处理。
3、会员发帖仅代表会员个人观点,并不代表本站赞同其观点和对其真实性负责。
4、本站一律禁止以任何方式发布或转载任何违法的相关信息,访客发现请向站长举报
5、站长邮箱:yeweds@126.com 除非注明,本帖由novski在本站《function》版块原创发布, 转载请注明出处!
最新回复 (0)
  • 理解这一点的关键是 表达式的 i++ i ,它的效果是加 1 i (即将值存储 i+1 在变量中 i ),但这并不意味着在确定值时才会进行存储。

    在类似表达式中, i++ + ++i 加法左侧的值是 i ,而右侧的值是“i+1”。

    但是,当任一侧的效果发生时,它是未定义的,因此整个表达式( i++ + ++i )的值是未定义的。第一个引用将 i 使用当前语句之前的值 i 还是右侧效果之后的值(未确认执行顺序),反之亦然,第二个引用将 i 使用第一个效果之后的值。C 标准明确指出是未定义的,定义它会将优化器限制在特定的执行顺序,这是无益的。

    对于编译器来说,注意到净效果是增加 i 2 并评估(等于 i+i+1 并随后存储 i+2 在中 i )或者不这样做是完全合理的(并且可能是有效的)。

    您不应该做的是尝试弄清楚编译器的功能并按照它的方式运行。

    对编译器优化设置的更改,显然(对您而言!)与周围代码或编译器新版本的无关更改都可能改变行为。

    您将面临最耗时的错误之一,这些错误突然出现在看似未改变的代码中。

    编写您需要的代码(例如 2*i+1; i+=2; ),并意识到所有现代商业编译器(在优化时)都会将其转换为适合您平台的最有效代码,并且对所有人类读者都有明显且有保证的含义。

    我甚至建议永远不要 ++ 在除 standalone 之外的任何其他表达式中使用,而且这样做只是为了便于阅读。不要以为它比 standalone 更有效, i=i+1 因为所有现代商业编译器都会为两者生成相同的代码。他们并不愚蠢。

  • 但是,如果你坚持使用一个编译器,你会发现这种行为是持久的。嗯,不一定。例如,如果你更改优化标志,编译器可能很容易最终发出代码,使未定义的行为表现不同。如果你对附近的代码进行看似不相关的更改,也是如此。

  • 我编辑了这个问题,在函数参数求值中添加了 UB,因为这个问题经常被用作重复问题。(最后一个例子)

  • J W 2月前 0 只看Ta
    引用 5

    原因是程序运行了未定义的行为。问题在于求值顺序,因为没有按照 C++98 标准要求的序列点(按照 C++11 术语,没有操作在另一个操作之前或之后排序)。

    但是如果你坚持使用一个编译器,你会发现行为是持久的,只要你不添加函数调用或指针,这会使行为变得更加混乱。

    使用 Nuwen MinGW 15 GCC 7.1 您将获得:

     #include<stdio.h>
     int main(int argc, char ** argv)
     {
        int i = 0;
        i = i++ + ++i;
        printf("%d\n", i); // 2
    
        i = 1;
        i = (i++);
        printf("%d\n", i); //1
    
        volatile int u = 0;
        u = u++ + ++u;
        printf("%d\n", u); // 2
    
        u = 1;
        u = (u++);
        printf("%d\n", u); //1
    
        register int v = 0;
        v = v++ + ++v;
        printf("%d\n", v); //2
     }
    

    GCC 如何工作?它按从左到右的顺序对右侧 (RHS) 的子表达式进行求值,然后将值分配给左侧 (LHS)。这正是 Java 和 C# 的行为方式,并定义它们的标准。(是的,Java 和 C# 中的等效软件已经定义了行为)。它按从左到右的顺序在 RHS 语句中逐个评估每个子表达式;对于每个子表达式:首先评估 ++c(前增量),然后使用值 c 进行操作,然后使用后增量 c++)。

    根据 GCC C++:运算符

    在 GCC C++ 中,运算符的优先级控制着各个运算符的求值顺序

    GCC 理解的定义行为 C++ 中的等效代码:

    #include<stdio.h>
    int main(int argc, char ** argv)
    {
        int i = 0;
        //i = i++ + ++i;
        int r;
        r=i;
        i++;
        ++i;
        r+=i;
        i=r;
        printf("%d\n", i); // 2
    
        i = 1;
        //i = (i++);
        r=i;
        i++;
        i=r;
        printf("%d\n", i); // 1
    
        volatile int u = 0;
        //u = u++ + ++u;
        r=u;
        u++;
        ++u;
        r+=u;
        u=r;
        printf("%d\n", u); // 2
    
        u = 1;
        //u = (u++);
        r=u;
        u++;
        u=r;
        printf("%d\n", u); // 1
    
        register int v = 0;
        //v = v++ + ++v;
        r=v;
        v++;
        ++v;
        r+=v;
        v=r;
        printf("%d\n", v); //2
    }
    

    然后我们进入 Visual Studio。Visual Studio 2015,你会得到:

    #include<stdio.h>
    int main(int argc, char ** argv)
    {
        int i = 0;
        i = i++ + ++i;
        printf("%d\n", i); // 3
    
        i = 1;
        i = (i++);
        printf("%d\n", i); // 2 
    
        volatile int u = 0;
        u = u++ + ++u;
        printf("%d\n", u); // 3
    
        u = 1;
        u = (u++);
        printf("%d\n", u); // 2 
    
        register int v = 0;
        v = v++ + ++v;
        printf("%d\n", v); // 3 
    }
    

    Visual Studio 是如何工作的?它采用另一种方法,它在第一遍中评估所有前增量表达式,然后在第二遍的操作中使用变量值,在第三遍中从 RHS 分配给 LHS,然后在最后一遍中评估所有后增量表达式。

    因此,Visual C++ 理解的定义行为 C++ 中的等效内容是:

    #include<stdio.h>
    int main(int argc, char ** argv)
    {
        int r;
        int i = 0;
        //i = i++ + ++i;
        ++i;
        r = i + i;
        i = r;
        i++;
        printf("%d\n", i); // 3
    
        i = 1;
        //i = (i++);
        r = i;
        i = r;
        i++;
        printf("%d\n", i); // 2 
    
        volatile int u = 0;
        //u = u++ + ++u;
        ++u;
        r = u + u;
        u = r;
        u++;
        printf("%d\n", u); // 3
    
        u = 1;
        //u = (u++);
        r = u;
        u = r;
        u++;
        printf("%d\n", u); // 2 
    
        register int v = 0;
        //v = v++ + ++v;
        ++v;
        r = v + v;
        v = r;
        v++;
        printf("%d\n", v); // 3 
    }
    

    正如 Visual Studio 文档在 “优先级和评估顺序” :

    当多个运算符一起出现时,它们具有相同的优先级,并根据其结合性进行评估。表中的运算符在以后缀运算符开头的部分中进行了描述。

  • @SoupEndless 之所以有很多答案,是因为这是一个针对许多类似(但不是直接)重复问题的典型问题。如果没有为同一问题的小变体创建不同的典型帖子的开销,其他人通常会在很晚的时候(通常是几年后!)发布答案,使问题成为重复锤的理想候选者。这就是这里发生的事情。重复相同的答案是没有意义的(尤其是几年后,它已经得到了回答!)。所以后面的回答者并没有真正“错过重点”。这就是 SO 的工作方式。

  • @hackks 除了它是你答案的副本之外,这个答案还可以,但我想问的是,其他答案在这里做什么,为什么它们有如此多的代表,却忽略了问题的要点,那就是在例子中解释 UB 的细节。

  • @hackks 我没有阅读其他答案。我想用自己的语言解释我从 ISO 9899 官方网站 open-std.org/jtc1/sc22/wg14/www/docs/n1188.pdf 上提到的文档中学到的东西

  • 这个答案如何为现有答案添加新内容?此外,i=i++ 的解释与这个答案非常相似。

  • ISO W14 站点 n1188 文档 n1188 .

    我解释了这些想法。

    在这种情况下适用的标准 ISO 9899 的主要规则是 6.5p2。

    在前一个和下一个序列点之间,对象的存储值最多应通过表达式求值修改一次。此外,应只读取先前的值以确定要存储的值。

    表达式中的序列点如下: i=i++ 之前 i= 和之后 i++ .

    在我上面引用的论文中,它解释了你可以将程序理解为由小框组成,每个框包含 2 个连续序列点之间的指令。序列点在标准的附件 C 中定义,如果有 i=i++ 2 个序列点界定一个完整表达式。这样的表达式在语法上等同于 expression-statement 语法的 Backus-Naur 形式中的条目(语法在标准的附件 A 中提供)。

    因此盒子里面的指令顺序没有明确的顺序。

    i=i++
    

    可以解释为

    tmp = i
    i=i+1
    i = tmp
    

    tmp = i
    i = tmp
    i=i+1
    

    因为这两种解释代码的形式 i=i++ 都是有效的,而且都会产生不同的答案,所以行为是未定义的。

    因此,可以通过组成程序的每个框的开头和结尾看到一个序列点 [框是 C 中的原子单元],并且框内的指令顺序并非在所有情况下都定义。更改该顺序有时会改变结果。

    编辑:

    解释此类歧义的其他好资料来源是来自 c-faq 网站(也出版 成书 )的条目,即 这里 这里 这里 .

  • https://.com/questions/29505280/incrementing-array-index-in-c ,有人问到类似这样的陈述:

    int k[] = {0,1,2,3,4,5,6,7,8,9,10};
    int i = 0;
    int num;
    num = k[++i+k[++i]] + k[++i];
    printf("%d", num);
    

    打印出 7 ... OP 期望它打印 6。

    增量 ++i 不能保证在其余计算之前全部完成。事实上,不同的编译器会在这里得到不同的结果。在你提供的例子中,前 2 个 ++i 读取 k[] 的值 ++i 最后 k[] .

    num = k[i+1]+k[i+2] + k[i+3];
    i += 3
    

    现代编译器可以很好地优化这一点。事实上,可能比你最初编写的代码更好(假设它按照你希望的方式工作)。

  • 当然,它不适用于一个表达式内的不同变量。如果适用,那将是彻底的设计失败!在第二个示例中,您需要做的就是在语句结束和下一个语句开始之间增加这两个变量,这是可以保证的,正是因为序列点的概念是这一切的核心。

  • C 标准规定,一个变量在两个序列点之间最多只能被赋值一次。例如,分号就是一个序列点。
    因此,每个形式如下的语句:

    i = i++;
    i = i++ + ++i;
    

    等等都违反了该规则。标准还规定,行为是未定义的,而不是未指定的。有些编译器确实会检测到这些并产生一些结果,但这不符合标准。

    然而,两个不同的变量可以在两个序列点之间增加。

    while(*src++ = *dst++);
    

    以上是复制/分析字符串时的常见编码实践。

  • 您的问题可能不是“为什么这些构造在 C 中是未定义的行为?”。您的问题可能是“为什么此代码(使用 ++ )没有给我期望的值?”,并且有人将您的问题标记为重复,并将您发送到此处。

    这个 答案试图回答这个问题:为什么你的代码没有给你期望的答案,以及你如何学会识别(和避免)那些不能按预期工作的表达式。

    现在 ++ -- 的基本定义 ++x 与后缀形式的区别 x++ 。但这些运算符很难思考,所以为了确保你理解,也许你编写了一个小小的测试程序,涉及类似

    int x = 5;
    printf("%d %d %d\n", x, ++x, x++);
    

    但是,令你惊讶的是,这个程序并 没有 帮助你理解——它打印了一些奇怪的、无法解释的输出,暗示它可能 ++ 做了一些完全不同的事情,根本不是你想象的那样。

    或者,也许你正在看一个难以理解的表达,例如

    int x = 5;
    x = x++ + ++x;
    printf("%d\n", x);
    

    也许有人把这段代码当作谜题给你。这段代码同样毫无意义,特别是当你运行它时——如果你在两个不同的编译器下编译和运行它,你很可能会得到两个不同的答案!这是怎么回事?哪个答案是正确的?(答案是两者都正确,或者都不正确。)

    正如您现在所听到的,这些表达式是 未定义的 ,这意味着 C 语言无法保证它们会做什么。这是一个奇怪而令人不安的结果,因为您可能认为您编写的任何程序,只要它编译并运行,都会生成唯一、定义明确的输出。但在未定义行为的情况下,情况并非如此。

    什么使得表达式未定义?涉及 ++ 和的 -- 是否始终未定义?当然不是:这些是有用的运算符,如果正确使用它们,它们会得到完美定义。

    对于我们正在讨论的表达,当同时发生太多事情时,它们就会变得不确定,因为我们无法知道事情发生的顺序,但顺序对我们将得到的结果很重要。

    让我们回顾一下我在这个回答中使用的两个例子。当我写

    printf("%d %d %d\n", x, ++x, x++);
    

    问题是,在实际调用 之前 printf ,编译器是否会先计算 的值 x ,或者 x++ ,或者 ++x ?但事实证明 我们不知道 。C 中没有规则说函数的参数是从左到右、从右到左还是以其他顺序进行求值。所以我们不能说编译器会 x 先执行 ,然后 ++x ,然后 x++ ,或者 x++ 然后 ++x x 或者其他顺序。但顺序显然很重要,因为根据编译器使用的顺序,我们显然会得到不同的打印数字序列。

    这疯狂的表情怎么办?

    x = x++ + ++x;
    

    这个表达式的问题在于,它包含三种不同的尝试来修改 的值 x :(1)该 x++ 部分尝试取 的 x 值,加 1,将新值存储在 中 x ,并返回旧值;(2)该 ++x 部分尝试取 的 x 值,加 1,将新值存储在 中 x ,并返回新值;(3)该 x = 部分尝试将其他两个值的总和赋回 x 。这三种尝试中的哪一种会“获胜”?这三个值中的哪一个会真正决定 的最终值 x ?同样,也许令人惊讶的是,C 中没有规则告诉我们。

    您可能认为优先级、结合性或从左到右的求值会告诉您事情发生的顺序,但事实并非如此。您可能不相信我,但请相信我的话,我再说一遍:优先级和结合性并不能决定 C 中表达式求值顺序的每个方面。特别是,如果在一个表达式中有多个不同的位置我们尝试为某个东西分配一个新值,例如,优先级和结合性不会 x 告诉 我们 这些尝试中的哪一个是先发生的,哪一个是最后发生的,或者其他什么。


    因此,在了解了所有这些背景知识和介绍之后,如果您想确保所有程序都定义明确,那么哪些表达式可以编写,哪些表达式不能编写?

    这些表达都可以:

    y = x++;
    z = x++ + y++;
    x = x + 1;
    x = a[i++];
    x = a[i++] + b[j++];
    x[i++] = a[j++] + b[k++];
    x = *p++;
    x = *p++ + *q++;
    

    这些表达式都是未定义的:

    x = x++;
    x = x++ + ++x;
    y = x + x++;
    a[i] = i++;
    a[i++] = i;
    printf("%d %d %d\n", x, ++x, x++);
    

    最后一个问题是,如何判断哪些表达式是定义明确的,哪些表达式是未定义的?

    正如我之前所说,未定义的表达式是那些同时发生太多事情的表达式,你不能确定事情发生的顺序,而且顺序很重要:

    1. 如果一个变量在两个或多个不同的地方被修改(分配),你如何知道哪个修改先发生?
    2. 如果某个变量在一个地方被修改,而它的值在另一个地方被使用,你怎么知道它使用的是旧值还是新值?

    以 #1 为例,在表达式中

    x = x++ + ++x;
    

    有三次尝试修改 x .

    以 #2 为例,在表达式中

    y = x + x++;
    

    我们既使用 的值 x ,又对其进行修改。

    所以答案就是:确保在你编写的任何表达式中,每个变量最多被修改一次,并且如果修改了变量,则不要尝试在其他地方使用该变量的值。


    还有一件事。您可能想知道如何“修复”我在回答开始时提出的未定义表达式。

    对于 的情况 printf("%d %d %d\n", x, ++x, x++); ,这很容易——只需将其写成三个单独的 printf 调用:

    printf("%d ", x);
    printf("%d ", ++x);
    printf("%d\n", x++);
    

    现在行为已经定义得非常完美,您将获得合理的结果。

    对于 的情况 x = x++ + ++x ,没有办法修复它。没有办法编写它以保证其行为符合您的期望——但没关系,因为 x = x++ + ++x 无论如何您都不会像在真实程序中那样编写表达式。

  • 尽管之类的表达式的 a = a++ 语法 a++ + a++ 是合法的,但是 这些构造的 行为 未定义的, 因为 不遵守 C 标准中的 shall。C99 6.5p2 :

    1. 在前一个序列点和下一个序列点之间,对象的存储值最多应通过表达式求值修改一次。[72] 此外,应仅读取先前的值来确定要存储的值[73]

    脚注 73 进一步澄清了这一点

    1. 点2

      i = ++i + 1;a[i++] = i;

      点3

      i = i + 1;a[i] = i;

    (和 C11 的附件 C 中列出 C99

    1. p5

      • Between the evaluations of the function designator and actual arguments in a function call and the actual call. (6.5.2.2).
      • Between the evaluations of the first and second operands of the following operators: logical AND && (6.5.13); logical OR || (6.5.14); comma , (6.5.17).
      • Between the evaluations of the first operand of the conditional ? : operator and whichever of the second and third operands is evaluated (6.5.15).
      • The end of a full declarator: declarators (6.7.6);
      • Between the evaluation of a full expression and the next full expression to be evaluated. The following are full expressions: an initializer that is not part of a compound literal (6.7.9); the expression in an expression statement (6.8.3); the controlling expression of a selection statement (if or switch) (6.8.4); the controlling expression of a while or do statement (6.8.5); each of the (optional) expressions of a for statement (6.8.5.3); the (optional) expression in a return statement (6.8.6.4).
      • Immediately before a library function returns (7.1.4).
      • After the actions associated with each formatted input/output function conversion specifier (7.21.6, 7.29.2).
      • Immediately before and immediately after each call to a comparison function, and also between any call to a comparison function and any movement of the objects passed as arguments to that call (7.22.5).

    C11中 同一段的措辞 如下:

    1. 如果标量对象上的副作用相对于同一标量对象上的其他副作用或使用同一标量对象的值进行的值计算而言是无序的,则行为是未定义的。如果表达式的子表达式有多个允许的排序,则如果这种无序的副作用发生在任何排序中,则行为是未定义的。84)

    和 来 -Wall 检测程序中的此类错误 -Werror ,然后 GCC 将直接拒绝编译您的程序。以下是 gcc (Ubuntu 6.2.0-5ubuntu12) 6.2.0 20161005 的输出:

    % gcc plusplus.c -Wall -Werror -pedantic
    plusplus.c: In function ‘main’:
    plusplus.c:6:6: error: operation on ‘i’ may be undefined [-Werror=sequence-point]
        i = i++ + ++i;
        ~~^~~~~~~~~~~
    plusplus.c:6:6: error: operation on ‘i’ may be undefined [-Werror=sequence-point]
    plusplus.c:10:6: error: operation on ‘i’ may be undefined [-Werror=sequence-point]
        i = (i++);
        ~~^~~~~~~
    plusplus.c:14:6: error: operation on ‘u’ may be undefined [-Werror=sequence-point]
        u = u++ + ++u;
        ~~^~~~~~~~~~~
    plusplus.c:14:6: error: operation on ‘u’ may be undefined [-Werror=sequence-point]
    plusplus.c:18:6: error: operation on ‘u’ may be undefined [-Werror=sequence-point]
        u = (u++);
        ~~^~~~~~~
    plusplus.c:22:6: error: operation on ‘v’ may be undefined [-Werror=sequence-point]
        v = v++ + ++v;
        ~~^~~~~~~~~~~
    plusplus.c:22:6: error: operation on ‘v’ may be undefined [-Werror=sequence-point]
    cc1: all warnings being treated as errors
    

    重要的是要知道 what a sequence point is -- and what is a sequence point and what isn't 。例如, 逗号运算符 是一个序列点,所以

    j = (i ++, ++ i);
    

    定义明确,并且将增加 i 一,产生旧值,丢弃该值;然后在逗号运算符处,解决副作用;然后增加 i 一,结果值成为表达式的值 - 即,这只是一种人为的编写方式, j = (i += 2) 又是一种“聪明”的编写方式

    i += 2;
    j = i;
    

    但是, , 函数参数列表中 没有 逗号运算符,并且不同参数的求值之间没有序列点;相反,它们的求值彼此之间没有顺序;因此函数调用

    int i = 0;
    printf("%d %d\n", i++, ++i, i);
    

    具有 未定义的行为, 因为 there is no sequence point between the evaluations of i++ 被 和 修改了两次 ++i in function arguments 在前一个和下一个序列点之间, i 的值 i++ and ++i

  • 虽然任何编译器和处理器都不太可能真正这样做,但根据 C 标准,编译器使用以下序列实现 \'i++\' 是合法的:

    In a single operation, read `i` and lock it to prevent access until further notice
    Compute (1+read_value)
    In a single operation, unlock `i` and store the computed value
    

    虽然我认为没有任何处理器支持硬件以允许有效地完成这样的事情,但人们可以很容易地想象这种行为会使多线程代码更容易的情况(例如,它可以保证如果两个线程试图同时执行上述序列, i 则将增加两个)并且未来的某些处理器可能会提供类似的功能,这并非完全不可想象。

    如果编译器按上述方式编写 i++ (在标准下是合法的),并将上述指令散布在整个表达式的求值过程中(也是合法的),并且如果它没有注意到其他指令之一恰好访问了 i ,则编译器有可能(并且合法)生成一系列会导致死锁的指令。可以肯定的是,如果 i 在两个地方都使用同一个变量,编译器几乎肯定会检测到问题,但是如果例程接受对两个指针 p 和的 q ,并在上面的表达式中使用 (*p) (*q) (而不是使用 i p ,则编译器不需要识别或避免在为和传递同一个对象的地址时发生的死锁 q .

  • 引用 17

    @kavadias 该 printf 语句涉及未定义的行为,原因如上所述。您分别在第 3 个和第 4 个参数中写入 b 和 c,并在第 2 个参数中读取。但这些表达式(第 2、第 3 和第 4 个参数)之间没有顺序。gcc/clang 有一个选项 -Wsequence-point,也可以帮助找到这些。

  • 此序列 int a = 10, b = 20, c = 30; printf(\'a=%db=%dc=%d\n\', (a = a + b + c), (b = b + b), (c = c + c)); 似乎给出了稳定的行为(gcc v7.3.0 中从右到左的参数评估;结果 \'a=110 b=40 c=60\')。是不是因为赋值被视为“完整语句”,从而引入了一个序列点?这难道不应该导致从左到右的参数/语句评估吗?或者,这只是未定义行为的表现?

  • 这个问题经常被链接到与代码相关的问题,例如

    printf("%d %d\n", i, i++);
    

    或者

    printf("%d %d\n", ++i, i++);
    

    或类似变体。

    虽然这也是 如上所述的 未定义行为 printf() 但是与以下语句相比,存在细微的差别:

    x = i++ + i++;
    

    在以下声明中:

    printf("%d %d\n", ++i, i++);
    

    中的参数 求值顺序 printf() 指定这意味着,表达式 i++ ++i 可以按任何顺序求值。C11 标准 对此有一些相关描述:

    附件 J,未指明的行为

    在函数调用中,函数指示符、参数以及参数内的子表达式的求值顺序(6.5.2.2)。

    3.4.4、未指定的行为

    使用未指定的值,或本国际标准提供两种或多种可能性且不对在任何情况下选择哪种行为施加进一步要求的其他行为。

    示例:未指定行为的一个例子是函数参数的评估顺序。

    未指定的行为 本身 不是问题。请考虑以下示例:

    printf("%d %d\n", ++x, y++);
    

    这也有 未指定的行为, ++x 的求值顺序 y++ 未指定。但这是完全合法和有效的语句。 没有 未定义的行为。因为修改( ++x y++ 不同的 对象 进行的

    下列语句的含义

    printf("%d %d\n", ++i, i++);
    

    未定义 的行为 是这两个表达式修改了 同一个 对象 i 而没有中间的 序列点 .


    另一个细节是 printf() 调用中涉及的 逗号 分隔符 ,而不是 逗号运算符 .

    这是一个重要的区别,因为 逗号运算符 在其操作数的评估之间 引入了一个 序列点

    int i = 5;
    int j;
    
    j = (++i, i++);  // No undefined behaviour here because the comma operator 
                     // introduces a sequence point between '++i' and 'i++'
    
    printf("i=%d j=%d\n",i, j); // prints: i=7 j=6
    

    逗号运算符从左到右计算其操作数,并仅得出最后一个操作数的值。因此,在 中 j = 的旧值++i, i++); , ++i 递增 i 6 i++ ( i ( 6 ,该值被赋给 j 。然后 i 由于后递增 7 而变为

    因此,如果 逗号 是逗号运算符,则

    printf("%d %d\n", ++i, i++);
    

    不会有问题。但它会引发 未定义的行为, 因为 逗号 分隔符 .


    对于那些不熟悉 未定义行为 《每个 C 程序员都应该知道的未定义行为》 会有所帮助, 以理解 C 中未定义行为的概念和许多其他变体。

    这篇文章: 未定义、未指定和实现定义的行为 也是相关的。

  • @chux 是的,但您了解 C,并且熟悉自动增量运算符的正确定义。对这些运算符感到困惑的人并不困惑!特别是,如果您想象他们认为 ++i 和 i++ 基本上都是 i + 1 的快捷方式,我相信会更容易理解一些初学者的误解。不过,我已经软化了您评论的句子。

返回
作者最近主题: