Compilation séparée¶
Warning
Ce cours a été automatiquement traduit des transparents de M.Noyer par Félix qui continue le travail fait par Lorentzo et Elowan et Mehdi, nous ne nous accordons en aucun cas son travail, ce site à pour seul but d'être plus compréhensible pendant les périodes de révision que des diaporamas.
Crédits
- Ce cours d'Anne Canteault sur la compilation séparée.
- Ce chapitre sur les directives préprocesseur d'un cours de OpenClassRoom
- Ce tutoriel sur les Makefile
Objectif et principe¶
- Plutôt que de mettre tout le code dans un seul fichier, on le distribue dans plusieurs endroits : des modules.
- Dans un fichier donné, on met par exemple des outils de calculs mathématiques, dans un autre des fonctions de manipulation de chaînes de caractères etc.
- Cela permet de bien organiser un projet, d'améliorer la lisibilité du code et de faciliter la maintenance : en cas de changement de quelques lignes de code seulement, on n'a pas besoin de tout recompiler mais seulement les fichiers concernés.
Rappel sur les directives de préprocesseur¶
La directive include
¶
- La syntaxe pour inclure l'interface d'un fichier de bibliothèque est de placer son nom entre chevrons comme dans
<stdio.h>
. - Pour tout autre fichier, et en particulier pour NOS FICHIERS PERSONNELS, on écrit son nom entre guillements ; par exemple
#include "produit.h"
. - Dans tous les cas, le préprocesseur insère le contenu du fichier cible à la place du
#include nomDuFichier
Rappels sur la directive define
¶
- Lorsqu'il lit
#define TOTO Tata
, le préprocesseur remplace toute occurrence du premier mot par le second (mais pas dans les commentaires ni les expressions entre guillemets). - On peut effectuer des calculs arithmétiques en utilisant des constantes introduites par cette directive.
Hors programme.
On peut se servir de #define
pour définir des macros sans paramètre
- Observer les passages à la ligne avec un antislash « \ » comme en \(\texttt{Python}\)
- Après compilation et exécution :
On peut se servir de #define
pour définir des macros Avec paramètres :
- Après compilation et exécution :
Définitions sans valeurs de substitutions ♥¶
Parfois, on écrit juste :
Cette déclaration, combinée avec l'usage des conditions de préprocesseur (comme # ifndef
) permet d'éviter les inclusions infinie (lorsqu'un fichier A
importe un fichier B
qui importe A
).
Compilation séparée¶
Un projet en deux fichiers¶
- Dans un \(1\)er fichier \(\texttt{main.c}\) :
- Dans un \(2\)nd fichier \(\texttt{produit.h}\) (qui n'est pas vraiment un fichier d'en-tête).
Compilation en une fois¶
- On peut compiler ce projet à \(2\) fichiers en une seule fois avec la commande
gcc main.c -o main
- Le préprocesseur va juste regrouper (en les concaténant et en supprimant les commentaires) les deux fichiers en un seul et produira l'exécutable à partir de ce fichier concaténé.
- Le seul avantage de cette démarche est de rendre le code plus lisible mais on n'a rien gagné en terme de maintenance du projet.
Introduction d'un fichier d'en-tête¶
- Séparons les prototypes des corps des fonctions de \(\texttt{produit.h}\).
- On crée pour cela un fichier d'interface \(\texttt{produit.h}\) contenant les prototypes et un fichier \(\texttt{produit.c}\) contenant les codes.
- Dans le fichier \(\texttt{produit.c}\), incluons l'en-tête :
- Toutes les fonctions de produit.c destinées à être appelées par d'autres fichiers que produit.c lui-même devraient avoir leur prototype listé dans \(\texttt{produit.h}\).
- Comme de plus les interfaces listées dans produit.h correspondent à des fonctions dont le code est écrit ailleurs, on peut l'indiquer explicitement par le mot clé
extern
. - Le fichier d'en-tête \(\texttt{produit.h}\) contient donc :
Mot clé extern
pour les fonctions¶
- Pour une fonction les deux déclarations suivantes sont équivalentes
extern int fct(char c, float x)
etint fct(char c, float x)
. - La fonction sera utilisable dans les \(2\) cas à l'extérieur du fichier source. Son nom devient ce qu'on appelle un identificateur externe, c'est à dire qu'il est accessible à l'éditeur de lien.
- Le mot
static
empêche que l'identificateur de la fonction soit utilisé à l'extérieur du fichier source où elle est définie. On parle alors de fonction cachée ou privée.
static int fct(char c, float x)...
Mot clé extern
pour les variables¶
- Le mot clé extern devrait être réservé aux redéclaration
- Une déclaration contenant une initialisation constitue toujours une définition.
int x = 2;
etextern int x = 2;
sont équivalents.extern
ne sert à rien. - Une déclaration avec extern sans initialisation constitue toujours une redéclaration (donc jamais une définition) d'une variable pouvant , éventuellement, être définie ailleurs (donc dans le même fichier
source ou un autre).
extern int a
; fait référence à une variablea
définie ailleurs. - On retient la règle suivante de bonne pratique :
- Dans les définitions on n'utilise jamais
extern
et on s'interdit les déclarations multiples au sein d'un même fichier. On peut ou non initialiser. - Dans les redéclarations On utilise systématiquement
extern
et on s'abstient de toute initialisation.
- Dans les définitions on n'utilise jamais
Introduction d'un fichier d'en-tête¶
Fabriquons le fichier objet relatif au \(\texttt{main.c}\) :
Fabriquons le fichier objet relatif au \(\texttt{produit.c}\) :
Compilons l'ensemble
Remarquons qu'on peut toujours compiler tout en même temps :
Importation unique¶
- Comme on le constate, le fichier d'en-tête est importé deux fois :
- une première fois par \(\texttt{produit.c}\),
- une seconde fois par \(\texttt{main.c}\)
- Dans le fichier exécutable généré par \(\texttt{gcc}\), l'inclusion apparaît donc deux fois.
- Pour éviter cela, on crée une constante
PRODUIT_H
. - Cette constante, partagée par tout le projet, est définie la première fois qu'on inclue le fichier \(\texttt{produit.h}\).
- La seconde fois qu'on importe le fichier d'en-tête, la constante est déjà définie et on se débrouille pour empêcher une nouvelle inclusion des prototypes de \(\texttt{produit.c}\).
- Il y a une convention de nommage pour cette constante : nom du fichier d'en-tête mis en majuscules (mais sans l'extension \(\texttt{.h}\)) ; le tout suivi de \(\_\texttt{H}\).
ifndef
¶
- L'idée est de mettre tous les prototypes et déclarations diverses de \(\texttt{produit.h}\) dans la branche positive d'une instruction conditionnelle du préprocesseur.
- Si la condition est vérifiée, alors ces prototypes et déclarations sont bien ajoutés au fichier généré. Sinon, ils sont ignorés.
- Le fichier produit.h devient alors :
- Dans le fichier \(\texttt{produit.h}\) on a ajouté l'instruction de préprocessing
#ifndef PRODUIT_H
dont l'esprit est de signifier « si la constante PRODUIT_H n'a pas déjà été déclarée alors voici les prototypes à inclure : ... ». - et on compile en \(3\) temps :
Makefile¶
Présentation¶
- Il ne rentre pas dans les compétences exigibles des étudiants de CPGE qu'ils sachent écrire un \(\texttt{Makefile}\). C'est toutefois un outil très pratique pour automatiser la compilation d'un projet et optimiser le temps passé pour cette opération.
- On ne donne ici que quelques rudiments de cette technique qui nécessiterait un manuel à part entière. Consulter par exemple developpez.com pour plus d'informations.
Syntaxe¶
Un Makefile est constitué d'un ensemble de règles de la forme :
Utilisation¶
Supposons qu'on ait créé le fichier Makefile dans le répertoire courant. Il y a deux possibilités d'appel :
- Exécution de la première règle rencontrée : Cela s'obtient très facilement en tapant dans un terminal :
- Exécution d'une règle spécifique : Il suffit de saisir dans un terminal.
Évaluation des règles¶
L'évaluation d'une règle se fait en plusieurs étapes :
- Analyse des dépendances : si une dépendance est la cible d'une autre règle du Makefile, cette règle est préalablement évaluée.
- Exécution des commandes : Après analyse des dépendances, si
- la cible ne correspond pas à un fichier existant
- ou si un fichier dépendance est plus récent que la règle, les différentes commandes sont exécutées.
Un Makefile minimum¶
prod : produit.o main.o
gcc −o prod produit.o main.o
produit.o : produit.c
#compilation sans lien, avec messages d'erreur et optimisation
gcc −o produit.o −c produit.c −Wall −O3
main.o : main.c
#compilation sans lien, avec messages d'erreur et optimisation
gcc −o main.o −c main.c −Wall −O3
- La commande \(\texttt{make}\) lancée dans un terminal depuis le répertoire courant cherche à réaliser la première règle rencontrée : \(\texttt{prod}\).
- Il y a deux dépendances \(\texttt{produit.o}\) et \(\texttt{main.o}\) qui sont d'ailleurs des règles.
Dépendances de la règle \(\texttt{prod}\)¶
- On commence par exécuter la règle \(\texttt{produit.o}\) (\(1\)ere dépendance de la règle \(\texttt{prod}\)). L'unique dépendance de cette règle est un fichier \(\texttt{produit.c}\) qui n'est pas une règle. Le Makefile exécute la commande de cette règle \(\texttt{produit.o}\) si une des conditions suivantes est réalisée :
- le fichier \(\texttt{produit.o}\) n'existe pas dans le répertoire courant,
- ou bien le fichier \(\texttt{produit.c}\) est plus récent que \(\texttt{produit.o}\). Cela signifie que \(\texttt{produit.c}\) a été modifié depuis la dernière création du module objet \(\texttt{produit.o.}\) Il faut donc reconstruire ce dernier.
- Si une des deux conditions ci-dessus est vérifiée, on exécute la commande
gcc -o produit.o -c produit.c -Wall -O3
(compilation sans édition de lien \(\texttt{-c}\), avec Warnings \(\texttt{-Wall}\) et optimisation complète \(\texttt{-O3}\)). - Une fois cette règle exécutée, on lance la règle \(\texttt{main.o}\) qui fonctionne sur le même principe.
Exécution de la première règle¶
- Toutes les dépendances de prod étant construites, on exécute la commande associée à la règle \(\texttt{prod}\) si :
- l'un des fichiers objets \(\texttt{.o}\) n'existe pas dans le répertoire courant,
- ou l'un des fichiers objets \(\texttt{.o}\) est plus récent que \(\texttt{prod}\).
- Dans ce cas, c'est la commande \(\texttt{gcc -o prod produit.o main.o}\) qui est lancée
Nettoyage et régénération¶
Avec le Makefile précédent, on ne peut pas créer plusieurs exécutables, ni supprimer les fichiers intermédiaires \(\texttt{.o}\) (ils restent sur le disque dur) ni forcer la régénération complète du projet.
- En effet, les fichiers objets \(\texttt{produit.o}\) et \(\texttt{main.o}\) sont restés sur le disque dur. Lorsqu'on relance la commande \(\texttt{make}\), on obtient le message suivant :
On rajoute alors trois règles qui portent, par convention, les noms \(\texttt{all}\), \(\texttt{clean}\), \(\texttt{mrproper}\) :
- \(\texttt{all}\) : on la fait suivre de la ou des règles de construction d'exécutables ; pour nous c'est seulement \(\texttt{prod}\).
- \(\texttt{clean}\) : on décide ici de lui faire supprimer tous les fichiers objets (mais ce pourrait être d'autres types de fchiers).
- \(\texttt{mrproper}\) : un nettoyage complet. Le Makefile engendre un fichier exécutable \(\texttt{prod}\) et des fichiers objets. On déclare \(\texttt{clean}\) comme dépendance, ce qui a pour effet de supprimer les fichiers objets. Puis il reste à supprimer le fichier exécutable \(\texttt{prod}\).