Menu

Prototipi di funzione in C: dichiarazione e definizione

Perché il C deve vedere la forma di una funzione prima che tu la chiami: scrivere prototipi, risolvere gli errori di dichiarazione implicita, mettere i prototipi nei file header e la vera differenza tra f(void) e f().

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

Il compilatore legge dall'alto in basso

Un compilatore C elabora un file sorgente in una sola passata, dalla prima riga all'ultima. Quando arriva a una chiamata di funzione, deve già sapere tre cose: cosa restituisce la funzione, quanti argomenti prende e di che tipo sono. Senza queste informazioni non può generare codice corretto né controllare la chiamata.

Quindi questo è un problema:

#include <stdio.h>

int main(void) {
    printf("%d\n", add(2, 3));   /* add ancora sconosciuta */
    return 0;
}

int add(int a, int b) {
    return a + b;
}

La soluzione è un prototipo: la firma della funzione, scritta prima della chiamata, con un punto e virgola al posto del corpo.

Ora main può venire per prima, ed è così che è organizzata la maggior parte dei file C: i prototipi in cima dicono cosa offre il file, main si legge come uno schema, e i dettagli seguono sotto.

Dichiarazione e definizione

Due parole facili da confondere che conviene tenere separate:

  • Una dichiarazione dice che una funzione esiste e ne indica il tipo. Termina con un punto e virgola e non ha corpo. Puoi scriverla tutte le volte che vuoi.
  • Una definizione fornisce il corpo. Deve comparire esattamente una volta in tutto il programma: due definizioni della stessa funzione causano un errore del linker ("multiple definition of").
int add(int a, int b);                        /* dichiarazione (prototipo) */
int add(int a, int b) { return a + b; }       /* definizione, anche una dichiarazione */

I nomi dei parametri sono facoltativi in un prototipo; al compilatore interessano solo i tipi:

int add(int, int);              /* valido ed equivalente */
int add(int a, int b);          /* meglio: i nomi documentano l'ordine */

Usa i nomi. void drawRect(int, int, int, int); non dice nulla a chi legge, mentre void drawRect(int x, int y, int width, int height); dice tutto.

Cosa ti dà il prototipo

Non solo la possibilità di riordinare le definizioni: è ciò che fa controllare al compilatore le tue chiamate.

Con il prototipo visibile, il 3 e il 2 vengono convertiti in double prima della chiamata. Senza, verrebbero passati come int e scale li leggerebbe come double: valori senza senso, e nessuna diagnostica nei vecchi dialetti del C.

Il prototipo intercetta anche un numero sbagliato di argomenti e i tipi incompatibili in fase di compilazione:

scale(3.0);              /* errore: troppo pochi argomenti */
scale(3.0, "due");       /* errore: passa char * dove si aspetta double */

Sono gli errori che vuoi: rumorosi, in fase di compilazione, che indicano la riga giusta.

Errori di dichiarazione implicita

Chiama una funzione di cui il compilatore non ha mai sentito parlare e otterrai uno dei messaggi più comuni del C:

warning: implicit declaration of function 'add' [-Wimplicit-function-declaration]

In C89 il compilatore tirava a indovinare: supponeva che la funzione restituisse int e accettava qualunque argomento passato. Quella supposizione è sbagliata più spesso che no, e quando la funzione vera restituisce un double o un puntatore il risultato non ha senso. C99 ha eliminato le dichiarazioni implicite dal linguaggio, quindi questo è un errore da C99 in poi: nelle versioni recenti GCC e clang lo rifiutano di default.

Due cause, due soluzioni:

Una tua funzione, non ancora dichiarata. Aggiungi il prototipo sopra la chiamata, oppure sposta la definizione più in alto.

Una funzione di libreria di cui hai dimenticato l'header. I prototipi della libreria stanno nel suo header, quindi ti serve l'#include:

printf, scanf, fopen        ->  #include <stdio.h>
malloc, free, exit, atoi    ->  #include <stdlib.h>
strlen, strcpy, strcmp      ->  #include <string.h>
sqrt, pow, sin, fabs        ->  #include <math.h>
isdigit, toupper            ->  #include <ctype.h>
bool, true, false           ->  #include <stdbool.h>

Un messaggio affine, conflicting types for 'add', significa che prototipo e definizione non concordano: è diverso il tipo di un parametro, oppure il tipo di ritorno. Correggi quello sbagliato; devono corrispondere esattamente.

La ricorsione mutua ha bisogno di un prototipo

A volte riordinare non basta: due funzioni che si chiamano a vicenda non possono venire entrambe per prime.

Un solo prototipo rompe il cerchio. È lo stesso meccanismo su cui si basa una normale funzione ricorsiva: il nome di una funzione è visibile dentro il suo stesso corpo, quindi la ricorsione diretta non ha bisogno di prototipi, ma quella mutua sì.

Prototipi nei file header

Quando un programma si estende su più file .c, i prototipi si spostano in un header, così ogni file vede le stesse dichiarazioni da un unico posto.

mathutils.h, l'interfaccia:

#ifndef MATHUTILS_H
#define MATHUTILS_H

int add(int a, int b);
int multiply(int a, int b);
double average(const int values[], int count);

#endif

mathutils.c, l'implementazione:

#include "mathutils.h"

int add(int a, int b) {
    return a + b;
}

int multiply(int a, int b) {
    return a * b;
}

double average(const int values[], int count) {
    if (count == 0) return 0.0;
    int total = 0;
    for (int i = 0; i < count; i++) total += values[i];
    return (double) total / count;
}

main.c, chi la usa:

#include <stdio.h>
#include "mathutils.h"

int main(void) {
    printf("%d\n", add(2, 3));
    return 0;
}

Poi:

gcc main.c mathutils.c -o program

Qui vale la pena prendere due abitudini. L'involucro #ifndef / #define / #endif è un'include guard: impedisce che le dichiarazioni vengano elaborate due volte se l'header viene incluso per due strade diverse. E mathutils.c include il proprio header, cosa che sembra ridondante ma non lo è, perché fa controllare al compilatore ogni definizione rispetto alla dichiarazione che useranno gli altri file. Se un giorno divergono, te ne accorgi subito invece che in fase di linking. La pagina sui file header approfondisce il resto.

Le parentesi angolari (<stdio.h>) cercano nei percorsi di include di sistema; le virgolette ("mathutils.h") cercano prima nella cartella del tuo progetto. Usa le virgolette per i tuoi header.

f(void) contro f()

Questa sorprende davvero, ed è il motivo per cui ogni esempio di questa documentazione scrive int main(void).

void ping(void);      /* NON prende argomenti: le chiamate con argomenti vengono rifiutate */
void pong();          /* non dice NULLA sui parametri */

void ping(void); è un prototipo: dichiara che la funzione non prende parametri, quindi ping(1, 2, 3) è un errore di compilazione.

void pong(); è una dichiarazione vecchio stile ereditata dal C precedente allo standard. Dichiara il tipo di ritorno e niente di niente sui parametri, quindi il compilatore non può controllare le chiamate: pong(1, 2, 3) compila in silenzio e fa qualcosa di indefinito.

int main(void) {      /* corretto: main non prende argomenti */
int main() {          /* valido, ma senza controllo degli argomenti */

Scrivi sempre (void) per una funzione senza parametri. C23 cambia () perché significhi lo stesso di (void), eliminando finalmente la trappola, ma molto codice e molti compilatori non ci sono ancora arrivati, e (void) è corretto in ogni standard del C.

La stessa distinzione vale per la definizione. void ping(void) { } è una definizione in stile prototipo; void ping() { } non lo è, e non attiva il controllo delle chiamate che compaiono prima.

Errori comuni

  • Punto e virgola sulla definizione. int add(int a, int b); { return a + b; } dichiara add e poi lascia un blocco vagante. Il messaggio d'errore confonde; la causa è un solo carattere.
  • Punto e virgola mancante sul prototipo. Il compilatore prosegue la lettura in quello che segue e segnala qualcosa di incomprensibile qualche riga più sotto.
  • Prototipo e definizione in disaccordo. conflicting types for .... Rendili identici, o meglio ancora includi l'header nel file di implementazione, così il controllo è automatico.
  • Dichiarare una funzione dentro un'altra funzione. È valido (int add(int, int); dentro main), ma la dichiarazione è visibile solo in quella funzione. Metti i prototipi a livello di file.
  • Definire una funzione in un header. Includilo da due file .c e il linker segnala una definizione duplicata. Gli header contengono dichiarazioni; le definizioni vanno in un file .c.
  • Affidarsi a () per il controllo. Non controlla nulla. Scrivi (void).

Domande frequenti

Cos'è un prototipo di funzione in C?

La dichiarazione della firma di una funzione (tipo di ritorno, nome e tipi dei parametri) che termina con un punto e virgola invece che con un corpo: int add(int a, int b);. Dice al compilatore come si chiama la funzione, così le chiamate possono essere controllate, senza dire cosa fa.

Qual è la differenza tra dichiarazione e definizione in C?

Una dichiarazione introduce il nome e il tipo (int add(int, int);) e può comparire molte volte. Una definizione fornisce il corpo (int add(int a, int b) { return a + b; }) e deve comparire esattamente una volta in tutto il programma. Ogni definizione è anche una dichiarazione.

Come si risolve "implicit declaration of function" in C?

Aggiungi un prototipo prima della chiamata. Per le tue funzioni, metti returnType name(paramTypes); vicino all'inizio del file o in un header; per le funzioni di libreria, includi l'header giusto: #include <stdio.h> per printf, <stdlib.h> per malloc, <string.h> per strlen, <math.h> per sqrt.

Qual è la differenza tra f(void) e f() in C?

void f(void); dichiara una funzione che non prende argomenti, e il compilatore rifiuta qualsiasi chiamata che ne passi. void f(); è una dichiarazione vecchio stile che non dice nulla sui parametri, quindi il controllo degli argomenti è disattivato. Scrivi sempre (void); C23 finalmente rende equivalenti le due forme, ma il codice e i compilatori più vecchi le trattano ancora in modo diverso.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA