为什么无效的语句在C语言中被视为合法?


13

如果这个问题很天真,请原谅。考虑以下程序:

#include <stdio.h>

int main() {
  int i = 1;
  i = i + 2;
  5;
  i;
  printf("i: %d\n", i);
}

在上面的例子中,陈述5;i;似乎完全是多余的,但如果没有默认警告或错误的代码编译(但是,GCC则会引发warning: statement with no effect [-Wunused-value]警告时RAN -Wall)。它们对程序的其余部分没有影响,那么为什么首先将它们视为有效的语句?编译器会简单地忽略它们吗?允许这样的陈述有什么好处?


5
禁止此类声明有什么好处?
Mooing Duck

2
任何表达式都可以放在;其后作为语句。这将使该语言复杂化,从而添加更多关于何时不能成为表达式的规则
MM

3
您是否宁愿因为忽略返回值而导致代码无法编译printf()?该语句5;基本上说:“做什么5(什么都不做),然后忽略结果。您的语句printf(...)是“什么都不做printf(...)然后忽略结果(来自的返回值printf())。” C将它们视为相同。这也允许诸如(void) i;where i是您强制转换void为将其标记为故意未使用的函数的参数
Andrew Henle

1
@AndrewHenle:并不完全一样,因为printf()即使您忽略它最终返回的值,调用的确会起作用。相比之下,5;根本没有效果。
Nate Eldredge

1
因为丹尼斯·里奇,他不在身边告诉我们。
user207421

Answers:


10

允许这样的语句的好处之一是由宏或其他程序创建的代码,而不是由人类编写的代码。

例如,假设一个函数 int do_stuff(void)在成功时返回0或在失败时返回-1。可能是因为对“ stuff”的支持是可选的,所以您可以拥有一个

#if STUFF_SUPPORTED
#define do_stuff() really_do_stuff()
#else
#define do_stuff() (-1)
#endif

现在想象一些代码,如果可能的话,它想做些什么,但是可能会或可能不会真正在乎它是成功还是失败:

void func1(void) {
    if (do_stuff() == -1) {
        printf("stuff did not work\n");
    }
}

void func2(void) {
    do_stuff(); // don't care if it works or not
    more_stuff();
}

STUFF_SUPPORTED为0时,预处理器会将调用扩展func2为一个仅读取的语句

    (-1);

因此,编译器通道只会看到似乎“打扰”您的“多余”语句。然而,人还能做什么?如果是#define do_stuff() // nothing,则代码func1将中断。(而且,您仍然会有一个空的语句,其中的func2reads ;可能更为多余。)另一方面,如果您必须实际定义一个do_stuff()返回-1 的函数,则可能会产生函数调用的开销没有充分的理由。


No-op的更经典版本(或我的意思是普通版本)是((void)0)
乔纳森·勒夫勒

一个很好的例子是assert
尼尔

3

C中的简单语句以分号终止。

C语言中的简单语句是表达式。表达式是变量,常量和运算符的组合。每个表达式都会产生某种可以分配给变量的某种类型的值。

话虽如此,某些“智能编译器”可能会丢弃5个;和我; 陈述。


我无法想象有任何编译器除了丢弃它们之外不会对这些语句做任何事情。他们还能做什么?
杰里米·弗里斯纳

@JeremyFriesner:一个非常简单,未经优化的编译器可能会很好地生成代码以计算该值并将结果存储在寄存器中(从该点开始将被忽略)。
Nate Eldredge

C标准不是术语“简单语句”。一个表达式语句包括(可选的)的表达,随后分号。并非每个表达式都产生一个值。类型的表达式void没有价值。
基思·汤普森

2

允许无效的语句,因为禁止它们比允许它们更困难。当最初设计C且编译器更小,更简单时,这更有意义。

一个表达式语句包括一个表达式后跟一个分号。其行为是评估表达式并丢弃结果(如果有)。通常,目的是评估表达式具有副作用,但是要确定给定的表达式是否具有副作用并非总是容易甚至不可能的。

例如,函数调用是一个表达式,因此函数调用后跟一个分号是一条语句。这句话有副作用吗?

some_function();

没有看到的实现就无法分辨some_function

这个怎么样?

obj;

可能不是-但如果obj定义为volatile,则可以。

通过添加分号,允许将任何表达式转换为表达式语句,从而使语言定义更加简单。要求表达式具有副作用会增加语言定义和编译器的复杂性。C建立在一组一致的规则上(函数调用是表达式,赋值是表达式,后跟分号的表达式是语句),它使程序员可以执行自己想要的操作而不会阻止它们执行可能有意义的事情。


2

列出的无效语句expression语句的示例,其语法在C标准的 6.8.3p1节中给出,如下所示:

 表达声明expression opt 

6.5节中的所有内容都专用于表达式的定义,但概括地说,表达式由与运算符链接的常量和标识符组成。值得注意的是,表达式可以包含或不包含赋值运算符,并且可以包含或不包含函数调用。

因此,任何带有分号的表达式都可以视为表达式语句。实际上,代码中的每一行都是一个表达式语句的示例:

i = i + 2;
5;
i;
printf("i: %d\n", i);

某些运算符包含副作用,例如赋值运算符集和前后的递增/递减运算符,并且函数调用运算符() 可能会产生副作用,具体取决于所讨论的函数的作用。但是,不要求操作员之一必须具有副作用。

这是另一个例子:

atoi("1");

就像printf您的示例中的调用一样,这是在调用函数并丢弃结果,但与printf函数调用本身不同,它没有副作用。


1

有时这样的陈述非常方便:

int foo(int x, int y, int z)
{
    (void)y;   //prevents warning
    (void)z;

    return x*x;
}

或者,当参考手册告诉我们只是读取寄存器以归档内容时,例如清除或设置一些标志(在uC世界中非常常见的情况)

#define SREG   ((volatile uint32_t *)0x4000000)
#define DREG   ((volatile uint32_t *)0x4004000)

void readSREG(void)
{
    *SREG;   //we read it here
    *DREG;   // and here
}

https://godbolt.org/z/6wjh_5


*SREG为volatile时,*SREG;在C标准指定的模型中不起作用。C标准指定它具有可观察到的副作用。
埃里克·Postpischil

@EricPostpischil-不,它没有可观察到的效果,但是如果有效果。C个可见对象均未更改。
P__J__

C 2018 5.1.2.3 6将程序的可观察行为定义为包括“严格根据抽象机的规则评估对易失对象的访问。” 没有解释或演绎的问题;这是可观察到的行为的定义
埃里克·Postpischil
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.