🔝 Retour au Sommaire
Quand un paramètre est passé par référence, la fonction ne reçoit pas une copie de l'argument mais un alias — un autre nom pour la même variable. Toute modification effectuée sur le paramètre à l'intérieur de la fonction affecte directement la variable de l'appelant.
#include <iostream>
void doubler(int& x) {
x *= 2;
}
int main() {
int n = 5;
doubler(n);
std::cout << "n = " << n << "\n"; // n = 10
}Contrairement au passage par valeur (section 4.3.1), aucune copie n'est effectuée. Le paramètre x dans doubler est la variable n de main — il s'agit du même emplacement mémoire, simplement désigné par un nom différent.
Visualisons la stack au moment de l'appel doubler(n) :
┌──────────────────────────┐
│ main() │
│ n = 5 │ ← variable originale
├──────────────────────────┤
│ doubler() │
│ x ──────────────────────→ (alias vers n, même adresse)
└──────────────────────────┘
Après x *= 2 :
┌──────────────────────────┐
│ main() │
│ n = 10 │ ← modifié via l'alias x
├──────────────────────────┤
│ doubler() │
│ x ──────────────────────→ (toujours le même n)
└──────────────────────────┘
En interne, le compilateur implémente généralement une référence comme un pointeur constant déréférencé automatiquement, mais du point de vue du développeur, une référence se manipule exactement comme une variable ordinaire — sans * ni & dans le corps de la fonction.
Le passage par référence est le mécanisme standard en C++ moderne pour les paramètres in-out — c'est-à-dire des données que la fonction lit et modifie.
void incrementer(int& compteur) {
++compteur;
}
int main() {
int visites = 0;
incrementer(visites);
incrementer(visites);
incrementer(visites);
std::cout << visites << "\n"; // 3
}void ajouter_defauts(std::vector<std::string>& config) {
config.push_back("timeout=30");
config.push_back("retries=3");
config.push_back("verbose=false");
}
int main() {
std::vector<std::string> params = {"host=localhost"};
ajouter_defauts(params);
// params contient maintenant 4 éléments
}Ici, passer le vecteur par référence évite une copie coûteuse et permet à la fonction de modifier le contenu original. Les deux bénéfices (performance et sémantique) sont réunis.
L'exemple classique de la fonction swap illustre parfaitement le passage par référence — les deux paramètres doivent être modifiés :
void swap(int& a, int& b) {
int tmp = a;
a = b;
b = tmp;
}
int main() {
int x = 1, y = 2;
swap(x, y);
std::cout << "x=" << x << " y=" << y << "\n"; // x=2 y=1
}En pratique, la STL fournit std::swap (qui utilise la sémantique de mouvement pour les types complexes), mais l'exemple montre bien le mécanisme.
Avant C++17 et les structured bindings, le passage par référence était le moyen principal de « retourner » plusieurs valeurs depuis une fonction :
void decomposer(double valeur, int& partie_entiere, double& partie_decimale) {
partie_entiere = static_cast<int>(valeur);
partie_decimale = valeur - partie_entiere;
}
int main() {
int entier;
double decimale;
decomposer(3.14, entier, decimale);
std::cout << entier << " + " << decimale << "\n"; // 3 + 0.14
}En C++ moderne, cette approche est de moins en moins recommandée. On préfère retourner une structure, un std::pair, un std::tuple, ou utiliser les structured bindings (C++17) :
// ✅ Approche moderne — retour par valeur (RVO s'applique)
struct Decomposition {
int partie_entiere;
double partie_decimale;
};
Decomposition decomposer(double valeur) {
return {
static_cast<int>(valeur),
valeur - static_cast<int>(valeur)
};
}
int main() {
auto [entier, decimale] = decomposer(3.14); // Structured binding (C++17)
std::cout << entier << " + " << decimale << "\n";
}L'approche par valeur de retour est plus lisible, plus sûre (pas de paramètres non initialisés en entrée) et souvent aussi performante grâce à la copy elision (section 10.5).
Quelques caractéristiques des références en C++ qu'il est essentiel de connaître.
Contrairement à un pointeur, une référence ne peut pas exister sans être liée à un objet. Elle doit être initialisée au moment de sa création :
int n = 42;
int& ref = n; // ✅ ref est un alias pour n
// int& bad; // ❌ Erreur de compilation — référence non initialiséeUne fois liée à une variable, une référence désigne cette variable pour toute sa durée de vie. L'opérateur = sur une référence modifie la valeur de l'objet référencé, pas le lien lui-même :
int a = 10;
int b = 20;
int& ref = a; // ref est un alias pour a
ref = b; // a vaut maintenant 20 — ref pointe toujours vers a
std::cout << a << "\n"; // 20
std::cout << &ref << " == " << &a << "\n"; // mêmes adresses Il n'existe pas de « référence nulle » en C++ standard. Lier une référence à nullptr (via un déréférencement de pointeur nul) est un comportement indéfini :
int* ptr = nullptr;
// int& ref = *ptr; // ❌ Comportement indéfini — ne faites jamais çaC'est cette propriété qui rend les références plus sûres que les pointeurs pour la majorité des cas d'usage : si une fonction reçoit une référence, elle a la garantie qu'un objet valide existe derrière.
| Propriété | Référence (T&) |
Pointeur (T*) |
|---|---|---|
| Doit être initialisée | Oui — toujours | Non — peut être déclaré sans valeur |
| Peut être nulle | Non | Oui (nullptr) |
| Peut être réassignée | Non — alias permanent | Oui — peut pointer vers un autre objet |
| Syntaxe d'accès | Directe (ref.member) |
Déréférencement (ptr->member ou *ptr) |
| Arithmétique | Non | Oui (ptr + 1, ptr++) |
| Usage principal en C++ moderne | Passage de paramètres in-out | Nullable, interop C, structures de données |
C'est l'erreur la plus grave et la plus classique avec les références. Quand une fonction se termine, ses variables locales sont détruites. Une référence vers l'une d'entre elles devient un dangling reference — son utilisation est un comportement indéfini.
// ❌ DANGER — référence pendante (dangling reference)
int& creer_valeur() {
int local = 42;
return local; // ⚠️ Warning du compilateur — 'local' est détruit ici
}
int main() {
int& ref = creer_valeur();
std::cout << ref << "\n"; // Comportement indéfini — mémoire libérée
}Avec -Wall, GCC et Clang émettent un warning explicite (reference to local variable returned). Ne l'ignorez jamais.
Il est en revanche parfaitement correct de retourner une référence vers un objet dont la durée de vie dépasse celle de la fonction — un membre de classe, un paramètre reçu par référence, un objet statique :
// ✅ Correct — l'objet référencé survit à l'appel
int& premier_element(std::vector<int>& v) {
return v[0]; // Le vecteur appartient à l'appelant — il survit
}
// ✅ Correct — variable statique, durée de vie = programme entier
int& compteur_global() {
static int count = 0;
return count;
}Le passage par référence rend la modification de l'original silencieuse — il n'y a aucune indication syntaxique au site d'appel que la variable va être modifiée :
void traiter(std::vector<int>& data) {
data.clear(); // Supprime tous les éléments
}
int main() {
std::vector<int> mesures = {1, 2, 3, 4, 5};
traiter(mesures);
// mesures est maintenant vide — surprise ?
}À l'appel traiter(mesures), rien n'indique que mesures sera modifié. C'est un problème de lisibilité. Deux approches aident à le mitiger.
La première est d'utiliser des noms de fonctions explicites qui suggèrent la modification : vider_mesures, reset_config, trier_en_place. Si le nom de la fonction ne laisse pas présager une modification, le paramètre devrait probablement être const &.
La seconde est une convention de documentation. Les C++ Core Guidelines recommandent de réserver les paramètres T& exclusivement aux cas in-out, et de systématiquement utiliser const T& pour les paramètres en lecture seule. Si une fonction prend un T&, le lecteur sait qu'elle va modifier l'argument.
Une référence non-const (T&) ne peut pas se lier à un temporaire (une rvalue). C'est une protection du langage : modifier un temporaire n'aurait aucun sens puisqu'il sera détruit immédiatement.
void modifier(int& x) { x = 42; }
int main() {
// modifier(5); // ❌ Erreur — un littéral n'est pas une lvalue
// modifier(get_value()); // ❌ Erreur — un temporaire n'est pas une lvalue
int n = 5;
modifier(n); // ✅ OK — n est une lvalue
}Si la fonction n'a pas besoin de modifier l'argument, utiliser const int& permet d'accepter à la fois les lvalues et les temporaires — c'est le sujet de la section 4.3.3.
Quand deux références désignent le même objet, des effets de bord surprenants peuvent survenir :
void copier(int& destination, const int& source) {
destination = source;
}
int main() {
int x = 5;
copier(x, x); // destination et source désignent le même objet
// Fonctionne ici, mais dans des cas plus complexes,
// l'aliasing peut provoquer des bugs subtils
}Dans cet exemple simple, le résultat est correct. Mais dans des algorithmes plus complexes (par exemple un swap mal implémenté, ou des opérations sur des buffers), l'aliasing — le fait que deux références pointent vers le même objet — peut produire des résultats incorrects. C'est un problème à garder en tête, surtout dans le code générique.
| Aspect | Passage par référence |
|---|---|
| Syntaxe | void f(T& x) |
| Copie de l'argument ? | Non — alias vers l'original |
| Fonction modifie l'original ? | Oui |
| Accepte un temporaire ? | Non — uniquement des lvalues |
| Peut être nul ? | Non — garantie d'un objet valide |
| Usage principal | Paramètres in-out, modification de l'appelant |
| Danger principal | Dangling reference (retour de variable locale) |
La section 4.3.3 présente le passage par référence constante (const &), qui combine l'absence de copie (comme la référence) et l'impossibilité de modifier l'original (comme le passage par valeur). C'est le mode de passage le plus utilisé en C++ moderne pour les types non primitifs.