Aller au contenu principal

Programmation C · L2 · Section 6/12

Modularité et compilation

Progression

Points d’expérience : XPSérie de jours consécutifs : · —Progression du module : — / —compris

#Modularité et compilation

Un projet C grandit mal si tout vit dans un fichier. Ce chapitre pose l’architecture en modules : un en-tête qui expose l’interface, un .c qui garde l’implémentation, des gardes d’inclusion qui rendent les double-inclusions inoffensives. Puis nous suivons la chaîne de compilation à l’échelle d’un projet : compiler chaque module séparément, lier, mesurer l’effet des optimisations. L’ossature finale est un Makefile avec cibles de test et sanitizers, prête pour Vaλisp.

#1. Modules : l’en-tête est un contrat

Le fichier d’en-tête ne décrit que ce que l’appelant doit connaître : types exportés, prototypes, constantes. Tout le reste vit dans le .c.

cc

1/* compteur.h — contrat du module */2#ifndef COMPTEUR_H3#define COMPTEUR_H4 5typedef struct Compteur Compteur;   /* type opaque : la structure est privée */6 7Compteur *compteur_init(void);8void      compteur_incremente(Compteur *c);9long      compteur_valeur(const Compteur *c);10void      compteur_destroy(Compteur *c);11 12#endif /* COMPTEUR_H */

La garde #ifndef COMPTEUR_H / #define / #endif rend le fichier inoffensif inclus plusieurs fois : la deuxième inclusion trouve COMPTEUR_H défini et saute tout le contenu. Sans elle, deux inclusions d’un même en-tête (fréquent via des en-têtes intermédiaires) redéclarent tout et cassent la compilation. #pragma once est une alternative non standard mais universellement supportée ; la garde portable reste la référence.

Le .c correspondant détient la structure et inclut son propre en-tête (pour que le compilateur vérifie leur cohérence) :

cc

1/* compteur.c */2#include <stdlib.h>3#include "compteur.h"4 5struct Compteur { long n; };6 7Compteur *compteur_init(void) {8    Compteur *c = malloc(sizeof *c);9    if (c) c->n = 0;10    return c;11}12void compteur_incremente(Compteur *c) { if (c) ++c->n; }13long compteur_valeur(const Compteur *c) { return c ? c->n : 0; }14void compteur_destroy(Compteur *c) { free(c); }

Deux règles d’hygiène : "" pour les en-têtes du projet, &lt;&gt; pour les en-têtes système ; et ne jamais #include un .c : cela duplique les définitions et provoque des symboles définis plusieurs fois à l’édition de liens.

#2. Compiler séparément, lier ensuite

shsh

1cc -std=c17 -Wall -Wextra -Wpedantic -c compteur.c -o compteur.o2cc -std=c17 -Wall -Wextra -Wpedantic -c main.c     -o main.o3cc main.o compteur.o -o app

Chaque .c se compile indépendamment : changer compteur.c ne recompile pas main.c. Le linker assemble ensuite les fichiers objets. Les deux erreurs canoniques se lisent ainsi :

  • undefined reference to 'compteur_init' : le symbole est utilisé mais aucun objet ne le définit. Cause : .o oublié sur la ligne de lien, ou fonction déclarée dans le .h mais jamais écrite.
  • multiple definition of 'compteur_init' : deux objets définissent le même symbole. Cause typique : un .c inclus par un autre.

La déclaration static sur une fonction ou une variable globale la restreint à son fichier : c’est le mécanisme d’encapsulation en C, à utiliser pour tout ce qui n’est pas dans l’en-tête.

#3. Optimisations : ce que l’on achète et à quel prix

  • -O0 : aucune optimisation, compilation rapide, débogage pas à pas fidèle à la source. Le mode par défaut.
  • -O1 / -O2 : inlining, élimination de code mort, meilleure allocation de registres. -O2 est le standard de production.
  • -O3 : ajoute vectorisation et déroulage agressifs ; parfois plus rapide, parfois plus gros et pas plus rapide.
  • -Os : optimise en taille, utile pour l’embarqué.
  • -flto : optimisation à l’édition de liens, permet d’inliner à travers les frontières de fichiers. Se place à la compilation et au lien.

L’ordre de grandeur à retenir : entre -O0 et -O2, un calcul intensif gagne couramment un facteur 2 à 10 ; le code dominé par des entrées/sorties ne bouge pas. Mesurez toujours (time ./app) avant de conclure. Et souvenez-vous du chapitre d’introduction : un comportement indéfini latent peut se manifester précisément quand l’optimiseur commence à supposer des choses.

#4. Un Makefile incrémental avec cibles de test

makemake

1CC      := cc2CFLAGS  := -std=c17 -Wall -Wextra -Wpedantic3SAN     := -g -fsanitize=address,undefined4OBJS    := main.o compteur.o5 6app: $(OBJS)7	$(CC) $(OBJS) -o $@8 9%.o: %.c compteur.h10	$(CC) $(CFLAGS) -c $< -o $@11 12test: app13	./app --selftest14 

Points qui font la différence : %.o: %.c compteur.h recompile aussi quand l’en-tête change (l’oublier, c’est livrer des objets désynchronisés, bug des plus sournois) ; les recettes commencent par une tabulation, pas des espaces ; .PHONY évite qu’un fichier nommé test ne capture la cible.

#Atelier : structurer Vaλisp

Reprenez le code de Vaλisp (les jalons des chapitres précédents) et découpez-le en modules :

  • value.h / value.c : représentation des valeurs, constructeurs ;
  • arena.h / arena.c : allocateur ;
  • reader.h / reader.c : tokenizer et parseur ;
  • interp.h / interp.c : environnement et évaluation.

Chaque en-tête porte sa garde, chaque .c inclut son propre .h, toute fonction non exportée est static. Écrivez ensuite le Makefile ci-dessus adapté à cette liste d’objets.

Jalon observable :

shsh

1make            # recompile uniquement ce qui a changé2touch value.h && make   # recompile value.o ET reader.o ET interp.o (ils incluent value.h)3make test       # exécute ./app --selftest et affiche OK4make asan       # même test sous AddressSanitizer + UBSan, sans fuite ni erreur
Vérifier que la dépendance aux en-têtes fonctionne

Après touch value.h; make, la sortie doit montrer la recompilation de tous les .c qui incluent value.h, pas seulement value.c. Si un module n’est pas recompilé, sa règle %.o: %.c ne déclare pas value.h comme dépendance : ajoutez-la. C’est exactement le bug de désynchronisation que la règle doit empêcher.

Que signifie l'erreur « multiple definition of 'f' » à l'édition de liens ?
Que signifie l'erreur « multiple definition of 'f' » à l'édition de liens ?