web-dev-qa-db-fra.com

Quelle est exactement la différence entre "passer par référence" en C et en C ++?

L'expression "passer par référence" est utilisée par les développeurs C et C++, mais ils semblent être utilisés pour signifier des choses différentes. Quelle est exactement la différence entre cette phrase équivoque dans chaque langue?

26
Joseph Mansfield

Il y a des questions qui traitent déjà de la différence entre passer par référence et passer par valeur . En substance, le passage d'un argument par valeur à une fonction signifie que la fonction aura sa propre copie de l'argument - sa valeur est copiée. La modification de cette copie ne modifiera pas l'objet d'origine. Cependant, lors du passage par référence, le paramètre à l'intérieur de la fonction fait référence au même objet qui a été transmis - toutes les modifications à l'intérieur de la fonction seront visibles à l'extérieur.

Malheureusement, il existe deux façons d’utiliser les expressions "passer par valeur" et "passer par référence", ce qui peut prêter à confusion. Je crois que c'est en partie pourquoi les pointeurs et les références peuvent être difficiles à adopter pour les nouveaux programmeurs C++, surtout lorsqu'ils viennent d'un arrière-plan en C.

C

En C, tout passe par la valeur au sens technique. Autrement dit, tout ce que vous donnez comme argument à une fonction, il sera copié dans cette fonction. Par exemple, l'appel d'une fonction void foo(int) avec foo(x) copie la valeur de x comme paramètre de foo. Cela peut être vu dans un exemple simple:

void foo(int param) { param++; }

int main()
{
  int x = 5;
  foo(x);
  printf("%d\n",x); // x == 5
}

La valeur de x est copiée dans foo et cette copie est incrémentée. x dans main continue d'avoir sa valeur d'origine.

Comme vous le savez sûrement, les objets peuvent être de type pointeur. Par exemple, int* p Définit p comme un pointeur vers un int. Il est important de noter que le code suivant introduit deux objets:

int x = 5;
int* p = &x;

Le premier est de type int et a la valeur 5. Le second est de type int* Et sa valeur est l'adresse du premier objet.

Lorsque vous passez un pointeur sur une fonction, vous le transmettez toujours par valeur. L'adresse qu'il contient est copiée dans la fonction. Modifier ce pointeur à l'intérieur de la fonction ne changera pas le pointeur en dehors de la fonction - cependant, modifier l'objet vers lequel il pointe changera l'objet en dehors de la fonction. Mais pourquoi?

Comme deux pointeurs qui ont la même valeur pointent toujours vers le même objet (ils contiennent la même adresse), l'objet qui est pointé vers peut être accédé et modifié par les deux. Cela donne la sémantique d'avoir passé l'objet pointé par référence, bien qu'aucune référence n'ait jamais réellement existé - il n'y a tout simplement pas de références en C. Jetez un œil à l'exemple modifié:

void foo(int* param) { (*param)++; }

int main()
{
  int x = 5;
  foo(&x);
  printf("%d\n",x); // x == 6
}

On peut dire en passant le int* Dans une fonction, que le int vers lequel il pointe a été "passé par référence" mais en vérité le int n'a jamais été réellement passé nulle part - seul le pointeur a été copié dans la fonction. Cela nous donne le familier1 sens de "passer par valeur" et "passer par référence".

L'utilisation de cette terminologie est étayée par des termes de la norme. Lorsque vous avez un type de pointeur, le type vers lequel il pointe est connu sous le nom de type référencé . Autrement dit, le type référencé de int* Est int.

Un type de pointeur peut être dérivé d'un type de fonction, d'un type d'objet ou d'un type incomplet, appelé référencé tapez .

Alors que l'opérateur unaire * (Comme dans *p) Est connu sous le nom d'indirection dans la norme, il est aussi communément appelé déréférencement d'un pointeur. Cela favorise davantage la notion de "passage par référence" en C.

C++

C++ a adopté bon nombre de ses fonctionnalités de langage d'origine à partir de C. Parmi eux se trouvent des pointeurs et donc cette forme familière de "passage par référence" peut toujours être utilisée - *p Est toujours en train de déréférencer p. Cependant, l'utilisation du terme sera source de confusion, car C++ introduit une fonctionnalité que C n'a pas: la possibilité de vraiment passer les références .

Un type suivi d'une esperluette est un type de référence 2. Par exemple, int& Est une référence à un int. lors du passage d'un argument à une fonction qui prend le type de référence, l'objet est vraiment passé par référence. Il n'y a pas de pointeurs impliqués, pas de copie d'objets, rien. Le nom à l'intérieur de la fonction se réfère en fait exactement au même objet qui a été transmis. Pour contraster avec l'exemple ci-dessus:

void foo(int& param) { param++; }

int main()
{
  int x = 5;
  foo(x);
  std::cout << x << std::endl; // x == 6
}

Maintenant, la fonction foo a un paramètre qui fait référence à un int. Maintenant, lorsque vous passez x, param fait précisément référence au même objet. L'incrémentation de param a un changement visible sur la valeur de x et maintenant x a la valeur 6.

Dans cet exemple, rien n'a été transmis par valeur. Rien n'a été copié. Contrairement à C, où le passage par référence ne faisait en réalité que passer un pointeur par valeur, en C++, nous pouvons vraiment passer par référence.

En raison de cette ambiguïté potentielle dans le terme "passer par référence", il est préférable de ne l'utiliser que dans le contexte de C++ lorsque vous utilisez un type de référence. Si vous passez un pointeur, vous ne passez pas par référence, vous passez un pointeur par valeur (c'est-à-dire, bien sûr, à moins que vous ne passiez une référence à un pointeur! Par exemple int*&). Vous pouvez cependant rencontrer des utilisations de "passer par référence" lorsque des pointeurs sont utilisés, mais maintenant vous savez au moins ce qui se passe réellement.


Autres langues

D'autres langages de programmation compliquent encore les choses. Dans certains, comme Java, chaque variable que vous avez est connue comme une référence à un objet (pas la même chose qu'une référence en C++, plus comme un pointeur), mais ces références sont passées par valeur. Ainsi, même si vous semblez passer à une fonction par référence, ce que vous faites en réalité, c'est copier une référence dans la fonction par valeur. Cette subtile différence avec le passage par référence en C++ est remarquée lorsque vous assignez un nouvel objet à la référence passée en:

public void foo(Bar param) {
  param.something();
  param = new Bar();
}

Si vous deviez appeler cette fonction en Java, en passant un objet de type Bar, l'appel à param.something() serait appelé sur le même objet que vous avez passé. C'est parce que vous avez passé une référence à votre objet. Cependant, même si un nouveau Bar est affecté à param, l'objet en dehors de la fonction est toujours le même ancien objet. Le nouveau n'est jamais vu de l'extérieur. En effet, la référence à l'intérieur de foo est en cours de réaffectation à un nouvel objet. Ce type de réassignation de références est impossible avec les références C++.


1 Par "familier", je ne veux pas suggérer que la signification C de "passer par référence" est moins véridique que la signification C++, juste que C++ a vraiment des types de référence et donc vous passez réellement par référence . La signification C est une abstraction sur ce qui passe vraiment par la valeur.

2 Bien sûr, ce sont des références lvalue et nous avons maintenant des références rvalue aussi en C++ 11.

54
Joseph Mansfield