Fragen zum Garbage Collector -> Stack, heap, ref , pointer (confused^^)
-
Guten Morgen Männer,
ich lern mich gerade bischen in C# ein, und komme ursprünglich aus der C/C++ ecke. Da ich das "manuelle" Speichermanagment aus C++ gewohnt bin, hab ich ein paar fragen zur speicherverwaltung unter C#. Ich weis, das C# einen GC hat und weis auch wie dieser funktioniert. Nun aber noch paar fragen:
-Gibt es in C# einen stack? Ich nehmen man an schon, aber wann wird diese verwendet? Instanzen einer Klasse kann ich ja immer nur mit "new" anlegen.
Bspw:
//C++: foo *p = new foo(para); foo o(para); //C# foo obj = new foo(para);
Hier wird ja obj (C#) als referenz bezeichnet, kann ich den mit nem C++ pointer gleichsetzen? Die Syntax zwischen C# und C++ in dem beispiel beisen sich ziemlich?
Bspw2:
//C# int i=0; //oder int i= new int();
Ich als C++ler, könnte hier jetzt nich unterscheiden ob sich "i" um eine referenz (bzw. pointer) oder um eine statische variable (aufm Stack) handelt. Wie wird das gehandelt in c#?
Nächste Frage:
Ich weis das Objekt über den GC (nach dem nrigens mehr auf sie referenziert werden) gelöscht werden. Aber:
class foo : IDisposable{ public foo(){ } ~foo(){ } public void Dispose(){ } }
- Warum gibts einen Destruktor?
- Was unterscheidet Dispose mit dem Destruktor?So genug Stoff für euch
-
Speicherverwalter schrieb:
-Gibt es in C# einen stack? Ich nehmen man an schon, aber wann wird diese verwendet? Instanzen einer Klasse kann ich ja immer nur mit "new" anlegen.
Ja, gibt es. Dort werden zum einen die Referenzen auf Objekte im managed Heap abgelegt (also sowas wie die Verwaltungsdaten für referenzgezählte Smartpointer in C++), zum anderen alle Werttypen. Das sind zum Einen Builtins wie short, int, long und zum anderen alle mit dem Schlüsselwort struct definierten Typen. Beim Aufruf von Funktionen werden solche Werttypen auch vollständig kopiert, wie bei einer Übergabe by-value in C++.
//C++: foo *p = new foo(para); foo o(para); //C# foo obj = new foo(para);
Hier wird ja obj (C#) als referenz bezeichnet, kann ich den mit nem C++ pointer gleichsetzen? Die Syntax zwischen C# und C++ in dem beispiel beisen sich ziemlich?
Ja, kann man im Grunde gleichsetzen. Referenzen auf Referenztypen (class) in C# sind wie Pointer in C++, nur ohne Pointerarithmetik und undefinierte Werte.
//C# int i=0; //oder int i= new int();
Ich als C++ler, könnte hier jetzt nich unterscheiden ob sich "i" um eine referenz (bzw. pointer) oder um eine statische variable (aufm Stack) handelt. Wie wird das gehandelt in c#?
Hier ist der Typ ausschlaggebend. int -> Werttyp -> Stack. Die Zuweisung mit new initialisiert i lediglich mit dem Default, was hier 0 gleichkommt. Der Compiler gibt in folgendem die Warnung, dass der erste Wert von i nie benutzt wird. Dadurch wird deutlich, dass i in der ersten Zeile einen Wert erhält, dieser aber in der zweiten Zeile verworfen wird, wenn i einen neuen Wert erhält.
int i = new int(); // warning i = 0;
[quote]Ich weis das Objekt über den GC (nach dem nrigens mehr auf sie referenziert werden) gelöscht werden. Aber:
class foo : IDisposable{ public foo(){ } ~foo(){ } public void Dispose(){ } }
- Warum gibts einen Destruktor?
- Was unterscheidet Dispose mit dem Destruktor?Der Destruktor heißt Finalizer (er ist eben kein echter Destruktor), und wird vom GC aufgerufen, wenn das Objekt "zufällig" (sprich, während eines GC-Laufes) aufgegriffen wird. Da während eines GC-Laufes die Zerstörungsreihenfolge undefiniert ist, muss man bei Klassen, die Ressourcen verwalten, u.U. anders handeln, als wenn man das Objekt direkt im Code "disposed".
Ein Beispiel:
class DataSource : IDisposeable { DbConnection connection; DbTransaction transaction; ... void Dispose() { Dispose(true); // anzeigen, dass manuell disposed wird GC.SuppressFinalize(this); // und Finalizer-Aufruf durch GC verhindern } ~DataSource() { Dispose(false); // anzeigen, dass der GC das Objekt aufgegriffen hat } void Dispose(bool disposing) { if (disposing) { // wenn disposing false wäre, könnte der GC die Objekte connection und transaction bereits abgeräumt haben, so dass folgende Befehle nicht mehr sicher wären transaction.Dispose(); connection.Dispose(); } transaction = null; connection = null; } } // Benutzung using (DataSource ds = new DataSource()) { ... } // ruft automatisch ds.Dispose auf
Dabei ist sichergestellt, dass - auch wenn z.B. eine Exception fliegt - die Verbindung sauber abgeräumt wird, wodurch auch die Transaktion zeitig zurückgerollt werden kann. Würden wir das dem GC überlassen, wäre u.U. noch eine längere Zeit eine ungenutzte Verbindung und eine halb abgeschlossene Transaktion im Speicher.
-
ier wird ja obj (C#) als referenz bezeichnet, kann ich den mit nem C++ pointer gleichsetzen? Die Syntax zwischen C# und C++ in dem beispiel beisen sich ziemlich?
Ja, das kann man. Die Referenz in C# enthält noch zusätzliche Intelligenz.
-Gibt es in C# einen stack? Ich nehmen man an schon, aber wann wird diese verwendet? Instanzen einer Klasse kann ich ja immer nur mit "new" anlegen.
UND
Ich als C++ler, könnte hier jetzt nich unterscheiden ob sich "i" um eine referenz (bzw. pointer) oder um eine statische variable (aufm Stack) handelt. Wie wird das gehandelt in c#?
Es werden generell Werte Typen (struct, enum, ...) und Referenz Typen unterschieden.
In C++ entscheidet der Benutzer eines Types, wie er angelegt wird (Heap oder Stack). In C# entscheidet der Typ wie er angelegt wird.
http://www.c-sharpcorner.com/UploadFile/rmcochran/csharp_memory01122006130034PM/csharp_memory.aspx?ArticleID=9adb0e3c-b3f6-40b5-98b5-413b6d348b91Wertetypen können in verschiedenen Situation "geboxt" und "ungeboxt" werden. Boxing bedeutet, es wird ein Wrapper um ein Wertetype gelegt, der sich wie ein Referenztyp verhält. (Beim "auspacken wird dann von unboxing gesprochen.)
http://www.c-sharpcorner.com/UploadFile/ggaganesh/BoxNUnBox11082005073720AM/BoxNUnBox.aspx
MSDN Valuetypes: http://msdn.microsoft.com/en-us/library/s1ax56ch.aspx
MSDN Referencetypes: http://msdn.microsoft.com/en-us/library/490f96s2.aspxSimon
-
Ok danke ihr zwei, das hab ich sowei verstanden. Noch ne Frage zu der Disposable klasse von LordJaxom:
- Der GC ruft immer dispose mit "false" auf ?
- Kann man dispose manuell ohne das "using" dings da aufrufen?
- Der Using Block bedeutet entspricht der Lebenesdauer des Objekts? (wie Stack)
-
Speicherverwalter schrieb:
- Der GC ruft immer dispose mit "false" auf ?
IDisposeable.Dispose hat keine Parameter, und der GC ruft von sich aus auch nie IDisposeable.Dispose auf. Deshalb machen wir das ja im Finalizer selbst.
- Kann man dispose manuell ohne das "using" dings da aufrufen?
Ja.
- Der Using Block bedeutet entspricht der Lebenesdauer des Objekts? (wie Stack)
Muss nicht sein, ist aber meistens der Fall. Mit dem using-Block kann man sich jedenfalls ein bisschen RAII in C# machen
-
Hier noch ein Hinweis zum Dispose "Pattern":
http://msdn.microsoft.com/en-us/library/system.idisposable.aspxSimon
-
Ok danke jung, jetzt hab ich nur noch eine Frage hehe:
Gib es sowas wie ein void pointer bzw. Referenz?nehm ich da einfach "object" als schlüssewort?
-
Gib es sowas wie ein void pointer bzw. Referenz?
nehm ich da einfach "object" als schlüssewort?
ja, einfach object.
-
Ich wäre trotzdem vorsichtig damit Referenzen mit Zeigern zu vergleichen, gerade wenn jemand von C++ kommt. Ein Zeiger ist ein direkter Zugriff auf die Startadresse eines Objekts im Speicher. Löscht man den Zeiger, löscht man auch das Objekt.
Eine Referenz sagt gar nichts darüber aus wo im Speicher das Objekt ist. Vielmehr kann nur die Speicherverwaltung feststellen wo sich das Objekt gerade befindet. Erst in Verbindung mit der Speicherverwaltung kann man also von einer Referenz auf das Objekt auflösen.
An der Stelle muß man sich auch noch etwas wichtiges klarmachen: Anders als in C++ kann ein Objekt im Managed Heap bei jedem GC Durchlauf seine Position im Speicher verändern.
Ein auch entscheidender Unterschied:
Wenn man 3 Zeiger auf ein Objekt hat und auf einen der Zeiger delete aufruft, wird das Objekt gelöscht und die anderen beiden Zeiger sind invalid.
Wenn man dagegen 3 Referenzen auf ein Objekt hat und eine der Referenzen wird gelöscht (z.B. am Ende des using blocks) dann wird das Objekt NICHT gelöscht, sondern lebt solange weiter bis auch die letzte Referenz darauf gelöscht wird.
imho: Referenzen mit Zeigern zu vergleichen wenn man als C++ Progger C# lernen will ist "an accident waiting to happen". Es ist wichtiger zu verstehen, das eine Referenz _kein_ Zeiger ist, sondern etwas anderes.
-
Löscht man den Zeiger, löscht man auch das Objekt.
Zeiger werden nur dann gelöscht wenn Sie auf dem Heap erzeugt worden sind, was selten gemacht wird. Objekte, mit new erzeugt, werden mit delete über den Pointer gelöscht... der Pointer selbst wird dabei nicht gelöscht.
Mit dem nötigen Abstand, denke ich, ist der Vergleich C++ Pointer und C# Referenz nicht verkehrt.
Wenn man 3 Zeiger auf ein Objekt hat und auf einen der Zeiger delete aufruft, wird das Objekt gelöscht und die anderen beiden Zeiger sind invalid.
Wenn man dagegen 3 Referenzen auf ein Objekt hat und eine der Referenzen wird gelöscht (z.B. am Ende des using blocks) dann wird das Objekt NICHT gelöscht, sondern lebt solange weiter bis auch die letzte Referenz darauf gelöscht wird.
Nunja, dann stelle Dir die C# Referenz als Pointer mit Ref. Count vor...
Simon
-
Referenzen mit Zeigern zu vergleichen ist schon OK. Ist vom Prinzip her dasselbe. Unterscheiden tut sich die Art der Speicherverwaltung, was aber nix mit dem Unterschied Referenz vs. Zeiger zu tun hat. Man kann beide Speicherverwaltungs-Arten (manuell, GC) frei mit beiden "Verweis-Arten" (Zeiger, Referenz) kombinieren. Oder nochmal konkret: es gibt auch Systeme wo ein GC mit echten Zeigern eingesetzt wird.
Der gröbste Unterschied ist dass, wie schon erwähnt wurde, ein Zeiger auf eine feste Adresse zeigt, und eine Referenz ein etwas abstrakterer Verweis auf ein Objekt ist, wobei eben nicht garantiert ist dass sich die eigentliche Adresse des Objektes nicht ändert. Wichtig wird der Unterschied aber auch nur sobald man die Adresse eines Objektes irgendwo direkt verwendet, z.B. wenn man sie per PInvoke irgendeiner Funktion übergibt.
-
mal eine kurze Frage hierzu: in dem Dispose"Pattern" von MSDN wird ein Interopaufruf (closehandle) verwendet. Ist das wirklich notwendig ? Ich setze beim Cleanup in unsafe-Blöcken einfach IntPtr auf zero (wie es ja danach noch gemacht wird). Trotz teilweise großer Speichermengen (mehrere 100MB) und langen Laufzeiten (Tage) hatte ich kein Speicherleck. Ich weiß dass das nicht unbedingt beweisend ist aber dennoch: warum ?
-
Das Beispiel in der MSDN ruft vermutlich deshalb closehandle auf, weil es irgendwo ein Handle anfordert. Du musst alle Ressourcen aufräumen, die nicht von .NET stammen. Welche das sind, können wir kaum wissen, deshalb: "Cannot predict."
-
HappyIntPtr schrieb:
mal eine kurze Frage hierzu: in dem Dispose"Pattern" von MSDN wird ein Interopaufruf (closehandle) verwendet. Ist das wirklich notwendig ? Ich setze beim Cleanup in unsafe-Blöcken einfach IntPtr auf zero (wie es ja danach noch gemacht wird). Trotz teilweise großer Speichermengen (mehrere 100MB) und langen Laufzeiten (Tage) hatte ich kein Speicherleck. Ich weiß dass das nicht unbedingt beweisend ist aber dennoch: warum ?
Naja das kommt darauf an was in den IntPtr drinnensteht.
Wenn es ein Zeiger auf einen Speicherbereich ist der über die GC Funktionen "gepinnt" wurde, dann reicht es AFAIK nicht aus einfach den IntPtr auf IntPtr.Zero zu setzen - der Block würde sonst nie vom GC collected (weil dieser denkt dass ein Programmteil über den er keine Informationen hat den Speicher noch verwendet - sonst könnte es ja passieren dass der GC Speicher "einsammelt" der noch von irgendwelchen unmanaged DLLs verwendet wird). In dem Fall müsstest du den Speicher auch über die entsprechende GC Funktion wieder ent-pinnen - das Nullsetzen des IntPtr ist dann aber eine Fleissaufgabe.
Wenn der Speicher über das "fixed" Keyword gepinnt wurde dann musst du garnix machen, da der Block ja beim Verlassen des "fixed" Blocks automatisch "ent-pinnt" wird. Ein Nullsetzen des IntPtr ist dann aber auch nicht erforderlich.
Und wenn du irgendwie die Adresse eines nicht-gepinnten Speicherbereichs in einen IntPtr bekommst, dann ist das sowieso ein Fehler, da dann nicht garantiert ist dass die Adresse sich nicht ändert (und ein IntPtr eben keine Referenz ist die der GC entsprechend anpassen würde wenn er etwas verschiebt, sondern eine einfache Zahl, die der GC nie ändern wird). Ich wüsste jetzt zwar garnicht wie man das ohne gröbere Hacks hinbekommt, aber falls es geht ist es auf jeden Fall Unsinn. Dass es mit grösseren Speicherbereichen oft trotzdem geht (weil diese in einem Eigenen Bereich allokiert und nicht vom GC verschoben werden) ist wieder so ne Sache -- verlassen sollte man sich halt nicht darauf.