Question Quelles sont les différences entre une variable pointeur et une variable de référence en C ++?


Je sais que les références sont du sucre syntaxique, donc le code est 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éattribué n'importe quel nombre de fois tandis qu'une référence ne peut pas être réattribuée après la liaison.
  2. Les pointeurs ne peuvent pointer nulle part (NULL), alors qu'une référence se réfère toujours à un objet.
  3. Vous ne pouvez pas prendre l'adresse d'une référence comme vous pouvez 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 l'arithmétique du pointeur comme dans &obj + 5).

Pour clarifier une idée fausse:

Le standard C ++ est très prudent pour éviter de dicter comment un compilateur peut   implémenter des références, mais chaque compilateur C ++ implémente   références comme pointeurs. C'est-à-dire une déclaration telle que:

int &ri = i;

si ce n'est pas optimisé complètement, alloue la même quantité de stockage   comme un pointeur, et place l'adresse   de i dans ce stockage.

Ainsi, un pointeur et une référence utilisent tous les deux 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:


2617
2017-09-11 20:03


origine


Réponses:


  1. Un pointeur peut être réattribué:

    int x = 5;
    int y = 6;
    int *p;
    p =  &x;
    p = &y;
    *p = 10;
    assert(x == 5);
    assert(y == 10);
    

    Une référence ne peut pas, et doit être assignée à l'initialisation:

    int x = 5;
    int y = 6;
    int &r = x;
    
  2. Un pointeur a sa propre adresse de mémoire et sa taille sur la pile (4 octets sur x86), tandis qu'une référence partage la même adresse de mémoire (avec la variable d'origine) mais occupe également de l'espace sur la pile. Comme une référence a la même adresse que la variable d'origine elle-même, il est prudent de considérer une référence comme un autre nom pour la même variable. Remarque: ce que pointe un pointeur peut être sur la pile ou le tas. Idem une référence. Ma revendication dans cette déclaration n'est pas qu'un pointeur doit pointer vers la pile. Un pointeur est juste une variable qui contient une adresse mémoire. Cette variable est sur la pile. Depuis une référence a son propre espace sur la pile, et puisque l'adresse est la même que la variable qu'il référence. Plus sur pile vs tas. Cela implique qu'il existe une véritable adresse d'une référence que le compilateur ne vous dira pas.

    int x = 0;
    int &r = x;
    int *p = &x;
    int *p2 = &r;
    assert(p == p2);
    
  3. Vous pouvez avoir des pointeurs vers des pointeurs offrant des niveaux supplémentaires d'indirection. Alors que les références offrent seulement un niveau d'indirection.

    int x = 0;
    int y = 0;
    int *p = &x;
    int *q = &y;
    int **pp = &p;
    pp = &q;//*pp = q
    **pp = 4;
    assert(y == 4);
    assert(x == 0);
    
  4. Le pointeur peut être affecté nullptr directement, alors que la référence ne peut pas. Si vous essayez assez fort, et vous savez comment, vous pouvez faire l'adresse d'une référence nullptr. De même, si vous essayez assez fort, vous pouvez avoir une référence à un pointeur, et cette référence peut contenir nullptr.

    int *p = nullptr;
    int &r = nullptr; <--- compiling error
    int &r = *p;  <--- likely no compiling error, especially if the nullptr is hidden behind a function call, yet it refers to a non-existent int at address 0
    
  5. Les pointeurs peuvent itérer sur un tableau, vous pouvez utiliser ++pour aller à l'élément suivant pointé vers un pointeur, et + 4 aller au 5ème élément. Peu importe la taille de l'objet pointé par le pointeur.

  6. Un pointeur doit être déréférencé avec * pour accéder à l'emplacement de la mémoire vers lequel il pointe, alors qu'une référence peut être utilisée directement. Un pointeur vers une classe / structure utilise -> pour accéder à ses membres alors qu'une référence utilise un ..

  7. Un pointeur est une variable qui contient une adresse mémoire. Indépendamment de la manière dont une référence est implémentée, une référence a la même adresse mémoire que l'élément auquel elle fait référence.

  8. Les références ne peuvent pas être empilées dans un tableau, alors que les pointeurs peuvent être (Mentionné par l'utilisateur @litb)

  9. Les références Const peuvent être liées à des temporaires. Les pointeurs ne peuvent pas (pas sans une certaine indirection):

    const int &x = int(12); //legal C++
    int *y = &int(12); //illegal to dereference a temporary.
    

    Cela fait const& plus sûr pour une utilisation dans les listes d'arguments et ainsi de suite.


1369
2018-02-27 21:26



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

UNE référence peut être considéré comme un pointeur constant (à ne pas confondre avec un pointeur vers une valeur constante!) avec une indirection automatique, c'est-à-dire que le compilateur appliquera * opérateur pour vous.

Toutes les références doivent être initialisées avec une valeur non nulle ou la compilation échouera. Il n'est pas possible d'obtenir l'adresse d'une référence - l'opérateur d'adresse retournera à la place l'adresse de la valeur référencée - il n'est pas non plus possible de faire de l'arithmétique sur les références.

Les programmeurs C peuvent ne pas aimer les références C ++ car il ne sera plus évident quand une indirection se produit ou si un argument est passé par valeur ou par pointeur sans regarder les signatures de fonction.

Les programmeurs C ++ peuvent ne pas aimer utiliser des pointeurs car ils sont considérés comme dangereux - bien que les références ne soient pas plus sûres que les pointeurs constants sauf dans les cas les plus triviaux - manquent de commodité d'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 en utilisant une adresse dans le   langue d'assemblage sous-jacente, s'il vous plaît faire ne pas penser à une référence en tant que   pointeur regardant drôle à un objet. Une référence est L'object. C'est   pas un pointeur sur l'objet, ni une copie de l'objet. Il est la   objet.

Mais si une référence vraiment étaient l'objet, comment pourrait-il y avoir des références qui pendent? Dans les langages non gérés, il est impossible que les références soient plus «sûres» que les pointeurs - il n'y a généralement pas moyen d'alias de manière fiable sur les limites de la portée!

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

Venant d'un arrière-plan C, les références C ++ peuvent ressembler à un concept un peu stupide, mais on devrait toujours les utiliser à la place des pointeurs si possible: indirection automatique est pratique, et les références deviennent particulièrement utiles lorsqu'il s'agit de RAII - mais pas à cause d'un avantage de sécurité perçu, mais plutôt parce qu'ils rendent l'écriture de code idiomatique moins gênante.

RAII est l'un des concepts centraux de C ++, mais il interagit de manière non triviale avec la sémantique de copie. Passer des objets par référence permet d'éviter ces problèmes car il n'y a pas de copie. Si les références n'étaient pas présentes dans la langue, vous devriez utiliser des pointeurs, qui sont plus encombrants à utiliser, violant ainsi le principe de conception du langage selon lequel la solution des meilleures pratiques devrait être plus facile que les alternatives.


301
2017-09-11 21:43



Si vous voulez être vraiment pédant, il y a une chose que vous pouvez faire avec une référence que vous ne pouvez pas faire avec un pointeur: étendre la durée de vie d'un objet temporaire. En C ++, si vous liez une référence const à un objet temporaire, la durée de vie de cet objet devient la durée de vie de la référence.

std::string s1 = "123";
std::string s2 = "456";

std::string s3_copy = s1 + s2;
const std::string& s3_reference = s1 + s2;

Dans cet exemple, s3_copy copie l'objet temporaire résultant de la concaténation. Alors que la référence s3 devient essentiellement l'objet temporaire. C'est vraiment une référence à un objet temporaire qui a maintenant la même durée de vie que la référence.

Si vous essayez ceci sans le const il devrait échouer à compiler. Vous ne pouvez pas lier une référence non-const à un objet temporaire, vous ne pouvez pas non plus prendre son adresse d'ailleurs.


151
2017-09-11 21:06



Contrairement à l'opinion populaire, il est possible d'avoir une référence qui est NULL.

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

Certes, c'est beaucoup plus difficile à 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 sont ne pas intrinsèquement sûr en C ++!

Techniquement c'est un référence non valide, pas une référence nulle. C ++ ne supporte pas les références nulles en tant que concept comme vous pourriez le trouver dans d'autres langages. Il existe également d'autres types de références invalides. Tout référence invalide soulève le spectre de comportement indéfini, tout comme utiliser un pointeur invalide.

L'erreur réelle est 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 sur 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érencer un pointeur NULL, vous plantez directement à cet endroit et il ne faudra pas beaucoup de débogage pour le comprendre.

Mon exemple ci-dessus est court et artificiel. Voici un exemple plus concret.

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éitérer que la seule façon d'obtenir une référence nulle est le code mal formé, et une fois que vous l'avez, vous obtenez un comportement indéfini. Il jamais il est logique de vérifier une référence nulle; par exemple, vous pouvez essayer if(&bar==NULL)... mais le compilateur pourrait optimiser la déclaration hors de l'existence! Une référence valide ne peut jamais être NULL, donc à partir de la vue du compilateur la comparaison est toujours fausse, et il est libre d'éliminer le if clause comme code mort - c'est l'essence du comportement indéfini.

La meilleure façon d'éviter les problèmes est d'éviter de déréférencer un pointeur NULL pour créer une référence. Voici une manière automatisée d'accomplir ceci.

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 vieux regard sur ce problème de quelqu'un avec de meilleures compétences en écriture, voir Références nulles de Jim Hyslop et Herb Sutter.

Pour un autre exemple des dangers de déréférencement d'un pointeur nul, voir Exposer un comportement indéfini lorsque vous essayez de porter le code sur une autre plate-forme par Raymond Chen.


104
2017-09-11 22:10



Vous avez oublié la partie la plus importante:

member-access avec des utilisations de pointeurs -> 
accès-membre avec références utilisations .

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


96
2017-09-11 20:07



En dehors du sucre syntaxique, une référence est un const pointeur (ne pas pointeur vers un const). Vous devez définir à quoi il se réfère lorsque vous déclarez la variable de référence et vous ne pouvez pas le modifier ultérieurement.

Mise à jour: maintenant que j'y pense un peu plus, il y a une différence importante.

La cible d'un pointeur de const peut être remplacée en prenant son adresse et en utilisant une distribution constante.

La cible d'une référence ne peut être remplacée en aucune façon en dessous de UB.

Cela devrait permettre au compilateur de faire plus d'optimisation sur une référence.


95
2017-09-19 12:23



En fait, une référence n'est pas vraiment comme un pointeur.

Un compilateur garde des "références" aux variables, associant un nom à une adresse mémoire; c'est son travail de traduire n'importe quel nom de variable en une adresse mémoire lors de la compilation.

Lorsque vous créez une référence, vous dites seulement au compilateur que vous affectez un autre nom à la variable pointeur; C'est pourquoi les références ne peuvent pas "pointer sur nul", car une variable ne peut pas être, et ne pas être.

Les pointeurs sont des variables; ils contiennent l'adresse d'une autre variable, ou peuvent être nuls. L'important est qu'un pointeur ait une valeur, alors qu'une référence n'a qu'une variable à laquelle elle fait référence.

Maintenant, une explication du code réel:

int a = 0;
int& b = a;

Ici, vous ne créez pas une autre variable qui pointe vers a; vous ajoutez juste un autre nom au contenu de la mémoire détenant la valeur de a. Cette mémoire a maintenant deux noms, a et b, et il peut être adressé en utilisant l'un ou l'autre nom.

void increment(int& n)
{
    n = n + 1;
}

int a;
increment(a);

Lors de l'appel d'une fonction, le compilateur génère généralement des espaces mémoire pour les arguments à copier. La signature de la fonction définit les espaces qui doivent être créés et donne le nom qui doit être utilisé pour ces espaces. Déclarer un paramètre comme référence indique simplement au compilateur d'utiliser l'espace mémoire de la variable d'entrée au lieu d'allouer un nouvel espace mémoire pendant l'appel de la méthode. Il peut sembler étrange de dire que votre fonction manipulera directement une variable déclarée dans la portée appelante, mais rappelez-vous que lors de l'exécution du code compilé, il n'y a plus de portée; il y a juste de la mémoire plate et votre code de fonction pourrait manipuler n'importe quelle variable.

Maintenant, il peut y avoir des cas où votre compilateur peut ne pas être capable de connaître la référence lors de la compilation, comme lors de l'utilisation d'une variable externe. Ainsi, une référence peut ou non être implémentée en tant que pointeur dans le code sous-jacent. Mais dans les exemples que je vous ai donnés, il ne sera probablement pas implémenté avec un pointeur.


57
2017-09-01 03:44



Les références sont très similaires aux pointeurs, mais elles sont spécialement conçues pour faciliter l'optimisation des compilateurs.

  • Les références sont conçues de telle sorte qu'il est beaucoup plus facile pour le compilateur de trouver quels alias de référence quelles variables. Deux caractéristiques majeures sont très importantes: pas de "référence arithmétique" et pas de réaffectation des références. Ceux-ci permettent au compilateur de déterminer quelles références alias quelles variables à la compilation.
  • Les références sont autorisées à se référer à des variables qui n'ont pas d'adresses mémoire, comme celles que le compilateur choisit de placer 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 qui optimise peut réaliser que nous accédons à un [0] et un [1] tout à fait un tas. Il aimerait 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 faire une telle optimisation, il faut prouver que rien ne peut changer array [1] lors de l'appel. C'est plutôt facile à faire. i n'est jamais inférieur à 2, donc array [i] ne peut jamais se référer à array [1]. maybeModify () reçoit a0 comme référence (aliasing array [0]). Parce qu'il n'y a pas d'arithmétique "de référence", le compilateur doit juste prouver que maybeModify n'obtient jamais l'adresse de x, et il a prouvé que rien ne change array [1].

Il doit aussi prouver qu'il n'y a aucun moyen qu'un futur appel puisse lire / écrire un [0] alors que nous en avons une copie temporaire dans a0. C'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 des 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 maybeModify ne modifie jamais array [1], parce que nous lui avons déjà donné un pointeur; le chat est sorti du sac. Maintenant, il doit faire la preuve beaucoup plus difficile: une analyse statique de maybeModify pour prouver qu'il n'écrit jamais à & x + 1. Il doit également prouver qu'il ne sauvegarde jamais un pointeur qui peut se référer à array [0], qui est juste aussi difficile.

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

Bien sûr, à moins de telles optimisations intelligentes, les compilateurs transformeront effectivement les références en pointeurs si nécessaire.


50
2017-09-11 20:12