如何根据操作数的顺序进行C赋值

如何根据操作数的顺序进行C赋值,c,language-lawyer,C,Language Lawyer,我相信你们大多数人都知道,C并没有指定对运算符(如+)的操作数进行求值的顺序 例如,x=exp1+exp2可以通过先计算exp1,然后计算exp2,然后添加结果来计算。但是,它可能首先评估exp2 根据先计算+的左操作数还是右操作数,如何进行将1或2赋值给x的C赋值?检查这一点的最简单代码如下所示: #include <stdio.h> int i; int inc_i(void) { return ++i; } int get_i(void) { return i;

我相信你们大多数人都知道,C并没有指定对运算符(如+)的操作数进行求值的顺序

例如,
x=exp1+exp2
可以通过先计算exp1,然后计算exp2,然后添加结果来计算。但是,它可能首先评估exp2


根据先计算+的左操作数还是右操作数,如何进行将1或2赋值给x的C赋值?

检查这一点的最简单代码如下所示:

#include <stdio.h>

int i;

int inc_i(void)
{
  return ++i;
}

int get_i(void)
{ 
  return i;
}

int
main (void)
{
  i = 0;
  printf("%i\n", get_i() + inc_i());
  i = 0;
  printf("%i\n", inc_i() + get_i());
}

这告诉我表达式的左侧首先求值。然而,虽然我认为这是一个有趣的思维实验,但我不建议依赖此检查在程序中执行任何有意义的逻辑-这会带来麻烦。

你不能。不可靠。每当您允许这样的情况在代码中发生时,C标准将其称为“未定义行为”,并允许编译器执行任何它想要的操作。而且编译器是臭名昭著的,尤其是在最近几十年中,因为它们在遇到未定义的行为时完全不可预测。最糟糕的是,甚至可能很难配置编译器来警告您发生了这种情况(它不可能总是知道)。你有责任不让这种情况发生

这听起来可能很疯狂,但它有一个很好的观点。CPU是不同的。在某些情况下,按一个顺序做事可能是有道理的。在另一种情况下,相同的顺序将非常缓慢,但在另一个顺序中做事情是非常快的。也许下一代CPU将改变指令的执行方式,现在三阶操作最有意义。也许编译器开发人员找到了一种方法,通过某种方式来生成更好的代码

仅仅因为您设法说服编译器一次生成生成特定操作顺序的代码,并不意味着下次编译时它不会更改。可能操作数的顺序根本不重要,它们的计算顺序与编译器内部数据结构中存储的顺序无关,而这恰好是一个哈希表,其中的哈希函数在每次执行时都会发生变化,或者下次更新编译器时,哈希函数也会发生变化,或者您恰好在代码之前或之后添加了另一个表达式,从而更改了顺序,或者在您的计算机上更新另一个程序,该程序也会更新编译器使用的库,而该库恰好改变了某些内容在数据结构中的存储顺序,这将改变编译器生成代码的顺序


注意。可以编写您想要的表达式,其中计算顺序不会导致未定义的行为,只是“不确定序列”。在这种情况下,编译器不能自由地执行它想要的任何操作,它只允许按照它想要的顺序计算表达式。在大多数实际情况下,结果是相同的。

编写这样一个表达式的问题是,不能在同一个变量中写入两次,否则代码将调用未定义的行为,这意味着会出现错误,任何事情都可能发生。如果在表达式内修改变量,则不允许在同一表达式内再次访问同一变量,除非访问由所谓的序列点分隔。(C116.5/2)

所以我们不能编写这样的代码:

// BAD CODE, undefined behavior
_Bool first=false;
x = (first?(first=true,exp1):0) + (first?(first=true,exp2):0);
此代码调用了未定义的行为,因为对表达式不同位置之间的
first
的访问是不排序的


但是,通过使用函数可以实现类似的代码。无论何时调用函数,在参数求值之后、调用函数之前都有一个序列点。在函数返回之前还有一个序列点。因此,这样的代码是可能的:

n = expr(1) + expr(2)
这只是有未指定的行为-因为对+的操作数的求值是未指定的。但任何地方都没有未排序的访问,这意味着代码不会崩溃等。我们只是不知道或假设它将给出什么结果。例如:

#include <stdio.h>

static int first=0;

int expr (int n)
{
  if(first == 0)
  {
    first = n;
    return first;
  }

  return 0;
}

int main (void)
{
  int n;
  printf("%d\n", n = expr(1) + expr(2)); // unspecified order of evaluation
  first=0;
  printf("%d\n", n = expr(1) + expr(2)); // unspecified order of evaluation

  return 0;
}
#包括
静态int first=0;
整数表达式(整数n)
{
如果(第一个==0)
{
第一个=n;
先返回;
}
返回0;
}
内部主(空)
{
int n;
printf(“%d\n”,n=expr(1)+expr(2));//未指定的求值顺序
第一个=0;
printf(“%d\n”,n=expr(1)+expr(2));//未指定的求值顺序
返回0;
}
这在我的系统上给出了输出
11
,但也可能给出了输出
12
21
22
。下次我执行程序时,它可能会给出一个不同的结果-我们不能假设任何事情,除了程序将以一种未记录的方式进行决定性的行为


这一点未明确说明的主要原因是,它允许不同的编译器根据情况以不同的方式优化其表达式解析树。这反过来又可以实现尽可能快的执行时间和/或编译时间

可以调用两个函数来修改全局变量。无论如何,这毫无意义。双方可以同时评估,你的实际问题是什么?你为什么觉得你需要知道这些?这比你想象的还要糟糕。根据
exp1
exp2
的复杂程度,子表达式和副作用的评估可能会相互交错。我不明白为什么这个问题如此不受欢迎。这是一个有趣的话题,很有价值。这不是一个实际问题,那又怎样?如果没有定义顺序,这意味着编译器可以选择任何顺序,甚至是随机的,并且在两个编译或两条指令的序列中不“一致”。它可以选择它想要的任何合适的标准,也可以选择给定表达式的顺序。如果你
#include <stdio.h>

static int first=0;

int expr (int n)
{
  if(first == 0)
  {
    first = n;
    return first;
  }

  return 0;
}

int main (void)
{
  int n;
  printf("%d\n", n = expr(1) + expr(2)); // unspecified order of evaluation
  first=0;
  printf("%d\n", n = expr(1) + expr(2)); // unspecified order of evaluation

  return 0;
}