Tous | Tutoriels du Mercredi | Basic Casio | C/C++/ASM | LuaFX | Graphisme | Transferts | Logiciels | Diverses astuces

Calculatrice
Toutes
Graphs 35 à 100
Graph 25
Graphs 75/85/95 (SD)
Graph 100(+)
Classpad 300/330 (+)
Fx-CG 10/20
Fx-CP 400
Graph 90+E

Retour à la liste des tutoriels
Tutoriel Casio : TDM 07 : Écrire des programmes C avec plusieurs fichiers
Tutoriel rédigé le : 2018-10-05 19:01  par Lephenixnoir  Catégorie : Tutoriels du Mercredi  Calculatrice : Toutes

Discutez de ce tutoriel sur le forum >> Voir le sujet dédié (15 commentaires)

TDM 07 : Écrire des programmes C avec plusieurs fichiers
Le Tuto Du Mercredi [TDM] est une idée qui fut proposée par Ne0tux. Un mercredi sur deux, nous postons un tutoriel sur l'Utilisation de la calculatrice, le Transfert, les Graphismes, la Programmation, ou encore la Conception de jeu. Aujourd'hui est le jour de la 7ème édition!

Écrire des add-ins avec plusieurs fichiers sources

Niveau : ★ ★ ☆ ☆ ☆

Ce tutoriel de C est destiné aux débutants du langage, pour qui écrire un add-in avec plusieurs fichiers de code est synonyme de variables globales perdues, pas moyen de communiquer entre fichiers... et aussi à tous ceux qui n'arrivent pas à se convaincre que les fichiers d'en-tête en .h servent à quelque chose.

Vous verrez que ce n'est pas aussi compliqué que ça en l'air, le problème est que le compilateur ne regarde qu'un seul fichier à la fois, donc il faut lui dire ce qu'il doit s'attendre à voir dans les autres. Et c'est parti !

Partie I - Comment les programmes C sont compilés

Contrairement aux programmes Basic, quand vous compilez un add-in qui possède de nombreux fichiers de code (aussi dits « fichiers sources » ou « fichiers .c »), tous les fichiers sont compilés indépendamment. Pour chaque fichier source, la compilation crée un fichier fichier objet (.o ou .obj) qui contient du code assembleur. Ce n'est qu'une fois tous les fichiers compilés que l'on commence à les réunir pour former la première version complète de l'add-in (ici game.elf).


L'intérêt de cette méthode par rapport à compiler tous les fichiers d'un coup, est que si vous avez modifié uniquement gui.c depuis la dernière compilation, vous n'avez que gui.c à recompiler, les autres fichiers objets peuvent être réutilisés. Cette distinction qui peut paraître futile pour un petit projet est cruciale pour les gros programmes qui prennent de longues minutes à compiler entièrement.

Cela pose par contre un problème : quand on crée une nouvelle partie, main.c fait appel à la fonction map_generate() qui est dans map.c... mais comme les deux fichiers source sont compilés indépendamment, comment le compilateur peut-il savoir de quelle fonction je parle et où elle se
trouve
?

Eh bien, c'est le programmeur qui donne au compilateur les informations sur la fonction. Il lui indique son nom, ses arguments, le type de sa valeur de retour... c'est-à-dire son prototype (aussi appelé « signature »). Quant à savoir où elle est, le compilateur s'en moque ! Il indique juste dans le code assembleur « ici il faut appeler map_generate() » et ensuite c'est le linker, dans la dernière phase de compilation, qui recolle les morceaux et vérifie que toutes les fonctions sont là.

Pour les variables globales, ça se passe exactement pareil : le programmeur indique au compilateur le nom et le type de la variable, ce qui suffit pour compiler ; ensuite le linker recolle les morceaux et vérifie que la variable mentionnée existe bel et bien dans un des fichiers.

Allons donc explorer comment diable on peut indiquer au compilateur qu'il existe dans d'autres fichiers des variables globales et des fonctions que l'on veut utiliser.

Partie II - Déclarations et définitions

Désormais, on veut non seulement pouvoir créer des variables ou des fonctions, mais parfois on veut aussi pouvoir indiquer au compilateur que des variables ou fonctions existent quelque part ailleurs, sans dire où et sans dire ce qu'elles contiennent. Ces deux actions sont appelées respectivement définition et déclaration.

Les définitions, c'est ce que vous utilisez d'habitude : ça crée un objet qui contient des données ou du code. Tout ce qui porte un nom et existe dans un programme C est défini quelque part.

/* Définition, car crée des données */
int random_number = 42;

/* Définition, car crée du code */
int randomize(int x)
{
    return x + 42;
}

Vous n'avez pas le droit de définir plusieurs objets avec le même nom, sinon le compilateur ne pourra pas savoir duquel vous parlez. Le message d'erreur que vous aurez si vous essayez le faire sous le SDK, vous l'avez sans doute déjà croisé (« symbole » est juste un synonyme de « nom » pour le compilateur) :

** L2300 (E) Duplicate symbol "_random_number" in "C:\..."

Par contre vous pouvez déclarer vos objets autant de fois que vous le voulez, quand ils existent autant le faire savoir à tout le monde. Soit dit en passant, lorsque vous définissez une fonction ou une variable globale, le compilateur va en déduire qu'elle existe (dans le fichier où vous venez de la définir), ce qui la déclare implicitement.

Maintenant, supposons que vous avez défini dans map.c une fonction map_generate() et que vous voulez la déclarer dans main.c pour pouvoir l'y utiliser. Le compilateur a besoin d'un certain nombre d'informations qui sont :

• Le nom de la fonction
• Combien elle a de paramètres et quels sont leurs types
• Quel est le type de la valeur de retour
• (facultatif) Le nom des arguments (totalement ignoré, peut être n'importe quoi)

Essentiellement ces informations forment le prototype de map_generate(). Pour écrire un prototype, tout ce que vous avez à faire est de copier la première ligne de la fonction (tout jusqu'à l'accolade ouvrante) et ajouter un point-virgule. Par exemple, si la fonction est définie comme ceci :

/* Dans map.c */
int map_generate(double density, int use_noise, int enable_trees)
{
    /* ... */
}

Alors vous pouvez la déclarer partout ailleurs, notamment dans un autre fichier, en y ajoutant la ligne suivante :

int map_generate(double density, int use_noise, int enable_trees);

Voilà un exemple d'utilisation tout bête.

/* Dans main.c */
int map_generate(double density, int use_noise, int enable_trees);

void new_game(void)
{
    map_generate(0.1, 0, 1);
    /* ... */
}

Une part de tarte, n'est-ce-pas !

Si vous êtes malin, vous avez peut-être essayé de mettre un faux prototype dans votre programme. C'est très dangereux car le compilateur, incapable de vérifier ce que vous racontez puisqu'il n'a pas accès au code de la fonction, va vous croire sur parole et tout peut exploser quand vous appelerez la fonction ! Vous devez toujours faire attention à ce que vos prototypes soient rigoureusement exacts, et les fichiers d'en-tête vous y aideront dans la section suivante.

Mais avant cela, voyons comment on peut faire pour permettre à main.c d'utiliser une variable globale définie dans gui.c. C'est presque pareil, sauf que cette fois on utilise le mot clé extern qui sert à dire que la variable est... à l'extérieur !

/* Dans gui.c */
int number_of_widgets = 42;

/* Dans main.c */
extern int number_of_widgets;

void new_widget(const char *name)
{
    number_of_widgets++;
    /* ... */
}

Comme vous pouvez le voir, je n'ai pas recopié le =42 car le fichier main.c doit juste indiquer au compilateur que la variable existe et se moque de savoir à quelle valeur elle a été initialisée. De plus une déclaration ne doit pas créer de données ; on ne peut donc initialiser une variable que lors de sa définition.

Vous pouvez aussi mettre extern dans un prototype de fonction, mais ça ne sert à rien car il y est automatiquement ! S'il n'y a pas une accolade ouvrante avec du code dedans, le compilateur comprend tout de suite qu'il s'agit d'une déclaration.

Si tout s'est bien passé, vous pouvez maintenant partager vos fonctions et variables globales entre plusieurs fichiers. Continuez quand même à lire, la suite est importante !

Partie III - Les fichiers d'en-tête

Maintenant que vous avez mis des prototypes et des déclarations extern dans tous vos fichiers, vous pouvez enfin utiliser les objets définis dans les autres fichiers. Mais que se passe-t-il si vous voulez les modifier ?

Mettons que j'ajoute un paramètre à la fonction map_generate(), il faut que je modifie la définition de la fonction, mais aussi tous les prototypes qui se trouvent partout ailleurs dans le code ! C'est très fastidieux et le risque de se planter ou d'en oublier est énorme !

Eh oui, plus vous dupliquez la même information un grand nombre de fois, plus vous prenez le risque de ne pas réussir à gérer toutes ces copies !

C'est ici que les fichiers .h interviennent pour nous sauver la vie. Pour rappel, les fichiers d'en-tête sont ceux qu'on inclut en utilisant #include :

#include <stdio.h>

En fait, cette commande copie littéralement les contenus du fichier stdio.h (qui se trouve dans un répertoire du SDK) au milieu du fichier source où le #include a été écrit. Vous voyez où je veux en venir ?

C'est ça, vous pouvez mettre toutes vos déclarations externes et vos prototypes dans un fichier d'en-tête ! Ensuite vous n'avez qu'à inclure cet en-tête chaque fois que vous voulez utiliser une fonction définie dans un autre fichier. Par exemple :

/* Dans map.c */
int map_generate(double density, int use_noise, int enable_trees)
{
    /* ... */
}

/* Dans map.h */
int map_generate(double density, int use_noise, int enable_trees);

/* Dans main.c */
#include "map.h"

void new_game(void)
{
    map_generate(0.1, 0, 1);
    /* ... */
}

Vous noterez que j'ai utilisé des guillemets "" au lieu de chevrons <> dans le #include. C'est la règle quand vous voulez utiliser un fichier d'en-tête qui est à côté de vos fichiers source (dans le même dossier,
j'entends).

Il est très important d'inclure map.h dans map.c parce que même si vous n'avez désormais plus que deux copies de vos prototypes de fonctions, il faut encore qu'ils soient les mêmes ! En incluant map.h dans map.c, vous donnez au compilateur la possiblité de vérifier que votre déclaration est conforme à votre définition, et cela renforce grandement la sécurité de votre programme.

Je vous conseille très fortement d'écrire un fichier d'en-tête par fichier source et ne pas succomber à la tentation de mettre toutes les déclarations dans le même fichier mon_super_projet.h. Je dois ajouter que jamais, jamais, jamais vous ne devez mettre de définitions dans un fichier d'en-tête. Le code, il est dans les sources, et nulle part ailleurs.

Résumé

Le compilateur traite tous les fichiers sources indépendamment, il a donc besoin de votre aide lorsque vous utilisez des objets définis dans d'autres fichiers.

Lorsque vous créez un fichier map.c dont les fonctions vont être utilisées ailleurs, créez immédiatement un fichier map.h et copiez-y les prototypes de toutes les fonctions qui doivent être « publiques », plus des déclarations externes pour toutes les variables globales « publiques » :

/* Dans map.c */
#include "map.h"

int generated_maps = 0;

int map_generate(double density, int use_noise, int enable_trees)
{
    generated_maps++;
    /* ... */
}

/* Dans map.h */
extern int generated_maps;

int map_generate(double density, int use_noise, int enable_trees);

Ensuite, tous les fichiers qui veulent utiliser les fonctions de map.c n'ont qu'à inclure map.h pour y avoir accès :

/* Dans main.c */
#include "map.h"

void new_game(void)
{
    map_generate(0.1, 0, 1);
    /* ... */
}

Voilà, vous savez désormais tout sur les interactions entre fichiers dans les programmes à plusieurs fichiers sources ! Si vous avez des questions, lâchez-vous dans les commentaires !

Liens utiles :
Voir le TDM précédent : TDM n°6 – Principes d'animation.

Fichier joint


Discutez de ce tutoriel sur le forum >> Voir le sujet dédié (15 commentaires)

Planète Casio v42 © créé par Neuronix et Muelsaco 2004 - 2018 | Il y a 23 connectés | Nous contacter | Qui sommes-nous ? | Licences et remerciements

Planète Casio est un site communautaire indépendant, géré bénévolement et n'est donc pas affilié à Casio | Toute reproduction de Planète Casio, même partielle, est interdite
Les fichiers, programmes et autres publications présents sur Planète Casio restent la propriété de leurs auteurs respectifs et peuvent être soumis à des licences ou des copyrights.
CASIO est une marque déposée par CASIO Computer Co., Ltd