Quelle est la différence entre une référence C # et un pointeur?


85

Je ne comprends pas très bien la différence entre une référence C # et un pointeur. Ils indiquent tous les deux une place dans la mémoire, n'est-ce pas? La seule différence que je peux comprendre est que les pointeurs ne sont pas aussi intelligents, ne peuvent pointer vers rien sur le tas, sont exempts de garbage collection et ne peuvent référencer que des structures ou des types de base.

L'une des raisons pour lesquelles je pose la question est qu'il existe une perception selon laquelle les gens ont besoin de bien comprendre les pointeurs (de C, je suppose) pour être un bon programmeur. Beaucoup de gens qui apprennent des langues de niveau supérieur passent à côté de cela et ont donc cette faiblesse.

Je ne comprends pas ce qui est si complexe dans un pointeur? C'est fondamentalement juste une référence à un endroit dans la mémoire n'est-ce pas? Il peut renvoyer son emplacement et interagir directement avec l'objet à cet emplacement?

Ai-je manqué un point énorme?


1
La réponse courte est oui, vous avez manqué quelque chose de raisonnablement significatif, et c'est la raison de "... la perception que les gens ont besoin de comprendre les pointeurs". Astuce: C # n'est pas le seul langage disponible.
jdigital

Réponses:


50

Les références C # peuvent et seront déplacées par le garbage collector, mais les pointeurs normaux sont statiques. C'est pourquoi nous utilisons le fixedmot-clé lors de l'acquisition d'un pointeur vers un élément de tableau, pour l'empêcher de se déplacer.

EDIT: Conceptuellement, oui. Ils sont plus ou moins les mêmes.


N'y a-t-il pas une autre commande qui empêche une référence C # d'avoir l'objet de sa référence déplacé par le GC?
Richard

Oh désolé, je pensais que c'était autre chose parce que le message faisait référence à un pointeur.
Richard

Oui, un GCHandle.Alloc ou un Marshal.AllocHGlobal (au-delà du fixe)
ctacke

Il est corrigé en C #, pin_ptr en C ++ / CLI
Mehrdad Afshari

Marshal.AllocHGlobal n'allouera pas du tout de mémoire dans le tas managé et, naturellement, il n'est pas soumis au garbage collection.
Mehrdad Afshari le

130

Il existe une distinction légère, mais extrêmement importante, entre un pointeur et une référence. Un pointeur pointe vers un endroit en mémoire tandis qu'une référence pointe vers un objet en mémoire. Les pointeurs ne sont pas "type safe" dans le sens où vous ne pouvez pas garantir l'exactitude de la mémoire vers laquelle ils pointent.

Prenons par exemple le code suivant

int* p1 = GetAPointer();

Il s'agit d'un type sûr dans le sens où GetAPointer doit retourner un type compatible avec int *. Pourtant, il n'y a toujours aucune garantie que * p1 pointera réellement vers un int. Il peut s'agir d'un caractère, d'un double ou simplement d'un pointeur dans la mémoire aléatoire.

Une référence pointe cependant vers un objet spécifique. Les objets peuvent être déplacés dans la mémoire mais la référence ne peut pas être invalidée (sauf si vous utilisez un code non sécurisé). Les références sont beaucoup plus sûres à cet égard que les pointeurs.

string str = GetAString();

Dans ce cas, str a l'un des deux états 1) il ne pointe vers aucun objet et par conséquent est nul ou 2) il pointe vers une chaîne valide. C'est ça. Le CLR garantit que tel est le cas. Il ne peut pas et ne sera pas pour un pointeur.


14
Excellente explication.
iTayb

13

Une référence est un pointeur "abstrait": vous ne pouvez pas faire d'arithmétique avec une référence et vous ne pouvez pas jouer d'astuces de bas niveau avec sa valeur.


8

Une différence majeure entre une référence et un pointeur est qu'un pointeur est une collection de bits dont le contenu n'a d'importance que lorsqu'il est activement utilisé comme pointeur, tandis qu'une référence encapsule non seulement un ensemble de bits, mais aussi des métadonnées qui conservent le cadre sous-jacent informé de son existence. Si un pointeur existe sur un objet en mémoire et que cet objet est supprimé mais que le pointeur n'est pas effacé, la poursuite de l'existence du pointeur ne causera aucun dommage à moins que ou jusqu'à ce qu'une tentative soit faite pour accéder à la mémoire vers laquelle il pointe. Si aucune tentative n'est faite pour utiliser le pointeur, rien ne se souciera de son existence. En revanche, les frameworks basés sur des références comme .NET ou la JVM exigent qu'il soit toujours possible pour le système d'identifier chaque référence d'objet existante, et chaque référence d'objet existante doit toujours être soitnull ou bien identifier un objet de son type approprié.

Notez que chaque référence d'objet encapsule en fait deux types d'informations: (1) le contenu du champ de l'objet qu'elle identifie, et (2) l'ensemble des autres références au même objet. Bien qu'il n'y ait aucun mécanisme par lequel le système peut identifier rapidement toutes les références qui existent à un objet, l'ensemble des autres références qui existent à un objet peut souvent être la chose la plus importante encapsulée par une référence (cela est particulièrement vrai lorsque les choses de type Objectsont utilisées comme des choses comme des jetons de verrouillage). Bien que le système conserve quelques bits de données pour chaque objet pour une utilisation dans GetHashCode, les objets n'ont pas d'identité réelle au-delà de l'ensemble de références qui leur existent. Si Xcontient la seule référence existante à un objet, remplacerXavec une référence à un nouvel objet avec le même contenu de champ n'aura aucun effet identifiable sauf pour changer les bits retournés par GetHashCode(), et même cet effet n'est pas garanti.


5

Les pointeurs pointent vers un emplacement dans l'espace d'adressage mémoire. Les références pointent vers une structure de données. Les structures de données sont toutes déplacées tout le temps (enfin, pas si souvent, mais de temps en temps) par le garbage collector (pour le compactage de l'espace mémoire). De plus, comme vous l'avez dit, les structures de données sans références seront récupérées après un certain temps.

En outre, les pointeurs ne sont utilisables que dans un contexte non sécurisé.


5

Je pense qu'il est important pour les développeurs de comprendre le concept de pointeur, c'est-à-dire de comprendre l'indirection. Cela ne veut pas dire qu'ils doivent nécessairement utiliser des pointeurs. Il est également important de comprendre que le concept d'une référence diffère de la notion de pointeur , bien que subtilement, mais que la mise en œuvre d'une référence presque toujours est un pointeur.

C'est-à-dire qu'une variable contenant une référence est juste un bloc de mémoire de la taille d'un pointeur contenant un pointeur vers l'objet. Cependant, cette variable ne peut pas être utilisée de la même manière qu'une variable pointeur peut être utilisée. En C # (et C, et C ++, ...), un pointeur peut être indexé comme un tableau, mais pas une référence. En C #, une référence est suivie par le garbage collector, un pointeur ne peut pas l'être. En C ++, un pointeur peut être réaffecté, une référence ne le peut pas. Syntaxiquement et sémantiquement, les pointeurs et les références sont assez différents, mais mécaniquement, ils sont identiques.


La chose du tableau semble intéressante, est-ce essentiellement là où vous pouvez dire au pointeur de décaler l'emplacement de la mémoire comme un tableau alors que vous ne pouvez pas le faire avec une référence? Je ne peux pas penser quand cela serait utile mais intéressant néanmoins.
Richard

Si p est un int * (un pointeur vers un int), alors (p + 1) est l'adresse identifiée par p + 4 octets (la taille d'un int). Et p [1] est identique à * (p + 1) (c'est-à-dire qu'il "déréférence" l'adresse 4 octets après p). En revanche, avec une référence de tableau (en C #), l'opérateur [] effectue un appel de fonction.
P Daddy le

5

Tout d'abord, je pense que vous devez définir un "pointeur" dans votre sématique. Voulez-vous dire le pointeur que vous pouvez créer dans un code non sécurisé avec fixe ? Voulez-vous dire un IntPtr que vous obtenez peut-être d'un appel natif ou de Marshal.AllocHGlobal ? Voulez-vous dire un GCHandle ? Tout est essentiellement la même chose - une représentation d'une adresse mémoire où quelque chose est stocké - que ce soit une classe, un nombre, une structure, peu importe. Et pour mémoire, ils peuvent certainement être sur le tas.

Un pointeur (toutes les versions ci-dessus) est un élément fixe. Le GC n'a aucune idée de ce qui se trouve à cette adresse, et n'a donc pas la capacité de gérer la mémoire ou la vie de l'objet. Cela signifie que vous perdez tous les avantages d'un système de ramassage des ordures. Vous devez gérer manuellement la mémoire des objets et vous risquez des fuites.

Une référence en revanche est à peu près un "pointeur géré" que le GC connaît. C'est toujours l'adresse d'un objet, mais maintenant le GC connaît les détails de la cible, il peut donc la déplacer, faire des compactions, finaliser, éliminer et toutes les autres choses intéressantes qu'un environnement géré fait.

La principale différence réside vraiment dans la manière et la raison pour laquelle vous les utiliseriez. Dans la grande majorité des cas dans un langage géré, vous allez utiliser une référence d'objet. Les pointeurs deviennent pratiques pour l'interopérabilité et le besoin rare d'un travail très rapide.

Edit: En fait, voici un bon exemple de cas où vous pourriez utiliser un "pointeur" dans du code managé - dans ce cas, c'est un GCHandle, mais la même chose aurait pu être faite avec AllocHGlobal ou en utilisant fixed sur un tableau d'octets ou une structure. J'ai tendance à préférer le GCHandle car il me semble plus ".NET".


Un petit problème que vous ne devriez peut-être pas dire "pointeur géré" ici - même avec des guillemets effrayants - car c'est quelque chose de très différent d'une référence d'objet, en IL. Bien qu'il existe une syntaxe pour les pointeurs managés en C ++ / CLI, ils ne sont généralement pas accessibles à partir de C #. En IL, ils sont obtenus avec les instructions (ie) ldloca et ldarga.
Glenn Slayden

5

Un pointeur peut pointer vers n'importe quel octet dans l'espace d'adressage de l'application. Une référence est étroitement contrainte et contrôlée et gérée par l'environnement .NET.


1

Ce qui les rend quelque peu complexes, ce n'est pas ce qu'ils sont, mais ce que vous pouvez en faire. Et lorsque vous avez un pointeur vers un pointeur vers un pointeur. C'est là que ça commence vraiment à s'amuser.


1

L'un des plus grands avantages des références par rapport aux pointeurs est une plus grande simplicité et une meilleure lisibilité. Comme toujours, lorsque vous simplifiez quelque chose, vous le rendez plus facile à utiliser, mais au prix de la flexibilité et du contrôle, vous obtenez des éléments de bas niveau (comme d'autres personnes l'ont mentionné).

Les pointeurs sont souvent critiqués pour leur «laideur».

class* myClass = new class();

Maintenant, chaque fois que vous l'utilisez, vous devez d'abord le déréférencer soit en

myClass->Method() or (*myClass).Method()

Malgré la perte de lisibilité et l'ajout de complexité, les gens devaient encore utiliser souvent des pointeurs comme paramètres afin de pouvoir modifier l'objet réel (au lieu de passer par valeur) et pour gagner en performances, ne pas avoir à copier des objets énormes.

Pour moi, c'est pourquoi les références sont «nées» en premier lieu pour offrir le même avantage que les pointeurs, mais sans toute cette syntaxe de pointeur. Vous pouvez maintenant transmettre l'objet réel (pas seulement sa valeur) ET vous disposez d'une manière plus lisible et normale d'interagir avec l'objet.

MyMethod(&type parameter)
{
   parameter.DoThis()
   parameter.DoThat()
}

Les références C ++ différaient des références C # / Java en ce qu'une fois que vous leur attribuiez une valeur qui l'était, vous ne pouviez pas la réattribuer (et elle doit être affectée lorsqu'elle a été déclarée). C'était la même chose que d'utiliser un pointeur const (un pointeur qui ne pouvait pas être redirigé vers un autre objet).

Java et C # sont des langages modernes de très haut niveau qui ont nettoyé une grande partie des dégâts qui s'étaient accumulés en C / C ++ au fil des ans et les pointeurs étaient certainement l'une de ces choses qui devaient être «nettoyées».

Dans la mesure où votre commentaire sur la connaissance des pointeurs fait de vous un programmeur plus fort, cela est vrai dans la plupart des cas. Si vous savez «comment» quelque chose fonctionne plutôt que de simplement l'utiliser sans le savoir, je dirais que cela peut souvent vous donner un avantage. La valeur d'un avantage variera toujours. Après tout, utiliser quelque chose sans savoir comment il est implémenté est l'une des nombreuses beautés de la POO et des interfaces.

Dans cet exemple spécifique, qu'est-ce que savoir sur les pointeurs vous aiderait avec des références? Comprendre qu'une référence C # n'est PAS l'objet lui-même mais pointe vers l'objet est un concept très important.

# 1: Vous ne passez PAS par valeur Eh bien pour commencer, lorsque vous utilisez un pointeur, vous savez que le pointeur ne contient qu'une adresse, c'est tout. La variable elle-même est presque vide et c'est pourquoi il est si agréable de la passer en argument. En plus du gain de performances, vous travaillez avec l'objet réel de sorte que les modifications que vous apportez ne sont pas temporaires

# 2: Polymorphisme / Interfaces Lorsque vous avez une référence qui est un type d'interface et qui pointe vers un objet, vous ne pouvez appeler que des méthodes de cette interface même si l'objet peut avoir beaucoup plus de capacités. Les objets peuvent également implémenter les mêmes méthodes différemment.

Si vous comprenez bien ces concepts, je ne pense pas que vous manquez trop de ne pas avoir utilisé de pointeurs. C ++ est souvent utilisé comme langage d'apprentissage de la programmation car il est parfois bon de se salir les mains. En outre, travailler avec des aspects de niveau inférieur vous permet d'apprécier le confort d'une langue moderne. J'ai commencé avec C ++ et je suis maintenant un programmeur C # et j'ai l'impression que travailler avec des pointeurs bruts m'a aidé à mieux comprendre ce qui se passe sous le capot.

Je ne pense pas qu'il soit nécessaire que tout le monde commence par des pointeurs, mais ce qui est important, c'est qu'ils comprennent pourquoi les références sont utilisées à la place des types valeur et la meilleure façon de comprendre cela est de regarder son ancêtre, le pointeur.


1
Personnellement, je pense que C # aurait été un meilleur langage si la plupart des endroits qui l'utilisaient .utilisé ->, mais foo.bar(123)était synonyme d'un appel à la méthode statique fooClass.bar(ref foo, 123). Cela aurait permis des choses comme myString.Append("George"); [ce qui modifierait la variable myString ], et rendrait plus évidente la différence de sens entre myStruct.field = 3;et myClassObject->field = 3;.
supercat du
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.