118

我有这个简单的程序:

#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 语言的自由格式能力,这是导致此错误和相关错误的原因。

4

5 回答 5

222

C 是一种自由格式的语言。这意味着您可以通过多种方式对其进行格式化,并且它仍然是一个合法的程序。

例如像这样的语句

a = b * c;

可以写成

a=b*c;

或喜欢

a
=
b
*
c
;

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

temp = *a
*a = *b;

它认为它意味着

temp = *a * a = *b;

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

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

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

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


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

这当然会导致一次修复一个错误和频繁的重新编译,这对于大型项目来说可能很麻烦。不过,识别此类后续错误需要经验,并且在多次查看它们之后,更容易找出真正的错误并在每次重新编译时修复多个错误。

于 2016-10-19T15:12:29.063 回答
30

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

有三件事要记住。

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

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

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

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

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

于 2016-10-19T21:13:54.413 回答
4

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

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>是一个有效的陈述。如前所述,第一个*取消引用指针,而第二个将两个值相乘。

于 2016-10-20T07:50:17.493 回答
3

有一部波兰电影名为“Nic Śmiesznego”(“没什么好笑的”)。这是一个场景中相关对话的摘录,它准确地说明了为什么编译器开发人员可能会有点害羞地宣布这些缺失的分号而鲁莽地放弃。

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

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

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

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

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

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

导演:剧本里?在这个剧本中为我找到它。

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

导演:翻页。

亚当:哦,废话……

导演:给我读。

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

看,通常不可能提前说出您的意思是森林而不是墓碑森林。

于 2020-12-04T01:21:06.840 回答
3

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

int foo;
...
float foo;

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

于 2016-10-20T15:16:08.267 回答