Comment utiliser les parameters de référence en C ++?

J’essaie de comprendre comment utiliser les parameters de référence. Il y a plusieurs exemples dans mon texte, mais ils sont trop compliqués pour que je comprenne pourquoi et comment les utiliser.

Comment et pourquoi voudriez-vous utiliser une référence? Que se passerait-il si vous ne deveniez pas une référence au paramètre, mais au lieu de cela?

Par exemple, quelle est la différence entre ces fonctions:

 int doSomething(int& a, int& b); int doSomething(int a, int b); 

Je comprends que des variables de référence sont utilisées pour modifier une référence formelle, ce qui permet ensuite un échange bidirectionnel de parameters. Cependant, c’est l’étendue de mes connaissances, et un exemple plus concret serait très utile.

Considérez une référence comme un alias . Lorsque vous invoquez quelque chose sur une référence, vous l’invoquez vraiment sur l’object auquel la référence fait référence.

 int i; int& j = i; // j is an alias to i j = 5; // same as i = 5 

En ce qui concerne les fonctions, considérez:

 void foo(int i) { i = 5; } 

Au-dessus, int i est une valeur et l’argument passé est passé par valeur . Cela signifie que si nous disons:

 int x = 2; foo(x); 

i serai une copie de x . Ainsi, le réglage de i à 5 n’a aucun effet sur x , car la copie de x est modifiée. Cependant, si nous faisons une référence:

 void foo(int& i) // i is an alias for a variable { i = 5; } 

Alors dire foo(x) ne fait plus de copie de x ; i x . Donc si on dit foo(x) , dans la fonction i = 5; est exactement le même que x = 5; et x change.

Espérons que cela clarifie un peu.


Pourquoi est-ce important? Lorsque vous programmez, vous ne voulez jamais copier et coller du code. Vous voulez créer une fonction qui effectue une tâche et le fait bien. Chaque fois que cette tâche doit être effectuée, vous utilisez cette fonction.

Disons que nous voulons échanger deux variables. Cela ressemble à ceci:

 int x, y; // swap: int temp = x; // store the value of x x = y; // make x equal to y y = temp; // make y equal to the old value of x 

D’accord! Super. Nous voulons en faire une fonction, car: swap(x, y); est beaucoup plus facile à lire. Alors, essayons ceci:

 void swap(int x, int y) { int temp = x; x = y; y = temp; } 

Cela ne marchera pas! Le problème est que cela échange des copies de deux variables. C’est:

 int a, b; swap(a, b); // hm, x and y are copies of a and b...a and b remain unchanged 

En C, où les références n’existent pas, la solution consistait à transmettre l’adresse de ces variables; c’est-à-dire, utilisez des pointeurs *:

 void swap(int* x, int* y) { int temp = *x; *x = *y; *y = temp; } int a, b; swap(&a, &b); 

Cela fonctionne bien. Cependant, c’est un peu maladroit à utiliser, et en fait un peu dangereux. swap(nullptr, nullptr) , swap(nullptr, nullptr) deux nuls et déréférencera des pointeurs nuls … un comportement indéfini! Fixable avec quelques contrôles:

 void swap(int* x, int* y) { if (x == nullptr || y == nullptr) return; // one is null; this is a meaningless operation int temp = *x; *x = *y; *y = temp; } 

Mais à quel point notre code est maladroit. C ++ introduit des références pour résoudre ce problème. Si nous pouvons simplement aliaser une variable, nous obtenons le code que nous recherchions:

 void swap(int& x, int& y) { int temp = x; x = y; y = temp; } int a, b; swap(a, b); // inside, x and y are really a and b 

À la fois faciles à utiliser et sûrs. (Nous ne pouvons pas transmettre accidentellement un null, il n’y a pas de références null.) Cela fonctionne car le swap qui se produit à l’intérieur de la fonction se produit réellement sur les variables faisant l’object d’un alias en dehors de la fonction.

(Remarque: n’écrivez jamais une fonction de swap . 🙂 Une déjà présente dans l’en-tête , et elle est conçue pour fonctionner avec n’importe quel type.)


Une autre utilisation consiste à supprimer cette copie qui se produit lorsque vous appelez une fonction. Considérons que nous avons un type de données très volumineux. Copier cet object prend beaucoup de temps et nous aimerions éviter cela:

 struct big_data { char data[9999999]; }; // big! void do_something(big_data data); big_data d; do_something(d); // ouch, making a copy of all that data :< 

Cependant, tout ce dont nous avons réellement besoin est un alias de la variable, alors indiquons-le. (Encore une fois, en C, nous passerions l'adresse de notre type de données volumineuses, en résolvant le problème de copie mais en introduisant de la maladresse.):

 void do_something(big_data& data); big_data d; do_something(d); // no copies at all! data aliases d within the function 

C'est pourquoi vous entendrez dire que vous devez toujours passer les choses par référence, sauf s'il s'agit de types primitifs. (Parce que passer en interne un alias est probablement fait avec un pointeur, comme en C. Pour les petits objects, il est juste plus rapide de faire la copie alors se soucier des pointeurs.)

Gardez à l'esprit que vous devez être correct. Cela signifie que si votre fonction ne modifie pas le paramètre, marquez-le comme const . Si do_something ci-dessus ne regardait que mais ne do_something pas les data , nous le do_something comme const :

 void do_something(const big_data& data); // alias a big_data, and don't change it 

Nous évitons la copie et nous disons "hé, nous ne modifierons pas cela." Cela a d'autres effets secondaires (avec des choses comme les variables temporaires), mais vous ne devriez pas vous en préoccuper maintenant.

En revanche, notre fonction de swap ne peut pas être const , car nous modifions en effet les alias.

J'espère que cela clarifie un peu plus.


* Didacticiel sur les pointeurs bruts:

Un pointeur est une variable qui contient l'adresse d'une autre variable. Par exemple:

 int i; // normal int int* p; // points to an integer (is not an integer!) p = &i; // &i means "address of i". p is pointing to i *p = 2; // *p means "dereference p". that is, this goes to the int // pointed to by p (i), and sets it to 2. 

Donc, si vous avez vu la fonction d'échange de pointeur-version, nous passons l'adresse des variables que nous voulons permuter, puis nous effectuons le swap, le déréférencement pour obtenir et définir des valeurs.

Prenons un exemple simple d’une fonction nommée increment qui incrémente son argument. Considérer:

 void increment(int input) { input++; } 

ce qui ne fonctionnera pas comme le changement se produit sur la copie de l’argument passé à la fonction sur le paramètre réel. Alors

 int i = 1; std::cout< 

produira 1 1 en sortie.

Pour que la fonction fonctionne sur le paramètre réel passé, nous passons sa reference à la fonction en tant que:

 void increment(int &input) { // note the & input++; } 

la modification apscope à la input dans la fonction est en cours sur le paramètre réel. Cela produira le résultat attendu de 1 2

La réponse de GMan vous donne la liste des références. Je voulais juste vous montrer une fonction très basique qui doit utiliser des références: swap , qui échange deux variables. Ici c’est pour int s (comme vous l’avez demandé):

 // changes to a & b hold when the function exits void swap(int& a, int& b) { int tmp = a; a = b; b = tmp; } // changes to a & b are local to swap_noref and will go away when the function exits void swap_noref(int a, int b) { int tmp = a; a = b; b = tmp; } // changes swap_ptr makes to the variables pointed to by pa & pb // are visible outside swap_ptr, but changes to pa and pb won't be visible void swap_ptr(int *pa, int *pb) { int tmp = *pa; *pa = *pb; *pb = tmp; } int main() { int x = 17; int y = 42; // next line will print "x: 17; y: 42" std::cout << "x: " << x << "; y: " << y << std::endl // swap can alter x & y swap(x,y); // next line will print "x: 42; y: 17" std::cout << "x: " << x << "; y: " << y << std::endl // swap_noref can't alter x or y swap_noref(x,y); // next line will print "x: 42; y: 17" std::cout << "x: " << x << "; y: " << y << std::endl // swap_ptr can alter x & y swap_ptr(&x,&y); // next line will print "x: 17; y: 42" std::cout << "x: " << x << "; y: " << y << std::endl } 

Il y a une implémentation de swap plus intelligente pour int s qui n'a pas besoin de temporaire. Cependant, ici, je me soucie plus de la clarté que de l'intelligence.

Sans références (ou pointeurs), swap_noref ne peut pas modifier les variables qui lui sont transmises, ce qui signifie qu'il ne peut tout simplement pas fonctionner. swap_ptr peut modifier les variables, mais il utilise des pointeurs, qui sont en désordre (lorsque les références ne le réduisent pas, les pointeurs peuvent faire le travail). swap est le plus simple dans son ensemble.

Sur les pointeurs

Les pointeurs vous permettent de faire les mêmes choses que les références. Cependant, les pointeurs placent davantage de responsabilités sur le programmeur pour les gérer et sur la mémoire vers laquelle ils pointent (un sujet appelé « gestion de la mémoire » - mais ne vous inquiétez pas pour le moment). Par conséquent, les références devraient être votre outil préféré pour le moment.

Considérez les variables comme des noms liés à des cases qui stockent une valeur. Les constantes sont des noms liés directement aux valeurs. Les deux cartes mappent les valeurs, mais la valeur des constantes ne peut pas être modifiée. Bien que la valeur contenue dans une boîte puisse changer, la liaison de nom à la boîte ne peut pas, ce qui explique pourquoi une référence ne peut pas être modifiée pour faire référence à une variable différente.

Deux opérations de base sur les variables consistent à obtenir la valeur actuelle (en utilisant simplement le nom de la variable) et en atsortingbuant une nouvelle valeur (l'opérateur d'affectation, '='). Les valeurs sont stockées en mémoire (la case contenant une valeur est simplement une région contiguë de la mémoire). Par exemple,

 int a = 17; 

se traduit par quelque chose comme (note: dans la suite, "foo @ 0xDEADBEEF" signifie une variable avec le nom "foo" stockée à l'adresse "0xDEADBEEF". Les adresses de mémoire ont été constituées):

  ____ a @ 0x1000: | 17 | ---- 

Tout ce qui est stocké en mémoire a une adresse de départ, il y a donc une opération supplémentaire: obtenir l'adresse de la valeur ("&" est l'adresse de l'opérateur). Un pointeur est une variable qui stocke une adresse.

 int *pa = &a; 

résulte en:

  ______ ____ pa @ 0x10A0: |0x1000| ------> @ 0x1000: | 17 | ------ ---- 

Notez qu'un pointeur stocke simplement une adresse mémoire, il n'a donc pas access au nom de ce qu'il pointe. En fait, les pointeurs peuvent indiquer des choses sans nom, mais c'est un sujet pour un autre jour.

Il y a quelques opérations sur les pointeurs. Vous pouvez déréférencer un pointeur (l'opérateur "*"), qui vous donne les données pointées par le pointeur. Le déréférencement est le contraire de l'obtention de l'adresse: *&a est la même boîte que a , &*pa est la même valeur que pa , et *pa est la même boîte que a . En particulier, pa dans l'exemple contient 0x1000; * pa signifie "l'int en mémoire à l'emplacement pa", ou "l'int en mémoire à l'emplacement 0x1000". "a" est également "l'int à l'emplacement de la mémoire 0x1000". Les autres opérations sur les pointeurs sont l'addition et la soustraction, mais c'est aussi un sujet pour un autre jour.

 // Passes in mutable references of a and b. int doSomething(int& a, int& b) { a = 5; cout << "1: " << a << b; // prints 1: 5,6 } a = 0; b = 6; doSomething(a, b); cout << "2: " << a << ", " << b; // prints 2: 5,6 

Alternativement,

 // Passes in copied values of a and b. int doSomething(int a, int b) { a = 5; cout << "1: " << a << b; // prints 1: 5,6 } a = 0; b = 6; doSomething(a, b); cout << "2: " << a << ", " << b; // prints 2: 0,6 

Ou la version const:

 // Passes in const references a and b. int doSomething(const int &a, const int &b) { a = 5; // COMPILE ERROR, cannot assign to const reference. cout << "1: " << b; // prints 1: 6 } a = 0; b = 6; doSomething(a, b); 

Les références sont utilisées pour transmettre des emplacements de variables, elles n'ont donc pas besoin d'être copiées sur la stack vers la nouvelle fonction.

Une simple paire d’exemples que vous pouvez exécuter en ligne.

Le premier utilise une fonction normale et le second utilise des références:

  • Exemple 1 (pas de référence)
  • Exemple 2 (référence)

Edit – voici le code source, si vous n’aimez pas les liens:

Exemple 1

 using namespace std; void foo(int y){ y=2; } int main(){ int x=1; foo(x); cout< 

Exemple 2

 using namespace std; void foo(int & y){ y=2; } int main(){ int x=1; foo(x); cout< 

Je ne sais pas si c’est le plus basique, mais voilà …

 typedef int Element; typedef std::list ElementList; // Defined elsewhere. bool CanReadElement(void); Element ReadSingleElement(void); int ReadElementsIntoList(int count, ElementList& elems) { int elemsRead = 0; while(elemsRead < count && CanReadElement()) elems.push_back(ReadSingleElement()); return count; } 

Ici, nous utilisons une référence pour passer notre liste d'éléments dans ReadElementsIntoList() . De cette façon, la fonction charge les éléments directement dans la liste. Si nous n'utilisions pas de référence, elems serait une copie de la liste transmise, à laquelle seraient ajoutés les éléments, mais les elems seraient alors supprimés au retour de la fonction.

Cela fonctionne dans les deux sens. Dans le cas de count , nous ne le faisons pas comme une référence, car nous ne voulons pas modifier le compte transmis, mais plutôt le nombre d'éléments lus. Cela permet au code d'appel de comparer le nombre d'éléments réellement lus au nombre demandé. S'ils ne correspondent pas, CanReadElement() doit avoir renvoyé la valeur false , et essayer immédiatement de lire d'autres échouerait probablement. S'ils correspondent, alors peut-être que le nombre était inférieur au nombre d'éléments disponibles, et une autre lecture serait appropriée. Enfin, si ReadElementsIntoList() devait modifier le count interne, il pourrait le faire sans ranger l'appelant.

Que diriez-vous par métaphore: Disons que votre fonction compte les haricots dans un pot. Il a besoin du pot de haricots et vous devez connaître le résultat qui ne peut pas être la valeur de retour (pour un certain nombre de raisons). Vous pouvez lui envoyer le jar et la valeur de la variable, mais vous ne saurez jamais si ou en quoi cela change la valeur. Au lieu de cela, vous devez lui envoyer cette variable via une enveloppe adressée de retour, afin qu’elle puisse y mettre la valeur et savoir qu’elle a écrit le résultat à la valeur de ladite adresse.

Corrigez-moi si je me trompe, mais une référence est seulement un pointeur déréférencé, ou?

La différence avec un pointeur est que vous ne pouvez pas facilement commettre une valeur NULL.