Les fonctions en C sont
toutes externes puisque l’on ne peut pas avoir de fonctions déclarées
ou définies à l’intérieur d’une autre fonction.
Masquage
d’information : la notion d’interface et de confidentialité
Il est possible de rendre une fonction ou un
objet externe invisible de l’extérieur du fichier dans lequel
elles sont définies (confidentiel). Ils ne constituent pas (ils
ne font pas partie de) l’interface du fichier source, du module.
Ceci est obtenu en faisant précéder la définition de
l’objet ou de la fonction par le
mot cléstatic (attention,
ce mot-clé static a un tout
autre sens pour une varaiablle déclarée dans une fonction).
static int
top-secret (void) ; /* Une fonction
confidentielle */
Par défaut, une fonction et un
objet externe ne sont pas confidentielles.
extern int publique (void) ;
constitue une déclaration externe
(non confidentielle) comme ceele-ci :
int public (void) ;
On a donc en C une forme rudimentaires de modules avec masquage de
l’information. Un fichier est donc composé :
De définitions
externes (exportées). Il s’agit de définitions
d’objets et de fonctions
externes visibles de l’extérieur du fichier. Ces fonctions peuvent
être appelées de l’extérieur, c’est à dire à partir d’autres
fichiers sources. Quant aux objets, ils sont membres du fichier
(one verra ce que cela veut dire en terme d’allocation mémoire), mais
accessibles également à partir d’autres fichiers. Ces fonctions et
objets constituent l’interface du module. S’il est, rappelons
le, déconseillé d’utiliser de façon immodérée les variables
globales (Les fonctions doivent
de façon prioritaire communiquer explicitement via leurs
paramètres, il est encore plus déconseillé de faire de la
communication inter-fichiers via des variables
(communiquer de préférence via des fonctions) ;
D’une partie cachée composée de définitions
d’objets (variables) et de
fonctions
confidentielles. Elles sont à usage purement interne
au fichier ;
Quand un fichier source est compilé, le compilateur ne
dispose d’aucune information sur les autres fichiers source, voir
les bibliothèques. Les
compilations se
font de façon complètement indépendante.
C’est le rôle de l’édition de lien de regrouper les fichiers
sorce et d’effectuer certaines vérifications.
Il doit vérifier entre autres les règles suivantes :
Règle 2 : Quand un fichier comporte une déclaration
externe (non confidentielle), mais ne comporte pas de définition
correspondante, cette dernière doit obligatoirement se trouver
dans un autre fichier.
L’éditeur de liens
n’est pas contraint d’effectuer beaucoup plus de vérifications
que celle déduites de ces règles.
Attention
Il est possible, par exemple en C que la définition
et les déclarations
d’une même fonction
dans des fichiers différents ne coïncident pas (nombre et
type de
paramètres), et que l’éditeur ne dise rien. Le même
problème existe avec les variables
externes inter-fichiers.
Un problème est décomposé en sous-problèmes selon le schéma
ci-dessous. Ce programme peut être formé d’un seul fichier, ou de
plusieurs fichiers en utilisant la compilation
séparée.
La version compilation séparée
peut correspondre à un travail en équipe. A chaque niveau de
l’arbre, les membres de l’équipe s’entendent sur la décomposition
du problème à effectuer, et ils se séparent pour réaliser
séparément chacun des sous-problèmes.
Dans la version mono-fichier, tous les fichiers sont déclarés au
même niveau (En C, contrairement à Pascal, on ne peut pas
déclarer de fonctions dans une
fonction), et on ne
traduit donc pas la structure en arbre
du programme.
Il conviendrait de mettre le mot réservéstatic devant
chaque sous-fonction pour les
rendre confidentielles (invisibles en dehors de ce fichier). Si
on ne le fait pas on pollue l’espace des noms gérés par
l’éditeur de lien.
Cette pollution pourrait s’avérer gênante en cas d’extension
futures du programme (la fonction cherche
étant par omission une fonction externe,
on ne peut pas déclarer une autre fonction externe de
même nom).
A chaque définition de
fonction correspond un fichier. Dans chaque fichier, on doit
placer les déclarations
(importations) des fonctions
utilisées. La structure en arbre
n’est pas non plus traduite. L’espace des noms géré par
l’éditeur de liens
est là irrémédiablement pollué. On ne peut pas avoir deux
fonctions externes
de même nom (En Ada, ce phénomène n’intervient pas. opere serait
une sous-unité de compilation de la
fonction main, et
non une unité de compilation à part
entière. Le nom opere garderait sont caractère local à main, bien que
correspondant à une sous-unité compilée séparément. Le nom
véritable de opere en Ada serait main.opere).
Du fait qu’en C ANSI, on doit déclarer dans un fichier les
fonctions que l’on
utilise, il n’y a aucun ordre de compilation entre
les fichiers du programme. Les fichiers sont compilables de façon
complètement indépendante. Ceci est même vrai si les
différentes fonctions »
partagent » des déclarations
communes de type (types utilisés pour
déclarer les paramètres), car l’équivalence de type
(voir chapitre 4) se fait pas par nom, mais par structure. Deux
déclarations de
type (par typedef) se
trouvant dans deux fichiers distincts sont équivalents, s’ils ont
la même structure.
Si on veut éviter d’avoir à écrire plusieurs fois, les mêmes
déclarations, et
éviter de ce fait les erreurs d’incohérences, il faut écrire des
fichiers include et utiliser la directive#include du
préprocesseur. Un
fichier include n’est pas compilé. Le texte qu’il contient est
inséré dans les fichiers source qui contiennent une directive#include à ce
fichier, avant que la compilation
véritable de ces fichiers source débute.
Version avec fichier .h et utilisation de
#include
La compilation séparée
en C est mal adaptée à l’approche descendante de la
programmation, puisque les noms des sous-unités polluent
l’éditeur de lien. Il
est vrai que c’est
surtout pour l’approche ascendante que la compilation séparée
est intéressante (« programming in the large »).
La programmation ascendante ou par interface
Lors de l’analyse d’un problème, on peut s’apercevoir que l’on a
besoin d’un composant logiciel (module) n’existant pas déjà (une
bibliothèque
mathématique spéciale, une structure de
données, un couche de protocole réseau, …).
Le premier travail est de spécifier ce module, en particulier les
services qu’il rend. A la fin de ce travail, on doit disposer
d’un fichier de déclarations
constituant l’interface du module.
Ce fichier interface est un fichier include (de suffixe .h)
comportant :
Pour pouvoir utiliser ce composant que l’’on espère le plus
réutilisable possible, l’application cliente devra insérer dans
les fichiers source une directive#include
composant.h. Il pourra alors déclarer des objets des types déclarés dans
composant.h, il pourra appeler les fonctions fournies
par l’interface.
Le fournisseur de service placera la réalisation des services
dans le fichier source realisation.c qui devra comporter :
Ce fichier pourra comporter de plus toutes les réalisations et
définitions
nécessaires pour la réalisation, mais non utiles et donc cachées
aux utilisateurs du composant (types, variables et
fonctions
confidentielles, clause #include pour
utiliser d’autres modules).
Cette architecture logicielle permet aux clients et au
réalisateur du module de travailler séparément. Les compilations de
client1.C et et de réalisation peuvent se faire de façon
indépendante.
Il est à noter cependant un défaut de C. Les noms de l’interface
polluent l’éditeur de lien et
empêchent d’utiliser ailleurs des fonctions de même
nom. Il suffirait pour pallier ce défaut que le module ait un
vrai statut dans le
langage C (paquetage en Ada, paquetages et classes en Java, …).
Le nom d’un objet ou d’une fonction de
l’interface serait alors précédé par le nom du module.