Programmation C
Modularité
|
|

|
|

|
Construction d'un programme sous la forme de plusieurs fichiers sources
- Problèmes
- Impossibilité pratique de délopper en un seul fichier un programme au delà de quelques milliers de lignes
- Développement avec analyse des problèmes en vue d'une décomposition en sous-problèmes indépendants fréquemment développés par des personnes différentes -> comment marier les contributions élémentaires
?
- Elément de solution : Ecrire un programme en plusieurs fichiers
- Maintien de chaque fichier suffisament petit pour être gérable
- Résolution d'un problème par fichier
- Attribution de tout ou partie des fichiers de façon exclusive aux développeurs
|
Fichiers .c et fichiers .h
- Comment faire pour qu'un fichier source puisse utiliser ce qui a été écrit dans un autre fichier code source ?
- Problème de compilation
- Comment connaître le contenu d'un premier fichier de code source depuis un second fichier de code source pour que le premier puisse correctement utiliser le second ?
- Ecriture de fichiers .c contenant des définitions de variables, de constantes, de fonctions, ... c'est à dire du code qui, une fois compilé, donnera du langage machine
- Ecriture de fichiers .h (entête, header) contenant des déclarations "externes" c'est à dire du code source qui "nomme" ce qui est effectivement défini ailleurs dans un fichier
.c, mais dont la compilation ne conduit pas à l'obtention de langage machine, et qui spécifie au compilateur les informations nécessaires à la vérification que l'utilisation qui est faite de ce
code est correcte :
- Signatures de fonctions
- #define
- struct
- typedef
- enum
- Déclarations de variables globales externes
- Déclarations de constantes globales externes
- ...
- En général, obtention de couples de fichiers nomFichier.c et nomFichier.h où nomFichier.c contient l'ensemble de ce qui a été écrit pour résoudre un problème et nomFichier.h décrit ce qui doit être
"visible" du contenu du fichier nomFichier.c pour que celui-ci puisse être utilisé
- Pour pouvoir utiliser correctement un fichier .c, inclusion via #include du fichier .h de description du fichier.c (généralement au début des fichiers .c)
- Possibilité d'inclure plus d'un fichier .h au début d'un fichier .c
- Compilation directe d'un fichier .h proscrite, compilation des fichiers .h unquement lorsqu'ils sont inclus dans des fichiers .c via #include
- Problème d'édition de liens
- Résultat de la compilation de chaque fichier .c : un fichier objet contenant la traduction du code source du fichier .c en langage machine
- Edition de liens : regroupement du contenu de tous les fichiers objets nécessaires en un seul fichier exécutable
Elimination éventuelle de tout ce qui est non utilisé dans les fichiers objet (fonctions non appelées, variables globales non utilisées...)
- Possibilité de transformer un ou plusieurs fichiers objets en un fichier librairie qui pourra être linké à la place de l'ensemble des fichiers obj
- Résumé :
- Pour la compilation, utilisation des fichiers .c et .h
- Via #include, utilisation des fichiers .h par les fichiers .c
- Via #include, utilisation possible des fichiers .h par les fichiers .h
- Auto-interdiction d'utiliser via #include un fichier .c depuis un fichier .c ou un fichier .h
- Pour l'édition de liens, utilisation des fichiers objets et des librairies (fichiers .c et .h inutiles à cette phase)
- Exemple
#include <stdio.h>
#include <math.h>
#include "Position.h"
const position ORIGINE = { 0.0,0.0};
void afficherPosition(position p) {
printf("[%7.3lf,%7.3lf]", p.x, p.y);
}
double calculerDistanceOrigine(position p) {
return calculerDistance(p,ORIGINE);
}
double calculerDistance(position p1, position p2) {
return sqrt(pow(p1.x-p2.x,2.0)+ pow(p1.y - p2.y, 2.0));
}
|
Position.c
|
#define C1 1.0
enum {
V1 = 2,
V2,
V3
};
typedef struct {
double x;
double y;
} position;
extern const position ORIGINE;
void afficherPosition(position p);
double calculerDistanceOrigine(position p);
double calculerDistance(position p1,position p2);
|
Position.h
|
#include <stdio.h>
#include "Position.h"
//#include "Position.h"
void main(void) {
position p = { -1.1, 2.2 };
afficherPosition(p);
printf("\n");
afficherPosition(ORIGINE);
printf("\n");
printf("\n");
printf("%lf\n", calculerDistanceOrigine(p));
printf("%lf\n", calculerDistance(p,ORIGINE));
printf("%lf %d %d %d\n", C1,V1,V2,V3);
}
|
Modularite.c - Modularite1.exe
|
[ -1.100, 2.200]
[ 0.000, 0.000]
2.459675
2.459675
1.000000 2 3 4
|
|
- Contenu d'un fichier .h
- Signature de fonction
- Entête de la fonction suivie d'un point-virgule
- Indication obligatoire des types, indication optionnelle des noms des paramètres (seuls les types ont à être connus pour vérifier la compilabilité)
Recommandation : pour la compréhensibilité, mettre les noms des paramètres
- Exemples
- double sqrt(double v);
- double sin(double);
- Déclaration de variable globale
- Utilisation du mot réservé "extern" (placé en début de déclaration) pour différencier la syntaxe de déclaration de la syntaxe de définition
- Exemples
- extern int nombreFenetres;
- extern char filename[50];
- Déclaration de constante globale
- Utilisation du mot réservé "extern" (placé en début de déclaration) pour différencier la syntaxe de déclaration de la syntaxe de définition
- Exemple
- extern const int NOMBRE_PLANETES = 9;
- #define
- Pas de syntaxe particulière car un #define n'est pas une définition mais une déclaration
- struct
- Pas de syntaxe particulière car une déclaration de struct n'est pas une définition mais est utilisée pour réaliser des définitions
- enum
- Pas de syntaxe particulière car une enum n'est pas une définition mais une déclaration
- ...
- Placement dans les fichiers .h du strict nécessaire à l'utilisation de ce qui doit être utilisé du fichier .c correspondant
|
#ifndef et #pragma once
- Problème : Au cours de la compilation d'un fichier, généralement impossibilité absolue en langage C de déclarer plusieurs "choses" du même nom, même si ces déclarations sont identiques
- Solution 1 : Utiliser le préprocesseur pour définir dynamiquement ce qui doit être compilé ou non sur la base de l'existence ou non de macros
- Solution 2 : Utiliser le préprocesseur et sa directive #pragma once
- Solution 1
- Pour chaque fichier .h, écriture du contenu du fichier .h dans un
#ifndef NOM_MACRO
#define NOM_MACRO
...
#endif
à la place des points de suspension
- Choix de NOM_MACRO spécifique et exclusif pour chaque fichier .h (idée : convenir d'un format faisant intervenir le nom du fichier)
- A la première inclusion du fichier .h, définition de la macro car elle n'existe pas, et compilation du contenu réel du fichier
Aux inclusions suivantes du fichier .h, existence de la macro donc non redéfinition et non nouvelle compilation du contenu réel du fichier
-> Inclusion unique du contenu réel du fichier
- Problème : compilation possiblement perturbée en cas de nom de macro dupliquée (possible si beaucoup de fichiers)
- Exemple
#ifndef __POSITION__
#define __POSITION__
#define C1 1.0
enum {
V1 = 2,
V2,
V3
};
typedef struct {
double x;
double y;
} position;
extern const position ORIGINE;
void afficherPosition(position p);
double calculerDistanceOrigine(position p);
double calculerDistance(position p1,position p2);
#endif
|
Position.h - Modularite2.exe
|
[ -1.100, 2.200] [ 0.000, 0.000]
2.459675
2.459675
1.000000 2 3 4
|
|
- Solution 2
- Placement en tout début de chaque fichier .h de la directive #pragma once
-> l'ensemble du reste du fichier .h ne sera inclus qu'une fois même si les #include éventuellement imbriqués auraient dû conduire à plusieurs inclusions pour la compilation d'un fichier
.c
- Exemple
#pragma once
#define C1 1.0
enum {
V1 = 2,
V2,
V3
};
typedef struct {
double x;
double y;
} position;
extern const position ORIGINE;
void afficherPosition(position p);
double calculerDistanceOrigine(position p);
double calculerDistance(position p1,position p2);
|
Position.h - Modularite3.exe
|
[ -1.100, 2.200] [ 0.000, 0.000]
2.459675
2.459675
1.000000 2 3 4
|
|
- Pour les développements "modernes", utiliser #pragma once qui évite le problème éventuel des macros dupliquées
|
Mot clé static
- Très difficile de ne pas encourir de problèmes d'homonymie pour les gros projets
- Construction d'un programme par assemblage de centaines, de milliers, ..., de beaucoup trop de fonctions devant toutes porter un nom unique même si elles sont dans des modules différents
- Possibilité de définir des variables globales en grand nombre et dans n'importe quel module
- Utilisation du mot réservé static pour définir des variables, des constantes et des fonctions définies uniquement dans le module (dans le fichier .c) où sont placées leurs définitions
- Logiquement pas de déclaration dans les fichiers .h de ce qui est défini en static dans les fichiers .c
- Fonctionnalité de développement pouvant aussi servir à cacher l'existence de telle ou telle fonction ou de telle ou telle variable ou constante
- Exemple
#include <stdio.h>
static int cpt = 0;
static void afficher(void) {
printf("Module A\n");
}
int valeurCompteurA(void) {
return cpt;
}
void incrementerCompteurA(void) {
cpt++;
printf("Incrementation : ");
afficher();
}
|
Static-ModuleA.c
|
#ifndef ____STATIC_MODULEA____
#define ____STATIC_MODULEA____
int valeurCompteurA(void);
void incrementerCompteurA(void);
#endif
|
Static-ModuleA.h
|
#include <stdio.h>
static int cpt = 0;
static void afficher(void) {
printf("Module B\n");
}
int valeurCompteurB(void) {
return cpt;
}
void incrementerCompteurB(void) {
cpt++;
printf("Incrementation : ");
afficher();
}
|
Static-ModuleB.c
|
#ifndef ____STATIC_MODULEB____
#define ____STATIC_MODULEB____
int valeurCompteurB(void);
void incrementerCompteurB(void);
#endif
|
Static-ModuleB.h
|
#include <stdio.h>
#include "Static-ModuleA.h"
#include "Static-ModuleB.h"
static void afficher(void) {
printf("Affichage de la fonction main\n");
}
void main(void) {
afficher();
incrementerCompteurA();
incrementerCompteurB();
incrementerCompteurA();
incrementerCompteurA();
printf("Compteur A : %d\n", valeurCompteurA());
printf("Compteur B : %d\n", valeurCompteurB());
}
|
Static.c - Static.exe
|
Affichage de la fonction main
Incrementation : Module A
Incrementation : Module B
Incrementation : Module A
Incrementation : Module A
Compteur A : 3
Compteur B : 1
|
|
- Solution pas parfaite
- Fonctionnalité relativement limitée se limitant à permettre de définir plusieurs fonctions portant le même nom dans un même programme pour peu qu'elles soient définies en static et placées dans
des fichiers .c différents
- ...
|
Types struct opaques
- Pas d'existence en C de la possibilité de définir des champs de struct en "public" ou en "private"
- Par défaut : public
- Pas d'existence de la notion d'encapsulation
- Problème car pas forcément souhaité et/ou souhaitable
- que l'utilisateur d'une struct connaissent les champs de cette struct
- que les champs d'une struct soient accessibles directement en lecture et/ou écriture (accès sans contrôle particulièrement en écriture)
- Solution : l'opacification
- Déclaration partielle des struct dans les fichiers .h et déclaration complète dans les fichiers .c correspondants
- Déclaration partielle : Déclaration d'une struct en indiquant seulement son nom c'est à dire sans indiquer ses champs
- Déclaration complète : Déclaration classique d'une struct avec indication exhaustive de ses champs
- Façon de faire possible car une déclaration complète viendra toujours "finir" la déclaration partielle
- Intérêt : définition possible de pointeurs sur ces structs
- Syntaxe
struct nomStruct;
- Quelle utilisation ?
- Toute déclaration de variable externe, de constante externe, de signature de fonction
- Toute déclaration ou définition de pointeur sur struct
- Quelles restrictions ?
- Impossible à utiliser pour une définition de variable, de constante, de fonction
- Impossible à utiliser quand les noms des champs doivent être utilisés
- En pratique
- Utilisation de pointeurs pour l'interface de programmation d'une struct opacifiée -> pas d'obligation d'avoir une déclaration complète
- Existence d'une (ou plusieurs) fonction permettant de créer une struct opacifiée par allocation dynamique de mémoire et de retourner cette struct
- Existence d'une fonction permettant de réaliser toutes les opérations nécessaires à la libération d'une struct opacifiée
- Existence de toutes les fonctions nécessaires à l'accès aux champs masqués
- Setters
- Getters
- Fnctions spécifiques
- Exemple
#ifndef ____OPACIFICATION_SOMMET____
#define ____OPACIFICATION_SOMMET____
typedef struct sommet sommet;
sommet* creerSommet(void);
void affecterXSommet(sommet* s, double x);
double retournerXSommet(sommet* s);
void affecterYSommet(sommet* s, double y);
double retournerYSommet(sommet* s);
void libererSommet(sommet* s);
#endif
|
Opacification-Sommet.h
|
#include <stdio.h>
#include <stdlib.h>
#include "Opacification-Sommet.h"
struct sommet {
double x;
double y;
};
sommet* creerSommet(void) {
return (sommet*) calloc(1, sizeof(sommet));
}
void affecterXSommet(sommet* s, double x) {
s->x = x;
}
double retournerXSommet(sommet* s) {
return s->x;
}
void affecterYSommet(sommet* s, double y) {
s->y = y;
}
double retournerYSommet(sommet* s) {
return s->y;
}
void libererSommet(sommet* s) {
if (s != NULL) {
free(s);
}
}
|
Opacification-Sommet.c
|
#ifndef ____OPACIFICATION_COMPTEUR____
#define ____OPACIFICATION_COMPTEUR____
typedef struct compteur * compteur;
compteur creerCompteur(void);
void incrementerCompteur(compteur c);
int retournerValeurCompteur(compteur c);
void libererCompteur(compteur c);
#endif
|
Opacification-Compteur.h
|
#include <stdio.h>
#include <stdlib.h>
#include "Opacification-Compteur.h"
struct compteur {
int cpt;
};
compteur creerCompteur(void) {
return (compteur) calloc(1, sizeof(compteur));
}
void incrementerCompteur(compteur c) {
c->cpt++;
}
int retournerValeurCompteur(compteur c) {
return c->cpt;
}
void libererCompteur(compteur c) {
if (c != NULL) {
free(c);
}
}
|
Opacification-Compteur.c
|
#include <stdio.h>
#include "Opacification-Sommet.h"
#include "Opacification-Compteur.h"
int main(void) {
sommet *s = creerSommet();
affecterXSommet(s, 2.83);
printf("Abscisse de s : %lf\n", retournerXSommet(s));
affecterYSommet(s, 1.12);
printf("Ordonnee de s : %lf\n", retournerYSommet(s));
libererSommet(s);
printf("\n");
compteur c1 = creerCompteur();
compteur c2 = creerCompteur();
incrementerCompteur(c1);
incrementerCompteur(c1);
incrementerCompteur(c1);
incrementerCompteur(c2);
printf("Valeur de c1 : %d\n", retournerValeurCompteur(c1));
printf("Valeur de c2 : %d\n", retournerValeurCompteur(c2));
libererCompteur(c1);
libererCompteur(c2);
printf("\n");
return 0;
}
|
Opacification-Main.c - Opacification-Main.exe
|
Abscisse de s : 2.830000
Ordonnee de s : 1.120000
Valeur de c1 : 3
Valeur de c2 : 1
|
|
- En conclusion
- Accès directs aux champs seulement réalisés dans le module où la déclaration complète de la struct est réalisée les rendant possibles
- Rapprochement avec ce qui se fait en programmation orientée objet
- Implantation d'un équivalent aux constructeurs
- Implantation d'un équivalent aux destructeurs
- Implantation de getters et de setters pour accéder aux champs depuis l'"extérieur"
- Remarque : possibilité d'étendre l'opacification à l'immuabilité (pas de setter, pas de fonction ayant pour conséquence la modification des valeurs des champs)
|