Cours langage C visibilité des variables, tutoriel & guide de travaux pratiques en pdf.
Visibilité des variables
La question de la visibilité des identificateurs (c’est-à-dire « quels sont les identificateurs auxquels on peut faire référence en un point d’un programme ? ») est réglée en C comme dans la plupart des langages comportant la structure de bloc, avec une simplification : les fonctions ne peuvent pas être imbriquées les unes dans les autres, et une complication : tout bloc peut comporter ses propres définitions de variables locales.
Un bloc est une suite de déclarations et d’instructions encadrée par une accolade ouvrante « { » et l’accolade fermante « } » correspondante. Le corps d’une fonction est lui-même un bloc, mais d’autres blocs peuvent être imbriqués dans celui-là.
VARIABLES LOCALES. Tout bloc peut comporter un ensemble de déclarations de variables, qui sont alors dites locales au bloc en question. Une variable locale ne peut être référencée que depuis l’intérieur du bloc ou elle est définie ; en aucun cas on ne peut y faire référence depuis un point extérieur à ce bloc. Dans le bloc ou il est déclaré, le nom d’une variable locale masque toute variable de même nom définie dans un bloc englobant le bloc en question.
Toutes les déclarations de variables locales à un bloc doivent être écrites au début du bloc, avant la première instruction.
ARGUMENTS FORMEL. Pour ce qui concerne leur visibilité, les arguments formels des fonctions sont considérés comme des variables locales du niveau le plus haut, c’est-à-dire des variables déclarées au début du bloc le plus extérieur 9. Un argument formel est accessible de l’intérieur de la fonction, partout ou une variable locale plus profonde ne le masque pas. En aucun cas on ne peut y faire référence depuis l’extérieur de la fonction.
VARIABLES GLOBALES. Le nom d’une variable globale ou d’une fonction peut être utilisé depuis n’importe quel point compris entre sa déclaration (pour une fonction : la fin de la déclaration de son en-tête) et la fin du fichier ou la déclaration figure, sous réserve de ne pas être masquée par une variable locale ou un argument formel de même nom.
La question de la visibilité inter-fichiers est examinée à la section 1.6. On peut noter d’ores et déjà qu’elle ne se pose que pour les variables globales et les fonctions, et qu’elle concerne l’édition de liens, non la compilation, car le compilateur ne traduit qu’un fichier source à la fois et, pendant la traduction d’un fichier, il ne « voit » pas les autres.
9Par conséquent, on ne doit pas déclarer un argument formel et une variable locale du niveau le plus haut avec le même nom.
Allocation et durée de vie des variables
Les variables globales sont toujours statiques, c’est-à-dire permanentes : elles existent pendant toute la durée de l’exécution. Le système d’exploitation se charge, immédiatement avant l’activation du programme, de les allouer dans un espace mémoire de taille adéquate, éventuellement garni de valeurs initiales.
A l’opposé, les variables locales et les arguments formels des fonctions sont automatiques : l’espace correspondant est alloué lors de l’activation de la fonction ou du bloc en question et il est rendu au système lorsque le contrôle quitte cette fonction ou ce bloc. Certains qualifieurs (static, register, voir les sections 1.5.5 et 1.5.6) permettent de modifier l’allocation et la durée de vie des variables locales. Remarque. On note une grande similitude entre les variables locales et les arguments formels des fonctions : ils ont la même visibilité et la même durée de vie. En réalité c’est presque la même chose : les arguments formels sont de vraies variables locales avec l’unique particularité d’être automatiquement initialisés (par les valeurs des arguments effectifs) lors de l’activation de la fonction.
Initialisation des variables
Variables statiques. En toute circonstance la déclaration d’une variable statique peut indiquer une valeur initiale à ranger dans la variable. Cela est vrai y compris pour des variables de types complexes (tableaux ou structures). Exemple :
double x = 0.5e3;
int t[5] = { 11, 22, 33, 44, 55 };
Bien que la syntaxe soit analogue, une telle initialisation n’a rien en commun avec une affectation comme celles qui sont faites durant l’exécution du programme. Il s’agit ici uniquement de préciser la valeur qui doit être déposée dans l’espace alloué à la variable, avant que l’exécution ne commence.
Par conséquent :
• la valeur initiale doit être définie par une expression constante (calculable durant la compilation) ;
• une telle initialisation est entièrement gratuite, elle n’a aucune incidence ni sur la taille ni sur la durée du programme exécutable produit. Les variables statiques pour lesquelles aucune valeur initiale n’est indiquée sont remplies de zéros. L’interprétation de ces zéros dépend du type de la variable.
Variables automatiques. Les arguments formels des fonctions sont automatiquement initialisés lors de leur création (au moment de l’appel de la fonction) par les valeurs des arguments effectifs. Cela est la définition même des arguments des fonctions.
La déclaration d’une variable locale peut elle aussi comporter une initialisation. Mais il ne s’agit pas de la même sorte d’initialisation que pour les variables statiques : l’initialisation représente ici une affectation tout à fait ordinaire. Ainsi, placée à l’intérieur d’un bloc, la construction
int i = exp; /* déclaration + initialisation */
équivaut au couple
int i; /* déclaration */
…
i = exp ; /* affectation */
Par conséquent :
• l’expression qui donne la valeur initiale n’a pas à être constante, puisqu’elle est évaluée à l’exécution, chaque fois que la fonction ou le bloc est activé ;
• une telle initialisation « coûte » le même prix que l’affectation correspondante, c’est-à-dire le temps d’évaluation de l’expression qui définit la valeur initiale.
Les variables automatiques pour lesquelles aucune valeur initiale n’est indiquée sont allouées avec une valeur imprévisible.
Remarque : dans le C original, une variable automatique ne peut être initialisée que si elle est simple (c’est-à-dire autre que tableau ou structure). Cette limitation ne fait pas partie du C ANSI.
Variables locales statiques
Le qualifieur static, placé devant la déclaration d’une variable locale, produit une variable qui est
• pour sa visibilité, locale ;
• pour sa durée de vie, statique (c’est-à-dire permanente).
Elle n’est accessible que depuis l’intérieur du bloc ou elle est déclarée, mais elle est créée au début de l’activation du programme et elle existe aussi longtemps que dure l’exécution de celui-ci. Exemple :
void bizarre1(void) {
static int cpt = 1000;
printf(« %d « , cpt);
cpt++;
}
Lorsque la déclaration d’une telle variable comporte une initialisation, il s’agit de l’initialisation d’une variable statique : elle est effectuée une seule fois avant l’activation du programme. D’autre part, une variable locale statique conserve sa valeur entre deux activations consécutives de la fonction. Ainsi, des appels successifs de la fonction ci-dessus produisent l’affichage des valeurs 1000, 1001, 1002, etc. On aurait pu obtenir un effet analogue avec le programme
int cpt = 1000;
void bizarre2(void) {
printf(« %d « , cpt);
cpt++;
}
mais ici la variable cpt est globale et peut donc être modifiée inconsidérément par une autre fonction, ou entrer en conflit avec un autre objet de même nom, tandis que dans la première version elle n’est visible que depuis l’intérieur de la fonction et donc à l’abri des manipulations maladroites et des collisions de noms. On notera pour finir que la version suivante est erronée :
void bizarre3(void) {
int cpt = 1000;
printf(« %d « , cpt);
cpt++;
}
En effet, tous les appels de bizarre3 afficheront la même valeur 1000.
Attention. Malgré tout le bien qu’on vient d’en dire, les variables locales statiques ont une particularité potentiellement fort dangereuse : il en existe une seule instance pour toutes les activations de la fonction dans laquelle elles sont déclarées. Ainsi, dans l’exemple suivant :
void fonction_suspecte(void) {
static int i;
…
ff fonction_suspecte(); fi
…
}
la valeur de la variable i avant et après l’appel de fonction suspecte (c’est-à-dire aux points ff et fi) peut ne pas être la même, car la deuxième activation de fonction suspecte accède aussi à i. Cela est tout à fait inhabituel pour une variable locale. Conséquence à retenir : les variables locales statiques se marient mal avec la récursivité.
Variables critiques
Le qualifieur register précédant une déclaration de variable informe le compilateur que la variable en question est très fréquemment accédée pendant l’exécution du programme et qu’il y a donc lieu de prendre toutes les dispositions utiles pour en accélérer l’accès. Par exemple, dans certains calculateurs de telles variables sont logées dans un registre de l’unité centrale de traitement (CPU) plutôt que dans la mémoire centrale ; de cette manière l’accès à leur valeur ne met pas en œuvre le bus de la machine.
Les variables ainsi déclarées doivent être locales et d’un type simple (nombre, pointeur). Elles sont automatiquement initialisées à zéro chaque fois qu’elles sont créées. Le compilateur accorde ce traitement spécial aux variables dans l’ordre ou elles figurent dans les déclarations. Lorsque cela n’est plus possible (par exemple, parce que tous les registres de la CPU sont pris) les déclarations register restantes sont ignorées. Il convient donc d’appliquer ce qualifieur aux variables les plus critiques d’abord. Exemple :
char *strcpy(char *dest, char *srce) {
register char *d = dest, *s = srce;
while ((*d++ = *s++) != 0)
;
return dest;
}
Attention. L’utilisation du qualifieur register est intéressante lorsque l’on doit utiliser un compilateur rustique, peu « optimisateur ». Or de nos jours les compilateurs de C ont fini par devenir très perfectionnés et intègrent des algorithmes d’optimisation, parmi lesquels la détermination des variables critiques et leur allocation dans les registres de la CPU. Il s’avère alors que le programmeur, en appliquant le qualifieur register à ses variables préférées (qu’il croit critiques alors qu’elles ne le sont pas réellement), gène le travail du compilateur et obtient un programme moins efficace que s’il n’avait jamais utilisé ce qualifieur.
Variables constantes et volatiles
Le qualifieur const placé devant une variable ou un argument formel informe le compilateur que la variable ou l’argument en question ne changera pas de valeur tout au long de l’exécution du programme ou de l’activation de la fonction. Ce renseignement permet au compilateur d’optimiser la gestion de la variable, la nature exacte d’une telle optimisation n’étant pas spécifiée. Par exemple un compilateur peut juger utile de ne pas allouer du tout une variable qualifiée const et de remplacer ses occurrences par la valeur initiale 10 indiquée lors de la déclaration. Il est conseillé de toujours déclarer const les variables et les arguments formels qui peuvent l’être.
Note. C’est regrettable mais, pour la plupart des compilateurs, une variable qualifiée const n’est pas tout à fait une expression constante au sens de la section 1.3.4. En particulier, pour ces compilateurs une variable, même qualifiée const, ne peut pas être utilisée pour indiquer le nombre d’éléments dans une déclaration de tableau.
Le C ANSI introduit aussi les notions de pointeur constant et de pointeur sur constante, expliquées à la section 5.4.2.
Le sens du qualifieur volatile dépend lui aussi de l’implémentation. Il diminue le nombre d’hypothèses, et donc d’optimisations, que le compilateur peut faire sur une variable ainsi qualifiée. Par exemple toute variable dont la valeur peut être modifiée de manière asynchrone (dans une fonction de détection d’interruption, ou par un canal d’entrée-sortie, etc.) doit être qualifiée volatile, sur les systèmes ou cela a un sens. Cela prévient le compilateur que sa valeur peut changer mystérieusement, y compris dans une section du programme qui ne comporte aucune référence à cette variable.
Les compilateurs sont tenus de signaler toute tentative décelable de modification d’une variable const. Mis à part cela, sur un système particulier ces deux qualifieurs peuvent n’avoir aucun autre effet. Ils n’appartiennent pas au C original.
10La déclaration d’une variable const doit nécessairement comporter une initialisation car sinon, une telle variable ne pouvant pas être affectée par la suite, elle n’aurait jamais de valeur définie.
Variables, fonctions et compilation séparée
Identificateurs publics et privés
Examinons maintenant les règles qui régissent la visibilité inter-fichiers des identificateurs. La question ne concerne que les noms de variables et de fonctions, car les autres identificateurs (noms de structures, de types, etc.) n’existent que pendant la compilation et ne peuvent pas être partagés par deux fichiers. Il n’y a pas de problème pour les variables locales, dont la visibilité se réduit à l’étendue de la fonction ou du bloc contenant leur définition. Il ne sera donc question que des noms des variables globales et des noms des fonctions.
Jargon. Identificateurs publics et privés. Un nom de variable ou de fonction défini dans un fichier source et pouvant être utilisé dans d’autres fichiers sources est dit public. Un identificateur qui n’est pas public est appelé privé.
Règle 1 :
• Sauf indication contraire, tout identificateur global est public ;
• le qualifieur static, précédant la déclaration d’un identificateur global, rend celui-ci privé.
On prendra garde au fait que le qualifieur static n’a pas le même effet quand il s’applique à un identificateur local (static change la durée de vie, d’automatique en statique, sans toucher à la visibilité) et quand il s’applique à un identificateur global (static change la visibilité, de publique en privée, sans modifier la durée de vie).
Lorsqu’un programme est décomposé en plusieurs fichiers sources il est fortement conseillé, pour ne pas dire obligatoire, d’utiliser le qualifieur static pour rendre privés tous les identificateurs qui peuvent l’être. Si on ne suit pas cette recommandation on verra des fichiers qui étaient corrects séparément devenir erronés lorsqu’ils sont reliés, uniquement parce qu’ils partagent à tort des identificateurs publics.
Déclaration d’objets externes
Nous ne considérons donc désormais que les noms publics. Un identificateur référencé dans un fichier alors qu’il est défini dans un autre fichier est appelé externe. En général, les noms externes doivent faire l’objet d’une déclaration : le compilateur ne traitant qu’un fichier à la fois, les propriétés de l’objet externe doivent être indiquées pour que la compilation puisse avoir lieu correctement.
Jargon :
Définition et déclaration d’une variable ou d’une fonction. Aussi bien une déclaration qu’une définition d’un nom de variable ou de fonction est une formule qui spécifie la classe syntaxique (variable ou fonction) et les attributs (type, valeur initiale, etc.) de l’identificateur en question. En plus de cela :
• une définition produit la création de l’objet dont l’identificateur est le nom ;
• une déclaration se limite à indiquer que l’objet en question a dû être créé dans un autre fichier qui sera fourni lors de l’édition de liens. (« Créer » une variable ou une fonction c’est réserver l’espace correspondant, rempli par l’éventuelle valeur initiale de la variable ou par le code de la fonction).
Règle 2 :
• Toute variable doit avoir été définie (c’est-à-dire déclarée normalement) ou déclarée externe avant son utilisation ;
• une fonction peut être référencée alors qu’elle n’a encore fait l’objet d’aucune définition ni déclaration externe ; elle est alors supposée être
• externe,
• à résultat entier (int),
• sans prototype (cf. section IV.B) ;
• par conséquent, si une fonction n’est pas à résultat entier alors elle doit être soit définie soit déclarée externe avant son appel, même si elle est ultérieurement définie dans le fichier ou figure l’appel.
La déclaration externe d’une variable s’obtient en faisant précéder une déclaration ordinaire du mot-clé extern.
Exemple :
extern unsigned long n;
Dans un autre fichier cette variable aura été définie :
unsigned long n;
La déclaration externe d’une variable doit être identique, au mot extern près, à sa définition. Sauf pour les deux points suivants :
• une déclaration externe ne doit pas comporter d’initialisateur (puisque la déclaration externe n’alloue pas la variable),
• dans une déclaration externe de tableau, il est inutile d’indiquer la taille de celui-ci (puisque la déclaration externe n’alloue pas le tableau).
Exemple. Dans le fichier ou sont définies les variables n et table, on écrira :
unsigned long n = 1000;
int table[100];
Dans un autre fichier, ou ces variables sont uniquement référencées, on écrira :
extern unsigned long n;
extern int table[];
La déclaration externe d’une fonction s’obtient en écrivant l’en-tête de la fonction, précédé du mot extern et suivi d’un point-virgule ; le mot extern est facultatif. Exemple : définition de la fonction
double carre(double x) {
return x * x;
}
Déclaration externe dans un autre fichier :
double carre(double x);
ou
double carre(double);
ou l’un ou l’autre de ces énoncés, précédé du mot extern.
En syntaxe originale (c’est-à-dire « sans prototype ») il faut en outre ne pas écrire les arguments formels.
Définition :
double carre(x)
double x;
{
return x * x;
}
Déclaration externe dans un autre fichier :
double carre();
Règle 3 :
Dans l’ensemble des fichiers qui constituent un programme, chaque nom public :
• doit faire l’objet d’une et une seule définition ;
• peut être déclaré externe (y compris dans le fichier ou il est défini) un nombre quelconque de fois.
Cette règle volontariste est simple et elle exprime la meilleure façcon de programmer. Il faut savoir cependant que chaque système tolère des écarts, qui révèlent surtout la rusticité de l’éditeur de liens sous-jacent. La clarté des concepts et la fiabilité des programmes y perdent beaucoup.
I – Eléments de base
I-A – Structure générale d’un programme
I-B – Considérations lexicales
I-C – Constantes littérales
I-D – Types fondamentaux
I-E – Variables
I-F – Variables, fonctions et compilation séparée
II – Opérateurs et expressions
II-A – Généralités
II-B – Présentation détaillée des opérateurs
II-C – Autres remarques
III – Instructions
III-A – Syntaxe
III-B – Présentation détaillée des instructions
IV – Fonctions
IV-A – Syntaxe ANSI ou « avec prototype »
IV-B – Syntaxe originale ou « sans prototype »
IV-C – Arguments des fonctions
V – Objets structurés
V-A – Tableaux
V-B – Structures et unions
V-C – Enumérations
V-D – Déclarateurs complexes
VI – Pointeurs
VI-A – Généralités
VI-B – Les pointeurs et les tableaux
VI-C – Les adresses des fonctions
VI-D – Structures récursives
VII – Entrées-sorties
VII-A – Flots
VII-B – Lecture et écriture textuelles
VII-C – Opérations en mode binaire
VII-D – Exemples
VII-E – Les fichiers de bas niveau d’UNIX
VIII – Autres éléments du langage C
VIII-A – Le préprocesseur
VIII-B – La modularité de C
VIII-C – Deux ou trois choses bien pratiques
VIII-D – La bibliothèque standard