为什么在C语言中没有实际作用的语句被认为是合法的?

13

如果这个问题比较幼稚,请原谅。考虑下面的程序:

#include <stdio.h>

int main() {
  int i = 1;
  i = i + 2;
  5;
  i;
  printf("i: %d\n", i);
}
在上面的例子中,语句 5;i; 看起来完全是多余的,然而代码默认情况下可以编译通过(然而,gcc 在使用 -Wall 运行时会抛出一个 warning: statement with no effect [-Wunused-value] 警告)。它们对程序的其余部分没有任何影响,那么为什么它们被认为是有效的语句呢?编译器是否会简单地忽略它们?允许这样的语句有什么好处吗?

5
禁止此类言论有哪些好处? - Mooing Duck
2
任何表达式都可以通过在其后加上;来成为语句。增加更多关于表达式不能成为语句的规则会使语言变得复杂。 - M.M
4
你更希望因为忽略 printf() 的返回值而导致代码无法编译吗?语句 5; 基本上是说“执行 5 而忽略结果”。你的语句 printf(...) 是“执行 printf(...) 并忽略结果(从 printf() 返回的值)”。C把它们视为相同的。这也允许出现像 (void) i; 这样的代码,其中 i 是传递给函数的参数,你将其强制转换为 void 以标记它为有意未使用。 - Andrew Henle
1
@AndrewHenle:这并不完全相同,因为调用printf()确实有影响,即使您忽略了最终返回的值。相比之下,5;根本没有任何影响。 - Nate Eldredge
1
因为丹尼斯·里奇已经离开我们,无法告诉我们了。 - user207421
显示剩余3条评论
5个回答

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中的代码将会出错。(而且你仍然会在func2中有一个空语句,它只是读取了;,这可能更加多余。) 另一方面,如果您必须实际定义一个返回-1的do_stuff()函数,则可能会因没有充分理由而产生函数调用的成本。


更经典的 no-op 版本(或者我应该说是常见版本)是 ((void)0) - Jonathan Leffler
一个很好的例子是assert - Neil

3

C语言中的简单语句以分号结尾。

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

话虽如此,“聪明的编译器”可能会忽略5;和i;语句。


1
我无法想象任何编译器会对这些语句做出除了丢弃它们之外的任何操作。它还能做什么呢? - Jeremy Friesner
@JeremyFriesner:一个非常简单的、非优化的编译器很可能会生成计算值并将结果放入寄存器的代码(从此忽略该值)。 - Nate Eldredge
C标准中没有“简单语句”这个术语。表达式语句由一个(可选的)表达式后跟一个分号组成。并非每个表达式都会产生值;类型为void的表达式没有值。 - Keith Thompson

2

允许没有效果的语句,因为禁止它们比允许它们更困难。这在C语言最初设计时以及编译器更小、更简单的时候更为相关。

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

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

some_function();

如果没有查看 some_function 的实现代码,就无法确定。

这个怎么样?

obj;

可能不会--但如果obj被定义为volatile,那么就会发生这种情况。
通过添加分号允许将任何表达式转换为表达式语句,可以使语言定义更简单。要求表达式具有副作用会增加语言定义和编译器的复杂性。C语言建立在一套一致的规则上(函数调用是表达式、赋值是表达式、后跟分号的表达式是语句),并且允许程序员随心所欲地编写代码,而无需阻止他们进行可能有或没有意义的操作。

2
你列出的没有效果的语句都是表达式语句的例子,其语法在C标准的6.8.3p1节中给出如下:
 expression-statement:
   expressionopt ;
第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;
}

当参考手册告诉我们只需读取寄存器以实现某些功能时(例如在单片机领域中清除或设置某个标志非常常见的情况),我们需要这样做。
#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是易失性的时,*SREG;在C标准指定的模型中没有任何效果。C标准规定它具有可观察的副作用。 - Eric Postpischil
@EricPostpischil - 不,它没有observable效果,但是它有影响。C语言中的任何可见对象都没有改变。 - 0___________
C 2018 5.1.2.3和6条规定了程序的“可观测行为”,其中包括“对易失对象的访问严格按照抽象机器的规则进行评估”。没有解释或推论的问题;这是“可观测行为”的定义。 - Eric Postpischil

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