Menu

Errori comuni in C: gli sbagli dei principianti e come correggerli

Gli errori che ogni programmatore C fa almeno una volta: punto e virgola mancanti, = al posto di ==, dichiarazioni implicite, scanf senza &, stringhe confrontate con ==, divisione intera, variabili non inizializzate ed errori di uno. Per ognuno sintomo, causa e soluzione.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Ogni programmatore C incontra la stessa breve lista di errori, di solito nella prima settimana e ogni tanto dieci anni dopo. Vale la pena catalogarli perché la maggior parte produce messaggi di errore che non li descrivono, o peggio, nessun messaggio.

Ogni voce qui sotto è un sintomo, la causa che c'è dietro e la soluzione.

Punto e virgola mancante e l'effetto a cascata

Sintomo: un muro di errori, tutti segnalati su righe che sembrano corrette.

program.c:6:5: error: expected ';' before 'printf'
program.c:7:5: error: expected declaration specifiers before 'return'

Causa: il C termina le istruzioni con ;. Se ne dimentichi uno, il compilatore attacca la riga successiva a quella corrente, poi segnala la confusione nel punto in cui il testo combinato smette di avere senso: di solito la riga seguente.

int main(void) {
    int x = 5      /* punto e virgola mancante */
    printf("%d\n", x);
    return 0;
}

Soluzione: quando compare una raffica di errori, correggi solo il primo e ricompila. Tutto quello che viene dopo potrebbe esserne una conseguenza. E controlla sempre la riga prima di quella indicata dal compilatore.

Lo stesso effetto a cascata nasce da una graffa non chiusa o da un commento /* non terminato, e in quel caso gli errori possono comparire decine di righe più in là.

Un'altra nota: niente punto e virgola dopo l'intestazione di un if, for, while o dopo la graffa di chiusura di una definizione di funzione. if (x > 0); compila senza problemi e non fa nulla: il ; è l'intero corpo.

Assegnazione al posto del confronto

Sintomo: una condizione sempre vera, o una variabile che cambia misteriosamente.

int x = 5;
if (x = 10) {              /* assegna 10, poi verifica 10 -> vero */
    printf("x vale dieci\n");  /* stampa sempre; ora x vale 10 */
}

Causa: = assegna, == confronta. Il valore di un'assegnazione è il valore assegnato, quindi if (x = 10) verifica 10, che è diverso da zero e quindi vero. Il compilatore lo accetta perché ogni tanto è proprio quello che si intende.

Soluzione: usa == in ogni condizione e attiva -Wall perché il compilatore ti avvisi:

warning: suggest parentheses around assignment used as truth value

Se vuoi davvero un'assegnazione dentro una condizione, cosa comune con while ((c = getchar()) != EOF), le parentesi in più lo dichiarano e fanno tacere l'avviso.

Dichiarazione implicita di una funzione

Sintomo:

warning: implicit declaration of function 'printf'
warning: implicit declaration of function 'malloc'

...seguito a volte da strani risultati a runtime o da un errore del linker.

Causa: il compilatore ha incontrato la chiamata a una funzione per cui non ha una dichiarazione. Nei dialetti precedenti a C99 presumeva che la funzione restituisse int e andava avanti; sui sistemi a 64 bit questa ipotesi tronca a 32 bit un puntatore restituito, ed è così che un <stdlib.h> mancante trasforma malloc in un crash.

Soluzione: includi l'header giusto.

FunzioneHeader
printf, scanf, fopen<stdio.h>
malloc, free, atoi, rand, exit<stdlib.h>
strlen, strcpy, strcmp<string.h>
sqrt, pow, fabs<math.h>
isdigit, toupper<ctype.h>
time<time.h>

Per le tue funzioni, lo stesso avviso significa che ne hai chiamata una prima di definirla. Metti un prototipo sopra main:

scanf senza la &

Sintomo: il programma va in crash quando legge l'input, oppure non legge nulla e lascia la variabile invariata.

int age;
scanf("%d", age);     /* manca la & : passa il valore, non l'indirizzo */

Causa: scanf scrive dentro la tua variabile, quindi le serve l'indirizzo della variabile. Passare age consegna qualunque valore spazzatura contenesse, e scanf lo tratta come un posto in cui scrivere: di solito è un segfault.

Soluzione: & prima della variabile, per ogni tipo tranne gli array, che sono già un indirizzo:

Due trappole vicine della stessa famiglia: in scanf usa %lf per un double (lì %f significa float, e scrivere i byte di un float dentro un double lo lascia sbagliato), e limita sempre un %s con una larghezza, %49s per un buffer di 50 byte, altrimenti una parola lunga lo fa traboccare.

Meglio ancora: leggi un'intera riga con fgets e analizzala, così non può traboccare e non lascia input residuo nel buffer. Approfondimenti su entrambe in scanf.

Confrontare stringhe con ==

Sintomo: due stringhe chiaramente uguali risultano diverse.

char a[] = "hello";
char b[] = "hello";

if (a == b) {              /* confronta due indirizzi: falso */
    printf("uguali\n");
}

Causa: una stringa C non è un valore, è un puntatore al primo carattere. == confronta i puntatori. Due array che contengono lo stesso testo vivono a indirizzi diversi, quindi il test è falso. (A complicare le cose, confrontare due letterali identici a volte dà vero, perché il compilatore può memorizzarne una sola copia, e questo rende il bug intermittente.)

Soluzione: strcmp, ricordando che restituisce 0 se sono uguali:

Anche la lettura invertita trae in inganno: if (strcmp(a, b)) è vero quando le stringhe sono diverse, perché un risultato diverso da zero significa "non uguali". Scrivi sempre esplicitamente == 0.

Divisione intera

Sintomo: una media pari a 0, una percentuale che vale sempre 0 o 100, un rapporto che ha perso la parte frazionaria.

int correct = 7, total = 10;
double score = correct / total;      /* 0.0, non 0.7 */

Causa: entrambi gli operandi sono int, quindi C esegue una divisione intera e tronca prima che il risultato venga assegnato a un double. 7 / 10 vale 0; convertire 0 in double dà 0.0.

Soluzione: rendi uno degli operandi in virgola mobile prima della divisione:

Fare il cast di un operando promuove automaticamente anche l'altro. Fare il cast del risultato è troppo tardi: il troncamento è già avvenuto. Vedi il type casting.

La stessa trappola si nasconde in espressioni come (a + b) / 2 per un punto medio e 1 / 2 * x, che vale sempre 0 qualunque sia x.

return mancante o sbagliato

Sintomo: una funzione restituisce un numero sbagliato ma plausibile, diverso a ogni esecuzione o a ogni build.

int add(int a, int b) {
    int sum = a + b;
    /* nessuna istruzione return */
}

Causa: arrivare alla fine di una funzione non void senza restituire nulla produce un valore non specificato, in pratica quello che si trovava per caso nel registro di ritorno. Se il chiamante lo usa è comportamento indefinito.

La forma più subdola restituisce un valore su alcuni percorsi e non su altri:

int classify(int n) {
    if (n > 0) return 1;
    if (n < 0) return -1;
    /* con n == 0 si arriva alla fine senza return */
}

Soluzione: restituisci un valore su ogni percorso e compila con -Wall: il messaggio di GCC "control reaches end of non-void function" individua entrambe le versioni.

Variabili non inizializzate

Sintomo: output spazzatura, o risultati che cambiano tra un'esecuzione e l'altra e tra livelli di ottimizzazione.

int total;                       /* contiene quello che c'era sullo stack */
for (int i = 1; i <= 5; i++) {
    total += i;                  /* somma a un valore spazzatura */
}
printf("%d\n", total);           /* un numero enorme */

Causa: le variabili locali non vengono azzerate. Una variabile globale o static viene impostata a zero automaticamente; una locale parte con i byte che si trovavano già a quell'indirizzo dello stack.

Soluzione: inizializza nel punto della dichiarazione. Non costa nulla ed elimina un'intera categoria di bug:

-Wall -Wextra avvisa per molti di questi casi ("may be used uninitialized"), e -fsanitize=memory o valgrind individuano il resto. Questo errore è particolarmente brutto perché un puntatore non inizializzato porta dritto a un segmentation fault.

Errori di uno (off-by-one)

Sintomo: l'ultimo elemento viene saltato, oppure si tocca un elemento di troppo e il programma si comporta male più avanti.

int arr[5];
for (int i = 0; i <= 5; i++) {   /* tocca arr[5], che non esiste */
    arr[i] = i;
}

Causa: un array di n elementi ha indici da 0 a n - 1. <= esegue un passaggio in più.

Soluzione: lo schema i < n, calcolando n dall'array invece di scrivere il numero due volte:

La versione con le stringhe, dimenticare lo spazio per '\0', è lo stesso errore con un altro vestito, e char word[5] con dentro "hello" è un buffer overrun.

Altri due più piccoli da conoscere

Punto e virgola dopo l'intestazione di un ciclo. for (int i = 0; i < 10; i++); seguito da un blocco tra graffe esegue il ciclo dieci volte senza fare nulla, poi esegue il blocco una volta. Compila senza problemi.

sizeof su un puntatore. Dentro una funzione, un parametro array è un puntatore, quindi sizeof(arr) è la dimensione del puntatore (8 byte), non quella dell'array. Passa la lunghezza come argomento separato:

L'abitudine che evita quasi tutto questo

Compila con gli avvisi attivi, fin dal primo programma:

gcc -Wall -Wextra -g program.c -o program

-Wall -Wextra individua l'assegnazione nella condizione, il return mancante, la lettura non inizializzata, la variabile inutilizzata e lo specificatore di printf che non corrisponde al suo argomento. Aggiungere -fsanitize=address,undefined durante lo sviluppo individua quasi tutto il resto nel momento esatto in cui accade.

Tratta ogni avviso come un errore che non hai ancora incontrato. Un programma C che compila senza avvisi non è garantito corretto, ma quasi ogni programma C che va in crash prima stava avvisando di qualcosa.

Domande frequenti

Cosa significa 'implicit declaration of function' in C?

Il compilatore ha incontrato la chiamata a una funzione che non ha mai visto dichiarata. Quasi sempre hai dimenticato un #include: printf richiede <stdio.h>, malloc richiede <stdlib.h>, strlen richiede <string.h>. Può anche voler dire che hai chiamato una tua funzione prima di definirla, cosa che si risolve con un prototipo sopra main.

Perché il mio programma C segnala un errore su una riga che sembra corretta?

Di solito perché l'errore vero è sulla riga precedente. Un punto e virgola mancante, una graffa non chiusa o un commento non terminato fanno leggere al compilatore le tue due righe come una sola, quindi segnala la confusione nel punto in cui il testo diventa finalmente impossibile da analizzare. Controlla sempre prima la riga sopra quella segnalata.

Perché in C non posso confrontare le stringhe con ==?

Perché una stringa C è un char *, cioè un puntatore, quindi == confronta due indirizzi e non i caratteri a cui puntano. Due stringhe identiche memorizzate in posti diversi danno false. Usa strcmp(a, b) == 0 di <string.h>, che restituisce 0 quando il contenuto coincide.

Perché 5 / 2 dà 2 in C?

Perché entrambi gli operandi sono interi, quindi C esegue una divisione intera e scarta la parte frazionaria. Rendi uno dei due un valore in virgola mobile per ottenere 2.5: 5.0 / 2, oppure (double)a / b quando lavori con variabili. Fare il cast del risultato è troppo tardi: (double)(5 / 2) vale 2.0.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA