为什么编译器不报告缺少分号?

Why doesn't the compiler report a missing semicolon?

我有这个简单的程序:

#include <stdio.h>

struct S
{
    int i;
};

void swap(struct S *a, struct S *b)
{
    struct S temp;
    temp = *a    /* Oops, missing a semicolon here... */
    *a = *b;
    *b = temp;
}

int main(void)
{
    struct S a = { 1 };
    struct S b = { 2 };

    swap(&a, &b);
}

如所见on e.g. ideone.com这给出了一个错误:

prog.c: In function 'swap':
prog.c:12:5: error: invalid operands to binary * (have 'struct S' and 'struct S *')
     *a = *b;
     ^

为什么编译器没有检测到丢失的分号?


注意:这个问题及其答案的动机是 this question. While there are other questions 与此类似,我没有发现任何提及 C 语言的自由形式能力的内容,这是导致此问题和相关错误的原因。

C 是一种自由格式 语言。这意味着您可以用多种方式格式化它,它仍然是一个合法的程序。

例如这样的语句

a = b * c;

可以这样写

a=b*c;

或喜欢

a
=
b
*
c
;

所以当编译器看到这些行时

temp = *a
*a = *b;

它认为它意味着

temp = *a * a = *b;

这当然不是一个有效的表达式,编译器会抱怨这个而不是缺少分号。它无效的原因是因为 a 是一个指向结构的指针,所以 *a * a 试图将一个结构实例 (*a) 与一个指向结构的指针 (a).

虽然编译器无法检测到丢失的分号,但它还会在错误的行上报告完全不相关的错误。注意到这一点很重要,因为无论您如何查看报告错误的行,那里都没有错误。有时像这样的问题需要您查看 行以查看它们是否正常且没有错误。

有时您甚至必须查看另一个文件才能找到错误。例如,如果头文件在头文件中最后一次定义结构,并且缺少终止结构的分号,则错误不会出现在头文件中,而是出现在包含头文件的文件中。

有时情况会变得更糟:如果您包含两个(或更多)头文件,并且第一个包含不完整的声明,很可能语法错误将在第二个头文件中指出。


与此相关的是后续错误的概念。一些错误,通常是由于实际上缺少分号,被报告为 多个 错误。这就是修复错误时从头开始很重要的原因,因为修复第一个错误可能会使多个错误消失。

这当然会导致一次修复一个错误和频繁的重新编译,这对于大型项目来说可能很麻烦。识别此类后续错误是经验带来的东西,在看到它们几次之后,更容易挖掘出真正的错误并在每次重新编译时修复多个错误。

Why doesn't the compiler detect the missing semicolon?

要记住三件事。

  1. C 中的行结尾只是普通的空格。
  2. C 中的
  3. * 既可以是一元运算符,也可以是二元运算符。作为一元运算符,它表示 "dereference",作为二元运算符,它表示 "multiply".
  4. 一元运算符和二元运算符之间的区别取决于它们出现的上下文。

这两个事实的结果就是我们解析的时候。

 temp = *a    /* Oops, missing a semicolon here... */
 *a = *b;

第一个和最后一个 * 被解释为一元,但第二个 * 被解释为二进制。从语法的角度来看,这看起来不错。

只有当编译器尝试在运算符的操作数类型的上下文中解释运算符时,才会在解析后出现错误。

上面有一些很好的答案,但我会详细说明。

temp = *a *a = *b;

这实际上是 x = y = z; 的情况,其中 xy 都分配了 z 的值。

你说的是the contents of address (a times a) become equal to the contents of b, as does temp.

简而言之,*a *a = <any integer value>是一个有效的陈述。如前所述,第一个 * 取消引用指针,而第二个将两个值相乘。

大多数编译器按顺序解析源文件,并报告发现错误的行。 C 程序的前 12 行可能是有效(无错误)C 程序的开始。程序的前 13 行不能。一些编译器会注意到他们遇到的东西的位置,这些东西本身并不是错误,并且在大多数情况下不会在代码的后面触发错误,但与其他东西结合起来可能无效。例如:

int foo;
...
float foo;

声明 int foo; 本身就可以了。同样声明 float foo;。一些编译器可能会记录第一个声明出现的行号,并将信息性消息与该行相关联,以帮助程序员识别早期定义实际上是错误定义的情况。编译器还可以保留与 do 等内容关联的行号,如果关联的 while 未出现在正确的位置,则可以报告。然而,对于问题的可能位置紧接在发现错误的行之前的情况,编译器通常不会为该位置添加额外的报告。

有一部波兰电影名为“Nic Śmiesznego”(“没什么好笑的”)。以下是相关对话的摘录 from a scene,其中 确切地说明了为什么 编译器开发人员可能会有点害羞地宣布这样不计后果地放弃分号。

导演:“这个”是什么意思?!你是说这个物体在我的视野中?用你的手指指出来,因为我愿意相信我在做梦。

亚当:这个,就在这里(分)。

导演:这个?这是什么?!

亚当:你什么意思?这是一片森林。

导演:你能告诉我他妈的为什么我需要一片森林吗?

亚当: “该死的地狱”怎么来的?这里,在剧本里,它说了一片森林,它说...

导演:在剧本里?帮我在这个剧本里找。

亚当: 这里:(读到)“当他们来到路的尽头时,他们面前出现了一片森林”

导演:翻页

亚当:哦废话...

导演:读给我听

亚当:他们面前出现了一片...墓碑林。

看,通常不可能提前说出你真正指的是一片森林,而不是一片墓碑森林。