Почему компилятор не сообщает об отсутствии точки с запятой?

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 Обнаруживаемость. Другой вопрос не может быть обнаружен при поиске такого рода проблем. Это можно было бы отредактировать таким образом, но для этого потребовалось бы немного изменить, что сделало бы это совершенно другим вопросом, ИМО.
Какой-то чувак-программист
4
@TavianBarnes: Исходный вопрос касался ошибки. Этот вопрос спрашивает, почему компилятор (по крайней мере, для OP) неправильно сообщает местоположение ошибки.
TonyK
80
Укажите для размышления: если бы компилятор мог систематически обнаруживать пропущенные точки с запятой, языку не потребовались бы точки с запятой для начала.
Euro Micelli
5
Задача компилятора - сообщить об ошибке. Ваша задача - выяснить, что нужно изменить, чтобы исправить ошибку.
Дэвид Шварц,

Ответы:

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
13
@ dan04: Если бы кто-то действительно это сделал ... НЕТ!
Кевин
2
+1 за совет относительно (а) начала с первой сообщенной ошибки; и (б) смотреть назад, откуда сообщается об ошибке. Вы знаете, что являетесь настоящим программистом, когда автоматически смотрите на строку, перед которой сообщается об ошибке :-)
TripeHound
@TripeHound ОСОБЕННО, когда есть очень большое количество ошибок или строки, которые ранее были скомпилированы, выдают ошибки ...
Tin Wizard
1
Как обычно бывает с мета, кто-то уже спросил - meta.stackoverflow.com/questions/266663/…
StoryTeller - Unslander Monica
27

Почему компилятор не обнаруживает отсутствующую точку с запятой?

Следует запомнить три вещи.

  1. Окончания строк в C - это просто пробелы.
  2. *в C может быть как унарным, так и бинарным оператором. Как унарный оператор он означает «разыменование», как бинарный оператор - «умножить».
  3. Разница между унарными и бинарными операторами определяется контекстом, в котором они рассматриваются.

Результатом этих двух фактов является анализ.

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

Первый и последний *интерпретируются как унарные, а второй *интерпретируется как двоичный. С точки зрения синтаксиса это выглядит нормально.

Ошибка возникает только после синтаксического анализа, когда компилятор пытается интерпретировать операторы в контексте их типов операндов.

plugwash
источник
4

Несколько хороших ответов выше, но я уточню.

temp = *a *a = *b;

На самом деле это тот случай, x = y = z;когда обоим xи yприсваивается значение 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
Я кодирую pre C99, так что никаких bools :-) Но вы все-таки хорошо замечаете (+1), хотя порядок назначения на самом деле не был
сутью
1
Но в данном случае yэто даже не переменная, это выражение *a *a, и вы не можете присвоить результат умножения.
Barmar
@Barmar действительно, но компилятор не заходит так далеко, он уже решил, что операнды для «двоичного *» недействительны, прежде чем он посмотрит на оператор присваивания.
plugwash 01
3

Большинство компиляторов анализируют исходные файлы по порядку и сообщают строку, в которой обнаруживают, что что-то не так. Первые 12 строк вашей программы C могут быть началом действующей (безошибочной) программы C. Первые 13 строк вашей программы не могут. Некоторые компиляторы будут отмечать расположение вещей, с которыми они сталкиваются, которые сами по себе не являются ошибками, и в большинстве случаев не будут вызывать ошибки позже в коде, но могут быть недопустимыми в сочетании с чем-то еще. Например:

int foo;
...
float foo;

Само int foo;по себе заявление было бы прекрасно. Аналогично декларации float foo;. Некоторые компиляторы могут записывать номер строки, в которой появилось первое объявление, и связывать с этой строкой информационное сообщение, чтобы помочь программисту идентифицировать случаи, когда более раннее определение на самом деле является ошибочным. Компиляторы также могут сохранять номера строк, связанные с чем-то вроде a do, о котором можно сообщить, если связанный whileне отображается в нужном месте. Однако в случаях, когда вероятное место возникновения проблемы должно быть непосредственно перед строкой, в которой обнаружена ошибка, компиляторы обычно не утруждают себя добавлением дополнительного отчета для этой позиции.

Supercat
источник