c++ passage par - Quelles sont les différences entre une variable de pointeur et une variable de référence en C ++?





15 Answers

Qu'est-ce qu'une référence C ++ ( pour les programmeurs C )

Une référence peut être considérée comme un pointeur constant (à ne pas confondre avec un pointeur sur une valeur constante!) Avec une indirection automatique, c'est-à-dire que le compilateur appliquera l'opérateur * à votre place.

Toutes les références doivent être initialisées avec une valeur non nulle, sinon la compilation échouera. Il n'est pas non plus possible d'obtenir l'adresse d'une référence. L'opérateur d'adresse renverra l'adresse de la valeur référencée. Il n'est pas non plus possible de faire de l'arithmétique sur des références.

Les programmeurs C peuvent ne pas aimer les références C ++ car cela ne sera plus évident en cas d'indirection ou si un argument est passé par valeur ou par pointeur sans examiner les signatures des fonctions.

Les programmeurs C ++ peuvent ne pas aimer utiliser les pointeurs car ils sont considérés comme peu sûrs - bien que les références ne soient pas vraiment plus sûres que les pointeurs constants, sauf dans les cas les plus triviaux - manquent de la commodité de l'indirection automatique et portent une connotation sémantique différente.

Considérez la déclaration suivante de la FAQ C ++ :

Même si une référence est souvent implémentée à l'aide d'une adresse dans le langage d'assemblage sous-jacent, ne considérez pas une référence comme un pointeur amusant sur un objet. Une référence est l'objet. Ce n'est pas un pointeur sur l'objet, ni une copie de l'objet. C'est l'objet.

Mais si une référence était vraiment l'objet, comment pourrait-il y avoir des références en suspens? Dans les langues non gérées, il est impossible pour les références d'être «plus sûres» que les pointeurs - il n'y a généralement pas de moyen de créer des alias fiables entre les limites de la portée!

Pourquoi je considère les références C ++ utiles

Venant d’un contexte C, les références C ++ peuvent sembler un concept quelque peu idiot, mais vous devriez quand même les utiliser à la place des pointeurs: l’indirection automatique est pratique, et les références deviennent particulièrement utiles lorsqu’il s’agit de RAII - mais pas pour des raisons de sécurité. avantage, mais plutôt parce qu’ils rendent l’écriture de code idiomatique moins compliquée.

RAII est l'un des concepts centraux du C ++, mais il interagit de manière non triviale avec la sémantique de la copie. Passer des objets par référence évite ces problèmes car aucune copie n'est impliquée. Si les références ne sont pas présentes dans la langue, vous devrez utiliser des pointeurs, qui sont plus encombrants à utiliser, enfreignant ainsi le principe de conception de langue selon lequel la solution de meilleure pratique devrait être plus facile que les alternatives.

open classroom difference

Je sais que les références sont du sucre syntaxique. Le code est donc plus facile à lire et à écrire.

Mais quelles sont les différences?

Résumé des réponses et des liens ci-dessous:

  1. Un pointeur peut être réaffecté un nombre quelconque de fois, tandis qu'une référence ne peut pas être réaffectée après la liaison.
  2. Les pointeurs peuvent pointer nulle part ( NULL ), alors qu'une référence fait toujours référence à un objet.
  3. Vous ne pouvez pas prendre l'adresse d'une référence comme vous pouvez le faire avec des pointeurs.
  4. Il n'y a pas d '"arithmétique de référence" (mais vous pouvez prendre l'adresse d'un objet pointé par une référence et faire de l'arithmétique de pointeur dessus comme dans &obj + 5 ).

Pour clarifier une idée fausse:

La norme C ++ veille à ne pas indiquer comment un compilateur peut implémenter des références, mais chaque compilateur C ++ implémente des références en tant que pointeurs. C'est-à-dire une déclaration telle que:

int &ri = i;

s'il n'est pas optimisé complètement , alloue la même quantité de mémoire qu'un pointeur et place l'adresse de i dans cette mémoire.

Ainsi, un pointeur et une référence utilisent la même quantité de mémoire.

En règle générale,

  • Utilisez des références dans les paramètres de fonction et les types de retour pour fournir des interfaces utiles et auto-documentées.
  • Utilisez des pointeurs pour implémenter des algorithmes et des structures de données.

Lecture intéressante:




Contrairement aux idées reçues, il est possible d’avoir une référence NULL.

int * p = NULL;
int & r = *p;
r = 1;  // crash! (if you're lucky)

Certes, il est beaucoup plus difficile de faire avec une référence - mais si vous y parvenez, vous allez vous arracher les cheveux en essayant de le trouver. Les références ne sont pas intrinsèquement sûres en C ++!

Techniquement, il s'agit d'une référence non valide , pas d'une référence null. C ++ ne prend pas en charge les références nulles en tant que concept, contrairement à d'autres langages. Il existe également d'autres types de références non valides. Toute référence non valide soulève le spectre d' un comportement indéfini , comme le ferait un pointeur non valide.

L'erreur réelle réside dans le déréférencement du pointeur NULL, avant l'affectation à une référence. Mais je ne suis au courant d'aucun compilateur qui générera des erreurs à cette condition - l'erreur se propage à un point plus loin dans le code. C'est ce qui rend ce problème si insidieux. La plupart du temps, si vous déréférenciez un pointeur NULL, vous bloquez à cet endroit et le débogage n'est pas très long à comprendre.

Mon exemple ci-dessus est court et artificiel. Voici un exemple plus réaliste.

class MyClass
{
    ...
    virtual void DoSomething(int,int,int,int,int);
};

void Foo(const MyClass & bar)
{
    ...
    bar.DoSomething(i1,i2,i3,i4,i5);  // crash occurs here due to memory access violation - obvious why?
}

MyClass * GetInstance()
{
    if (somecondition)
        return NULL;
    ...
}

MyClass * p = GetInstance();
Foo(*p);

Je tiens à répéter que le seul moyen d'obtenir une référence null consiste à utiliser du code mal formé. Une fois que vous l'avez obtenue, vous obtenez un comportement indéfini. Il n’a jamais de sens de rechercher une référence nulle; Par exemple, vous pouvez essayer if(&bar==NULL)... mais le compilateur peut optimiser l’instruction hors existence! Une référence valide ne peut jamais être NULL. Par conséquent, du point de vue du compilateur, la comparaison est toujours fausse et il est libre d'éliminer la clause if tant que code mort. Il s'agit de l'essence du comportement indéfini.

La bonne façon d'éviter les ennuis est d'éviter de déréférencer un pointeur NULL pour créer une référence. Voici un moyen automatisé pour y parvenir.

template<typename T>
T& deref(T* p)
{
    if (p == NULL)
        throw std::invalid_argument(std::string("NULL reference"));
    return *p;
}

MyClass * p = GetInstance();
Foo(deref(p));

Pour un regard plus ancien sur ce problème d’une personne ayant de meilleures compétences en écriture, voir Références nulles de Jim Hyslop et Herb Sutter.

Pour un autre exemple des dangers liés à la déréférence d'un pointeur null, voir Exposer un comportement indéfini lors d'une tentative de transfert de code sur une autre plate-forme de Raymond Chen.




Vous avez oublié la partie la plus importante:

membre-access avec des pointeurs utilise ->
accès membre avec références utilise .

foo.bar est clairement supérieur à foo->bar de la même manière que vi est clairement supérieur à Emacs :-)




Les références sont très similaires aux pointeurs, mais elles sont spécialement conçues pour aider à optimiser les compilateurs.

  • Les références sont conçues de telle sorte qu'il est beaucoup plus facile pour le compilateur de tracer quels alias de référence quelles variables. Deux caractéristiques principales sont très importantes: aucune "arithmétique de référence" et aucune réaffectation de références. Celles-ci permettent au compilateur de déterminer quelles références alias quelles variables au moment de la compilation.
  • Les références sont autorisées à faire référence à des variables qui n'ont pas d'adresse mémoire, telles que celles que le compilateur choisit de mettre dans des registres. Si vous prenez l'adresse d'une variable locale, il est très difficile pour le compilateur de la mettre dans un registre.

Par exemple:

void maybeModify(int& x); // may modify x in some way

void hurtTheCompilersOptimizer(short size, int array[])
{
    // This function is designed to do something particularly troublesome
    // for optimizers. It will constantly call maybeModify on array[0] while
    // adding array[1] to array[2]..array[size-1]. There's no real reason to
    // do this, other than to demonstrate the power of references.
    for (int i = 2; i < (int)size; i++) {
        maybeModify(array[0]);
        array[i] += array[1];
    }
}

Un compilateur optimiseur peut se rendre compte que nous accédons à un assez grand nombre de [0] et de [1]. Nous aimerions optimiser l’algorithme pour:

void hurtTheCompilersOptimizer(short size, int array[])
{
    // Do the same thing as above, but instead of accessing array[1]
    // all the time, access it once and store the result in a register,
    // which is much faster to do arithmetic with.
    register int a0 = a[0];
    register int a1 = a[1]; // access a[1] once
    for (int i = 2; i < (int)size; i++) {
        maybeModify(a0); // Give maybeModify a reference to a register
        array[i] += a1;  // Use the saved register value over and over
    }
    a[0] = a0; // Store the modified a[0] back into the array
}

Pour effectuer une telle optimisation, il faut prouver que rien ne peut changer de tableau [1] pendant l'appel. C'est plutôt facile à faire. i n'est jamais inférieur à 2, donc array [i] ne peut jamais faire référence à array [1]. Il est donné a0 comme référence à peut-être peut-êtreModify () (tableau d'aliasing [0]). Puisqu'il n'y a pas d'arithmétique "de référence", le compilateur doit simplement prouver que peut-êtreModify n'obtient jamais l'adresse de x et qu'il a prouvé que rien ne change de tableau [1].

Il doit également prouver qu’il est impossible pour un futur appel de lire / écrire un [0] tant que nous en avons une copie dans le registre temporaire en a0. Cela est souvent trivial à prouver, car dans de nombreux cas, il est évident que la référence n’est jamais stockée dans une structure permanente comme une instance de classe.

Maintenant, faites la même chose avec les pointeurs

void maybeModify(int* x); // May modify x in some way

void hurtTheCompilersOptimizer(short size, int array[])
{
    // Same operation, only now with pointers, making the
    // optimization trickier.
    for (int i = 2; i < (int)size; i++) {
        maybeModify(&(array[0]));
        array[i] += array[1];
    }
}

Le comportement est le même; seulement maintenant il est beaucoup plus difficile de prouver que peut-êtreModify ne modifie jamais un tableau [1], car nous lui avons déjà donné un pointeur; le chat est sorti du sac. Il doit maintenant faire la preuve beaucoup plus difficile: une analyse statique de peut-modifier pour prouver qu’elle n’écrit jamais dans & x + 1. Elle doit également prouver qu’elle ne sauvegarde jamais un pointeur pouvant faire référence à aussi délicat.

Les compilateurs modernes s'améliorent de mieux en mieux en analyse statique, mais il est toujours agréable de les aider et d'utiliser des références.

Bien entendu, à moins d'optimisations aussi astucieuses, les compilateurs transformeront les références en indicateurs si nécessaire.

EDIT: Cinq ans après la publication de cette réponse, j’ai trouvé une différence technique réelle dans laquelle les références diffèrent d’une manière différente de voir le même concept d’adressage. Les références peuvent modifier la durée de vie des objets temporaires de la même manière que les pointeurs.

F createF(int argument);

void extending()
{
    const F& ref = createF(5);
    std::cout << ref.getArgument() << std::endl;
};

Normalement, les objets temporaires tels que celui créé par l'appel à createF(5) sont détruits à la fin de l'expression. Cependant, en liant cet objet à une référence ref , C ++ étendra la durée de vie de cet objet temporaire jusqu'à ce que ref disparaisse de sa portée.




Bien que les références et les pointeurs soient utilisés pour accéder indirectement à une autre valeur, il existe deux différences importantes entre les références et les pointeurs. La première est qu'une référence fait toujours référence à un objet: c'est une erreur de définir une référence sans l'initialiser. Le comportement de l'affectation est la deuxième différence importante: l'affectation à une référence modifie l'objet auquel la référence est liée; il ne lie pas la référence à un autre objet. Une fois initialisée, une référence fait toujours référence au même objet sous-jacent.

Considérons ces deux fragments de programme. Dans le premier, nous assignons un pointeur à un autre:

int ival = 1024, ival2 = 2048;
int *pi = &ival, *pi2 = &ival2;
pi = pi2;    // pi now points to ival2

Après l'affectation, ival, l'objet adressé par pi reste inchangé. L'affectation change la valeur de pi en la faisant pointer vers un autre objet. Considérons maintenant un programme similaire qui assigne deux références:

int &ri = ival, &ri2 = ival2;
ri = ri2;    // assigns ival2 to ival

Cette affectation change de valeur, la valeur référencée par ri et non la référence elle-même. Après l'affectation, les deux références font toujours référence à leurs objets d'origine et la valeur de ces objets est désormais la même.




Une référence est un alias pour une autre variable alors qu'un pointeur contient l'adresse mémoire d'une variable. Les références sont généralement utilisées comme paramètres de fonction pour que l'objet transmis ne soit pas la copie, mais l'objet lui-même.

    void fun(int &a, int &b); // A common usage of references.
    int a = 0;
    int &b = a; // b is an alias for a. Not so common to use. 



Ceci est basé sur le tutorial . Ce qui est écrit le rend plus clair:

>>> The address that locates a variable within memory is
    what we call a reference to that variable. (5th paragraph at page 63)

>>> The variable that stores the reference to another
    variable is what we call a pointer. (3rd paragraph at page 64)

Simplement pour s'en souvenir,

>>> reference stands for memory location
>>> pointer is a reference container (Maybe because we will use it for
several times, it is better to remember that reference.)

De plus, comme nous pouvons faire référence à presque tous les didacticiels de pointeur, un pointeur est un objet pris en charge par l'arithmétique de pointeur qui rend le pointeur similaire à un tableau.

Regardez la déclaration suivante,

int Tom(0);
int & alias_Tom = Tom;

alias_Tompeut être compris comme un alias of a variable(différent avec typedef, ce qui est alias of a type) Tom. Il est également correct d'oublier que la terminologie d'une telle déclaration est de créer une référence de Tom.




Une référence à un pointeur est possible en C ++, mais l'inverse n'est pas possible signifie qu'un pointeur sur une référence n'est pas possible. Une référence à un pointeur fournit une syntaxe de nettoyage permettant de modifier le pointeur. Regardez cet exemple:

#include<iostream>
using namespace std;

void swap(char * &str1, char * &str2)
{
  char *temp = str1;
  str1 = str2;
  str2 = temp;
}

int main()
{
  char *str1 = "Hi";
  char *str2 = "Hello";
  swap(str1, str2);
  cout<<"str1 is "<<str1<<endl;
  cout<<"str2 is "<<str2<<endl;
  return 0;
}

Et considérons la version C du programme ci-dessus. En C, vous devez utiliser un pointeur à l'autre (indirection multiple), ce qui crée de la confusion et rend le programme complexe.

#include<stdio.h>
/* Swaps strings by swapping pointers */
void swap1(char **str1_ptr, char **str2_ptr)
{
  char *temp = *str1_ptr;
  *str1_ptr = *str2_ptr;
  *str2_ptr = temp;
}

int main()
{
  char *str1 = "Hi";
  char *str2 = "Hello";
  swap1(&str1, &str2);
  printf("str1 is %s, str2 is %s", str1, str2);
  return 0;
}

Consultez la section suivante pour plus d'informations sur la référence au pointeur:

Comme je l'ai dit, un pointeur sur une référence n'est pas possible. Essayez le programme suivant:

#include <iostream>
using namespace std;

int main()
{
   int x = 10;
   int *ptr = &x;
   int &*ptr1 = ptr;
}



J'utilise des références sauf si j'ai besoin de l'un ou l'autre de ces éléments:

  • Les pointeurs nuls peuvent être utilisés comme valeur sentinelle, souvent un moyen peu coûteux d'éviter la surcharge de fonctions ou l'utilisation d'un bool.

  • Vous pouvez faire de l'arithmétique sur un pointeur. Par exemple,p += offset;




Une autre différence est que vous pouvez avoir des pointeurs sur un type de vide (et cela signifie un pointeur sur n'importe quoi) mais les références à vide sont interdites.

int a;
void * p = &a; // ok
void & p = a;  //  forbidden

Je ne peux pas dire que je suis vraiment heureux avec cette différence particulière. Je préférerais de beaucoup que cela soit autorisé avec la signification du mot référence à tout ce qui a une adresse et sinon le même comportement pour les références. Cela permettrait de définir certains équivalents des fonctions de la bibliothèque C telles que memcpy en utilisant des références.




En outre, une référence qui est un paramètre pour une fonction en ligne peut être gérée différemment d'un pointeur.

void increment(int *ptrint) { (*ptrint)++; }
void increment(int &refint) { refint++; }
void incptrtest()
{
    int testptr=0;
    increment(&testptr);
}
void increftest()
{
    int testref=0;
    increment(testref);
}

De nombreux compilateurs insérant la version du pointeur obligent en réalité une écriture en mémoire (nous prenons l'adresse explicitement). Cependant, ils laisseront la référence dans un registre plus optimal.

Bien sûr, pour les fonctions qui ne sont pas en ligne, le pointeur et la référence génèrent le même code et il est toujours préférable de transmettre les éléments intrinsèques par valeur plutôt que par référence s'ils ne sont pas modifiés et retournés par la fonction.




Une autre utilisation intéressante des références est de fournir un argument par défaut d'un type défini par l'utilisateur:

class UDT
{
public:
   UDT() : val_d(33) {};
   UDT(int val) : val_d(val) {};
   virtual ~UDT() {};
private:
   int val_d;
};

class UDT_Derived : public UDT
{
public:
   UDT_Derived() : UDT() {};
   virtual ~UDT_Derived() {};
};

class Behavior
{
public:
   Behavior(
      const UDT &udt = UDT()
   )  {};
};

int main()
{
   Behavior b; // take default

   UDT u(88);
   Behavior c(u);

   UDT_Derived ud;
   Behavior d(ud);

   return 1;
}

La saveur par défaut utilise l'aspect 'bind const reference to' temporaire 'des références.




Peut-être que certaines métaphores aideront; Dans le contexte de votre écran de bureau -

  • Une référence nécessite de spécifier une fenêtre réelle.
  • Un pointeur nécessite l'emplacement d'un espace sur l'écran qui, à son avis, ne contiendra aucune ou plusieurs instances de ce type de fenêtre.



Il existe une différence non technique très importante entre les pointeurs et les références: un argument passé à une fonction par un pointeur est beaucoup plus visible qu'un argument passé à une fonction par une référence non const. Par exemple:

void fn1(std::string s);
void fn2(const std::string& s);
void fn3(std::string& s);
void fn4(std::string* s);

void bar() {
    std::string x;
    fn1(x);  // Cannot modify x
    fn2(x);  // Cannot modify x (without const_cast)
    fn3(x);  // CAN modify x!
    fn4(&x); // Can modify x (but is obvious about it)
}

De retour en C, un appel qui ressemble à fn(x)ne peut être passé que par valeur, il ne peut donc pas être modifié x; pour modifier un argument, vous devez passer un pointeur fn(&x). Donc, si un argument n'était pas précédé d'un, &vous saviez qu'il ne serait pas modifié. (L'inverse, &moyen modifié, n'était pas vrai car il fallait parfois passer de grandes structures en lecture seule par constpointeur.)

Certains avancent que c'est une fonctionnalité tellement utile lors de la lecture de code que les paramètres de pointeur devraient toujours être utilisés pour des paramètres modifiables plutôt que des non- constréférences, même si la fonction ne s'attend jamais à un nullptr. C'est-à-dire que ces personnes soutiennent que les signatures de fonction comme fn3()ci-dessus ne devraient pas être autorisées. Les directives de style de C ++ de Google en sont un exemple.




Je décide toujours par this règle à partir de C ++ Core Guidelines:

Préférer T * à T & quand "aucun argument" est une option valide




Related