Was ist der Unterschied zwischen einer C # -Referenz und einem Zeiger?

85

Ich verstehe den Unterschied zwischen einer C # -Referenz und einem Zeiger nicht ganz. Sie zeigen beide auf einen Ort in der Erinnerung, nicht wahr? Der einzige Unterschied, den ich herausfinden kann, ist, dass Zeiger nicht so clever sind, auf nichts auf dem Heap verweisen können, von der Garbage Collection ausgenommen sind und nur auf Strukturen oder Basistypen verweisen können.

Einer der Gründe, die ich frage, ist die Wahrnehmung, dass Menschen Zeiger gut verstehen müssen (von C, denke ich), um ein guter Programmierer zu sein. Viele Leute, die höhere Sprachen lernen, verpassen dies und haben daher diese Schwäche.

Ich verstehe einfach nicht, was an einem Zeiger so komplex ist? Es ist im Grunde nur ein Hinweis auf einen Ort in der Erinnerung, nicht wahr? Kann es seinen Standort zurückgeben und direkt mit dem Objekt an diesem Standort interagieren?

Habe ich einen massiven Punkt verpasst?

Richard
quelle
1
Die kurze Antwort lautet: Ja, Sie haben etwas ziemlich Bedeutendes übersehen, und das ist der Grund für "... die Wahrnehmung, dass Menschen Zeiger verstehen müssen". Hinweis: C # ist nicht die einzige Sprache da draußen.
jdigital

Antworten:

48

C # -Referenzen können und werden vom Garbage Collector verschoben, normale Zeiger sind jedoch statisch. Aus diesem Grund verwenden wir fixedbeim Erfassen eines Zeigers auf ein Array-Element ein Schlüsselwort, um zu verhindern, dass es verschoben wird.

EDIT: Konzeptionell ja. Sie sind mehr oder weniger gleich.

Mehrdad Afshari
quelle
Gibt es nicht einen anderen Befehl, der verhindert, dass eine C # -Referenz das Objekt, auf das sie verweist, vom GC verschiebt?
Richard
Oh, tut mir leid, ich dachte, es wäre etwas anderes, weil der Beitrag auf einen Zeiger verweist.
Richard
Ja, ein GCHandle.Alloc oder ein Marshal.AllocHGlobal (darüber hinaus festgelegt)
ctacke
Es ist in C # behoben, pin_ptr in C ++ / CLI
Mehrdad Afshari
Marshal.AllocHGlobal reserviert überhaupt keinen Speicher im verwalteten Heap und unterliegt natürlich nicht der Speicherbereinigung.
Mehrdad Afshari
129

Es gibt eine leichte, aber äußerst wichtige Unterscheidung zwischen einem Zeiger und einer Referenz. Ein Zeiger zeigt auf eine Stelle im Speicher, während eine Referenz auf ein Objekt im Speicher zeigt. Zeiger sind nicht "typsicher" in dem Sinne, dass Sie die Richtigkeit des Speichers, auf den sie zeigen, nicht garantieren können.

Nehmen Sie zum Beispiel den folgenden Code

int* p1 = GetAPointer();

Dies ist typsicher in dem Sinne, dass GetAPointer einen mit int * kompatiblen Typ zurückgeben muss. Es gibt jedoch noch keine Garantie dafür, dass * p1 tatsächlich auf ein int verweist. Es kann ein Zeichen, ein Doppel oder nur ein Zeiger in den zufälligen Speicher sein.

Eine Referenz verweist jedoch auf ein bestimmtes Objekt. Objekte können im Speicher verschoben werden, aber die Referenz kann nicht ungültig gemacht werden (es sei denn, Sie verwenden unsicheren Code). Referenzen sind in dieser Hinsicht viel sicherer als Zeiger.

string str = GetAString();

In diesem Fall hat str einen von zwei Zuständen 1) es zeigt auf kein Objekt und ist daher null oder 2) es zeigt auf einen gültigen String. Das ist es. Die CLR garantiert dies. Es kann und wird nicht für einen Zeiger.

JaredPar
quelle
13
Tolle Erklärung.
iTayb
13

Eine Referenz ist ein "abstrakter" Zeiger: Sie können mit einer Referenz nicht rechnen und mit ihrem Wert keine Streiche auf niedriger Ebene spielen.

Chris Conway
quelle
8

Ein Hauptunterschied zwischen einer Referenz und einem Zeiger besteht darin, dass ein Zeiger eine Sammlung von Bits ist, deren Inhalt nur dann von Bedeutung ist, wenn er aktiv als Zeiger verwendet wird, während eine Referenz nicht nur einen Satz von Bits, sondern auch einige Metadaten enthält, die die zugrunde liegender Rahmen über seine Existenz informiert. Wenn ein Zeiger auf ein Objekt im Speicher vorhanden ist und dieses Objekt gelöscht wird, der Zeiger jedoch nicht gelöscht wird, verursacht das Fortbestehen des Zeigers keinen Schaden, es sei denn oder bis versucht wird, auf den Speicher zuzugreifen, auf den er verweist. Wenn nicht versucht wird, den Zeiger zu verwenden, kümmert sich nichts um seine Existenz. Im Gegensatz dazu erfordern referenzbasierte Frameworks wie .NET oder die JVM, dass das System immer jede vorhandene Objektreferenz identifizieren kann, und jede vorhandene Objektreferenz muss immer entweder vorhanden seinnull oder identifizieren Sie ein Objekt seines richtigen Typs.

Beachten Sie, dass jede Objektreferenz tatsächlich zwei Arten von Informationen enthält: (1) den Feldinhalt des Objekts, das sie identifiziert, und (2) die Menge anderer Referenzen auf dasselbe Objekt. Obwohl es keinen Mechanismus gibt, mit dem das System alle auf ein Objekt vorhandenen Referenzen schnell identifizieren kann, ist die Menge der anderen Referenzen, die auf ein Objekt existieren, häufig das Wichtigste, was in einer Referenz enthalten ist (dies gilt insbesondere dann, wenn Dinge vom Typ Objectwerden als Dinge wie Sperren verwendet. Obwohl das System einige Datenbits für jedes Objekt zur Verwendung in speichert GetHashCode, haben Objekte keine echte Identität, die über den Satz von Referenzen hinausgeht, die auf sie existieren. Wenn Xder einzige noch vorhandene Verweis auf ein Objekt vorhanden ist, wird ersetztXBei einem Verweis auf ein neues Objekt mit demselben Feldinhalt hat dies keinen erkennbaren Effekt, außer dass die von zurückgegebenen Bits geändert werden GetHashCode(), und selbst dieser Effekt ist nicht garantiert.

Superkatze
quelle
6

Zuerst denke ich, dass Sie einen "Zeiger" in Ihrer Sematik definieren müssen. Meinen Sie den Zeiger, den Sie in unsicherem Code mit festem erstellen können ? Meinen Sie einen IntPtr , den Sie möglicherweise von einem nativen Anruf oder von Marshal.AllocHGlobal erhalten ? Meinst du einen GCHandle ? Alle sind im Wesentlichen dasselbe - eine Darstellung einer Speicheradresse, in der etwas gespeichert ist - sei es eine Klasse, eine Zahl, eine Struktur, was auch immer. Und für die Aufzeichnung können sie sicherlich auf dem Haufen sein.

Ein Zeiger (alle oben genannten Versionen) ist ein fester Punkt. Der GC hat keine Ahnung, was sich an dieser Adresse befindet, und kann daher den Speicher oder die Lebensdauer des Objekts nicht verwalten. Das bedeutet, dass Sie alle Vorteile eines Müllsammelsystems verlieren. Sie müssen den Objektspeicher manuell verwalten und es besteht die Gefahr von Undichtigkeiten.

Eine Referenz hingegen ist so ziemlich ein "verwalteter Zeiger", den der GC kennt. Es ist immer noch eine Adresse eines Objekts, aber jetzt kennt der GC Details des Ziels, sodass er es verschieben, Komprimierungen vornehmen, finalisieren, entsorgen und all die anderen netten Dinge, die eine verwaltete Umgebung tut.

Der Hauptunterschied besteht darin, wie und warum Sie sie verwenden würden. In den meisten Fällen in einer verwalteten Sprache verwenden Sie eine Objektreferenz. Zeiger sind praktisch für Interop und die seltene Notwendigkeit, wirklich schnell zu arbeiten.

Bearbeiten: In der Tat ist hier ein gutes Beispiel dafür, wann Sie einen "Zeiger" in verwaltetem Code verwenden könnten - in diesem Fall ist es ein GCHandle, aber genau das gleiche könnte mit AllocHGlobal oder mit einem festen Byte-Array oder einer Struktur geschehen sein. Ich bevorzuge den GCHandle, weil er sich für mich eher ".NET" anfühlt.

ctacke
quelle
Ein kleiner Streitpunkt, den Sie hier vielleicht nicht sagen sollten - "verwalteter Zeiger" - auch bei Angstzitaten -, weil dies etwas ganz anderes ist als eine Objektreferenz in IL. Obwohl es in C ++ / CLI eine Syntax für verwaltete Zeiger gibt, kann von C # aus nicht auf sie zugegriffen werden. In IL werden sie mit den Anweisungen (dh) ldloca und ldarga erhalten.
Glenn Slayden
5

Zeiger zeigen auf eine Stelle im Speicheradressraum. Referenzen verweisen auf eine Datenstruktur. Alle Datenstrukturen wurden ständig (nicht so oft, aber ab und zu) vom Garbage Collector verschoben (um den Speicherplatz zu komprimieren). Wie Sie bereits sagten, wird bei Datenstrukturen ohne Referenzen nach einer Weile Müll gesammelt.

Außerdem können Zeiger nur in unsicheren Kontexten verwendet werden.

Tamas Czinege
quelle
5

Ich denke, es ist wichtig für Entwickler, das Konzept eines Zeigers zu verstehen, dh die Indirektion zu verstehen. Das bedeutet nicht, dass sie unbedingt Zeiger verwenden müssen. Es ist auch wichtig zu verstehen , dass das Konzept einer Referenz unterscheidet mich von dem Konzept des Zeigers , wenn auch nur subtil, aber , dass die Umsetzung einer Referenz fast immer ist ein Zeiger.

Das heißt, eine Variable, die eine Referenz enthält, ist nur ein zeigergroßer Speicherblock, der einen Zeiger auf das Objekt enthält. Diese Variable kann jedoch nicht auf die gleiche Weise verwendet werden wie eine Zeigervariable. In C # (und C und C ++, ...) kann ein Zeiger wie ein Array indiziert werden, eine Referenz jedoch nicht. In C # wird eine Referenz vom Garbage Collector verfolgt, ein Zeiger kann dies nicht sein. In C ++ kann ein Zeiger neu zugewiesen werden, eine Referenz nicht. Syntaktisch und semantisch sind Zeiger und Referenzen sehr unterschiedlich, aber mechanisch sind sie gleich.

P Papa
quelle
Das Array klingt interessant. Können Sie dem Zeiger im Grunde sagen, dass er den Speicherort wie ein Array versetzen soll, während Sie dies nicht mit einer Referenz tun können? Ich kann mir nicht vorstellen, wann das nützlich, aber trotzdem interessant wäre.
Richard
Wenn p ein int * ist (ein Zeiger auf ein int), dann ist (p + 1) die Adresse, die durch p + 4 Bytes (die Größe eines int) identifiziert wird. Und p [1] ist dasselbe wie * (p + 1) (das heißt, es "dereferenziert" die Adresse 4 Bytes nach p). Im Gegensatz dazu führt der Operator [] mit einer Array-Referenz (in C #) einen Funktionsaufruf aus.
P Daddy
5

Ein Zeiger kann auf ein beliebiges Byte im Adressraum der Anwendung zeigen. Eine Referenz ist stark eingeschränkt und wird von der .NET-Umgebung gesteuert und verwaltet.

jdigital
quelle
1

Die Sache mit Zeigern, die sie etwas komplexer macht, ist nicht, was sie sind, sondern was Sie damit machen können. Und wenn Sie einen Zeiger auf einen Zeiger auf einen Zeiger haben. Dann wird es richtig lustig.

Robert C. Barth
quelle
1

Einer der größten Vorteile von Referenzen gegenüber Zeigern ist die größere Einfachheit und Lesbarkeit. Wie immer, wenn Sie etwas vereinfachen, vereinfachen Sie die Verwendung, aber auf Kosten der Flexibilität und Kontrolle erhalten Sie die Low-Level-Inhalte (wie andere bereits erwähnt haben).

Zeiger werden oft als "hässlich" kritisiert.

class* myClass = new class();

Jedes Mal, wenn Sie es verwenden, müssen Sie es zuerst zuerst dereferenzieren

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

Trotz des Verlusts der Lesbarkeit und der Erhöhung der Komplexität mussten die Benutzer häufig Zeiger als Parameter verwenden, damit Sie das tatsächliche Objekt ändern konnten (anstatt den Wert zu übergeben) und um den Leistungsgewinn zu erzielen, dass keine großen Objekte kopiert werden mussten.

Für mich ist dies der Grund, warum Referenzen in erster Linie "geboren" wurden, um den gleichen Vorteil wie Zeiger zu bieten, jedoch ohne all diese Zeigersyntax. Jetzt können Sie das eigentliche Objekt übergeben (nicht nur seinen Wert) UND Sie haben eine besser lesbare, normale Art der Interaktion mit dem Objekt.

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

C ++ - Referenzen unterschieden sich von C # / Java-Referenzen darin, dass Sie einen Wert, der es war, nicht erneut zuweisen konnten (und er muss zugewiesen werden, als er deklariert wurde). Dies war dasselbe wie die Verwendung eines const-Zeigers (ein Zeiger, der nicht erneut auf ein anderes Objekt gerichtet werden konnte).

Java und C # sind moderne Sprachen auf sehr hohem Niveau, die viele der in C / C ++ im Laufe der Jahre angesammelten Probleme beseitigt haben, und Zeiger waren definitiv eines der Dinge, die "bereinigt" werden mussten.

Soweit Ihr Kommentar zum Erkennen von Zeigern Sie zu einem stärkeren Programmierer macht, trifft dies in den meisten Fällen zu. Wenn Sie wissen, wie etwas funktioniert, anstatt es nur zu verwenden, ohne es zu wissen, würde ich sagen, dass dies Ihnen oft einen Vorteil verschaffen kann. Wie stark eine Kante ist, variiert immer. Schließlich ist es eine der vielen Schönheiten von OOP und Interfaces, etwas zu verwenden, ohne zu wissen, wie es implementiert ist.

Was würde Ihnen in diesem speziellen Beispiel das Wissen über Zeiger bei Referenzen helfen? Es ist ein sehr wichtiges Konzept zu verstehen, dass eine C # -Referenz NICHT das Objekt selbst ist, sondern auf das Objekt verweist.

# 1: Sie übergeben NICHT den Wert Gut für den Anfang, wenn Sie einen Zeiger verwenden, wissen Sie, dass der Zeiger nur eine Adresse enthält, das war's. Die Variable selbst ist fast leer und deshalb ist es so schön, sie als Argumente zu übergeben. Zusätzlich zum Leistungsgewinn arbeiten Sie mit dem eigentlichen Objekt, sodass alle Änderungen, die Sie vornehmen, nicht vorübergehend sind

# 2: Polymorphismus / Schnittstellen Wenn Sie eine Referenz haben, die ein Schnittstellentyp ist und auf ein Objekt verweist, können Sie nur Methoden dieser Schnittstelle aufrufen, obwohl das Objekt möglicherweise über viel mehr Fähigkeiten verfügt. Die Objekte können dieselben Methoden auch unterschiedlich implementieren.

Wenn Sie diese Konzepte gut verstehen, dann glaube ich nicht, dass Ihnen zu viel fehlt, wenn Sie keine Zeiger verwendet haben. C ++ wird oft als Sprache zum Erlernen der Programmierung verwendet, da es manchmal gut ist, sich die Hände schmutzig zu machen. Wenn Sie mit Aspekten auf niedrigerer Ebene arbeiten, schätzen Sie auch den Komfort einer modernen Sprache. Ich habe mit C ++ angefangen und bin jetzt ein C # -Programmierer. Ich habe das Gefühl, dass die Arbeit mit rohen Zeigern mir geholfen hat, besser zu verstehen, was unter der Haube vor sich geht.

Ich denke nicht, dass jeder mit Zeigern beginnen muss, aber wichtig ist, dass er versteht, warum Referenzen anstelle von Werttypen verwendet werden, und dass der beste Weg, dies zu verstehen, darin besteht, seinen Vorfahren, den Zeiger, zu betrachten.

Despertar
quelle
1
Persönlich denke ich, dass C # eine bessere Sprache gewesen wäre, wenn die meisten Orte .verwendet hätten ->, aber foo.bar(123)synonym mit einem Aufruf der statischen Methode fooClass.bar(ref foo, 123). Das hätte Dinge erlaubt wie myString.Append("George"); [was die Variable modifizieren würde myString] und machte den Unterschied in der Bedeutung zwischen myStruct.field = 3;und deutlicher myClassObject->field = 3;.
Supercat