Variables locales, globales et statiques en C

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]

Commentaires

Articles connexes

Pipe : une méthode de communication inter-processus

Par défaut, il est difficile de faire communiquer deux processus entre eux.

Lire la suite

Envoyer et intercepter un signal en C

À force d’être confrontés à des segfaults ou a des erreurs de bus, on se sera déjà familiarisé avec l’idée d’un signal informatique.

Lire la suite

Malloc : allouer de la mémoire en C

Dans les langages de programmation compilés comme le C, il est souvent intéressant voire nécessaire d’allouer de la mémoire de façon dynamique sur le tas, pour accommoder des variables de grande taille ou de taille incertaine.

Lire la suite