Découper son projet

Ce chapitre est la suite directe de celui consacré aux fonctions : nous allons voir comment découper nos projets en plusieurs fichiers. En effet, même si l’on découpe bien son projet en fonctions, ce dernier est difficile à relire si tout est contenu dans le même fichier. Ce chapitre a donc pour but de vous apprendre à découper vos projets efficacement.

Portée et masquage

La notion de portée

Avant de voir comment diviser nos programmes en plusieurs fichiers, il est nécessaire de vous présenter une notion importante, celle de portée. La portée d’une variable est la partie du programme où cette dernière est utilisable. Il existe plusieurs types de portées, cependant nous n’en verrons que deux1 :

  • au niveau d’un bloc ;
  • au niveau d’un fichier.
Au niveau d’un bloc

Une portée au niveau d’un bloc signifie qu’une variable est utilisable, visible de sa déclaration jusqu’à la fin du bloc dans lequel elle est déclarée. Illustration.

#include <stdio.h>

int main(void)
{
    {
        int nombre = 3;
        printf("%d\n", nombre);
    }

    /* Incorrect ! */
    printf("%d\n", nombre);
    return 0;
}

Dans ce code, la variable nombre est déclarée dans un sous-bloc. Sa portée est donc limitée à ce dernier et elle ne peut pas être utilisée en dehors.

Au niveau d’un fichier

Une portée au niveau d’un fichier signifie qu’une variable est utilisable, visible, de sa déclaration jusqu’à la fin du fichier dans lequel elle est déclarée. En fait, il s’agit de la portée des variables « globales » dont nous avons parlé dans le chapitre sur les fonctions.

#include <stdio.h>

int nombre = 3;

int triple(void)
{
    return nombre * 3;
}

int main(void)
{
    nombre = triple();
    printf("%d\n", nombre);
    return 0;
}

Dans ce code, la variable nombre a une portée au niveau du fichier et peut par conséquent être aussi bien utilisée dans la fonction triple() que dans la fonction main().

La notion de masquage

En voyant les deux types de portées, vous vous êtes peut-être posé la question suivante : que se passe-t-il s’il existe plusieurs variables de même nom ? bien, cela dépend de la portée de ces dernières. Si elles ont la même portée comme dans l’exemple ci-dessous, alors le compilateur sera incapable de déterminer à quelle variable ou à quelle fonction le nom fait référence et, dès lors, retournera une erreur.

int main(void)
{
    int nombre = 10;
    int nombre = 20;

    return 0;
}

En revanche, si elles ont des portées différentes, alors celle ayant la portée la plus petite sera privilégiée, on dit qu’elle masque celle·s de portée·s plus grande·s. Autrement dit, dans l’exemple qui suit, c’est la variable du bloc de la fonction main() qui sera affichée.

#include <stdio.h>

int nombre = 10;

int main(void)
{
    int nombre = 20;

    printf("%d\n", nombre);
    return 0;
}

Notez que nous disons : « celle·s de portée plus petite », car les variables déclarées dans un sous-bloc ont une portée plus limitée (car confinée au sous-bloc) que celle déclarée dans un bloc supérieur. Ainsi, le code ci-dessous est parfaitement valide et affichera 30.

#include <stdio.h>

int nombre = 10;

int main(void)
{
    int nombre = 20;

    if (nombre == 20)
    {
        int nombre = 30;

        printf("%d\n", nombre);
    }

    return 0;
}

  1. Il existe en vérité 4 types de portées, si vous souhaitez approfondir le sujet, voyez le cours les identificateurs en langage C.

Partager des fonctions et variables

Voyons à présent comment répartir nos fonctions et variables entre différents fichiers.

Les fonctions

Dans l’extrait précédent, nous avions, entre autres, créé une fonction triple() que nous avons placée dans le même fichier que la fonction main(). Essayons à présent de les répartir dans deux fichiers distincts. Pour ce faire, il vous suffit de créer un second fichier avec l’extension « .c ». Dans notre cas, il s’agira de « main.c » et de « autre.c ».

autre.c
int triple(int nombre)
{
    return nombre * 3;
}
main.c
int main(void)
{
    int nombre = triple(3);
    return 0;
}

La compilation se réalise de la même manière qu’auparavant, si ce n’est qu’il vous est nécessaire de spécifier les deux noms de fichier.

gcc -Wall -Wextra -pedantic -std=c11 -fno-common -fno-builtin main.c autre.c

À noter que vous pouvez également utiliser une forme raccourcie où *.c correspond à tous les fichiers portant l’extension « .c » du dossier courant.

gcc -Wall -Wextra -pedantic -std=c11 -fno-common -fno-builtin *.c

Si vous testez ce code, vous aurez droit à un bel avertissement de votre compilateur du type « implicit declaration of function 'triple' ». Quel est le problème ? Le problème est que la fonction triple() n’est pas déclarée dans le fichier main.c et que le compilateur ne la connaît donc pas lorsqu’il compile le fichier. Pour corriger cette situation, nous devons déclarer la fonction en signalant au compilateur que cette dernière se situe dans un autre fichier. Pour ce faire, nous allons inclure le prototype de la fonction triple() dans le fichier main.c en le précédant du mot-clé extern, qui peut être compris comme « la fonction se trouve dans ce fichier ou dans un autre » (le « ou dans un autre » explique l’emploi du terme « externe »).

autre.c
int triple(int nombre)
{
    return nombre * 3;
}
main.c
extern int triple(int nombre);

int main(void)
{
    int nombre = triple(3);

    return 0;
}

En termes techniques, on dit que la fonction triple() est définie dans le fichier « autre.c » (car c’est là que se situe le corps de la fonction) et qu’elle est déclarée dans le fichier « main.c ». Sachez qu’une fonction ne peut être définie qu’une seule et unique fois.

Pour information, notez que le mot-clé extern est facultatif devant un prototype (il est implicitement inséré par le compilateur). Nous vous conseillons cependant de l’utiliser par souci de clarté et de symétrie avec les déclarations de variables (voyez ci-dessous).

Les variables

La même méthode peut être appliquée aux variables, mais uniquement à celle ayant une portée au niveau d’un fichier. Également, à l’inverse des fonctions, il est plus difficile de distinguer une définition d’une déclaration de variable (elles n’ont pas de corps comme les fonctions). La règle pour les différencier est qu’une déclaration sera précédée du mot-clé extern alors que la définition non1. C’est à vous de voir dans quel fichier vous souhaitez définir la variable, mais elle ne peut être définie qu’une seule et unique fois. Enfin, sachez que seule la définition peut comporter une initialisation. Ainsi, cet exemple est tout à fait valide.

autre.c
int nombre = 10;   /* Une définition */
extern int autre;  /* Une déclaration */
main.c
extern int nombre;  /* Une déclaration */
int autre = 10;     /* Une définition */

Alors que celui-ci, non.

autre.c
int nombre = 10;        /* Il existe une autre définition */
extern int autre = 10;  /* Une déclaration ne peut pas comprendre une initialisation */
main.c
int nombre = 20;  /* Il existe une autre définition */
int autre = 10;   /* Une définition */

Notez qu’une déclaration de fonction ou de variable peut parfaitement être insérée au sein d’un bloc, auquel cas elle aura une portée au niveau d’un bloc.

int
main(void)
{
    extern int nombre;

    {
        extern int triple(int nombre);
        printf("%d\n", triple(nombre));
    }

    printf("%d\n", triple(nombre)); /* triple() n'existe plus. */
    return 0;
}

  1. La règle est en vérité un peu plus complexe que cela, voyez le cours sur les identificateurs en langage C si vous souhaitez davantage de détails.

Fonctions et variables exclusives

Dans la section précédente, nous vous avons dit qu’il n’était possible de définir une variable ou une fonction qu’une seule fois. Toutefois, ce n’est pas tout à fait vrai… Il est possible de rendre une variable (ayant une portée au niveau d’un fichier) ou une fonction exclusive à un fichier donné (on peut aussi dire « locale » à un fichier).

Une variable ou une fonction exclusive à un fichier est propre à ce dernier et ne peut être utilisée qu’au sein de celui-ci. Dans un tel cas, l’existence de plusieurs définitions ne posent pas de problèmes puisque soit chacune d’entre elles est confinée à un fichier, soit l’une d’entre elle est partagée et les autres sont exclusives.

Les fonctions

Afin de rendre une fonction exclusive à un fichier, il est nécessaire de précéder sa définition du mot-clé static.

main.c
#include <stdio.h>

extern void dis_bonjour(void);

static void bonjour(void)
{
    printf("Bonjour depuis main.c.\n");
}

int main(void)
{
    bonjour();
    dis_bonjour();
    return 0;
}
bonjour.c
#include <stdio.h>

static void bonjour(void)
{
    printf("Bonjour depuis bonjour.c.\n");
}

void dis_bonjour(void)
{
    bonjour();
}
Résultat
Bonjour depuis main.c.
Bonjour depuis bonjour.c.

Dans l’exemple ci-dessus, nous avons défini deux fois la fonction bonjour(), en précédant chaque définition du mot-clé static, rendant ces définitions propres, respectivement, au fichier main.c et au fichier bonjour.c.

De même, il est possible d’avoir une définition partagée et une ou plusieurs définitions exclusives. Dans l’exemple ci-dessous, il existe deux définitions de la fonction bonjour() : une propre au fichier main.c et une partagée dont la définition est dans le fichier bonjour.c et est utilisée dans le fichier dis_bonjour.c.

main.c
#include <stdio.h>

extern void dis_bonjour(void);

static void bonjour(void)
{
    printf("Bonjour depuis main.c.\n");
}

int main(void)
{
    bonjour();
    dis_bonjour();
    return 0;
}
bonjour.c
#include <stdio.h>

void bonjour(void)
{
    printf("Bonjour depuis bonjour.c.\n");
}
dis_bonjour.c
extern void bonjour(void);

void dis_bonjour(void)
{
    bonjour();
}
Résultat
Bonjour depuis main.c.
Bonjour depuis bonjour.c.
Les variables

Comme pour les fonctions, une variable ayant une portée au niveau d’un fichier peut être rendue exclusive à un fichier en précédant sa définition du mot-clé static.

Distinguez bien les deux utilisations différentes du mot-clé static :

  • Si la variable a une portée au niveau d’un bloc, le mot-clé static modifie sa classe de stockage pour la rendre statique (sa durée de vie s’étend à la durée d’exécution du programme) ;
  • Si la variable a une portée au niveau d’un fichier, le mot-clé static rend la variable exclusive au fichier.
main.c
#include <stdio.h>

extern void affiche_nombre(void);
static int nombre = 20;

int main(void)
{
    printf("nombre = %d\n", nombre);
    affiche_nombre();
    return 0;
}
nombre.c
#include <stdio.h>

static int nombre = 10;

void affiche_nombre(void)
{
    printf("nombre = %d\n", nombre);
}
Résultat
nombre = 20
nombre = 10

De même, comme pour les fonctions, une définition partagée peut coexister avec une ou plusieurs définitions exclusives.

main.c
#include <stdio.h>

extern void affiche_nombre(void);
static int nombre = 20;

int main(void)
{
    printf("nombre = %d\n", nombre);
    affiche_nombre();
    return 0;
}
nombre.c
int nombre = 10;
affiche_nombre.c
#include <stdio.h>

extern int nombre;

void affiche_nombre(void)
{
    printf("nombre = %d\n", nombre);
}
Résultat
nombre = 20
nombre = 10

Notez que si vous essayez d’utiliser une fonction ou une variable exclusive dans un autre fichier, vous obtiendrez, assez logiquement, une erreur lors de la compilation du type « undefined reference to […] ».

Les fichiers d'en-têtes

Pour terminer ce chapitre, il ne nous reste plus qu’à voir les fichiers d’en-têtes.

Jusqu’à présent, lorsque vous voulez utiliser une fonction ou une variable définie dans un autre fichier, vous insérez sa déclaration dans le fichier ciblé. Seulement voilà, si vous utilisez dix fichiers et que vous décidez un jour d’ajouter ou de supprimer une fonction ou une variable ou encore de modifier une déclaration, vous vous retrouvez Gros-Jean comme devant et vous êtes bon pour modifier les dix fichiers, ce qui n’est pas très pratique…

Pour résoudre ce problème, on utilise des fichiers d’en-têtes (d’extension « .h »). Ces derniers contiennent conventionnellement des déclarations de fonctions et de variables et sont inclus via la directive #include dans les fichiers qui utilisent les fonctions et variables en question.

Les fichiers d’en-têtes n’ont pas besoin d’être spécifiés lors de la compilation, ils seront automatiquement inclus.

La structure d’un fichier d’en-tête est généralement de la forme suivante.

#ifndef CONSTANTE_H
#define CONSTANTE_H

/* Les déclarations */

#endif

Les directives du préprocesseur sont là pour éviter les inclusions multiples ou en cascades. Cela arrive typiquement si vous incluez un en-tête A, qui inclue un en-tête B et qui à son tour inclue l’en-tête A. Vous voyez qu’une boucle se forme (A inclue B, B inclue A, A inclue B, etc.). C’est ce type de situation que ces directives évitent. Aussi, prenez soin de les inclure dans chacun de vos fichiers d’en-têtes.

Vous pouvez remplacer CONSTANTE par ce que vous voulez, le plus simple et le plus fréquent étant le nom de votre fichier, par exemple AUTRE_H si votre fichier se nomme « autre.h ». Voici un exemple d’utilisation de fichiers d’en-têtes.

autre.h
#ifndef AUTRE_H
#define AUTRE_H

extern int triple(int nombre);
extern int variable;

#endif
autre.c
#include "autre.h"

int variable = 42;


int triple(int nombre)
{
    return nombre * 3;
}
main.c
#include <stdio.h>

#include "autre.h"


int main(void)
{
    printf("%d\n", triple(variable));
    return 0;
}

Plusieurs remarques à propos de ce code :

  • dans la directive d’inclusion, les fichiers d’en-têtes sont entre guillemets et non entre chevrons comme les fichiers d’en-têtes de la bibliothèque standard ;
  • les fichiers sources et d’en-têtes correspondants portent le même nom ;
  • nous vous conseillons d’inclure le fichier d’en-tête dans le fichier source correspondant (dans mon cas « autre.h » dans « autre.c ») afin d’éviter des problèmes de portée.

Dans le chapitre suivant, nous aborderons un point essentiel que nous verrons en deux temps : la gestion d’erreurs.

En résumé
  1. Une variable avec une portée au niveau d’un bloc est utilisable de sa déclaration jusqu’à la fin du bloc dans lequel elle est déclarée ;
  2. Une variable avec une portée au niveau d’un fichier est utilisable de sa déclaration jusqu’à la fin du fichier dans lequel elle est déclarée ;
  3. Dans le cas où deux variables ont le même nom, mais des portées différentes, celle ayant la portée la plus petite masque celle ayant la portée la plus grande ;
  4. Il est possible de répartir les définitions de fonction et de variable (ayant une portée au niveau d’un fichier) entre différents fichiers ;
  5. Pour utiliser une fonction ou une variable (ayant une portée au niveau d’un fichier) définie dans un autre fichier, il est nécessaire d’insérer une déclaration dans le fichier où elle doit être utilisée ;
  6. Une fonction ou une variable (ayant une portée au niveau d’un fichier) dont la définition est précédée du mot-clé static ne peut être utilisée que dans le fichier où elle est définie ;
  7. Les fichiers d’en-tête peuvent être utilisés pour inclure facilement les déclarations nécessaires à un ou plusieurs fichiers source.