web-dev-qa-db-fra.com

Comment utiliser les paramètres de référence en C ++?

J'essaie de comprendre comment utiliser les paramètres 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 faisiez pas du paramètre une référence, mais laissiez à la place le & off?

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 les variables de référence sont utilisées pour changer une référence formelle->, qui permet alors un échange bidirectionnel de paramètres. Cependant, c'est l'étendue de mes connaissances, et un exemple plus concret serait très utile.

51
Sagistic

Considérez une référence comme n alias . Lorsque vous invoquez quelque chose sur une référence, vous l'invoquez vraiment sur l'objet auquel la référence se réfère.

int i;
int& j = i; // j is an alias to i

j = 5; // same as i = 5

En ce qui concerne les fonctions, pensez à:

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

Ci-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 sera une copie de x. Ainsi, définir i sur 5 n'a aucun effet sur x, car c'est la copie de x qui est modifiée. Cependant, si nous faisons de i une référence:

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

Dire ensuite foo(x) ne fait plus une copie de x; i est x. Donc, si nous disons foo(x), à l'intérieur de la fonction i = 5; Est exactement la même chose 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 fait une tâche et qui le fait bien. Chaque fois que cette tâche doit être effectuée, vous utilisez cette fonction.

Disons donc 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. Essayons donc ceci:

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

Ça ne marchera pas! Le problème est qu'il s'agit d'échanger 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 était de passer 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), échange deux riens et déréférence les pointeurs nuls ... comportement indéfini! Fixable avec quelques vérifications:

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 regarde à quel point notre code est devenu maladroit. C++ introduit des références pour résoudre ce problème. Si nous pouvons simplement alias 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 facile à utiliser et sûr. (Nous ne pouvons pas accidentellement passer une valeur nulle, il n'y a pas de références nulles.) Cela fonctionne parce que l'échange qui se produit à l'intérieur de la fonction se produit réellement sur les variables étant aliasées en dehors de la fonction.

(Remarque, n'écrivez jamais une fonction swap. :) Il en existe déjà une dans l'en-tête <algorithm>, 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érez que nous avons un type de données très volumineux. La copie de cet objet 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 vraiment besoin, c'est d'un alias pour la variable, alors indiquons cela. (Encore une fois, en C, nous transmettions l'adresse de notre type de données volumineuses, résolvant le problème de copie mais introduisant une 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 passer les choses par référence tout le temps, sauf s'il s'agit de types primitifs. (Parce que le passage interne d'un alias se fait probablement avec un pointeur, comme en C. Pour les petits objets, il est juste plus rapide de faire la copie que de se soucier des pointeurs.)

Gardez à l'esprit que vous devez être const-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 modifiait pas data, nous le marquerions 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 le modifierons pas". Cela a d'autres effets secondaires (avec des choses comme des variables temporaires), mais vous ne devriez pas vous en soucier maintenant.

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

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


* Tutoriel sur les pointeurs approximatifs:

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 version de pointeur, nous transmettons l'adresse des variables que nous voulons échanger, puis nous effectuons l'échange, déréférençant pour obtenir et définir des valeurs.

113
GManNickG

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 car la modification a lieu sur la copie de l'argument passé à la fonction sur le paramètre réel. Alors

int i = 1;
std::cout<<i<<" ";
increment(i);
std::cout<<i<<" ";

produira 1 1 en sortie.

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

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

la modification apportée à input à l'intérieur de la fonction est en fait apportée au paramètre réel. Cela produira la sortie attendue de 1 2

4
codaddict

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. Le voici pour ints (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 existe une implémentation de swap plus intelligente pour ints qui n'a pas besoin d'un 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 altérer les variables, mais il utilise des pointeurs, qui sont désordonnés (quand les références ne le coupent pas tout à fait, cependant, les pointeurs peuvent faire le travail). swap est globalement le plus simple.

Sur les pointeurs

Les pointeurs vous permettent de faire les mêmes choses que les références. Cependant, les pointeurs donnent plus de responsabilité au programmeur pour les gérer et la mémoire vers laquelle ils pointent (un sujet appelé " gestion de la mémoire " - mais ne vous en faites pas pour l'instant). Par conséquent, les références devraient être votre outil préféré pour l'instant.

Considérez les variables comme des noms liés à des boîtes qui stockent une valeur. Les constantes sont des noms liés directement à des valeurs. Les deux mappent les noms aux 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 du nom à la boîte ne peut pas, c'est 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 sont l'obtention de la valeur actuelle (effectuée simplement en utilisant le nom de la variable) et l'attribution d'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;

donne quelque chose comme (remarque: dans ce qui suit, "foo @ 0xDEADBEEF" représente une variable avec le nom "foo" stockée à l'adresse "0xDEADBEEF". Les adresses 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 de plus: obtenir l'adresse de la valeur ("&" est l'opérateur d'adresse de). 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, donc il n'a pas accès au nom de ce vers quoi il pointe. En fait, les pointeurs peuvent pointer vers des choses sans noms, 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 vers lesquelles le pointeur pointe. Le déréférencement est le contraire de l'obtention de l'adresse: *&a est la même case que a, &*pa a la même valeur que pa et *pa est la même case 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 mémoire 0x1000". D'autres opérations sur les pointeurs sont l'addition et la soustraction, mais c'est aussi un sujet pour un autre jour.

4
outis
// 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 passer les emplacements des variables, elles n'ont donc pas besoin d'être copiées sur la pile dans la nouvelle fonction.

1
Stephen

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:


Modifier - voici le code source au cas où vous n'aimeriez pas les liens:

Exemple 1

using namespace std;

void foo(int y){
    y=2;
}

int main(){
    int x=1;
    foo(x);
    cout<<x;//outputs 1
}


Exemple 2

using namespace std;

void foo(int & y){
    y=2;
}

int main(){
    int x=1;
    foo(x);
    cout<<x;//outputs 2
}
1
Cam

Que diriez-vous de la métaphore: Supposons 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 pot et la valeur de la variable, mais vous ne saurez jamais si ou à quoi il change la valeur. Au lieu de cela, vous devez lui envoyer cette variable via une enveloppe adressée de retour, afin qu'il puisse y mettre la valeur et savoir que le résultat est écrit dans la valeur à ladite adresse.

0
George R

Je ne sais pas si c'est le plus basique, mais voilà ...

typedef int Element;
typedef std::list<Element> 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, alors elems serait un copie de la liste transmise, qui aurait des éléments ajoutés, mais alors elems serait ignoré au retour de la fonction.

Cela fonctionne dans les deux sens. Dans le cas de count, nous ne le faisons pas en faisons une référence, car nous ne voulons pas modifier le nombre passé, en renvoyant à la place le nombre d'éléments lus. Cela permet au code appelant de comparer le nombre d'éléments réellement lus au numéro demandé; s'ils ne correspondent pas, alors CanReadElement() doit avoir renvoyé false, et essayer immédiatement d'en lire plus échouera probablement. S'ils correspondent, alors peut-être que count était inférieur au nombre d'éléments disponibles, et une lecture supplémentaire serait appropriée. Enfin, si ReadElementsIntoList() devait modifier count en interne, il pourrait le faire sans débloquer l'appelant.

0
Mike DeSimone

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

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

0
Biber