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

У меня есть эта простая программа:

#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, что является причиной этого и связанных с ним ошибок.

Ответ 1

C - язык свободной формы. Это означает, что вы можете форматировать его разными способами, и это все равно будет юридической программой.

Например, выражение типа

a = b * c;

может быть написано как

a=b*c;

или как

a
=
b
*
c
;

Итак, когда компилятор видит строки

temp = *a
*a = *b;

он думает, что это означает

temp = *a * a = *b;

Это, конечно, не допустимое выражение, и компилятор будет жаловаться на это вместо отсутствующей точки с запятой. Причина, по которой это неверно, состоит в том, что a является указателем на структуру, поэтому *a * a пытается умножить экземпляр структуры (*a) на указатель на структуру (a).

Пока компилятор не может обнаружить отсутствующую точку с запятой, он также сообщает о полностью несвязанной ошибке в неправильной строке. Это важно заметить, потому что независимо от того, насколько вы смотрите на строку, где сообщается об ошибке, там нет ошибки. Иногда такие проблемы потребуют от вас взглянуть на предыдущие строки, чтобы убедиться, что они в порядке и без ошибок.

Иногда вам даже приходится искать в другом файле, чтобы найти ошибку. Например, если файл заголовка определяет структуру, последнюю из которых он делает в файле заголовка, а точка с запятой, заканчивающая структуру, отсутствует, то ошибка не будет в файле заголовка, а в файле, который включает заголовочный файл.

И иногда это становится еще хуже: если вы включаете два (или более) заголовочных файла, а первый содержит неполное объявление, скорее всего синтаксическая ошибка будет указана во втором файле заголовка.


В связи с этим существует концепция последующих ошибок. Некоторые ошибки, как правило, из-за отсутствующих точек с запятой, сообщаются как несколько ошибок. Вот почему важно начинать с вершины при исправлении ошибок, так как исправление первой ошибки может привести к исчезновению нескольких ошибок.

Это, конечно, может привести к фиксации одной ошибки за раз и частым перекомпиляциям, которые могут быть громоздкими с большими проектами. Признание таких последующих ошибок - это то, что приходит с опытом, и, увидев их несколько раз, легче выкапывать реальные ошибки и исправлять более одной ошибки при перекомпиляции.

Ответ 2

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

Можно запомнить три вещи.

  • Конечные строки в C - это просто обычные пробелы.
  • * в C может быть как унарным, так и двоичным оператором. В качестве унарного оператора это означает "разыменование", поскольку двоичный оператор означает "умножить".
  • Разница между унарными и двоичными операторами определяется из контекста, в котором они видны.

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

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

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

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

Ответ 3

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

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> - действительный оператор. Как указывалось ранее, первый * разыменовывает указатель, а второй умножает два значения.

Ответ 4

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

int foo;
...
float foo;

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