Por que o compilador não informa um ponto-e-vírgula ausente?

115

Eu tenho este programa simples:

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

Conforme visto em, por exemplo, ideone.com, isso dá um erro:

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

Por que o compilador não detecta o ponto-e-vírgula ausente?


Nota: Esta pergunta e sua resposta são motivadas por esta pergunta . Embora existam outras questões semelhantes a esta, não encontrei nada mencionando a capacidade de forma livre da linguagem C, que é o que está causando isso e erros relacionados.

Algum cara programador
fonte
16
O que motivou esta postagem?
R Sahu
10
@TavianBarnes Discoverability. A outra questão não pode ser descoberta ao pesquisar esse tipo de problema. Poderia ser editado dessa forma, mas isso exigiria mudanças um pouco ou muito, tornando-se uma questão totalmente diferente da IMO.
Algum programador de
4
@TavianBarnes: A pergunta original era perguntar sobre o erro. Esta questão está perguntando por que o compilador parece (pelo menos para o OP) estar relatando incorretamente a localização do erro.
TonyK
80
Ponto para ponderar: se um compilador pudesse detectar sistematicamente o ponto-e-vírgula faltando, a linguagem não precisaria de ponto-e-vírgula para começar.
Euro Micelli de
5
O trabalho do compilador é relatar o erro. É seu trabalho descobrir o que alterar para corrigir o erro.
David Schwartz de

Respostas:

213

C é uma linguagem de forma livre . Isso significa que você pode formatá-lo de várias maneiras e ainda será um programa legal.

Por exemplo, uma declaração como

a = b * c;

poderia ser escrito como

a=b*c;

ou como

a
=
b
*
c
;

Então, quando o compilador vê as linhas

temp = *a
*a = *b;

pensa que significa

temp = *a * a = *b;

Obviamente, essa não é uma expressão válida e o compilador reclamará disso em vez da falta do ponto-e-vírgula. A razão de não ser válido é porque aé um ponteiro para uma estrutura, portanto, *a * aestá tentando multiplicar uma instância de estrutura ( *a) com um ponteiro para uma estrutura ( a).

Embora o compilador não consiga detectar o ponto-e-vírgula ausente, ele também relata o erro totalmente não relacionado na linha errada. É importante notar isso porque não importa o quanto você olhe para a linha onde o erro é relatado, não há erro lá. Às vezes, problemas como esse exigirão que você examine as linhas anteriores para ver se estão corretas e sem erros.

Às vezes, você ainda precisa procurar em outro arquivo para encontrar o erro. Por exemplo, se um arquivo de cabeçalho está definindo uma estrutura da última vez que define no arquivo de cabeçalho, e o ponto-e-vírgula que encerra a estrutura está ausente, o erro não estará no arquivo de cabeçalho, mas no arquivo que inclui o arquivo de cabeçalho.

E às vezes fica ainda pior: se você incluir dois (ou mais) arquivos de cabeçalho, e o primeiro contiver uma declaração incompleta, muito provavelmente o erro de sintaxe será indicado no segundo arquivo de cabeçalho.


Relacionado a isso está o conceito de erros de acompanhamento . Alguns erros, normalmente devido à falta de ponto-e-vírgula, são relatados como múltiplos erros . É por isso que é importante começar do início ao corrigir os erros, pois corrigir o primeiro erro pode fazer com que vários erros desapareçam.

Isso, é claro, pode levar à correção de um erro por vez e recompilações frequentes, o que pode ser complicado em projetos grandes. Porém, reconhecer esses erros de acompanhamento é algo que vem com a experiência e, depois de vê-los algumas vezes, é mais fácil descobrir os erros reais e corrigir mais de um erro por recompilação.

Joachim Pileborg
fonte
16
Em C ++, temp = *a * a = *b poderia ser uma expressão válida se operator*estivesse sobrecarregada. (A pergunta está marcada como “C”, no entanto.)
dan04
13
@ dan04: Se alguém realmente fez isso ... NOPE!
Kevin
2
+1 para o conselho sobre (a) começar com o primeiro erro relatado; e (b) olhando para trás de onde o erro foi relatado. Você sabe que é um verdadeiro programador quando olha automaticamente para a linha anterior, onde um erro é relatado :-)
TripeHound
@TripeHound ESPECIALMENTE quando há um grande número de erros, ou linhas que compiladas anteriormente estão gerando erros ...
Tin Wizard
1
Como geralmente é o caso com meta, alguém já perguntou - meta.stackoverflow.com/questions/266663/…
StoryTeller - Unslander Monica
27

Por que o compilador não detecta o ponto-e-vírgula ausente?

Existem três coisas para lembrar.

  1. As terminações de linha em C são apenas espaços em branco comuns.
  2. *em C pode ser um operador unário e binário. Como um operador unário, significa "desreferenciar", como um operador binário significa "multiplicar".
  3. A diferença entre operadores unários e binários é determinada a partir do contexto em que são vistos.

O resultado desses dois fatos é quando analisamos.

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

O primeiro e o último *são interpretados como unários, mas o segundo *é interpretado como binário. De uma perspectiva de sintaxe, isso parece OK.

É apenas após a análise, quando o compilador tenta interpretar os operadores no contexto de seus tipos de operando, que um erro é visto.

plugwash
fonte
4

Algumas boas respostas acima, mas vou elaborar.

temp = *a *a = *b;

Na verdade, este é um caso em x = y = z;que xe ysão atribuídos o valor de z.

O que você está dizendo é the contents of address (a times a) become equal to the contents of b, as does temp.

Em suma, *a *a = <any integer value>é uma declaração válida. Como apontado anteriormente, o primeiro *desreferencia um ponteiro, enquanto o segundo multiplica dois valores.

Mawg diz reintegrar Monica
fonte
3
A desreferenciação tem prioridade, então é (conteúdo do endereço a) vezes (ponteiro para a). Você pode dizer, porque o erro de compilação diz "operandos inválidos para binário * (tem 'struct S' e 'struct S *')", que são esses dois tipos.
dascandy de
Eu codifico antes do C99, sem bools :-) Mas você tem razão (+1), embora a ordem de atribuição não seja realmente o ponto da minha resposta
Mawg diz para restabelecer Monica em
1
Mas, neste caso, ynem é uma variável, é a expressão *a *a, e você não pode atribuir ao resultado de uma multiplicação.
Barmar
@Barmar de fato, mas o compilador não foi tão longe, ele já decidiu que os operandos para o "binário *" são inválidos antes de olhar para o operador de atribuição.
plugwash de
3

A maioria dos compiladores analisa os arquivos de origem em ordem e relata a linha onde descobrem que algo estava errado. As primeiras 12 linhas de seu programa C podem ser o início de um programa C válido (sem erros). As primeiras 13 linhas do seu programa não podem. Alguns compiladores notarão a localização das coisas que encontram que não são erros em si mesmas e, na maioria dos casos, não disparam erros posteriormente no código, mas podem não ser válidos em combinação com outra coisa. Por exemplo:

int foo;
...
float foo;

A declaração int foo;por si só seria perfeitamente adequada. Da mesma forma a declaração float foo;. Alguns compiladores podem registrar o número da linha onde a primeira declaração apareceu e associar uma mensagem informativa a essa linha, para ajudar o programador a identificar os casos em que a definição anterior é, na verdade, a errada. Os compiladores também podem manter os números de linha associados a algo como um do, que pode ser relatado se o associado whilenão aparecer no lugar certo. Para casos em que a localização provável do problema seria imediatamente anterior à linha em que o erro foi descoberto, entretanto, os compiladores geralmente não se preocupam em adicionar um relatório extra para a posição.

supergato
fonte