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


115

我有这个简单的程序:

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


16
是什么促使了该职位?
R Sahu

10
@TavianBarnes可发现性。搜索此类问题时找不到其他问题。可以用这种方式进行编辑,但这需要稍做改动,这使IMO成为一个完全不同的问题。
某位编程人员花了

4
@TavianBarnes:最初的问题是询问错误。这个问题在问为什么编译器似乎(至少对OP而言)错误地报告了错误的位置。
TonyK

80
值得思考的是:如果编译器可以系统地检测到缺少的分号,那么该语言就不需要分号了。
Euro Micelli '16

5
编译器的工作是报告错误。确定更改内容以纠正错误是您的工作。
David Schwartz

Answers:


213

C是一种自由格式的语言。这意味着您可以采用多种方式设置其格式,并且仍然是合法程序。

例如类似

a = b * c;

可以写成

a=b*c;

或喜欢

a
=
b
*
c
;

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

temp = *a
*a = *b;

它认为这意味着

temp = *a * a = *b;

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

尽管编译器无法检测到丢失的分号,但它还会在错误的行上报告完全不相关的错误。注意这一点很重要,因为无论您在报告错误的行上看多少,都不会出现错误。有时,诸如此类的问题将需要您查看前面的行,以查看它们是否正常并且没有错误。

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

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


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

当然,这可能导致一次修复一个错误,并且频繁进行重新编译,这对于大型项目可能很麻烦。识别此类后续错误是经验的附带内容,经过几次查看后,找出真正的错误并修复每个重新编译的多个错误会更容易。


16
在C ++中,如果被重载,则temp = *a * a = *b 可能是有效的表达式operator*。(不过,该问题被标记为“ C”。)
dan04 '16

13
@ dan04:如果有人真的做到了……那就行了!
凯文

2
+1(a)从​​第一个报告的错误开始的建议;(b)从报告错误的地方向后看。你知道你是真实的程序员,当您在报告错误之前自动查看以下内容时:-)
TripeHound

@TripeHound尤其是在存在大量错误或先前编译的行引发错误的情况下……
Tin Wizard


27

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

要记住三件事。

  1. C中的行尾只是普通的空格。
  2. *C中的C既可以是一元运算符,也可以是二进制运算符。作为一元运算符,它表示“取消引用”;作为二元运算符,它表示“乘”。
  3. 一元运算符和二元运算符之间的差异是根据它们所在的上下文确定的。

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

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

第一个和最后一个*解释为一元,但是第二个*解释为二进制。从语法角度来看,这看起来还可以。

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


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


3
取消引用具有优先权,因此它是(地址a的内容)乘以(指向a的指针)。您可以知道,因为编译错误显示“这是两种类型的无效的二进制*操作数(具有'struct S'和'struct S *')”。
dascandy '16

我编写的是C99之前的代码,所以不用大惊小怪了:-)但是,您的确取得了不错的成绩(+1),尽管分配顺序并不是我回答的重点
毛格说应

1
但是在这种情况下,y它甚至不是变量,而是表达式*a *a,您不能将其赋给乘法结果。
Barmar

@Barmar确实可以,但是编译器还没走得那么远,在查看赋值运算符之前,它已经确定“二进制*”的操作数无效。
plugwash

3

大多数编译器按顺序分析源文件,并在发现错误之处报告行。C程序的前12行可能是有效(无错误)C程序的开始。程序的前13行不能。一些编译器会指出遇到的事物的位置,这些事物本身并不是错误,并且在大多数情况下,它们不会在代码稍后触发错误,但与其他事物结合使用时可能无效。例如:

int foo;
...
float foo;

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

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.