【问题标题】:Is it safe to do something like foo(x, &x)?执行 foo(x, &x) 之类的操作是否安全?
【发布时间】:2020-09-07 09:53:15
【问题描述】:

注意,我不会写这样的代码。我只是好奇,它会帮助我为另一个问题写一个更好的答案。但是假设我们有这个功能:

void foo(int a, int *b)
{
    *b  = 2*a;
}

然后这样称呼它:

int x=42;
foo(x, &x);

除了它是一种非常强烈的代码气味之外,这会导致任何实际问题吗?是UB还是违反了C标准中的任何规则?

【问题讨论】:

  • 没关系。调用时x 的当前值被复制。函数参数a 包含复制的值。
  • @4386427 我没有理由认为这是非法的。只是想让它 100% 确定。毕竟,C 有时会有点奇怪。 :)
  • @4386427 因为 C 中几乎没有什么是合法的。
  • @Boann:在对标准进行充分迂腐阅读的情况下,“严格符合”程序中几乎不允许出现任何内容。另一方面,对于那些仅仅寻求“符合”的程序来说,基本上没有什么是被禁止的。

标签: c language-lawyer undefined-behavior


【解决方案1】:

这个

int x=42;
foo(x, &x);

是格式良好的代码。在这种情况下,参数的评估顺序并不重要。

其实函数调用等价于

foo( 42, &x);

因为第一个参数是按值传递的。

【讨论】:

  • 注意 foo(x, &x) 是有效的,即使 x 值不能像上面的例子那样在编译时确定;无论评估顺序如何,您始终可以确定变量的值及其地址
  • 学究式:第二个参数也是按值传递的。指针参数作为副本传递给指针参数。
【解决方案2】:

代码还可以,但要深入挖掘,看看存在哪些特殊情况,哪些情况很好,哪些可能会导致问题:

  • 函数参数的求值顺序是未指定且未排序的,但在这种特定情况下无关紧要。

  • 函数参数在传递给函数之前被评估,然后在评估之后有一个序列点。这意味着参数中的所有计算和副作用都发生在调用函数之前(但彼此之间的顺序未指定)。

    所以即使是这样的(人工废话)代码实际上也是定义良好的:

      void foo(int a, int *b)
      {
         printf("%d\n", a); // prints 42
        *b = 2**b;          // gives 43 * 43 = 86
      }
      ...
      int x=42;
      foo(x++, &x);
    

    x++ vs &x 很好,因为&x 不是对象的值计算。并且由于序列点,x++ 出现在函数内部的计算之前,因此该部分也是明确定义的。

  • 函数内部的参数a当然是调用方x的本地副本,所以函数可以随心所欲。

  • 如果是两个指针指向同一个对象,那么它们会“重叠”,在某些情况下这是未定义的行为,具体取决于函数的作用。例如memcpy(&x, &x, sizeof x); 是未定义的行为。

  • 每个; 和函数末尾都有一个序列点。

  • 对于在文件范围内声明的变量,函数必须假定修改指向值的指针参数可能会修改文件范围变量。所以在这个代码的情况下:

      void foo(int a, int *b)
      {
        extern int x;
        x = 2*a;
        printf("%d\n", *b);
      }
    

    编译器必须在分配给x 之后获取*b 的值,因为它不能假设b 不指向x - 指针可能是一个别名 &x。这就是指针别名的各种规则的用武之地。

  • 同样,相同类型的两个指针参数可能在调用者代码中指向同一个对象,编译器不允许假设它们不指向,除非我们手动为它们添加restrict 限定符。

【讨论】:

  • @Bob__ 不,我假设读者要么是熟悉“最大咀嚼规则”的 C 书呆子,要么是发现代码可恶或令人困惑的普通人。在后者的情况下,我希望能成功地吓跑他们在实际应用程序中编写类似的代码:)
  • 还不错;)。
  • 精确且有据可查。在void foo(int a, int *b)中是否可以假设b在进入函数时不能是a的地址?
  • @chqrlie 我不明白怎么可能?由于a 具有本地范围。
  • @Lundin:当然我也没有,但我想知道编译器是否利用了这个约束。
【解决方案3】:

问题已经是well answered,但由于评估顺序,即使不在单独的函数中,上述问题也可以工作。

即使下面的代码也能正常工作。

void foo(int *a, int *b) {
  *b = 2*(*a);
}

int x = 42;
foo(&x, &x);

并且x 在执行后将包含84。赋值的右侧在左侧之前进行评估(这就是为什么我们可以这样做 x = x+1; 并让数学家发疯:))

【讨论】:

  • C 标准没有说赋值的右侧符号在左侧之前进行评估。 C 2018 6.5.16 3 明确表示:“......操作数的评估是无序的。”它确实说“……更新左操作数的存储值的副作用是在左操作数和右操作数的值计算之后排序的……”,但这使得更新相对于任何其他副作用没有排序,并留下所有其他副作用相对于彼此和值计算是无序的。
  • 也许您可以添加一个带有restrict 的示例,以显示何时调用foo(&x,&x) 无效?
  • "赋值的右边先于左边求值(这就是为什么我们可以做 x = x+1; 并且把数学家逼疯 :))" 相反,我们不'不知道先评估哪一方,目的是让程序员抓狂。较新版本的 C++ 对此进行了良好定义,但在 C 和 C++17 之前的版本中定义很差。必须让他们雇佣语言律师。
  • @Lundin:右操作数在左操作数之前排序。这会使像 TinyCC 这样的一次性代码生成更难执行。
  • @chqrlie 自 2011 年以来 C++ 的强劲趋势是从高效和硬件相关的编程转向元编程。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-08-27
  • 1970-01-01
  • 1970-01-01
  • 2014-11-11
  • 1970-01-01
  • 2011-10-21
  • 2011-05-19
相关资源
最近更新 更多