Warum sollte ich einen Zeiger anstelle des Objekts selbst verwenden?


1602

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?


405
Ein großes Lob an Sie, dass Sie diese Praxis in Frage gestellt haben, anstatt sie nur zu befolgen. Meistens werden Zeiger überbeansprucht.
Luchian Grigore

120
Wenn Sie keinen Grund sehen, Zeiger zu verwenden, tun Sie dies nicht. Objekte bevorzugen. Bevorzugen Sie Objekte vor unique_ptr vor shared_ptr vor Raw-Zeigern.
Stefan

113
Hinweis: In Java ist alles (außer Basistypen) ein Zeiger. Sie sollten also lieber das Gegenteil fragen: Warum brauche ich einfache Objekte?
Karoly Horvath

119
Beachten Sie, dass Zeiger in Java durch die Syntax ausgeblendet sind. In C ++ wird der Unterschied zwischen einem Zeiger und einem Nichtzeiger im Code explizit angegeben. Java verwendet überall Zeiger.
Daniel Martín

216
Nah als zu breit ? Ernsthaft? Bitte Leute, beachten Sie, dass diese Java ++ - Programmierweise sehr verbreitet ist und eines der wichtigsten Probleme in der C ++ - Community darstellt . Es sollte ernst genommen werden.
Manu343726

Antworten:


1574

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 tun new Object(), hat das Objekt eine dynamische Speicherdauer, was bedeutet, dass es so lange am Leben bleibt, bis Sie es explizit deleteangeben. 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:

  1. Sie benötigen das Objekt, um den aktuellen Bereich zu überleben - dieses bestimmte Objekt an diesem bestimmten Speicherort, keine Kopie davon. Wenn Sie mit dem Kopieren / Verschieben des Objekts einverstanden sind (meistens sollten Sie dies tun), sollten Sie ein automatisches Objekt bevorzugen.
  2. Sie müssen viel Speicher zuweisen , wodurch der Stapel leicht gefüllt werden kann. Es wäre schön, wenn wir uns nicht darum kümmern müssten (die meiste Zeit sollten Sie das nicht müssen), da dies wirklich außerhalb des Zuständigkeitsbereichs von C ++ liegt, aber leider müssen wir uns mit der Realität der Systeme befassen Wir entwickeln für.

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_ptrund std::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 .

  1. 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.

  2. 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.

  3. 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 verwenden boost::optional).

  4. 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 .

  5. 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 getElementfunktion 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.


83
"Sie benötigen das Objekt, um den aktuellen Bereich zu überleben." - Ein zusätzlicher Hinweis dazu: Es gibt Fälle, in denen es so aussieht, als ob Sie das Objekt benötigen, um den aktuellen Bereich zu überleben, aber Sie tun es wirklich nicht. Wenn Sie Ihr Objekt beispielsweise in einen Vektor einfügen, wird das Objekt in den Vektor kopiert (oder verschoben), und das ursprüngliche Objekt kann sicher zerstört werden, wenn sein Gültigkeitsbereich endet.

25
Denken Sie daran, dass s / copy / move / jetzt an vielen Stellen. Das Zurückgeben eines Objekts bedeutet definitiv keine Bewegung. Sie sollten auch beachten, dass der Zugriff auf ein Objekt über einen Zeiger orthogonal zu seiner Erstellung ist.
Welpe

15
Ich vermisse einen expliziten Verweis auf RAII zu dieser Antwort. In C ++ dreht sich alles (fast alles) um Ressourcenverwaltung, und RAII ist der Weg, dies unter C ++ zu tun (und das Hauptproblem, das rohe Zeiger erzeugen: RAII brechen)
Manu343726

11
Vor C ++ 11 gab es intelligente Zeiger, z. B. boost :: shared_ptr und boost :: scoped_ptr. Andere Projekte haben ihre eigene Entsprechung. Sie können keine Verschiebungssemantik erhalten, und die Zuweisung von std :: auto_ptr ist fehlerhaft, sodass C ++ 11 die Dinge verbessert, aber der Rat ist immer noch gut. (Und ein trauriger Trottel, es reicht nicht aus, Zugriff auf einen C ++ 11-Compiler zu haben. Es ist erforderlich, dass alle Compiler, mit denen Ihr Code möglicherweise arbeiten soll, C ++ 11 unterstützen. Ja, Oracle Solaris Studio, das bin ich
schau

7
@ MDMoore313 Sie können schreibenObject myObject(param1, etc...)
user000001

173

Es gibt viele Anwendungsfälle für Zeiger.

Polymorphes Verhalten . Bei polymorphen Typen werden Zeiger (oder Referenzen) verwendet, um ein Schneiden zu vermeiden:

class Base { ... };
class Derived : public Base { ... };

void fun(Base b) { ... }
void gun(Base* b) { ... }
void hun(Base& b) { ... }

Derived d;
fun(d);    // oops, all Derived parts silently "sliced" off
gun(&d);   // OK, a Derived object IS-A Base object
hun(d);    // also OK, reference also doesn't slice

Referenzsemantik und Vermeiden des Kopierens . Bei nicht polymorphen Typen verhindert ein Zeiger (oder eine Referenz) das Kopieren eines möglicherweise teuren Objekts

Base b;
fun(b);  // copies b, potentially expensive 
gun(&b); // takes a pointer to b, no copying
hun(b);  // regular syntax, behaves as a pointer

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 newOperator ist in modernem C ++ ein Anti-Pattern . Verwenden Sie eine spezielle Ressourcenklasse (einen der Standardcontainer) oder einen intelligenten Zeiger ( std::unique_ptr<>oder std::shared_ptr<>). Erwägen:

{
    auto b = new Base;
    ...       // oops, if an exception is thrown, destructor not called!
    delete b;
}

vs.

{
    auto b = std::make_unique<Base>();
    ...       // OK, now exception safe
}

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 newweder von Ihnen noch innerhalb einer Ressourcenklasse erstellt wurden) werden zerstört, wenn der Gültigkeitsbereich verlassen wird.


17
"Das Erstellen eines Zeigers auf eine Ressource mit dem neuen Operator ist ein Anti-Pattern" Ich denke, Sie könnten sogar verbessern, dass ein Raw-Zeiger etwas besitzt, das ein Anti-Pattern ist . Nicht nur die Erstellung, sondern auch die Übergabe von Rohzeigern als Argumente oder Rückgabewerte, die eine Eigentumsübertragung implizieren. IMHO ist seit unique_ptr/ move semantics
dyp

1
@dyp tnx, aktualisiert und Verweis auf die C ++ - FAQ-Fragen und Antworten zu diesem Thema.
TemplateRex

4
Die Verwendung intelligenter Zeiger überall ist ein Anti-Muster. Es gibt einige Sonderfälle, in denen dies anwendbar ist, aber meistens spricht der gleiche Grund, der für eine dynamische Zuordnung (willkürliche Lebensdauer) spricht, auch gegen einen der üblichen intelligenten Zeiger.
James Kanze

2
@JamesKanze Ich wollte nicht implizieren, dass intelligente Zeiger überall verwendet werden sollten, nur für den Besitz, und dass rohe Zeiger nicht für den Besitz verwendet werden sollten, sondern nur für Ansichten.
TemplateRex

2
@TemplateRex Das scheint etwas albern, da dies hun(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). Auch const&.
JAB

130

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:

Object object1 = new Object(); //A new object is allocated by Java
Object object2 = new Object(); //Another new object is allocated by Java

object1 = object2; 
//object1 now points to the object originally allocated for object2
//The object originally allocated for object1 is now "dead" - nothing points to it, so it
//will be reclaimed by the Garbage Collector.
//If either object1 or object2 is changed, the change will be reflected to the other

Das nächste Äquivalent dazu ist:

C ++:

Object * object1 = new Object(); //A new object is allocated on the heap
Object * object2 = new Object(); //Another new object is allocated on the heap
delete object1;
//Since C++ does not have a garbage collector, if we don't do that, the next line would 
//cause a "memory leak", i.e. a piece of claimed memory that the app cannot use 
//and that we have no way to reclaim...

object1 = object2; //Same as Java, object1 points to object2.

Sehen wir uns den alternativen C ++ - Weg an:

Object object1; //A new object is allocated on the STACK
Object object2; //Another new object is allocated on the STACK
object1 = object2;//!!!! This is different! The CONTENTS of object2 are COPIED onto object1,
//using the "copy assignment operator", the definition of operator =.
//But, the two objects are still different. Change one, the other remains unchanged.
//Also, the objects get automatically destroyed once the function returns...

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:

int object1; //An integer is allocated on the stack.
int object2; //Another integer is allocated on the stack.
object1 = object2; //The value of object2 is copied to object1.

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.


7
Object2 is now "dead": Ich denke du meinst myObject1oder genauer the object pointed to by myObject1.
Clément

2
Tatsächlich! Ein bisschen umformuliert.
Gerasimos R

2
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 Sie newRaws verwenden, sollten Sie newed-Objekte so schnell wie möglich in RAII-Wrapper einschließen.
PSkocik

8
In der Tat wäre es so, wenn dies ein Programm wäre und nichts anderes um es herum vor sich geht. Zum Glück ist dies nur ein Erklärungsausschnitt, der zeigt, wie sich ein Zeiger in C ++ verhält - und einer der wenigen Orte, an denen ein RAII-Objekt keinen Rohzeiger ersetzen kann, ist das Lernen und Lernen von Rohzeigern ...
Gerasimos R

80

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.


7
Dies trägt wirklich zu der Mischung nützlicher Informationen bei. Ich bin froh, dass Sie eine Antwort darauf gegeben haben!
TemplateRex

3
std :: shared_ptr <T> funktioniert auch mit Vorwärtsdeklarationen von T. (std :: unique_ptr <T> nicht )
berkus

13
@berkus: std::unique_ptr<T>funktioniert mit Vorwärtsdeklarationen von T. Sie müssen nur sicherstellen, dass der Destruktor von ein vollständiger Typ ist , wenn er std::unique_ptr<T>aufgerufen Twird. Dies bedeutet normalerweise, dass Ihre Klasse, die die std::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).
David Stone

Werden Module dies beheben?
Trevor Hickey

@TrevorHickey Alter Kommentar, den ich kenne, aber trotzdem zu beantworten. Module entfernen die Abhängigkeit nicht, sollten jedoch das Einbeziehen der Abhängigkeit sehr billig und in Bezug auf die Leistungskosten nahezu kostenlos machen. Wenn die allgemeine Beschleunigung von Modulen ausreicht, um Ihre Kompilierungszeiten in einen akzeptablen Bereich zu bringen, ist dies ebenfalls kein Problem mehr.
Aidiakapi

79

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

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?

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:

{
    std::string s;
}
// s is destroyed here

Wenn Sie dagegen einen dynamisch zugewiesenen Zeiger verwenden, muss sein Destruktor manuell aufgerufen werden. deleteruft diesen Destruktor für Sie auf.

{
    std::string* s = new std::string;
}
delete s; // destructor called

Dies hat nichts mit der newin C # und Java vorherrschenden Syntax zu tun . Sie werden für ganz andere Zwecke eingesetzt.

Vorteile der dynamischen Allokation

1. Sie müssen die Größe des Arrays nicht im Voraus kennen

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:

char buffer[100];
std::cin >> buffer;
// bad input = buffer overflow

Wenn Sie std::stringstattdessen 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:

int * pointer;
std::cout << "How many items do you need?";
std::cin >> n;
pointer = new int[n];

Randnotiz : Ein Fehler, den viele Anfänger machen, ist die Verwendung von Arrays variabler Länge. Dies ist eine GNU-Erweiterung und auch eine in Clang, da sie viele der GCC-Erweiterungen widerspiegeln. Daher int arr[n]sollte man sich nicht auf Folgendes verlassen.

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.

2. Arrays sind keine Zeiger

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 sizeofInformationen. 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:

int arr[5] = {1, 2, 3, 4, 5}; // initialization 
int arr[] = {1, 2, 3, 4, 5}; // The standard dictates that the size of the array
                             // be given by the amount of members in the initializer  
arr = { 1, 2, 3, 4, 5 }; // ERROR

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.

3. Polymorphismus

Java und C # verfügen über Funktionen, mit denen Sie Objekte als andere behandeln können, z. B. mithilfe des asSchlüsselworts. Wenn also jemand ein EntityObjekt als PlayerObjekt 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:

std::vector<Base*> vector;
vector.push_back(&square);
vector.push_back(&triangle);
for (auto& e : vector)
{
     auto test = dynamic_cast<Triangle*>(e); // I only care about triangles
     if (!test) // not a triangle
        e.GenericFunction();
     else
        e.TriangleOnlyMagic();
}

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_castkönnen Sie das asSchlüsselwort simulieren . Wenn ein Cast fehlschlägt, wird ein ungültiger Zeiger zurückgegeben. Dies !testist 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 newSchlü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.

1. (N) RVO - Aka, (benannt) Rückgabewertoptimierung

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 deleteZuordnung 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.


"Also! Test ist im Wesentlichen eine Abkürzung, um zu überprüfen, ob der Test NULL oder ein ungültiger Zeiger ist, was bedeutet, dass die Umwandlung fehlgeschlagen ist." Ich denke, dieser Satz muss aus Gründen der Klarheit umgeschrieben werden.
Berkus

4
"Die Java-Hype-Maschine möchte, dass Sie glauben" - vielleicht 1997, aber das ist jetzt anachronistisch, es gibt keine Motivation mehr, Java 2014 mit C ++ zu vergleichen.
Matt R

15
Alte Frage, aber im Codesegment { std::string* s = new std::string; } delete s; // destructor called... das wird sicher deletenicht funktionieren, weil der Compiler nicht mehr weiß, was sist?
Dachs5000

2
Ich gebe NICHT -1, aber ich bin nicht einverstanden mit den Eröffnungserklärungen wie geschrieben. Erstens bin ich nicht der Meinung, dass es einen "Hype" gibt - vielleicht um das Jahr 2000, aber jetzt sind beide Sprachen gut verstanden. Zweitens würde ich argumentieren, dass sie ziemlich ähnlich sind - C ++ ist das Kind von C, das mit Simula verheiratet ist, Java fügt Virtual Machine, Garbage Collector hinzu und HEAVILY reduziert Features, und C # optimiert und führt fehlende Features in Java wieder ein. Ja, dies macht Muster und gültige Verwendung HUGELY unterschiedlich, aber es ist vorteilhaft, die gemeinsame Infrastruktur / das gemeinsame Design zu verstehen, damit man die Unterschiede erkennen kann.
Gerasimos R

1
@ James Matta: Sie haben natürlich Recht, dass Speicher Speicher ist, und beide werden aus demselben physischen Speicher zugewiesen. Eine Sache, die berücksichtigt werden muss, ist jedoch, dass es sehr häufig vorkommt, dass bessere Leistungsmerkmale beim Arbeiten mit vom Stapel zugewiesenen Objekten erzielt werden, da der Stapel - oder zumindest die höchsten Ebenen - haben eine sehr hohe Wahrscheinlichkeit, im Cache "heiß" zu sein, wenn Funktionen ein- und ausgehen, während der Heap keinen solchen Vorteil hat. Wenn Sie also Zeiger im Heap verfolgen, können mehrere Cache-Fehler auftreten Sie würden wahrscheinlich nicht auf dem Stapel. Aber all diese "Zufälligkeit" begünstigt normalerweise den Stapel.
Gerasimos R

23

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):

  • Typ ist jedem gegeben
  • Der Modifikator Wert / Zeiger / Referenz ist individuell

Beispiel:

struct MyStruct
{
    int* someIntPointer, someInt; //here comes the surprise
    MyStruct *somePointer;
    MyStruct &someReference;
};

MyStruct s1; //we allocated an object on stack, not in heap

s1.someInt = 1; //someInt is of type 'int', not 'int*' - value/pointer modifier is individual
s1.someIntPointer = &s1.someInt;
*s1.someIntPointer = 2; //now s1.someInt has value '2'
s1.somePointer = &s1;
s1.someReference = s1; //note there is no '&' operator: reference tries to look like value
s1.somePointer->someInt = 3; //now s1.someInt has value '3'
*(s1.somePointer).someInt = 3; //same as above line
*s1.somePointer->someIntPointer = 4; //now s1.someInt has value '4'

s1.someReference.someInt = 5; //now s1.someInt has value '5'
                              //although someReference is not value, it's members are accessed through '.'

MyStruct s2 = s1; //'NO WAY' the compiler will say. Go define your '=' operator and come back.

//OK, assume we have '=' defined in MyStruct

s2.someInt = 0; //s2.someInt == 0, but s1.someInt is still 5 - it's two completely different objects, not the references to the same one

1
std::auto_ptrist veraltet, bitte nicht benutzen.
Neil

2
Ziemlich sicher, dass Sie keine Referenz als Mitglied haben können, ohne einem Konstruktor auch eine Initialisierungsliste bereitzustellen, die die Referenzvariable enthält. (Eine Referenz muss sofort initialisiert werden. Selbst der Konstruktorkörper ist zu spät, um sie festzulegen, IIRC.)
CHA

20

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 zuweisen Object* obj = new Object(), leben diese weiterhin im Heap, bis Sie aufrufen delete 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.


6
Object objist nicht immer auf dem Stapel - zum Beispiel Globals oder Mitgliedsvariablen.
vierundvierzig

2
@LightnessRacesinOrbit Ich erwähnte nur die in einem Block zugewiesenen Objekte, nicht globale und Mitgliedsvariablen. Die Sache ist, dass es nicht klar war, jetzt korrigiert - hinzugefügt "innerhalb eines Blocks" in der Antwort. Hoffe, dass es jetzt keine falschen Informationen sind :)
Karthik Kalyanasundaram

20

Aber ich kann nicht herausfinden, warum wir es so verwenden sollten?

Ich werde vergleichen, wie es im Funktionskörper funktioniert, wenn Sie Folgendes verwenden:

Object myObject;

Innerhalb der Funktion wird Ihre myObjectzerstö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:

 Object *myObject = new Object;

Dann wird die Objektklasseninstanz, auf die gezeigt myObjectwird, 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 Objectindem Sie Werte in shared_ptr / unique_ptr speichern.

std::shared_ptr<std::string> safe_str = make_shared<std::string>("make_shared");

// since c++14
std::unique_ptr<std::string> safe_str = make_unique<std::string>("make_shared"); 

Außerdem werden Objekte sehr oft in Containern wie Karten oder Vektoren gespeichert. Sie verwalten automatisch die Lebensdauer Ihrer Objekte.


1
then myObject will not get destroyed once function endsEs wird absolut.
Leichtigkeitsrennen im Orbit

6
Im Zeigerfall myObjectwird 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.
CHao

Es wurde behoben, dass lokale Variablen (einschließlich Zeiger) natürlich freigegeben werden - sie befinden sich auf dem Stapel.
Uhr

13

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.


6

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:

Object *myObject = new Object;

Wie funktioniert es? Es erstellt einen Zeiger vom ObjectTyp, 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üsselwort new), müssen Sie den Speicher auch manuell freigeben, was bedeutet, dass Sie im Code Folgendes haben sollten:

delete myObject;

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:

  1. 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.
  2. void foo(std::string* xml); In diesem Fall übergeben Sie den Zeiger mit der gleichen Geschwindigkeit wie die Übergabe der size_tVariablen an das Objekt. Diese Deklaration ist jedoch fehleranfällig, da Sie den NULLZeiger oder einen ungültigen Zeiger übergeben können. Zeiger werden normalerweise verwendet, Cweil sie keine Referenzen haben.
  3. 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).
  4. void foo(const std::string* xml); Hier ist das gleiche wie bei der Sekunde, nur der Zeigerwert kann nicht geändert werden.
  5. 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 newoder 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 newIhnen 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 erinnern std::stringist auch container, manche jungs haben es vergessen :)


5

Nehmen wir an, Sie haben class Adas enthalten class BWenn Sie eine Funktion von class Baußerhalb aufrufen möchten, class Aerhalten Sie einfach einen Zeiger auf diese Klasse und Sie können tun, was Sie wollen, und es ändert auch den Kontext von class Bin Ihrerclass A

Aber seien Sie vorsichtig mit dynamischen Objekten


5

Es gibt viele Vorteile der Verwendung von Zeigern auf Objekte -

  1. Effizienz (wie Sie bereits betont haben). Das Übergeben von Objekten an Funktionen bedeutet das Erstellen neuer Objektkopien.
  2. Arbeiten mit Objekten aus Bibliotheken von Drittanbietern. Wenn Ihr Objekt zu einem Code eines Drittanbieters gehört und die Autoren die Verwendung ihrer Objekte nur über Zeiger (keine Kopierkonstruktoren usw.) beabsichtigen, können Sie dieses Objekt nur mithilfe von Zeigern umgehen. Das Übergeben von Werten kann Probleme verursachen. (Deep Copy / Flat Copy Probleme).
  3. Wenn das Objekt eine Ressource besitzt und Sie möchten, dass der Besitz nicht mit anderen Objekten übertragen wird.

3

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.


3
Object *myObject = new Object;

Dadurch wird ein Verweis auf ein Objekt (auf dem Heap) erstellt, das explizit gelöscht werden muss, um einen Speicherverlust zu vermeiden .

Object myObject;

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.


1

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.


Hash-Tabellen? Vielleicht in einigen JVMs, aber rechnen Sie nicht damit.
Zan Lynx

Was ist mit der JVM, die mit Java geliefert wird? Natürlich können Sie ALLES implementieren, was Sie sich vorstellen können, wie eine JVM, die Zeiger direkt verwendet, oder eine Methode, die Zeigermathematik ausführt. Das ist so, als würde man sagen "Menschen sterben nicht an Erkältung" und eine Antwort erhalten "Vielleicht rechnen die meisten Menschen nicht, aber rechnen nicht damit!" Ha ha.
RioRicoRick

2
@RioRicoRick HotSpot implementiert Java-Referenzen als native Zeiger, siehe docs.oracle.com/javase/7/docs/technotes/guides/vm/… Soweit ich sehen kann, macht JRockit dasselbe. Beide unterstützen die OOP-Komprimierung, verwenden jedoch keine Hash-Tabellen. Die Leistungsfolgen wären wahrscheinlich katastrophal. Außerdem scheint "es ist nur Ihre Präferenz" zu implizieren, dass die beiden lediglich unterschiedliche Syntaxen für gleichwertiges Verhalten sind, was sie natürlich nicht sind.
Max Barraclough


0

Mit Zeigern ,

  • kann direkt mit der Erinnerung sprechen.

  • kann durch Manipulieren von Zeigern viele Speicherverluste eines Programms verhindern.


4
" In C ++ können Sie mithilfe von Zeigern einen benutzerdefinierten Garbage Collector für Ihr eigenes Programm erstellen ", was nach einer schrecklichen Idee klingt.
Quant

0

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.


0

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.


0

Ich werde einen wichtigen Anwendungsfall des Zeigers einschließen. Wenn Sie ein Objekt in der Basisklasse speichern, kann es jedoch polymorph sein.

Class Base1 {
};

Class Derived1 : public Base1 {
};


Class Base2 {
  Base *bObj;
  virtual void createMemerObects() = 0;
};

Class Derived2 {
  virtual void createMemerObects() {
    bObj = new Derived1();
  }
};

In diesem Fall können Sie bObj nicht als direktes Objekt deklarieren. Sie müssen einen Zeiger haben.


-5

"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 ...


14
Sie sollten es vorziehen, als Referenz zu gehen
Bolov

2
Ich empfehle, wie bei einer Variablen, die std::string test;wir haben void 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)
Top- Meister

-7

Es gibt bereits viele ausgezeichnete Antworten, aber ich möchte Ihnen ein Beispiel geben:

Ich habe eine einfache Gegenstandsklasse:

 class Item
    {
    public: 
      std::string name;
      int weight;
      int price;
    };

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.


2
Vergleichen Sie dann die Zeiger oder vergleichen Sie immer noch die tatsächlichen Objekte? Ich bezweifle sehr, dass eine andere Indirektionsebene die Leistung verbessern kann. Bitte Code angeben! Räumen Sie danach richtig auf?
Stefan

1
@stefan Ich vergleiche die Daten (insbesondere das Namensfeld) der Objekte sowohl für die Sortierung als auch für die Suche. Ich räume richtig auf, wie ich schon in der Post erwähnt habe. Die Beschleunigung ist wahrscheinlich auf zwei Faktoren zurückzuführen: 1) std :: vector push_back () kopiert die Objekte, sodass für die Zeigerversion nur ein einziger Zeiger pro Objekt kopiert werden muss. Dies hat mehrere Auswirkungen auf die Leistung, da nicht nur weniger Daten kopiert werden, sondern auch der Speicherzuweiser für Vektorklassen weniger überlastet wird.
Darren

2
Hier ist Code, der für Ihr Beispiel praktisch keinen Unterschied zeigt: Sortieren. Der Zeigercode ist 6% schneller als der Nichtzeigercode für die Sortierung allein, aber insgesamt 10% langsamer als der Nichtzeigercode. ideone.com/G0c7zw
stefan

3
Schlüsselwort : push_back. Natürlich kopiert dies. Sie sollten emplacebeim Erstellen Ihrer Objekte an Ort und Stelle gewesen sein (es sei denn, Sie müssen sie an anderer Stelle zwischengespeichert haben).
underscore_d

1
Zeigervektoren sind fast immer falsch. Bitte empfehlen Sie sie nicht, ohne die Vorbehalte sowie die Vor- und Nachteile ausführlich zu erläutern. Sie scheinen einen Profi gefunden zu haben, der nur eine Folge eines schlecht codierten Gegenbeispiels ist, und ihn falsch dargestellt zu haben
Lightness Races in Orbit
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.