K
Klaus82 schrieb:
und der Pointer zeigt nun auf eine Adresse auf dem Heap.
Die Adresse steht nicht im Heap, sondern ist in der Zeigervariablen gespeichert.
Klaus82 schrieb:
Oder umgekehrt, dass der Pointer eine neue Adresse bekommt und damit der Inhalt der vorherigen Adresse nicht mehr zugänglich ist (garbage).
Das würde man Leck (leak) nennen.
Klaus82 schrieb:
Der nächste Level scheint dann erreicht zu werden, wenn es um (Kopier?) Konstruktoren und Zuweisung geht. Der Konstruktor wird meist im Zusammenhang mit Klassen genannt, aber eigentlich ruft eine implizite Initialisierung schon einen Konstruktor auf:
int i(5);
Und genauso ruft eine explizite Initialisierung den Zuweisungsoperator = auf:
int i = 5;
Der Schluss ist falsch. Die letzte Codezeile ruft nicht den Zuweisungsoperator aus; denn eine Zuweisung ist etwas anderes als eine Initialisierung. Der Unterschied: Bei der Zuweisung gibt es die Variable/das Objekt schon vorher. Bei einer Initialisierung nicht. Die Initialisierungssyntax mit dem Gleichzeichen nennt sich "Kopier-Initialisierung". Das andere mit der Klammer ist eine "direkte Initialisierung". Der Unterschied: Für die Kopierinitialisierung wird kein mit explicit markierter Konstruktor aufgerufen. Das Initialisieren von Funktionsparametern ist übrigens auch eine Kopierinitialisierung.
Klaus82 schrieb:
Einen Teil der Problematik verstehe ich so: Auf dem Heap wurde Speicher mittels new angefordert und die Adresse in einem Pointer p1 gespeichert. Nun möchte man die Adresse weitergeben, d.h. ein Objekt erzeugt sich seinen eigenen Pointer p2 und übernimmt die Adresse.
Jetzt haben wir schon zwei Pointer, die auf den Heap zeigen. Ergo die Frage: Wer ist jetzt dafür verantwortlich, dass der Heap wieder freigegeben wird?
Das schreibt dir keiner vor, wer dafür verantwortlich ist. Diese Frage musst du für Dich beim Design beantworten. Man hört aber immer wieder und immer lauter, dass nackte/rohe Zeiger nicht verwendet werden sollten, wenn man sich darin eine Adresse eines Objekts merkt, die man dann irgendwann wieder manuell löschen müsste. Nackte/rohe Zeiger sind nicht per se schlecht. Es gibt immer noch Einsatzzwecke. Allerdings klingt das in deinem Fall so, als hätte man lieber einen "besitzergreifenden" Zeiger verwenden sollen. Den gibt's in zwei Varianten: unique_ptr und shared_ptr. Wenn Du dir nicht sicher bist, welchen du von denen brauchst, probier es erst mit einem unique_ptr. Der ist "schlanker", aber auch unkopierbar. Dafür kann man ihn "moven". In deinem Fall könnte man also folgendes machen:
unique_ptr<int> up (new int(23));
int* q = up.get();
up ist der Zeiger, der sich für das Objekt "verantwortlich fühlt", er ist der "Besitzer". Und q zeigt einfach nur auf das Objekt. Hier ist kein delete notwendig; denn der unique_ptr löscht das Objekt wieder ganz automatisch (unique ownership). Wenn Du dich aber nicht ganz entscheiden kannst, wer von den beiden Zeigern der "Besitzer" sein soll dann könntest du einen shared_ptr verwenden (shared ownership):
auto sp = std::make_shared<int>(23);
auto q = sp;
Hier sind beide Zeiger vom Typ shared_ptr<int> und beide teilen sich den Besitz. Der letzte löscht das Objekt. Bei shared_ptr haste etwas mehr Overhead dabei, weil irgendwo ja gespeichert werden muss, wieviele shared_ptr sich noch auf das Objekt beziehen. (Dann gibt's auch noch weak_ptr, aber dazu sag ich erst mal nichts).
Klaus82 schrieb:
Zum Einen hat p1 seinen Zweck erfüllt, nachdem er die Adresse weitergeben hat und könnte ein delete folgen lassen. Dann wird p2 allerdings zum dangling pointer.
Oder wir nehmen p2 in die Verantwortung zur Freigabe des Speicherbereichs. Wenn p1 aber anschließend den Speicher freigeben möchte, dann ist dies bereits geschehen!
Wenn du p1 nicht mehr brauchst, nachdem du die Adresse nach p2 kopiert hast, dann könntest du als Typ für beide unique_ptr verwenden und dir das delete sparen.
Wie auch immer: Es kommt auf Dein Design an. Im Notfall: shared_ptr. Meist geht es auch anders. Und öfter als man denkt, kann man komplett auf Zeiger verzichten, sowohl die "dummen" als auch die "smarten".
Klaus82 schrieb:
Im Primer habe ich gelesen, dass es eine Art Smart Pointer gibt.
Ja, unique_ptr<T,D> und shared_ptr<T> würde man als smart pointer-Klassen bezeichnen.
Klaus82 schrieb:
Umgekehrt gibt es nun scheinbar auch diesen Kopier-, Zuweisungs- und Move Konstruktor, von denen ich den Sinn bzw. genauer den Unterschied immer noch nicht genau verstanden habe.
Das ist dann praktisch, wenn der "logische Zustand" oder die "logischen Teile" des Objekts sich in Wirklichkeit auf mehrere Speicherbereiche verteilt. Siehe std::vector. Logisch gesehen, ist das ein dynamisches Array. Physikalisch gesehen, besteht ein vector-Objekt aus ein paar Zeigern und verweist nur auf Freispeicher (die Vektor-Elemente). In genau solchen Situationen kann man einen Move-Konstructor dazu verwenden, so ein vector<T>-Objekt von einem Ort zum anderen Ort umziehen zu lassen, ohne dass man all seine Elemente kopieren müsste, weil die ja eh nicht direkt im vector-Objekt gespeichert werden, sondern nur "logisch" dazugehören.
Klaus82 schrieb:
Unter Kopieren kann ich mir vorstellen, dass ich zunächst Speicher bereitstelle und diesen dann mit dem Inhalt des gewünschten Objekts fülle. Nur warum? Hat das etwas mit den Schlagwörtern Instanz oder Kapselung zu tun?
Das machst du für ints und doubles ständig:
double quadriere(double x)
{
return x*x;
}
int main()
{
double a = 42;
double b = quadriere(x);
}
Hier werden doubles auch munter kopiert. Und du musst hier nix mit Zeigern machen. Und das will man auch meist mit komplizierteren Datenstrukturen hinbekommen.
Schau dir mal einen der Talks von Scot Meyers' zum Thema an. Da wird das schön illustriert, was bei Move-Semantik (typischerweise im Zusammenhang eines Vektors) passiert und wann das praktisch ist.