Variables locales, globales et statiques en C
- Mia Combeau
- C
- 17 juin 2022
Table des matières
Une variable, c’est un nom qu’on donne à un lieu de stockage en mémoire que notre programme peut ensuite manipuler. On peut préciser sa taille et son type en fonction des valeurs qu’elle contiendra (char, int, long). Mais on peut aussi contrôler sa longévité et sa portée lors de sa déclaration. C’est pour cela qu’il nous faut savoir distinguer les variables locales des variables globales ou encore des variables statiques quand on programme en C.
Variables locales
Les variables locales sont des variables très éphémères. Déclarées à l’intérieur d’une fonction, elles n’existent dans la mémoire vive que le temps de cette fonction. Elles disparaissent dès que leur fonction prend fin.
La portée d’une variable locale
Créons par exemple une variable nommée a
dans une fonction et essayons de l’imprimer dans une autre fonction :
#include <stdio.h>
void foo(void)
{
int a;
a = 10;
printf("Foo function: Variable a = %d\n", a);
} // la variable a cesse d'exister en mémoire ici.
int main(void)
{
foo();
printf("Main: Variable a = %d\n", a);
// ERREUR : main ne connait pas de variable a !
return (0);
}
On aura immédiatement une erreur de compilation. En effet, le système d’exploitation place la variable a
dans la stack quand on la déclare dans foo. Ensuite, quand la fonction foo prend fin, l’OS récupère la mémoire vive attribuée pour cette variable et cette fonction, pour l’attribuer ailleurs. La fonction main, elle, n’a aucune référence à une variable nommée a
car cette variable n’a pas été déclarée dans cette fonction. Mais même si en avait connaissance, elle ne pourrait pas y accéder vu que la variable n’existe déjà plus.
Heureusement, on a des moyens de passer les valeurs des variables d’une fonction à une autre. On peut faire en sorte que notre fonction foo retourne la valeur de a
, comme ceci :
#include <stdio.h>
int foo(void)
{
int a;
a = 10;
printf("Foo: Variable a = %d\n", a);
return (a);
} // la variable a cesse d'exister ici, mais on a envoyé sa valeur
// en retour avant sa destruction.
int main(void)
{
int value;
value = foo(); // la valeur de retour de foo est sauvegardée ici
printf("Main: Variable a = %d\n", value);
return (0);
}
Sinon, on peut simplement créer la variable a
dans la fonction main et l’envoyer en paramètres de foo :
#include <stdio.h>
void foo(int a)
{
// Foo a sa propre copie de la variable a, passée en paramètres
printf("Foo: Variable a = %d\n", a);
}
int main(void)
{
int a;
a = 10;
printf("Main: Variable a = %d\n", a);
foo(a); // On passe la variable a en paramètres de la fonction foo
return (0);
}
Cependant, dans ce cas, la variable a
de la fonction foo n’est pas la même que celle de la fonction main : c’est simplement une copie. Si on modifie la valeur de a
dans foo, la valeur de la variable a
dans le main restera inchangée :
#include <stdio.h>
void foo(int a)
{
// On change ici la valeur de a dans foo mais pas dans main
// puisque les deux variables a sont distinctes
a = 145;
printf("Foo: Variable a = %d\n", a); // a == 145
}
int main(void)
{
int a;
a = 10;
printf("Main: Variable a = %d\n", a); // a == 10
foo(a);
printf("Main: Variable a = %d\n", a); // a == 10
return (0);
}
On aura ici pour résultat :
Main: Variable a = 10
Foo: Variable a = 145
Main: Variable a = 10
Dans cet exemple, il n’y a pas de confusion ou de conflit entre les deux variables a
puisque les deux ont des portées différentes et sont propres à la fonction qui les déclare. Et en effet, la variable a
de la fonction foo est bien déclarée dans ses paramètres. Ces deux variables locales a
sont donc complètement indépendantes et font référence à deux adresses mémoires distinctes.
Changer la valeur d’une variable locale de l’extérieur
Alors comment peut-on faire pour changer la valeur de la variable a
à l’extérieur de la fonction dans laquelle elle est déclarée ? Il suffit de faire une petit acrobatie : passer l’adresse mémoire de la variable (son pointeur) et changer la valeur stockée à cet endroit dans la mémoire. Ceci marche puisque la fonction dans laquelle on l’a déclarée n’a pas encore pris fin.
#include <stdio.h>
void foo(int *a)
{
*a = 145; // On change ce qu'il y a à l'adresse de a
printf("Foo: Variable a = %d\n", *a); // *a == 145
}
int main(void)
{
int a;
a = 10;
printf("Main: Variable a = %d\n", a); // a == 10
foo(&a); // On passe l'adresse de a
printf("Main: Variable a = %d\n", a); // a == 145
return (0);
}
Résultat :
Main: Variable a = 10
Foo: Variable a = 145
Main: Variable a = 145
Ici, la fonction foo déclare une variable contenant l’adresse mémoire de la variable a, et peut donc changer la valeur stockée à cet endroit. C’est pourquoi lorsque la fonction main lit ensuite la valeur de cette variable, celle-ci aura changé de valeur.
Variables globales
Quand on déclare une variable en dehors de toute fonction, c’est une variable globale. C’est à dire qu’elle est accessible depuis n’importe quelle fonction du programme. Contrairement a une variable locale, une variable globale ne disparaît pas à la fin d’une fonction. Elle persiste tout au long du programme. C’est parce que typiquement, le système d’exploitation ne stocke une variable globale ni dans la stack ni dans la heap mais à un endroit à part, dédié aux globales.
De plus, une variable globale est toujours initialisée à 0 par défaut.
#include <stdio.h>
int a; // Variable globale initialisée à 0 par défaut
void foo(void)
{
a = 42; // Variable globale accessible sans déclaration
// préalable dans la fonction
printf("Foo: a = %d\n", a); // a == 42
}
int main(void)
{
printf("Main: a = %d\n", a); // a == 0
foo();
printf("Main: a = %d\n", a); // a == 42
a = 200;
printf("Main: a = %d\n", a); // a == 200
return (0);
}
Résultat :
Main: a = 0
Foo: a = 42
Main: a = 42
Main: a = 200
On peut voir ici qu’on n’a pas besoin de passer la variable ou son adresse mémoire en paramètres d’une fonction pour pouvoir y accéder et même la modifier.
Priorité à la variable locale
Il faut aussi noter qu’il risque d’y avoir une ambiguïté si on déclare une variable locale du même nom :
#include <stdio.h>
int a; // Variable globale initialisée à 0 par défaut
void foo(void)
{
a = 42;
printf("Foo: a = %d\n", a); // a == 42
}
void global_a(void)
{
// Imprime la valeur de la variable globale
printf("-------------- GLOBAL A: a = %d\n", a);
}
int main(void)
{
int a; // Variable locale du même nom que la globale
a = 100;
global_a(); // a globale == 0
printf("Main: a = %d\n", a); // a locale == 100
foo();
printf("Main: a = %d\n", a); // a locale == 100
global_a(); // a globale == 42
a = 200;
printf("Main: a = %d\n", a); // a locale == 200
global_a(); // a globale == 42
return (0);
}
Résultat :
-------------- GLOBAL A: a = 0
Main: a = 100
Foo: a = 42
Main: a = 100
-------------- GLOBAL A: a = 42
Main: a = 200
-------------- GLOBAL A: a = 42
Clairement, la variable locale l’emporte sur la variable globale du même nom. Il faut garder ça en tête, puisque ça pourrait engendrer quelques confusions lors du débogage d’un programme.
Portée d’une variable globale
N’importe quelle fonction du programme peut accéder à une variable globale. Si l’on souhaite utiliser une variable globale définie dans un fichier dans un autre fichier, il suffit de la déclarer de nouveau avec le mot-clef extern
. Ce mot-clef du compilateur, d’habitude implicite dans la déclaration de fonctions par exemple, signifie qu’on est en train de déclarer quelque chose qu’on va définir ailleurs.
Prenons notre exemple initial et séparons nos deux fonctions, main et foo, dans deux fichiers différents :
- main.c
- foo.c
Dans le fichier main.c
, on déclare la variable globale avec le mot-clef extern
pour dire que cette variable est définie dans un autre fichier. C’est une déclaration similaire au prototype de la fonction foo qu’on va aussi définir dans un autre fichier. Le compilateur comprend de façon implicite qu’il doit considérer le prototype de foo comme extern
aussi.
#include <stdio.h>
extern int a; // Variable globale définie ailleurs
void foo(void); // Prototype de foo, définie ailleurs
// c'est l'équivalent de
// extern void foo(void);
int main(void)
{
printf("Main: a = %d\n", a); // a == 100
foo();
printf("Main: a = %d\n", a); // a == 42
a = 200;
printf("Main: a = %d\n", a); // a == 200
return (0);
}
Dans le fichier foo.c
, on déclare et on définit la variable globale a
, ainsi que la fonction foo :
#include <stdio.h>
int a = 100; // Variable globale déclarée et définie ici
void foo(void)
{
a = 42;
printf("Foo: a = %d\n", a); // a == 42
}
Résultat :
Main: a = 100
Foo: a = 42
Main: a = 42
Main: a = 200
Tout comme les prototypes de fonctions, on peut bien entendu déclarer extern int a
dans un fichier header.h
. C’est d’ailleurs bien mieux que de définir une variable globale directement dans le header.
Evidemment, avoir une variable qui est accessible depuis n’importe quelle fonction dans n’importe quel fichier d’un programme peut vite se révéler problématique au niveau sécurité. C’est pourquoi on peut rendre nos variables globales statiques…
Variables statiques
On peut déclarer une variable, qu’elle soit locale ou globale, en tant que statique. Le mot-clef static
a une logique très simple. Une variable statique, c’est par défaut une variable globale : stockée ni dans la stack ni dans la heap, elle a la même “durée de vie” que le programme. Mais contrairement à une vraie globale, elle a aussi une portée limitée :
- à l’intérieur d’une fonction, c’est une variable globale mais visible uniquement à l’intérieur de la fonction dans laquelle on la déclare.
- à l’extérieur de toute fonction, c’est une variable globale visible uniquement dans le fichier (c’est à dire dans l’unité de compilation) dans lequel on la déclare.
Variables statiques locales
Une variable statique locale n’est donc en réalité pas véritablement une variable locale. C’est une variable globale déguisée, qui ne disparaît pas à la fin de la fonction dans laquelle on la déclare, vu qu’on ne la stocke pas dans la stack. Le mot static la confine cependant à la portée de sa fonction, comme une variable locale. Et comme toute variable globale, elle est toujours initialisée à 0 par défaut.
Comparons deux variables, une variable locale ordinaire et une variable statique déclarée dans une fonction :
#include <stdio.h>
void foo(void)
{
int a = 100;
static int b = 100;
printf("a = %d, b = %d\n", a, b);
a++;
b++;
}
int main(void)
{
foo();
foo();
foo();
foo();
foo();
return (0);
}
Résultat :
a = 100, b = 100
a = 100, b = 101
a = 100, b = 102
a = 100, b = 103
a = 100, b = 104
Quand on appelle la fonction foo plusieurs fois de suite, on peut voir que la variable statique b
est bien incrémentée, ce qui veut dire qu’elle conserve sa valeur même après la fin de sa fonction. Mais la variable a
, qui n’est pas statique, est réinitialisée à chaque appel à la fonction foo.
Cela ne veut cependant pas dire que la variable statique b
est accessible depuis n’importe quelle autre fonction. Elle existe bien toujours en mémoire après la fin de la fonction foo comme une variable globale, mais, comme une variable locale, on ne peut y accéder que dans la fonction foo. Si l’on tente de l’imprimer dans la fonction main par exemple, on aura encore une erreur de compilation. On peut toutefois envoyer son adresse mémoire à une autre fonction si besoin, comme on l’a vu avec les variables locales.
Variables statiques globales
En ce qui conserne les variables statiques déclarées en dehors de toute fonction, le mot-clef static
restreint leur portée au fichier dans lequel on les déclare. Toutes les fonctions dans le même fichier qui suivent la déclaration d’une variable statique globale pourront l’utiliser, mais on ne pourra pas y accéder depuis un autre fichier du programme.
Si l’on reprend notre exemple précédent de variable globale sur deux fichiers et qu’on transforme la variable globale dans foo.c
en statique, on aura des erreurs de compilation, des “undefined reference to ‘a’”. Cela vient du fait qu’on déclare dans main.c
qu’il y a une définition extern
de a
ailleurs dans le programme. Sauf que a
est statique et par conséquent “invisible” aux yeux du compilateur. Sans trouver de définition valide de la variable globale a
, le compilateur renvoie une erreur. Pour le compilateur, extern
et static
sont diamétralement opposés.
De même, il est possible et bien avisé d’utiliser le mot-clef static
lors de définitions de fonctions qui sont utilisées dans un seul fichier d’un programme. Sinon, le compilateur pense par defaut que les fonctions sont déclarées extern
et vont surement devoir être liées à d’autres fichiers. Utiliser le mot-clef static
, c’est une simple mesure de sécurité pour un programme. Cela peut aussi permettre une compilation plus rapide dans certains cas.
Sources et lectures supplémentaires
- B. Kernighan, D. Ritchie, 1988, The C Programming Language, Second Edition, “4.6 Static Variables”, p. 83
- StackOverflow, C/C++ global vs static global [ stackoverflow,com]
- StackOverflow, Static declaration of m follows non-static declaration [ stackoverflow.com]
- Geeks for Geeks, Understanding “extern” keyword in C [ geeksforgeeks.com]
- StackOverflow, Why and when to use static structures in C programming? [ stackoverflow.com]
- StackOverflow, Reasons to use Static functions and variables in C [ stackoverflow.com]
- StackOverflow, Global declaration is in stack or heap? [ stackoverflow.com]