Menu

Les prototypes de fonctions en C : déclaration et définition

Pourquoi le C doit voir la forme d'une fonction avant que vous ne l'appeliez - écrire des prototypes, corriger les erreurs de déclaration implicite, placer les prototypes dans des fichiers d'en-tête, et la vraie différence entre f(void) et f().

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

Le compilateur lit de haut en bas

Un compilateur C traite un fichier source en une passe, de la première ligne à la dernière. Quand il atteint un appel de fonction, il doit déjà savoir trois choses : ce que la fonction renvoie, combien d'arguments elle prend, et de quels types. Sans cela, il ne peut ni générer du code correct ni vérifier l'appel.

Donc ceci pose problème :

#include <stdio.h>

int main(void) {
    printf("%d\n", add(2, 3));   /* add n'a pas encore ete vue */
    return 0;
}

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

La solution est un prototype : la signature de la fonction, écrite avant l'appel, avec un point-virgule là où irait le corps.

Maintenant main peut venir en premier, ce qui est l'organisation de la plupart des fichiers C : les prototypes en haut disent ce que le fichier offre, main se lit comme le plan, et les détails suivent plus bas.

Déclaration contre définition

Deux mots faciles à confondre et qu'il vaut la peine de séparer :

  • Une déclaration dit qu'une fonction existe et donne son type. Elle se termine par un point-virgule et n'a pas de corps. Vous pouvez l'écrire autant de fois que vous voulez.
  • Une définition donne le corps. Elle doit apparaître exactement une fois dans tout le programme - deux définitions de la même fonction sont une erreur d'édition de liens (« multiple definition of »).
int add(int a, int b);                        /* declaration (prototype) */
int add(int a, int b) { return a + b; }       /* definition - aussi une declaration */

Les noms de paramètres sont facultatifs dans un prototype ; seuls les types comptent pour le compilateur :

int add(int, int);              /* legal, et equivalent */
int add(int a, int b);          /* mieux : les noms documentent l'ordre */

Mettez les noms. void drawRect(int, int, int, int); ne dit rien à un lecteur, alors que void drawRect(int x, int y, int width, int height); lui dit tout.

Ce que le prototype vous apporte

Pas seulement la possibilité de réordonner les définitions - c'est lui qui permet au compilateur de vérifier vos appels.

Avec le prototype visible, le 3 et le 2 sont convertis en double avant l'appel. Sans lui, ils seraient empilés comme des int et scale les lirait comme des double - du garbage, et aucun diagnostic dans les anciens dialectes du C.

Le prototype attrape aussi les nombres d'arguments erronés et les types incompatibles à la compilation :

scale(3.0);              /* erreur : trop peu d'arguments */
scale(3.0, "deux");      /* erreur : char * passe la ou un double est attendu */

Ce sont les erreurs que vous voulez - bruyantes, à la compilation, pointant la ligne.

Les erreurs de déclaration implicite

Appelez une fonction dont le compilateur n'a jamais entendu parler et vous obtenez l'un des messages les plus courants du C :

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

En C89, le compilateur devinait : il supposait que la fonction renvoyait un int et acceptait les arguments que vous passiez. Cette supposition est fausse plus souvent qu'à son tour, et quand la vraie fonction renvoie un double ou un pointeur, le résultat n'a aucun sens. C99 a supprimé les déclarations implicites du langage, donc c'est une erreur en C99 et après - GCC et clang modernes la rejettent par défaut dans les versions récentes.

Deux causes, deux solutions :

Votre propre fonction, pas encore déclarée. Ajoutez le prototype au-dessus de l'appel, ou déplacez la définition plus haut.

Une fonction de bibliothèque dont vous avez oublié l'en-tête. Les prototypes de la bibliothèque vivent dans son en-tête, il vous faut donc le #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 message voisin, conflicting types for 'add', signifie que le prototype et la définition ne sont pas d'accord - un type de paramètre diffère, ou le type de retour. Corrigez celui qui est faux ; ils doivent correspondre exactement.

La récursion mutuelle a besoin d'un prototype

Parfois, réordonner ne suffit pas : deux fonctions qui s'appellent l'une l'autre ne peuvent pas venir toutes les deux en premier.

Un prototype brise le cercle. C'est le même mécanisme sur lequel s'appuie une fonction récursive simple - le nom d'une fonction est visible dans son propre corps, donc la récursion directe n'a pas besoin de prototype, mais la récursion mutuelle si.

Les prototypes dans les fichiers d'en-tête

Dès qu'un programme s'étend sur plusieurs fichiers .c, les prototypes migrent dans un en-tête pour que chaque fichier voie les mêmes déclarations depuis un seul endroit.

mathutils.h - l'interface :

#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'implémentation :

#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 - un utilisateur :

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

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

Puis :

gcc main.c mathutils.c -o program

Deux habitudes valent d'être adoptées ici. L'enveloppe #ifndef / #define / #endif est une garde d'inclusion : elle empêche les déclarations d'être traitées deux fois si l'en-tête est inclus par deux chemins. Et mathutils.c inclut son propre en-tête - ce qui paraît redondant mais ne l'est pas, car cela fait vérifier au compilateur chaque définition par rapport à la déclaration que les autres fichiers utiliseront. Si elles divergent un jour, vous le découvrez immédiatement plutôt qu'à l'édition de liens. Les fichiers d'en-tête couvrent le reste.

Les chevrons (<stdio.h>) explorent les chemins d'inclusion du système ; les guillemets ("mathutils.h") explorent d'abord le répertoire de votre projet. Utilisez les guillemets pour vos propres en-têtes.

f(void) contre f()

Celle-ci est réellement surprenante, et c'est la raison pour laquelle chaque exemple de ces docs écrit int main(void).

void ping(void);      /* ne prend AUCUN argument - les appels avec arguments sont rejetes */
void pong();          /* ne dit RIEN des parametres */

void ping(void); est un prototype : il déclare que la fonction ne prend aucun paramètre, donc ping(1, 2, 3) est une erreur de compilation.

void pong(); est une déclaration à l'ancienne héritée du C d'avant la normalisation. Elle déclare le type de retour et absolument rien sur les paramètres, donc le compilateur ne peut pas vérifier les appels - pong(1, 2, 3) compile sans bruit et fait quelque chose d'indéfini.

int main(void) {      /* correct : main ne prend aucun argument */
int main() {          /* legal, mais la verification des arguments est desactivee */

Écrivez toujours (void) pour une fonction sans paramètre. C23 fait que () signifie la même chose que (void), ce qui supprime enfin le piège - mais beaucoup de code et beaucoup de compilateurs n'y sont pas encore, et (void) est correct dans toutes les normes du C.

La même distinction s'applique à la définition. void ping(void) { } est une définition de style prototype ; void ping() { } ne l'est pas, et n'active pas la vérification des appels qui la précèdent.

Erreurs courantes

  • Un point-virgule sur la définition. int add(int a, int b); { return a + b; } déclare add puis laisse un bloc égaré. Le message d'erreur est déroutant ; la cause est un caractère.
  • Un point-virgule manquant sur le prototype. Le compilateur poursuit sa lecture dans ce qui suit et signale quelque chose d'incompréhensible plusieurs lignes plus bas.
  • Prototype et définition en désaccord. conflicting types for .... Rendez-les identiques - ou mieux, incluez l'en-tête dans le fichier d'implémentation pour que la vérification soit automatique.
  • Déclarer une fonction dans une autre fonction. Légal (int add(int, int); dans main), mais la déclaration n'est alors visible que dans cette fonction. Mettez les prototypes à portée fichier.
  • Définir une fonction dans un en-tête. Incluez-le depuis deux fichiers .c et l'éditeur de liens signale une définition en double. Les en-têtes contiennent des déclarations ; les définitions vont dans un fichier .c.
  • Compter sur () pour la vérification. Cela ne vérifie rien. Écrivez (void).

Questions fréquentes

Qu'est-ce qu'un prototype de fonction en C ?

Une déclaration de la signature d'une fonction - type de retour, nom et types des paramètres - terminée par un point-virgule au lieu d'un corps : int add(int a, int b);. Elle dit au compilateur comment la fonction s'appelle afin que les appels puissent être vérifiés, sans dire ce qu'elle fait.

Quelle est la différence entre une déclaration et une définition en C ?

Une déclaration introduit le nom et le type (int add(int, int);) et peut apparaître plusieurs fois. Une définition fournit le corps (int add(int a, int b) { return a + b; }) et doit apparaître exactement une fois dans tout le programme. Toute définition est aussi une déclaration.

Comment corriger « implicit declaration of function » en C ?

Ajoutez un prototype avant l'appel. Pour vos propres fonctions, mettez typeRetour nom(typesParams); près du début du fichier ou dans un en-tête ; pour les fonctions de bibliothèque, incluez le bon en-tête - #include <stdio.h> pour printf, <stdlib.h> pour malloc, <string.h> pour strlen, <math.h> pour sqrt.

Quelle est la différence entre f(void) et f() en C ?

void f(void); déclare une fonction qui ne prend aucun argument, et le compilateur rejette tout appel qui en passe. void f(); est une déclaration à l'ancienne qui ne dit rien des paramètres, donc la vérification des arguments est désactivée. Écrivez toujours (void) ; C23 fait enfin que les deux signifient la même chose, mais le code et les compilateurs plus anciens diffèrent encore.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER