Dlaczego kompilator nie zgłasza brakującego średnika?

115

Mam taki prosty program:

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

Jak widać na przykład na ideone.com, powoduje to błąd:

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

Dlaczego kompilator nie wykrywa brakującego średnika?


Uwaga: to pytanie i odpowiedź na nie są motywowane tym pytaniem . Chociaż są inne pytania podobne do tego, nie znalazłem nic wspominającego o swobodnej pojemności języka C, która jest przyczyną tego i powiązanych błędów.

Jakiś koleś programista
źródło
16
Co zmotywowało ten post?
R Sahu
10
@TavianBarnes Odkrywczość. Drugiego pytania nie można znaleźć podczas wyszukiwania tego rodzaju problemów. Można to edytować w ten sposób, ale wymagałoby to zmiany trochę za bardzo, co spowodowałoby zupełnie inną kwestię IMO.
Jakiś programista,
4
@TavianBarnes: Pierwotne pytanie dotyczyło błędu. To pytanie dotyczy tego, dlaczego wydaje się, że kompilator (przynajmniej dla OP) błędnie zgłasza lokalizację błędu.
TonyK
80
Warto się zastanowić: gdyby kompilator mógł systematycznie wykrywać brakujące średniki, język nie potrzebowałby średników na początku.
Euro Micelli,
5
Zadaniem kompilatora jest zgłoszenie błędu. Twoim zadaniem jest dowiedzieć się, co zmienić, aby naprawić błąd.
David Schwartz

Odpowiedzi:

213

C jest językiem o dowolnej formie . Oznacza to, że możesz go sformatować na wiele sposobów i nadal będzie to legalny program.

Na przykład stwierdzenie takie jak

a = b * c;

można napisać jak

a=b*c;

lub jak

a
=
b
*
c
;

Więc kiedy kompilator zobaczy linie

temp = *a
*a = *b;

myśli, że to znaczy

temp = *a * a = *b;

Nie jest to oczywiście prawidłowe wyrażenie i kompilator będzie narzekał na to zamiast na brakujący średnik. Przyczyną niepoprawności jest to, że ajest wskaźnikiem do struktury, więc *a * apróbuje pomnożyć instancję struktury ( *a) za pomocą wskaźnika do struktury ( a).

Chociaż kompilator nie może wykryć brakującego średnika, zgłasza również całkowicie niezwiązany błąd w niewłaściwym wierszu. Jest to ważne, aby zauważyć, ponieważ bez względu na to, jak bardzo patrzysz na wiersz, w którym zgłoszony jest błąd, nie ma tam żadnego błędu. Czasami takie problemy wymagają spojrzenia na poprzednie wiersze, aby sprawdzić, czy są w porządku i bez błędów.

Czasami trzeba nawet zajrzeć do innego pliku, aby znaleźć błąd. Na przykład, jeśli plik nagłówkowy definiuje strukturę jako ostatnią w pliku nagłówkowym i brakuje średnika kończącego strukturę, to błąd nie będzie w pliku nagłówkowym, ale w pliku, który zawiera plik nagłówkowy.

A czasami jest jeszcze gorzej: jeśli dołączysz dwa (lub więcej) pliki nagłówkowe, a pierwszy zawiera niepełną deklarację, najprawdopodobniej błąd składni zostanie wskazany w drugim pliku nagłówkowym.


Wiąże się z tym pojęcie dalszych błędów. Niektóre błędy, zazwyczaj spowodowane brakującymi średnikami, są zgłaszane jako błędy wielokrotne . Dlatego ważne jest, aby przy naprawianiu błędów zaczynać od góry, ponieważ naprawienie pierwszego błędu może spowodować zniknięcie wielu błędów.

Może to oczywiście prowadzić do naprawiania jednego błędu na raz i częstych ponownych kompilacji, co może być kłopotliwe w przypadku dużych projektów. Rozpoznawanie takich błędów uzupełniających jest jednak czymś, co wiąże się z doświadczeniem, a po kilkukrotnym ich zobaczeniu łatwiej jest odszukać prawdziwe błędy i naprawić więcej niż jeden błąd na rekompilację.

Joachim Pileborg
źródło
16
W C ++ temp = *a * a = *b mogłoby to być prawidłowe wyrażenie, gdyby operator*zostało przeciążone. (Pytanie jest jednak oznaczone jako „C”).
dan04
13
@ dan04: Jeśli ktoś faktycznie to zrobił ... NIE!
Kevin
2
+1 za poradę dotyczącą (a) rozpoczęcia od pierwszego zgłoszonego błędu; oraz b) spojrzenie wstecz od miejsca zgłoszenia błędu. Wiesz, że jesteś prawdziwym programistą, kiedy automatycznie sprawdzasz linię przed zgłoszeniem błędu :-)
TripeHound
@TripeHound SZCZEGÓLNIE, gdy występuje bardzo duża liczba błędów lub wiersze, które wcześniej skompilowano, generują błędy ...
Tin Wizard
1
Jak to zwykle bywa w przypadku meta, ktoś już zapytał - meta.stackoverflow.com/questions/266663/...
StoryTeller - Unslander Monica
27

Dlaczego kompilator nie wykrywa brakującego średnika?

Należy pamiętać o trzech rzeczach.

  1. Końce linii w C to zwykłe białe znaki.
  2. *w C może być operatorem jednoargumentowym i binarnym. Jako operator jednoargumentowy oznacza „wyłuskiwanie”, jako operator binarny oznacza „mnożenie”.
  3. Różnica między operatorami jednoargumentowymi i binarnymi jest określana na podstawie kontekstu, w którym są widoczne.

Wynikiem tych dwóch faktów jest analiza.

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

Pierwsza i ostatnia *są interpretowane jako jednoargumentowe, ale druga *jest interpretowana jako binarna. Z punktu widzenia składni wygląda to OK.

Błąd jest widoczny dopiero po przeanalizowaniu, gdy kompilator próbuje zinterpretować operatory w kontekście ich typów operandów.

plugwash
źródło
4

Kilka dobrych odpowiedzi powyżej, ale opiszę je szczegółowo.

temp = *a *a = *b;

W rzeczywistości jest to przypadek, w x = y = z;którym oba xi ymają przypisaną wartość z.

To, co mówisz, jest the contents of address (a times a) become equal to the contents of b, as does temp.

Krótko mówiąc, *a *a = <any integer value>jest to ważne oświadczenie. Jak wcześniej wspomniano, pierwsza *wyłuskuje wskaźnik, podczas gdy druga mnoży dwie wartości.

Mawg mówi, że przywróć Monikę
źródło
3
Wyłuskiwanie ma priorytet, więc to (zawartość adresu a) razy (wskaźnik do a). Możesz to stwierdzić, ponieważ błąd kompilacji mówi, że „nieprawidłowe operandy do pliku binarnego * (mają 'struct S' i 'struct S *')”, które są tymi dwoma typami.
dascandy
Koduję przed C99, więc bez bools :-) Ale masz
rację
1
Ale w tym przypadku ynie jest nawet zmienną, jest to wyrażenie *a *ai nie można przypisać wyniku mnożenia.
Barmar
@Barmar w rzeczy samej, ale kompilator nie posunął się tak daleko, już zdecydował, że operandy „binarnego *” są nieprawidłowe, zanim spojrzy na operator przypisania.
plugwash
3

Większość kompilatorów parsuje pliki źródłowe po kolei i zgłasza wiersz, w którym odkrywają, że coś jest nie tak. Pierwsze 12 wierszy twojego programu w C może być początkiem prawidłowego (wolnego od błędów) programu w C. Pierwsze 13 wierszy twojego programu nie może. Niektóre kompilatory zanotują lokalizację napotkanych rzeczy, które same w sobie nie są błędami iw większości przypadków nie będą powodować błędów w dalszej części kodu, ale mogą nie być poprawne w połączeniu z czymś innym. Na przykład:

int foo;
...
float foo;

Sama deklaracja int foo;byłaby w porządku. Podobnie deklaracja float foo;. Niektóre kompilatory mogą zapisać numer linii, w której pojawiła się pierwsza deklaracja, i skojarzyć komunikat informacyjny z tą linią, aby pomóc programiście zidentyfikować przypadki, w których wcześniejsza definicja jest faktycznie błędna. Kompilatory mogą również przechowywać numery wierszy związane z czymś takim jak a do, co może zostać zgłoszone, jeśli skojarzony whilenie pojawi się we właściwym miejscu. Jednak w przypadkach, w których prawdopodobna lokalizacja problemu znajdowałaby się bezpośrednio przed wierszem, w którym wykryto błąd, kompilatory generalnie nie zawracają sobie głowy dodawaniem dodatkowego raportu dla pozycji.

superkat
źródło