为什么编译器不报告丢失的分号?

我有个简单的程序:

#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);
}

正如我们看到的 例如: ideone.com给出了一个错误:

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

为什么编译器不检测丢失的分号?


注意: 这个问题及其答案是由 这个问题激发的。虽然有类似的 其他问题,我没有发现任何提到 C 语言的自由形式的能力,这是什么导致这个和相关的错误。

14870 次浏览

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

For example a statement like

a = b * c;

可以写成

a=b*c;

或者喜欢

a
=
b
*
c
;

So when the compiler see the lines

temp = *a
*a = *b;

它认为这意味着

temp = *a * a = *b;

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

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

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

有时情况甚至更糟: 如果包含两个(或更多)头文件,而第一个头文件包含一个不完整的声明,那么语法错误很可能会在第二个头文件中显示出来。


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

这当然会导致一次修复一个错误,并且频繁地重新编译,这对于大型项目来说可能会很麻烦。不过,认识到这样的后续错误是需要经验的,在多次看到这些错误之后,就更容易发现真正的错误,并在每次重新编译时修复多个错误。

为什么编译器不检测丢失的分号?

There are three things to remember.

  1. C 语言中的行尾只是普通的空格。
  2. C 语言中的 *既可以是一元运算符,也可以是二元运算符。作为一元运算符,它的意思是“解引用”,作为二元运算符,它的意思是“乘”。
  3. 一元运算符和二元运算符之间的差异取决于它们所处的上下文。

这两个事实的结果是,当我们解析。

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

第一个和最后一个 *被解释为一进制,而第二个 *被解释为二进制。从语法的角度来看,这看起来没问题。

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

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

temp = *a *a = *b;

这实际上是 x = y = z;的情况,其中 xy都被赋予 z的值。

What you are saying is 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”(“没什么好笑的”)。下面是相关对话的摘录 从一个场景,它向 为什么展示了编译器开发人员可能有点害羞,不敢随意地宣布这种丢失的分号。

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

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

导演: 这是什么!

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

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

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

Director: In the screenplay? Find it in this screenplay for me.

亚当: “当他们来到路顶的时候,在他们前面出现了一片森林。”

Director: Flip the page.

亚当: 噢,该死..。

导演: 读给我听。

Adam: in front of them appeared a forest... of headstones.

一般情况下是不可能提前知道你说的是一片森林而不是一片墓碑森林的。