Ich komme aus einem Java-Hintergrund und habe begonnen, mit Objekten in C ++ zu arbeiten. Mir ist jedoch aufgefallen, dass Menschen häufig Zeiger auf Objekte und nicht auf die Objekte selbst verwenden, zum Beispiel diese Erklärung:
Object *myObject = new Object;
eher, als:
Object myObject;
Oder anstatt eine Funktion zu verwenden, sagen wir mal testFunc()
so:
myObject.testFunc();
wir müssen schreiben:
myObject->testFunc();
Aber ich kann nicht herausfinden, warum wir das so machen sollen. Ich würde annehmen, dass dies mit Effizienz und Geschwindigkeit zu tun hat, da wir direkten Zugriff auf die Speicheradresse erhalten. Habe ich recht?
Antworten:
Es ist sehr bedauerlich, dass Sie so oft eine dynamische Zuordnung sehen. Das zeigt nur, wie viele schlechte C ++ - Programmierer es gibt.
In gewisser Weise haben Sie zwei Fragen in einer zusammengefasst. Die erste ist, wann wir die dynamische Zuordnung verwenden sollten (using
new
)? Die zweite ist, wann wir Zeiger verwenden sollten?Die wichtige Nachricht zum Mitnehmen ist, dass Sie immer das für den Job geeignete Tool verwenden sollten . In fast allen Situationen gibt es etwas Passenderes und Sichereres als die manuelle dynamische Zuordnung und / oder die Verwendung von Rohzeigern.
Dynamische Zuordnung
In Ihrer Frage haben Sie zwei Möglichkeiten zum Erstellen eines Objekts aufgezeigt. Der Hauptunterschied ist die Speicherdauer des Objekts. Wenn Sie
Object myObject;
innerhalb eines Blocks arbeiten, wird das Objekt mit automatischer Speicherdauer erstellt. Dies bedeutet, dass es automatisch zerstört wird, wenn es den Gültigkeitsbereich verlässt. Wenn Sie dies tunnew Object()
, hat das Objekt eine dynamische Speicherdauer, was bedeutet, dass es so lange am Leben bleibt, bis Sie es explizitdelete
angeben. Sie sollten die dynamische Speicherdauer nur dann verwenden, wenn Sie sie benötigen. Das heißt, Sie sollten es immer vorziehen, Objekte mit automatischer Speicherdauer zu erstellen, wenn Sie können .Die beiden wichtigsten Situationen, in denen Sie möglicherweise eine dynamische Zuordnung benötigen:
Wenn Sie unbedingt eine dynamische Zuordnung benötigen, sollten Sie diese in einen intelligenten Zeiger oder einen anderen Typ kapseln, der RAII ausführt (wie die Standardcontainer). Intelligente Zeiger bieten eine Besitzersemantik für dynamisch zugewiesene Objekte. Schauen Sie sich zum Beispiel an
std::unique_ptr
undstd::shared_ptr
. Wenn Sie sie ordnungsgemäß verwenden, können Sie die Durchführung Ihrer eigenen Speicherverwaltung fast vollständig vermeiden (siehe Nullregel ).Zeiger
Es gibt jedoch andere allgemeinere Verwendungszwecke für Rohzeiger, die über die dynamische Zuordnung hinausgehen. Die meisten bieten jedoch Alternativen, die Sie bevorzugen sollten. Bevorzugen Sie nach wie vor immer die Alternativen, es sei denn, Sie benötigen wirklich Zeiger .
Sie benötigen Referenzsemantik . Manchmal möchten Sie ein Objekt mit einem Zeiger übergeben (unabhängig davon, wie es zugewiesen wurde), weil Sie möchten, dass die Funktion, an die Sie es übergeben, Zugriff auf dieses bestimmte Objekt hat (keine Kopie davon). In den meisten Situationen sollten Sie jedoch Referenztypen Zeigern vorziehen, da dies genau das ist, wofür sie entwickelt wurden. Beachten Sie, dass es nicht unbedingt darum geht, die Lebensdauer des Objekts über den aktuellen Bereich hinaus zu verlängern, wie in Situation 1 oben. Wenn Sie eine Kopie des Objekts übergeben möchten, benötigen Sie nach wie vor keine Referenzsemantik.
Du brauchst Polymorphismus . Sie können Funktionen nur polymorph (dh entsprechend dem dynamischen Typ eines Objekts) über einen Zeiger oder eine Referenz auf das Objekt aufrufen. Wenn dies das Verhalten ist, das Sie benötigen, müssen Sie Zeiger oder Referenzen verwenden. Auch hier sollten Referenzen bevorzugt werden.
Sie möchten darstellen, dass ein Objekt optional ist, indem Sie zulassen, dass ein Objekt übergeben wird,
nullptr
wenn das Objekt weggelassen wird. Wenn es sich um ein Argument handelt, sollten Sie lieber Standardargumente oder Funktionsüberladungen verwenden. Andernfalls sollten Sie vorzugsweise einen Typ verwenden, der dieses Verhalten kapselt, z. B.std::optional
(in C ++ 17 eingeführt - mit früheren C ++ - Standards verwendenboost::optional
).Sie möchten Kompilierungseinheiten entkoppeln, um die Kompilierungszeit zu verbessern . Die nützliche Eigenschaft eines Zeigers besteht darin, dass Sie nur eine Vorwärtsdeklaration des Typs benötigen, auf den verwiesen wird (um das Objekt tatsächlich zu verwenden, benötigen Sie eine Definition). Auf diese Weise können Sie Teile Ihres Kompilierungsprozesses entkoppeln, was die Kompilierungszeit erheblich verkürzen kann. Siehe die Pimpl-Sprache .
Sie müssen eine Schnittstelle zu einer C-Bibliothek oder einer Bibliothek im C-Stil herstellen. Zu diesem Zeitpunkt müssen Sie Rohzeiger verwenden. Das Beste, was Sie tun können, ist sicherzustellen, dass Sie Ihre rohen Zeiger nur im letztmöglichen Moment loslassen. Sie können einen Rohzeiger beispielsweise von einem intelligenten Zeiger abrufen, indem Sie dessen
get
Elementfunktion verwenden. Wenn eine Bibliothek eine Zuordnung für Sie durchführt, von der erwartet wird, dass Sie die Zuordnung über ein Handle aufheben, können Sie das Handle häufig in einen intelligenten Zeiger mit einem benutzerdefinierten Deleter einschließen, der die Zuordnung des Objekts entsprechend aufhebt.quelle
Object myObject(param1, etc...)
Es gibt viele Anwendungsfälle für Zeiger.
Polymorphes Verhalten . Bei polymorphen Typen werden Zeiger (oder Referenzen) verwendet, um ein Schneiden zu vermeiden:
Referenzsemantik und Vermeiden des Kopierens . Bei nicht polymorphen Typen verhindert ein Zeiger (oder eine Referenz) das Kopieren eines möglicherweise teuren Objekts
Beachten Sie, dass C ++ 11 über eine Verschiebungssemantik verfügt, mit der viele Kopien teurer Objekte in Funktionsargumente und als Rückgabewerte vermieden werden können. Die Verwendung eines Zeigers vermeidet diese jedoch definitiv und ermöglicht mehrere Zeiger auf dasselbe Objekt (während ein Objekt nur einmal verschoben werden kann).
Ressourcenbeschaffung . Das Erstellen eines Zeigers auf eine Ressource mit dem
new
Operator ist in modernem C ++ ein Anti-Pattern . Verwenden Sie eine spezielle Ressourcenklasse (einen der Standardcontainer) oder einen intelligenten Zeiger (std::unique_ptr<>
oderstd::shared_ptr<>
). Erwägen:vs.
Ein Rohzeiger sollte nur als "Ansicht" verwendet werden und in keiner Weise mit dem Eigentum verbunden sein, sei es durch direkte Erstellung oder implizit durch Rückgabewerte. Siehe auch diese Fragen und Antworten in den C ++ - FAQ .
Feinere Steuerung der Lebensdauer Jedes Mal, wenn ein gemeinsam genutzter Zeiger kopiert wird (z. B. als Funktionsargument), wird die Ressource, auf die er verweist, am Leben erhalten. Normale Objekte (die
new
weder von Ihnen noch innerhalb einer Ressourcenklasse erstellt wurden) werden zerstört, wenn der Gültigkeitsbereich verlassen wird.quelle
unique_ptr
/ move semanticshun(b)
auch die Kenntnis der Signatur erfordert, es sei denn, Sie wissen nicht, dass Sie bis zur Kompilierung den falschen Typ angegeben haben. Während das Referenzproblem normalerweise beim Kompilieren nicht abgefangen wird und das Debuggen mehr Aufwand erfordert, können Sie beim Überprüfen der Signatur, um sicherzustellen, dass die Argumente richtig sind, auch feststellen, ob es sich bei den Argumenten um Referenzen handelt So wird das Referenzbit zu einem Problem (insbesondere bei Verwendung von IDEs oder Texteditoren, die die Signatur ausgewählter Funktionen anzeigen). Auchconst&
.Es gibt viele ausgezeichnete Antworten auf diese Frage, einschließlich der wichtigen Anwendungsfälle von Vorwärtsdeklarationen, Polymorphismus usw., aber ich bin der Meinung, dass ein Teil der "Seele" Ihrer Frage nicht beantwortet wird - nämlich was die verschiedenen Syntaxen in Java und C ++ bedeuten.
Lassen Sie uns die Situation untersuchen, in der die beiden Sprachen verglichen werden:
Java:
Das nächste Äquivalent dazu ist:
C ++:
Sehen wir uns den alternativen C ++ - Weg an:
Die beste Art, sich das vorzustellen, ist, dass Java (implizit) mehr oder weniger Zeiger auf Objekte verarbeitet, während C ++ entweder Zeiger auf Objekte oder die Objekte selbst verarbeitet. Hiervon gibt es Ausnahmen. Wenn Sie beispielsweise Java-Primitivtypen deklarieren, handelt es sich um tatsächliche Werte, die kopiert werden, und nicht um Zeiger. Damit,
Java:
Das heißt, die Verwendung von Zeigern ist NICHT unbedingt die richtige oder die falsche Art, mit Dingen umzugehen. Andere Antworten haben dies jedoch zufriedenstellend behandelt. Die allgemeine Idee ist jedoch, dass Sie in C ++ viel mehr Kontrolle über die Lebensdauer der Objekte und darüber haben, wo sie leben werden.
Take-Home-Punkt - das
Object * object = new Object()
Konstrukt ist tatsächlich das, was der typischen Java-Semantik (oder C # -Semantik) am nächsten kommt.quelle
Object2 is now "dead"
: Ich denke du meinstmyObject1
oder genauerthe object pointed to by myObject1
.Object object1 = new Object(); Object object2 = new Object();
ist sehr schlechter Code. Der zweite neue oder der zweite Objektkonstruktor kann werfen, und jetzt ist Objekt1 durchgesickert. Wenn Sienew
Raws verwenden, sollten Sienew
ed-Objekte so schnell wie möglich in RAII-Wrapper einschließen.Ein weiterer guter Grund für die Verwendung von Zeigern wären Vorwärtsdeklarationen . In einem ausreichend großen Projekt können sie die Kompilierungszeit erheblich beschleunigen.
quelle
std::unique_ptr<T>
funktioniert mit Vorwärtsdeklarationen vonT
. Sie müssen nur sicherstellen, dass der Destruktor von ein vollständiger Typ ist , wenn erstd::unique_ptr<T>
aufgerufenT
wird. Dies bedeutet normalerweise, dass Ihre Klasse, die diestd::unique_ptr<T>
Deklaration enthält, ihren Destruktor in der Header-Datei deklariert und in der cpp-Datei implementiert (auch wenn die Implementierung leer ist).Vorwort
Java ist im Gegensatz zum Hype nichts anderes als C ++. Die Java-Hype-Maschine möchte, dass Sie glauben, dass die Sprachen ähnlich sind, da Java eine C ++ - ähnliche Syntax hat. Nichts kann weiter von der Wahrheit entfernt sein. Diese Fehlinformationen sind Teil des Grundes, warum Java-Programmierer zu C ++ wechseln und Java-ähnliche Syntax verwenden, ohne die Auswirkungen ihres Codes zu verstehen.
Weiter geht es
Im Gegenteil. Der Heap ist viel langsamer als der Stack, da der Stack im Vergleich zum Heap sehr einfach ist. Bei automatischen Speichervariablen (auch als Stapelvariablen bezeichnet) werden die Destruktoren aufgerufen, sobald sie den Gültigkeitsbereich verlassen. Zum Beispiel:
Wenn Sie dagegen einen dynamisch zugewiesenen Zeiger verwenden, muss sein Destruktor manuell aufgerufen werden.
delete
ruft diesen Destruktor für Sie auf.Dies hat nichts mit der
new
in C # und Java vorherrschenden Syntax zu tun . Sie werden für ganz andere Zwecke eingesetzt.Vorteile der dynamischen Allokation
Eines der ersten Probleme, auf das viele C ++ - Programmierer stoßen, besteht darin, dass Sie, wenn sie willkürliche Eingaben von Benutzern akzeptieren, einer Stapelvariablen nur eine feste Größe zuweisen können. Sie können die Größe von Arrays auch nicht ändern. Zum Beispiel:
Wenn Sie
std::string
stattdessen eine verwenden,std::string
ändert sich die Größe natürlich intern, sodass dies kein Problem darstellen sollte. Die Lösung für dieses Problem ist jedoch im Wesentlichen die dynamische Zuordnung. Sie können dynamischen Speicher basierend auf den Eingaben des Benutzers zuweisen, zum Beispiel:Da der Heap viel größer als der Stapel ist, kann man beliebig viel Speicher beliebig zuweisen / neu zuweisen, während der Stapel eine Einschränkung aufweist.
Wie ist das ein Vorteil, den Sie fragen? Die Antwort wird klar, sobald Sie die Verwirrung / den Mythos hinter Arrays und Zeigern verstanden haben. Es wird allgemein angenommen, dass sie gleich sind, aber nicht. Dieser Mythos beruht auf der Tatsache, dass Zeiger genau wie Arrays tiefgestellt werden können und aufgrund von Arrays in einer Funktionsdeklaration zu Zeigern auf der obersten Ebene zerfallen. Sobald jedoch ein Array in einen Zeiger zerfällt, verliert der Zeiger seine
sizeof
Informationen. Damitsizeof(pointer)
wird die Größe des Zeigers in Bytes geben, die in der Regel 8 Bytes auf einem 64-Bit - System.Sie können Arrays nicht zuweisen, sondern nur initialisieren. Zum Beispiel:
Auf der anderen Seite können Sie mit Zeigern machen, was Sie wollen. Leider verstehen Anfänger den Unterschied nicht, da die Unterscheidung zwischen Zeigern und Arrays in Java und C # von Hand erfolgt.
Java und C # verfügen über Funktionen, mit denen Sie Objekte als andere behandeln können, z. B. mithilfe des
as
Schlüsselworts. Wenn also jemand einEntity
Objekt alsPlayer
Objekt behandeln möchte , kann man dies tun.Player player = Entity as Player;
Dies ist sehr nützlich, wenn Sie Funktionen für einen homogenen Container aufrufen möchten, die nur für einen bestimmten Typ gelten sollen. Die Funktionalität kann auf ähnliche Weise wie folgt erreicht werden:Wenn also nur Triangles eine Rotate-Funktion hätten, wäre dies ein Compilerfehler, wenn Sie versuchen würden, sie für alle Objekte der Klasse aufzurufen. Mit
dynamic_cast
können Sie dasas
Schlüsselwort simulieren . Wenn ein Cast fehlschlägt, wird ein ungültiger Zeiger zurückgegeben. Dies!test
ist im Wesentlichen eine Abkürzung für die Überprüfung, obtest
NULL oder ein ungültiger Zeiger ist, was bedeutet, dass die Umwandlung fehlgeschlagen ist.Vorteile automatischer Variablen
Nachdem Sie all die großartigen Möglichkeiten der dynamischen Zuweisung gesehen haben, fragen Sie sich wahrscheinlich, warum niemand die dynamische Zuweisung NICHT ständig verwenden sollte. Ich habe dir schon einen Grund gesagt, der Haufen ist langsam. Und wenn Sie nicht all diesen Speicher benötigen, sollten Sie ihn nicht missbrauchen. Hier sind einige Nachteile in keiner bestimmten Reihenfolge:
Es ist fehleranfällig. Die manuelle Speicherzuweisung ist gefährlich und Sie sind anfällig für Undichtigkeiten. Wenn Sie mit dem Debugger oder
valgrind
(einem Tool für Speicherverluste) nicht vertraut sind , können Sie sich die Haare aus dem Kopf ziehen. Glücklicherweise lindern RAII-Redewendungen und intelligente Zeiger dies ein wenig, aber Sie müssen mit Praktiken wie der Dreierregel und der Fünf-Regel vertraut sein. Es gibt viele Informationen, die aufgenommen werden müssen, und Anfänger, die es entweder nicht wissen oder sich nicht darum kümmern, werden in diese Falle tappen.Es ist nicht notwendig. Im Gegensatz zu Java und C #, wo es idiomatisch ist, das
new
Schlüsselwort überall zu verwenden, sollten Sie es in C ++ nur verwenden, wenn Sie es benötigen. Der übliche Satz lautet: Wenn Sie einen Hammer haben, sieht alles wie ein Nagel aus. Während Anfänger , die mit C beginnen ++ sind von Zeigern Angst und lernen , durch Gewohnheit Stack - Variablen zu verwenden, Java und C # Programmierer beginnen mit Hilfe von Zeigern ohne es zu verstehen! Das bedeutet buchstäblich, auf dem falschen Fuß abzusteigen. Sie müssen alles aufgeben, was Sie wissen, denn die Syntax ist eine Sache, das Erlernen der Sprache eine andere.Eine Optimierung, die viele Compiler vornehmen, sind Elisions- und Rückgabewertoptimierungen . Diese Dinge können unnötige Kopien vermeiden, was für sehr große Objekte nützlich ist, wie z. B. einen Vektor, der viele Elemente enthält. Normalerweise besteht die übliche Praxis darin, Zeiger zum Übertragen des Eigentums zu verwenden, anstatt die großen Objekte zu kopieren, um sie zu verschieben . Dies hat zur Entstehung von Bewegungssemantik und intelligenten Zeigern geführt .
Wenn Sie Zeiger verwenden, tritt (N) RVO NICHT auf. Es ist vorteilhafter und weniger fehleranfällig, (N) RVO zu nutzen, als Zeiger zurückzugeben oder zu übergeben, wenn Sie sich Gedanken über die Optimierung machen. Fehlerlecks können auftreten, wenn der Aufrufer einer Funktion für die
delete
Zuordnung eines dynamisch zugewiesenen Objekts und dergleichen verantwortlich ist. Es kann schwierig sein, den Besitz eines Objekts zu verfolgen, wenn Zeiger wie eine heiße Kartoffel herumgereicht werden. Verwenden Sie einfach Stapelvariablen, da dies einfacher und besser ist.quelle
{ std::string* s = new std::string; } delete s; // destructor called
... das wird sicherdelete
nicht funktionieren, weil der Compiler nicht mehr weiß, wass
ist?C ++ bietet drei Möglichkeiten, ein Objekt zu übergeben: per Zeiger, als Referenz und nach Wert. Java beschränkt Sie auf letzteres (die einzige Ausnahme sind primitive Typen wie int, boolean usw.). Wenn Sie C ++ nicht nur wie ein seltsames Spielzeug verwenden möchten, sollten Sie den Unterschied zwischen diesen drei Möglichkeiten besser kennenlernen.
Java gibt vor, dass es kein Problem wie "Wer und wann sollte dies zerstören?" Gibt. Die Antwort lautet: Der Müllsammler, großartig und schrecklich. Trotzdem kann es keinen 100% igen Schutz gegen Speicherverluste bieten (ja, Java kann Speicherverluste verursachen ). Tatsächlich gibt Ihnen GC ein falsches Sicherheitsgefühl. Je größer Ihr SUV ist, desto länger ist Ihr Weg zum Evakuator.
Mit C ++ können Sie sich mit dem Lebenszyklusmanagement von Objekten vertraut machen. Nun, es gibt Mittel, um damit umzugehen ( Smart-Pointer- Familie, QObject in Qt usw.), aber keines von ihnen kann wie GC auf "Feuer und Vergessen" -Methode verwendet werden: Sie sollten es immer tun die Speicherbehandlung im Auge behalten. Sie sollten sich nicht nur darum kümmern, ein Objekt zu zerstören, sondern auch vermeiden, dasselbe Objekt mehr als einmal zu zerstören.
Noch keine Angst? Ok: zyklische Referenzen - behandeln Sie sie selbst, Mensch. Und denken Sie daran: Töten Sie jedes Objekt genau einmal, wir C ++ - Laufzeiten mögen nicht diejenigen, die sich mit Leichen anlegen, lassen die Toten in Ruhe.
Also zurück zu deiner Frage.
Wenn Sie Ihr Objekt nach Wert, nicht nach Zeiger oder Referenz weitergeben, kopieren Sie das Objekt (das gesamte Objekt, ob es sich um ein paar Bytes oder einen riesigen Datenbankspeicherauszug handelt - Sie sind klug genug, um Letzteres zu vermeiden. t du?) jedes Mal, wenn du '=' tust. Und um auf die Mitglieder des Objekts zuzugreifen, verwenden Sie '.' (Punkt).
Wenn Sie Ihr Objekt per Zeiger übergeben, kopieren Sie nur wenige Bytes (4 auf 32-Bit-Systemen, 8 auf 64-Bit-Systemen), nämlich - die Adresse dieses Objekts. Und um dies allen zu zeigen, verwenden Sie diesen ausgefallenen Operator '->', wenn Sie auf die Mitglieder zugreifen. Oder Sie können die Kombination von '*' und '.' Verwenden.
Wenn Sie Referenzen verwenden, erhalten Sie den Zeiger, der vorgibt, ein Wert zu sein. Es ist ein Zeiger, aber Sie greifen über '.' Auf die Mitglieder zu.
Und um Sie noch einmal zu verblüffen: Wenn Sie mehrere durch Kommas getrennte Variablen deklarieren, dann (achten Sie auf die Hände):
Beispiel:
quelle
std::auto_ptr
ist veraltet, bitte nicht benutzen.In C ++ leben auf dem Stapel zugewiesene Objekte (mithilfe der
Object object;
Anweisung innerhalb eines Blocks) nur in dem Bereich, in dem sie deklariert sind. Wenn der Codeblock die Ausführung beendet hat, werden die deklarierten Objekte zerstört. Wenn Sie dagegen Speicher auf dem Heap zuweisenObject* obj = new Object()
, leben diese weiterhin im Heap, bis Sie aufrufendelete obj
.Ich würde ein Objekt auf dem Heap erstellen, wenn ich das Objekt nicht nur in dem Codeblock verwenden möchte, der es deklariert / zugewiesen hat.
quelle
Object obj
ist nicht immer auf dem Stapel - zum Beispiel Globals oder Mitgliedsvariablen.Ich werde vergleichen, wie es im Funktionskörper funktioniert, wenn Sie Folgendes verwenden:
Innerhalb der Funktion wird Ihre
myObject
zerstört, sobald diese Funktion zurückkehrt. Dies ist also nützlich, wenn Sie Ihr Objekt nicht außerhalb Ihrer Funktion benötigen. Dieses Objekt wird auf den aktuellen Thread-Stapel gelegt.Wenn Sie in den Funktionskörper schreiben:
Dann wird die Objektklasseninstanz, auf die gezeigt
myObject
wird, nicht zerstört, sobald die Funktion endet und die Zuordnung auf dem Heap erfolgt.Wenn Sie ein Java-Programmierer sind, ist das zweite Beispiel näher an der Funktionsweise der Objektzuweisung unter Java. Diese Zeile:
Object *myObject = new Object;
entspricht Java :Object myObject = new Object();
. Der Unterschied besteht darin, dass unter Java myObject Müll gesammelt wird, während es unter c ++ nicht freigegeben wird. Sie müssen irgendwo explizit "delete myObject" aufrufen. Andernfalls treten Speicherlecks auf.Seit c ++ 11 können Sie sichere Methoden für dynamische Zuordnungen verwenden:
new Object
indem Sie Werte in shared_ptr / unique_ptr speichern.Außerdem werden Objekte sehr oft in Containern wie Karten oder Vektoren gespeichert. Sie verwalten automatisch die Lebensdauer Ihrer Objekte.
quelle
then myObject will not get destroyed once function ends
Es wird absolut.myObject
wird es weiterhin zerstört, genau wie jede andere lokale Variable. Der Unterschied besteht darin, dass sein Wert ein Zeiger auf ein Objekt ist, nicht auf das Objekt selbst, und die Zerstörung eines dummen Zeigers keinen Einfluss auf dessen Zeiger hat. Das Objekt wird also die Zerstörung überleben.Technisch gesehen handelt es sich um ein Speicherzuordnungsproblem, hier sind jedoch zwei weitere praktische Aspekte. Es hat mit zwei Dingen zu tun: 1) Bereich: Wenn Sie ein Objekt ohne Zeiger definieren, können Sie nach dem Codeblock, in dem es definiert ist, nicht mehr darauf zugreifen. Wenn Sie dagegen einen Zeiger mit "neu" definieren, sind Sie es Sie können von jedem Ort aus darauf zugreifen, an dem Sie einen Zeiger auf diesen Speicher haben, bis Sie auf demselben Zeiger "delete" aufrufen. 2) Wenn Sie Argumente an eine Funktion übergeben möchten, möchten Sie einen Zeiger oder eine Referenz übergeben, um effizienter zu sein. Wenn Sie ein Objekt übergeben, wird das Objekt kopiert. Wenn dies ein Objekt ist, das viel Speicher benötigt, kann dies CPU-verbrauchen (z. B. kopieren Sie einen Vektor voller Daten). Wenn Sie einen Zeiger übergeben, übergeben Sie nur ein Int (abhängig von der Implementierung, aber die meisten davon sind ein Int).
Ansonsten müssen Sie verstehen, dass "neu" Speicher auf dem Heap zuweist, der irgendwann freigegeben werden muss. Wenn Sie nicht "neu" verwenden müssen, empfehlen wir Ihnen, eine reguläre Objektdefinition "auf dem Stapel" zu verwenden.
quelle
Nun, die Hauptfrage ist, warum ich einen Zeiger anstelle des Objekts selbst verwenden sollte. Und meine Antwort, Sie sollten (fast) niemals einen Zeiger anstelle eines Objekts verwenden, da C ++ Referenzen hat , sicherer als Zeiger ist und die gleiche Leistung wie Zeiger garantiert.
Eine andere Sache, die Sie in Ihrer Frage erwähnt haben:
Wie funktioniert es? Es erstellt einen Zeiger vom
Object
Typ, weist Speicher für ein Objekt zu und ruft den Standardkonstruktor auf. Klingt gut, oder? Aber eigentlich ist es nicht so gut, wenn Sie Speicher dynamisch zugewiesen haben (verwendetes Schlüsselwortnew
), müssen Sie den Speicher auch manuell freigeben, was bedeutet, dass Sie im Code Folgendes haben sollten:Dies ruft den Destruktor auf und gibt Speicher frei, sieht einfach aus. In großen Projekten kann es jedoch schwierig sein, festzustellen, ob ein Thread Speicher freigegeben hat oder nicht. Zu diesem Zweck können Sie jedoch gemeinsam genutzte Zeiger ausprobieren . Diese verringern die Leistung geringfügig, sind jedoch viel einfacher zu bearbeiten Sie.
Und jetzt ist eine Einführung vorbei und zurück zur Frage.
Sie können Zeiger anstelle von Objekten verwenden, um eine bessere Leistung beim Übertragen von Daten zwischen Funktionen zu erzielen.
Schauen Sie, Sie haben
std::string
(es ist auch ein Objekt) und es enthält wirklich viele Daten, zum Beispiel großes XML. Jetzt müssen Sie es analysieren, aber dafür haben Sie eine Funktion,void foo(...)
die auf verschiedene Arten deklariert werden kann:void foo(std::string xml);
In diesem Fall kopieren Sie alle Daten von Ihrer Variablen in den Funktionsstapel. Dies dauert einige Zeit, sodass Ihre Leistung gering ist.void foo(std::string* xml);
In diesem Fall übergeben Sie den Zeiger mit der gleichen Geschwindigkeit wie die Übergabe dersize_t
Variablen an das Objekt. Diese Deklaration ist jedoch fehleranfällig, da Sie denNULL
Zeiger oder einen ungültigen Zeiger übergeben können. Zeiger werden normalerweise verwendet,C
weil sie keine Referenzen haben.void foo(std::string& xml);
Hier übergeben Sie eine Referenz, im Grunde ist es dasselbe wie das Übergeben eines Zeigers, aber der Compiler erledigt einige Dinge und Sie können keine ungültige Referenz übergeben (tatsächlich ist es möglich, eine Situation mit einer ungültigen Referenz zu erstellen, aber es betrügt den Compiler).void foo(const std::string* xml);
Hier ist das gleiche wie bei der Sekunde, nur der Zeigerwert kann nicht geändert werden.void foo(const std::string& xml);
Hier ist das gleiche wie beim dritten, aber der Objektwert kann nicht geändert werden.Was ich noch erwähnen möchte, Sie können diese 5 Möglichkeiten verwenden, um Daten zu übergeben, unabhängig davon, welche Zuordnungsmethode Sie gewählt haben (mit
new
oder regelmäßig ).Eine andere Sache zu erwähnen, wenn Sie ein Objekt auf normale Weise erstellen , weisen Sie Speicher im Stapel zu, aber während Sie es mit
new
Ihnen erstellen, weisen Sie Heap zu. Es ist viel schneller Stapel zuweisen, aber es ist ein bisschen ein kleines für wirklich große Arrays von Daten, so dass , wenn Sie großes Objekt benötigen , sollten Sie verwenden Haufen, da Sie Stack - Überlauf kommen können, aber in der Regel diese Frage wird unter Verwendung gelöst Container STL und erinnernstd::string
ist auch container, manche jungs haben es vergessen :)quelle
Nehmen wir an, Sie haben
class A
das enthaltenclass B
Wenn Sie eine Funktion vonclass B
außerhalb aufrufen möchten,class A
erhalten Sie einfach einen Zeiger auf diese Klasse und Sie können tun, was Sie wollen, und es ändert auch den Kontext vonclass B
in Ihrerclass A
Aber seien Sie vorsichtig mit dynamischen Objekten
quelle
Es gibt viele Vorteile der Verwendung von Zeigern auf Objekte -
quelle
Dies wurde ausführlich diskutiert, aber in Java ist alles ein Zeiger. Es wird nicht zwischen Stapel- und Heap-Zuweisungen unterschieden (alle Objekte werden auf dem Heap zugewiesen), sodass Sie nicht erkennen, dass Sie Zeiger verwenden. In C ++ können Sie die beiden abhängig von Ihrem Speicherbedarf mischen. Leistung und Speichernutzung sind in C ++ (duh) deterministischer.
quelle
Dadurch wird ein Verweis auf ein Objekt (auf dem Heap) erstellt, das explizit gelöscht werden muss, um einen Speicherverlust zu vermeiden .
Dadurch wird ein Objekt (myObject) vom automatischen Typ (auf dem Stapel) erstellt, das automatisch gelöscht wird, wenn das Objekt (myObject) den Gültigkeitsbereich verlässt.
quelle
Ein Zeiger verweist direkt auf den Speicherort eines Objekts. Java hat nichts dergleichen. Java verfügt über Referenzen, die über Hash-Tabellen auf die Position des Objekts verweisen. Mit diesen Referenzen können Sie in Java keine Zeigerarithmetik ausführen.
Um Ihre Frage zu beantworten, ist es nur Ihre Präferenz. Ich bevorzuge die Java-ähnliche Syntax.
quelle
Mit Zeigern ,
kann direkt mit der Erinnerung sprechen.
kann durch Manipulieren von Zeigern viele Speicherverluste eines Programms verhindern.
quelle
Ein Grund für die Verwendung von Zeigern ist die Schnittstelle zu C-Funktionen. Ein weiterer Grund ist, Speicherplatz zu sparen. Beispiel: Anstatt ein Objekt, das viele Daten enthält und über einen prozessorintensiven Kopierkonstruktor verfügt, an eine Funktion zu übergeben, übergeben Sie einfach einen Zeiger auf das Objekt, wodurch Speicher und Geschwindigkeit gespart werden, insbesondere wenn Sie sich in einer Schleife befinden In diesem Fall wäre eine Referenz besser, es sei denn, Sie verwenden ein Array im C-Stil.
quelle
In Bereichen, in denen die Speichernutzung am höchsten ist, sind Zeiger praktisch. Stellen Sie sich beispielsweise einen Minimax-Algorithmus vor, bei dem Tausende von Knoten mithilfe einer rekursiven Routine generiert werden und später zur Bewertung des nächstbesten Zugs im Spiel verwendet werden. Die Fähigkeit zur Freigabe oder zum Zurücksetzen (wie bei intelligenten Zeigern) reduziert den Speicherverbrauch erheblich. Während die Nicht-Zeiger-Variable weiterhin Speicherplatz belegt, bis ihr rekursiver Aufruf einen Wert zurückgibt.
quelle
Ich werde einen wichtigen Anwendungsfall des Zeigers einschließen. Wenn Sie ein Objekt in der Basisklasse speichern, kann es jedoch polymorph sein.
In diesem Fall können Sie bObj nicht als direktes Objekt deklarieren. Sie müssen einen Zeiger haben.
quelle
"Notwendigkeit ist die Mutter der Erfindung." Der wichtigste Unterschied, auf den ich hinweisen möchte, ist das Ergebnis meiner eigenen Erfahrung mit dem Codieren. Manchmal müssen Sie Objekte an Funktionen übergeben. In diesem Fall, wenn Ihr Objekt einer sehr großen Klasse angehört, kopiert das Übergeben als Objekt seinen Status (den Sie möglicherweise nicht möchten. UND KANN ÜBER DEN KOPF GROSS SEIN), was zu einem Overhead beim Kopieren des Objekts führt, während der Zeiger festgelegt ist 4-Byte-Größe (unter der Annahme von 32 Bit). Andere Gründe sind bereits oben erwähnt ...
quelle
std::string test;
wir habenvoid func(const std::string &) {}
, eine Konstantenreferenz zu übergeben, aber es sei denn, die Funktion muss die Eingabe ändern. In diesem Fall empfehle ich die Verwendung von Zeigern (damit jeder, der den Code liest&
und bemerkt , dass die Funktion ihre Eingabe ändern kann)Es gibt bereits viele ausgezeichnete Antworten, aber ich möchte Ihnen ein Beispiel geben:
Ich habe eine einfache Gegenstandsklasse:
Ich mache einen Vektor, um ein paar von ihnen zu halten.
std::vector<Item> inventory;
Ich erstelle eine Million Item-Objekte und schiebe sie zurück auf den Vektor. Ich sortiere den Vektor nach Namen und führe dann eine einfache iterative binäre Suche nach einem bestimmten Elementnamen durch. Ich teste das Programm und es dauert über 8 Minuten, bis die Ausführung abgeschlossen ist. Dann ändere ich meinen Inventarvektor wie folgt:
std::vector<Item *> inventory;
... und erstelle meine Millionen Item-Objekte über new. Die EINZIGEN Änderungen, die ich an meinem Code vornehme, sind die Verwendung der Zeiger auf Elemente, mit Ausnahme einer Schleife, die ich am Ende für die Speicherbereinigung hinzufüge. Dieses Programm läuft in weniger als 40 Sekunden oder besser als eine 10-fache Geschwindigkeitssteigerung. BEARBEITEN: Der Code befindet sich unter http://pastebin.com/DK24SPeW. Mit Compiler-Optimierungen wird auf dem Computer, auf dem ich ihn gerade getestet habe, nur eine 3,4-fache Steigerung angezeigt, was immer noch beträchtlich ist.
quelle
push_back
. Natürlich kopiert dies. Sie solltenemplace
beim Erstellen Ihrer Objekte an Ort und Stelle gewesen sein (es sei denn, Sie müssen sie an anderer Stelle zwischengespeichert haben).