Reference ✱
Your native language: with Chrome, right-click on the page and select « Translate into … »
English version:
Référence de variables
Introduction
Pour créer une référence vers une variable existante, il suffit d’utiliser la syntaxe suivante :
SYNTAXE - Création d’une référence
Type & NomRef = lvalue;
Une référence définit un alias vers un élément stocké en mémoire de façon pérenne (lvalue). Nous pouvons définir des références sur des variables mais aussi sur des lvalues plus complexes. Voici un exemple :
#include <iostream>
int main()
{
int a = 7;
int & refA = a; // definition of a reference to variable a
std::cout << refA; // ==>> 7
int arr[4] = { 1, 2, 3, 4};
int & refElem = arr[2]; // definition of a reference to arr[2]
std::cout << refElem; // ==>> 3
}
Avertissement
Lors de la création d’une référence, aucune copie n’est effectuée. La référence et l’élément original désignent une même chose. Utiliser l’une ou l’autre est équivalent.
La syntaxe nous interdit de créer une référence sans l’initialiser. Ainsi, on ne peut pas écrire :
int main()
{
int & ref; // ==> not possible, a reference must always be initialized during definition
}
Vérification
Veuillez exécuter le code suivant puis analyser les résultats et confirmer que la référence n’effectue pas de copie :
#include <iostream>
struct Vec
{
int x;
int y;
};
int main()
{
// int type
int n = 10;
int & refN = n;
refN++;
std::cout << n << " " << refN << std::endl;
// struct type
Vec obj;
obj.x = 10;
obj.y = 20;
Vec & alias = obj;
alias.x += 5;
alias.y += 5;
std::cout << alias.x;
}
Référence et passage d’arguments
Un paramètre de fonction qui correspond à une référence ne définit pas une nouvelle variable mais un alias renommant la donnée passée en argument.
Par conséquent, toutes les modifications effectuées sur la référence sont en fait effectuées sur la donnée passée en argument. Voici la syntaxe :
SYNTAXE - Passage d’un argument par référence :
… NomFonction(TypeVar & NomRef, …) { … }
Note
L’utilisation de références dans les paramètres de fonction permet de retourner plusieurs informations. L’instruction return ne permet elle que de retourner un unique élément ce qui peut parfois être limité.
Veuillez exécuter le code suivant puis analyser les résultats et confirmer qu’aucune copie n’est effectuée :
#include <iostream>
struct Vec
{
int x;
int y;
};
void F(int & n, Vec & obj)
{
n += 1;
obj.x += 2;
obj.y += 3;
}
int main()
{
// int type
int a = 10;
// struct type
Vec A;
A.x = 10;
A.y = 20;
std::cout << "Avant l'appel : " << a << " / " << A.x << " " << A.y << std::endl;
F(a,A);
std::cout << "Après l'appel : " << a << " / " << A.x << " " << A.y << std::endl;
}
Exercices
Fonctions disponibles
int F(int t) { t += 2; return t; } void I(int & t) { t += 7; } int Z(int & t) { t = 1; return 5; } int F(int & a, int & b) { return a+b; }
Exercice 1
int main() { int a; Z(a); I(a); std::cout << a; }
Exercice 2
int main() { int a = 8; a += F(a); I(a); std::cout << a; }
Exercice 3
int main() { int a = 8; std::cout << I(a+8); }
Exercice 4
int main() { int a = 8; int b = 3; std::cout << F(a,b); }
Exercice 5
int main() { int a = 8; int b = 3; std::cout << F(F(b,a)); }
Pour chaque exemple, indiquez l’affichage obtenu ou ERR si le programme émet une erreur :
Exercice 1
Exercice 4
Exercice 2
Exercice 5
Exercice 3
Exercices
Fonctions disponibles
int F(int & t) { t += 2; return t; }
void I(int & t) { t += 7; }
int & Z(int & t) { t = 0; return t; }
int & F(int & a, int & b) { return a; }
Exercice 6
int main()
{
int a = 8;
int b = 3;
std::cout << F(F(b,a));
}
Exercice 7
int main()
{
int a = 8;
std::cout << Z(a+1);
}
Exercice 8
int main()
{
int a = 8;
int b = 3;
Z(F(a,b))++;
std::cout << a;
}
Exercice 9
int main()
{
int a = 8;
std::cout << F(F(a));
}
Exercice 10
int main()
{
int a = 3;
int b = 2;
F(Z(a),Z(b))++;
std::cout << a;
}
Pour chaque exemple, indiquez l’affichage obtenu ou ERR si le programme émet une erreur :
Exercice 6
Exercice 9
Exercice 7
Exercice 10
Exercice 8
Cas pratiques
Voici quelques scénarios spécifiques qu’il faut connaître car ce sont de grands classiques.
Performance
Lorsque l’on transmet un élément prenant de la place mémoire, il est préférable de le passer par référence afin d’éviter une recopie des données. Cependant, si vous effectuez un passage par copie, vous n’aurez pas de message d’erreur, mais vous pourrez constater un ralentissement du programme.
Prenons l’exemple de la fonction transposition d’une matrice :
Matrix Transpose(Matrix M)
{
Matrix result;
...
return result;
}
Cette fonction compile sans erreur et fonctionne correctement. Cependant, l’argument : Matrix M pose problème. En effet, il s’agit de la définition d’une variable locale M. L’objet matrice passé en argument va donc être recopié, valeur par valeur, dans M ce qui représente un traitement inutile. Il faut donc préférer la version suivante :
Matrix Transpose(Matrix & M)
{
Matrix result;
...
return result;
}
Juste un signe & supplémentaire et un gain de performance à la clef.
REGLE : Il est préférable de passer les objets en paramètre par référence plutôt que par valeur, ceci afin d’éviter des copies inutiles.
Retour de référence depuis une variable locale
Techniquement, vous pouvez définir une fonction retournant une référence :
int & Test(int a)
{
int b = a * a + 3;
return b; // ERROR: returning reference to a local variable
}
int main()
{
int a = 7;
int & c = Test(a);
c++; // this line triggers a runtime error
std::cout << c;
}
La syntaxe de ce programme est correcte. Le code compile, pourtant son exécution produit une erreur système inconnue. Que se passe-t-il ?
Dans la fonction Test, nous créons une variable b. Cela est autorisée.
Nous retournons ensuite une référence vers cette variable b.
Cette référence sert à initialiser la référence c maintenant associée à b.
L’exécution de la ligne : c++; pourtant correcte, produit une erreur système.
Remontons à la source du problème. La référence c est faite vers la variable locale b de la fonction Test(). Hors cette variable est une variable locale dont la durée de vie s’arrête à la fin de la fonction. Ainsi, cette variable est détruite et la référence retournée désigne une variable n’existant plus. Lorsque l’on essaye d’utiliser cette référence, c’est le crash. A noter que le comportement du programme est indéterminé dans ce genre de situation :
Le programme peut crasher avec ou sans message (souvent sans).
Une autre variable peut être modifiée à la place de b, produisant un bug incompréhensible plus tard dans le programme.
Une partie de la mémoire contenant du code peut être modifiée ce qui fait que le programme peut se bloquer.
REGLE : On ne doit pas retourner une référence sur une variable locale à une fonction.
Une syntaxe d’appel ambiguë
Nous présentons dans le code ci-dessous, un passage par copie et un passage par référence :
#include <iostream>
void F1(int n) { n += 1; std::cout << n << std::endl; }
void F2(int &n) { n += 1; std::cout << n << std::endl; }
int main()
{
int a = 5;
F1(a); // pass by value
F2(a); // pass by reference
}
Le code ne produit pas d’erreur, il semble fonctionner mais là n’est pas le problème. Examinons la définition des deux fonctions disponibles. L’une utilise un passage par copie et l’autre par référence. A ce niveau, tout est clairement explicité grâce à l’utilisation du signe &. Examinons maintenant les appels dans la fonction main(), nous avons : F1(a) et F2(a). A ce niveau, on ne constate aucune différence de syntaxe.
ATTENTION : En examinant un appel de fonction, il est impossible de déterminer en C++ si les variables sont passées par copie ou par référence. C’est une faiblesse du langage.
Exercices
Pour chaque exemple, indiquez l’affirmation est vraie ou fausse :
Si une fonction retourne une référence sur une variable locale, cela la maintient en vie.
L’écriture F(a) correspond uniquement à un passage par copie.
Une const référence indique que l’on ne peut modifier l’élément référencé.
Le passage par référence permet de limiter le temps passé à faire des copies.
Travail à rendre sur Github Classroom
Exercice
Créez un fichier nommé 2_reference.cpp
Codez :
Une fonction void inc(…) qui incrémente l’entier passée en paramètre
Une fonction void makePositive(…) qui prend un entier et le transforme en nombre positif
Une fonction void rotate(…) qui prend trois entiers a,b,c et les décale sur la gauche a←b…
Une fonction void clamp(a,b,c) qui fait en sorte que a=b si a<b et a=c si a>c
Intégrez les fonctions de test suivantes dans votre code
Validez vos fonctions
Uploadez le fichier sur votre github
#include <iostream>
#include <cmath>
using namespace std;
int main()
{
// ---- inc ----
int x = 0; inc(x);
if (x != 1) cout << "[FAIL] inc: got=" << x << " expected=1\n";
x = -10; inc(x);
if (x != -9) cout << "[FAIL] inc: got=" << x << " expected=-9\n";
// ---- makePositive ----
x = -3; makePositive(x);
if ( x != 3 )
cout << "[FAIL] makePositive: got=" << x << " expected=3\n";
x = 12; makePositive(x);
if ( x != 12 )
cout << "[FAIL] makePositive: got=" << x << " expected=12\n";
// ---- rotate ----
int a=1,b=2,c=3; rotate(a,b,c);
if (!(a==2 && b==3 && c==1))
cout << "[FAIL] rotate (1,2,3): got=("<<a<<","<<b<<","<<c<<") expected=(2,3,1)\n";
a=7,b=8,c=9;
rotate(a,b,c); rotate(a,b,c); rotate(a,b,c);
if (!(a==7 && b==8 && c==9))
cout << "[FAIL] rotate x3: got=("<<a<<","<<b<<","<<c<<") expected=(7,8,9)\n";
// ---- clamp ----
int v = 3; clamp(v, 0, 5);
if (v != 3) cout << "[FAIL] clamp: got="<<v<<" expected=3\n";
v = -10; clamp(v, -3, 7);
if (v != -3) cout << "[FAIL] clamp: got="<<v<<" expected=-3\n";
v = 99; clamp(v, -3, 7);
if (v != 7) cout << "[FAIL] clamp: got="<<v<<" expected=7\n";
v = 5; clamp(v, 5, 10);
if (v != 5) cout << "[FAIL] clamp: got="<<v<<" expected=5\n";
v = 10; clamp(v, 5, 10);
if (v != 10) cout << "[FAIL] clamp: got="<<v<<" expected=10\n";
return 0;
}