Différence entre déclaration et définition
En C et C++, il est crucial de distinguer la déclaration de la définition. La définition d'une variable ou d'une fonction crée l'entité et lui alloue de l'espace mémoire. La déclaration, en revanche, informe le compilateur de l'existence et du type de l'entité sans réserver de mémoire.
Voici les règles fondamentales à retenir :
- Une définition alloue de la mémoire, contrairement à une déclaration.
- Une entité peut être déclarée plusieurs fois, mais elle ne doit être définie qu'une seule fois (règle de définition unique ou ODR).
- Par défaut, les fonctions ont un linkage externe (elles sont visibles globalement).
- Les variables globales ont également un linkage externe par défaut, tandis que les variables locales sont confinées à leur bloc.
Comprendre cette distinction est essentiel pour utiliser correctement les modificateurs de linkage comme static et extern.
Utilisation du mot-clé static
Variables locales statiques
Une variable locale classique est stockée dans la pile et détruite à la fin de son bloc d'exécution. Lorsqu'elle est précédée de static, elle est déplacée dans la zone de données statiques. Sa durée de vie s'étend alors sur toute l'exécution du programme, et elle n'est initialisée qu'une seule fois. Cependant, sa portée (visibiilté) reste limitée à la fonction où elle est déclarée.
#include <iostream>
void compter_evenements() {
static int compteur_appels = 0;
compteur_appels += 1;
std::cout << "Événement numéro : " << compteur_appels << std::endl;
}
int main() {
compter_evenements();
compter_evenements();
compter_evenements();
return 0;
}
La variable compteur_appels conserve sa valeur entre les appels, mais demeure inaccessible depuis l'extérieur de la fonction compter_evenements.
Variables globales statiques dans les en-têtes
Si une variable globale est déclarée static directement dans un fichier d'en-tête (.h), chaque fichier source (.cpp) qui inclut cet en-tête recevra sa propre copie indépendante de cette variable. Elles partageront le même nom et la même valeur initiale, mais résideront à des adresses mémoire distinctes.
// config.h
#ifndef CONFIG_H
#define CONFIG_H
static int niveau_debug = 1;
namespace module_test {
void augmenter_niveau();
void afficher_niveau();
void modifier_niveau_b();
void afficher_niveau_b();
}
#endif
// module_a.cpp
#include "config.h"
#include <iostream>
namespace module_test {
void augmenter_niveau() {
niveau_debug += 10;
}
void afficher_niveau() {
std::cout << "Module A - Valeur : " << niveau_debug
<< " | Adresse : " << &niveau_debug << std::endl;
}
}
// module_b.cpp
#include "config.h"
#include <iostream>
namespace module_test {
void modifier_niveau_b() {
niveau_debug = 99;
}
void afficher_niveau_b() {
std::cout << "Module B - Valeur : " << niveau_debug
<< " | Adresse : " << &niveau_debug << std::endl;
}
}
Si vous exécutez ces fonctions depuis un main.cpp, vous constaterez que les modifications apportées dans le module A n'affectent pas le module B, car les adresses mémoires diffèrent. C'est pourquoi il est fortement déconseillé de définir des variables globales statiques dans des fichiers d'en-tête.
Variables globales statiques dans les fichiers source
Pour restreindre la visibilité d'une variable globale à un seul fichier source (linkage interne), il faut la définir et la déclarer static directement dans le fichier .cpp. Cela évite les collisions de noms lors de l'édition de liens et prévient la pollusion de l'espace de noms global.
Partage de données avec le mot-clé extern
Pour partager une véritable variable globale entre plusieurs unités de traduction, il faut utiliser extern. La varible doit être définie dans un seul fichier source, et déclarée avec extern dans un en-tête inclus par les autres fichiers.
// etat_global.h
#ifndef ETAT_GLOBAL_H
#define ETAT_GLOBAL_H
// Déclaration (pas de mémoire allouée ici)
extern int score_joueur;
namespace jeu {
void ajouter_points(int pts);
void afficher_score();
}
#endif
// gestion_score.cpp
#include "etat_global.h"
// Définition unique (allocation mémoire)
int score_joueur = 0;
namespace jeu {
void ajouter_points(int pts) {
score_joueur += pts;
}
}
// affichage.cpp
#include "etat_global.h"
#include <iostream>
namespace jeu {
void afficher_score() {
std::cout << "Score actuel : " << score_joueur
<< " | Adresse : " << &score_joueur << std::endl;
}
}
// main.cpp
#include "etat_global.h"
int main() {
jeu::ajouter_points(50);
jeu::afficher_score();
jeu::ajouter_points(25);
jeu::afficher_score();
return 0;
}
Dans cette configuration, score_joueur réside à une adresse mémoire unique. Tous les fichiers qui incluent etat_global.h manipulent exactement la même instance de la variable, assurant ainsi la cohérence des données à travers l'ensemble du programme.