it-swarm.com.de

Warum sollte ich einen Zeiger anstelle des Objekts selbst verwenden?

Ich komme aus einem Java-Hintergrund und arbeite mit Objekten in C++. Mir ist jedoch aufgefallen, dass Menschen häufig Zeiger auf Objekte und nicht die Objekte selbst verwenden, beispielsweise diese Deklaration:

Object *myObject = new Object;

eher, als:

Object myObject;

Oder anstatt eine Funktion zu verwenden, sagen wir testFunc() wie folgt:

myObject.testFunc();

wir müssen schreiben:

myObject->testFunc();

Aber ich kann nicht verstehen, warum wir es so machen sollten. Ich würde davon ausgehen, dass dies mit Effizienz und Geschwindigkeit zusammenhängt, da wir direkt auf die Speicheradresse zugreifen. Habe ich recht?

1422
gEdringer

Es ist sehr bedauerlich, dass Sie so oft dynamische Zuordnung sehen. Das zeigt nur, wie viele schlechte C++ - Programmierer vorhanden sind.

In gewissem Sinne haben Sie zwei Fragen in einer zusammengefasst. Der erste ist, wann sollten wir dynamische Zuweisung (mit new) verwenden? Der zweite ist, wann sollten wir Zeiger verwenden?

Die wichtige Nachricht zum Mitnehmen ist, dass Sie immer das für den Job geeignete Werkzeug verwenden sollten. In fast allen Situationen ist etwas geeigneter und sicherer als die manuelle dynamische Zuordnung und/oder die Verwendung von Rohzeigern.

Dynamische Zuordnung

In Ihrer Frage haben Sie zwei Möglichkeiten gezeigt, wie Sie ein Objekt erstellen. Der Hauptunterschied ist die Speicherdauer des Objekts. Wenn Sie Object myObject; innerhalb eines Blocks ausführen, wird das Objekt mit einer automatischen Speicherdauer erstellt. Das bedeutet, dass es automatisch gelöscht wird, wenn es den Gültigkeitsbereich verlässt. Wenn Sie new Object() ausführen, verfügt das Objekt über eine dynamische Speicherdauer, dh es bleibt aktiv, bis Sie es explizit delete angeben. Sie sollten die dynamische Speicherdauer nur dann verwenden, wenn Sie sie benötigen. Das heißt, Sie sollten immer lieber Objekte mit automatischer Speicherdauer erstellen, wenn.

Die zwei wichtigsten Situationen, in denen Sie möglicherweise eine dynamische Zuordnung benötigen:

  1. Sie benötigen das Objekt, um den aktuellen Gültigkeitsbereich zu überleben - dieses bestimmte Objekt an diesem bestimmten Speicherort, keine Kopie davon. Wenn Sie mit dem Kopieren/Verschieben des Objekts einverstanden sind (die meiste Zeit sollten Sie dies tun), sollten Sie ein automatisches Objekt bevorzugen.
  2. Sie müssen viel Speicherplatz zuweisen, wodurch der Stapel leicht gefüllt werden kann. Es wäre schön, wenn wir uns nicht damit befassen müssten (die meiste Zeit sollten Sie nicht), da es außerhalb des Bereichs von C++ liegt, aber leider müssen wir uns mit der Realität der Systeme beschäftigen, die wir haben entwickle mich für.

Wenn Sie eine dynamische Zuweisung unbedingt benötigen, sollten Sie sie in einem intelligenten Zeiger oder einem anderen Typ einkapseln, der RAII (wie die Standardcontainer) ausführt. Intelligente Zeiger stellen die Besitz-Semantik dynamisch zugewiesener Objekte bereit. Schauen Sie sich zum Beispiel std::unique_ptr und std::shared_ptr an. Wenn Sie sie entsprechend verwenden, können Sie fast ganz auf Ihre eigene Speicherverwaltung verzichten (siehe/ Regel von Zero ).

Zeiger

Neben der dynamischen Zuweisung gibt es jedoch andere generelle Verwendungen für Rohzeiger. Die meisten bieten jedoch Alternativen, die Sie bevorzugen sollten. Nach wie vor bevorzugen immer die Alternativen, es sei denn, Sie benötigen wirklich Zeiger.

  1. Sie benötigen eine Referenzsemantik. Manchmal möchten Sie ein Objekt mithilfe eines Zeigers übergeben (unabhängig davon, wie es zugewiesen wurde), weil die Funktion, an die Sie es übergeben möchten, auf dieses bestimmte Objekt zugreifen kann (nicht auf eine Kopie davon). In den meisten Situationen sollten Sie jedoch Verweistypen den Zeigern vorziehen, da sie speziell dafür entwickelt wurden. Beachten Sie, dass dies nicht unbedingt eine Verlängerung der Lebensdauer des Objekts über den aktuellen Geltungsbereich hinaus ist, wie in Situation 1 oben. Wenn Sie eine Kopie des Objekts übergeben möchten, benötigen Sie keine Referenzsemantik.

  2. Sie brauchen Polymorphismus. Sie können Funktionen nur polymorph (dh je nach dynamischem Typ eines Objekts) über einen Zeiger oder eine Referenz auf das Objekt aufrufen. Wenn dies das gewünschte Verhalten ist, müssen Sie Zeiger oder Verweise verwenden. Auch hier sollten Referenzen bevorzugt werden.

  3. Sie möchten darstellen, dass ein Objekt optional ist, indem Sie zulassen, dass eine nullptr übergeben wird, wenn das Objekt weggelassen wird. Wenn es sich um ein Argument handelt, sollten Sie es vorziehen, Standardargumente oder Funktionsüberladungen zu verwenden. Andernfalls sollten Sie einen Typ vorziehen, der dieses Verhalten einkapselt, z. B. std::optional (in C++ 17 eingeführt - verwenden Sie bei früheren C++ - Standards boost::optional).

  4. Sie möchten die Kompilierungseinheiten entkoppeln, um die Kompilierzeit zu verbessern. Die nützliche Eigenschaft eines Zeigers besteht darin, dass Sie nur eine Vorwärtsdeklaration des angegebenen Typs benötigen (um das Objekt tatsächlich verwenden zu können, benötigen Sie eine Definition). Dadurch können Sie Teile Ihres Kompilierungsprozesses entkoppeln, wodurch sich die Kompilierungszeit erheblich verkürzen kann. Siehe das Pimpl-Idiom .

  5. Sie müssen eine Schnittstelle zu einer C-Bibliothek oder einer C-Stil-Bibliothek herstellen. An diesem Punkt sind Sie gezwungen, rohe Zeiger zu verwenden. Das Beste, was Sie tun können, ist sicherzustellen, dass Sie Ihre rohen Zeiger nur im letzten Moment loslassen. Sie können einen Rohzeiger beispielsweise von einem intelligenten Zeiger erhalten, indem Sie dessen Member-Funktion get verwenden. Wenn eine Bibliothek eine Zuweisung für Sie vornimmt, von der Sie erwarten, dass sie die Zuweisung über einen Handle aufhebt, können Sie den Griff häufig in einen intelligenten Zeiger mit einem benutzerdefinierten Deleter einschließen, der das Objekt entsprechend freigibt.

1420

Es gibt viele Anwendungsfälle für Zeiger. 

Polymorphes Verhalten. Bei polymorphen Typen werden Zeiger (oder Referenzen) verwendet, um das 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 vermeidet 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 eine Verschiebesemantik hat, durch die viele Kopien teurer Objekte in Funktionsargumente und als Rückgabewerte vermieden werden können. Durch die Verwendung eines Zeigers werden diese jedoch definitiv vermieden und mehrere Zeiger auf dasselbe Objekt zugelassen (während ein Objekt nur einmal verschoben werden kann).

Ressourcenerfassung. Das Erstellen eines Zeigers auf eine Ressource mit dem Operator new ist im modernen C++ ein anti-pattern. Verwenden Sie eine spezielle Ressourcenklasse (einen der Standard-Container) oder einen Smart-Pointer (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 "Sicht" verwendet werden und darf in keiner Weise mit dem Besitz verbunden sein, sei es durch direkte Erstellung oder implizit durch Rückgabewerte. Siehe auch diese Fragen und Antworten aus C++ FAQ .

Mehr feinkörnige Lebensdauerkontrolle Bei jedem Kopieren eines gemeinsamen Zeigers (z. B. als Funktionsargument) wird die Ressource, auf die er verweist, am Leben erhalten. Regelmäßige Objekte (nicht von new erstellt, entweder direkt von Ihnen oder innerhalb einer Ressourcenklasse) werden beim Verlassen des Gültigkeitsbereichs zerstört.

163
TemplateRex

Es gibt viele hervorragende Antworten auf diese Frage, einschließlich der wichtigen Anwendungsfälle für Vorwärtsdeklarationen, Polymorphismus usw., aber ich glaube, ein Teil der "Seele" Ihrer Frage wird nicht beantwortet - nämlich was die verschiedenen Syntaxen für Java und C++ bedeuten.

Untersuchen wir die Situation beim Vergleich der beiden Sprachen:

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 Ä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 die alternative C++ - Methode 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...

Am besten kann man daran denken, dass Java (mehr oder weniger) Zeiger auf Objekte (implizit) verarbeitet, während C++ entweder Zeiger auf Objekte oder die Objekte selbst behandeln kann .. _ Wenn Sie beispielsweise Java als "primitive" Typen deklarieren, handelt es sich um tatsächliche Werte, die kopiert werden, und nicht um Zeiger .

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 Verwenden von Zeigern ist jedoch NICHT notwendigerweise der richtige oder der falsche Weg, mit Dingen umzugehen. andere Antworten haben jedoch zufriedenstellend gedeckt. Die allgemeine Idee ist jedoch, dass Sie in C++ viel mehr Kontrolle über die Lebensdauer der Objekte haben und darüber, wo sie leben werden.

Take home point - das Object * object = new Object()-Konstrukt ist eigentlich das, was der typischen Java-Semantik (oder C # für diese Angelegenheit) am nächsten kommt.

121
Gerasimos R

Ein weiterer guter Grund, Zeiger zu verwenden, wäre für Vorwärtsdeklarationen . In einem ausreichend großen Projekt können sie die Kompilierzeit wirklich beschleunigen. 

76
Burnt Toast

Vorwort

Java ist im Gegensatz zum Hype nicht mit C++ zu vergleichen. Die Hype-Maschine Java 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 Fehlinformation ist Teil des Grundes, warum Java Programmierer zu C++ wechseln und eine Java-ähnliche Syntax verwenden, ohne die Auswirkungen ihres Codes zu verstehen.

Weiter geht's

Aber ich kann nicht herausfinden, warum wir das so machen sollen. Ich würde annehmen, dass es 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. Automatische Speichervariablen (auch als Stapelvariablen bezeichnet) haben ihre 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. delete ruft diesen Destruktor für Sie auf.

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

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

Vorteile der dynamischen Zuordnung

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 auch die Größe von Arrays nicht ändern. Zum Beispiel:

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

Wenn Sie stattdessen einen std::string verwendet haben, ändert sich die Größe von std::string intern, sodass dies kein Problem sein 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 sollte man sich nicht auf den folgenden int arr[n] verlassen.

Da der Heap viel größer als der Stack ist, kann beliebig viel Speicher zugewiesen/neu zugewiesen werden, während der Stack eine Einschränkung aufweist.

2. Arrays sind keine Zeiger

Wie ist das ein Vorteil, den Sie fragen? Die Antwort wird klar, wenn Sie die Verwirrung/den Mythos hinter Arrays und Zeigern verstehen. Es wird allgemein angenommen, dass sie gleich sind, aber nicht. Dieser Mythos beruht auf der Tatsache, dass Zeiger wie Arrays subskribiert werden können und dass 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 -Information. Also gibt sizeof(pointer) die Größe des Zeigers in Bytes an, was normalerweise 8 Bytes auf einem 64-Bit-System entspricht.

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 alles machen, was Sie wollen. Da die Unterscheidung zwischen Zeigern und Arrays in Java und C # von Hand gewellt ist, verstehen Anfänger den Unterschied leider nicht.

3. Polymorphismus

Java und C # verfügen über Funktionen, mit denen Sie Objekte wie andere behandeln können, z. B. mit dem Schlüsselwort as. Wenn jemand also ein Entity -Objekt als Player -Objekt behandeln möchte, kann er Player player = Entity as Player; ausführen. Dies ist sehr nützlich, wenn Sie Funktionen für einen homogenen Container aufrufen möchten, der nur für a gelten soll bestimmter Typ. 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_cast können Sie das Schlüsselwort as simulieren. Wenn eine Umwandlung fehlschlägt, wird ein ungültiger Zeiger zurückgegeben. Daher ist !test im Wesentlichen eine Abkürzung für die Überprüfung, ob test NULL ist oder ein ungültiger Zeiger, was bedeutet, dass die Umwandlung fehlgeschlagen ist.

Vorteile von automatischen Variablen

Nachdem Sie alle Vorteile der dynamischen Zuordnung gesehen haben, fragen Sie sich wahrscheinlich, warum niemand die dynamische Zuordnung die ganze Zeit NICHT verwenden würde. Ich habe dir bereits einen Grund genannt, der Haufen ist langsam. Und wenn Sie diesen ganzen Speicher nicht brauchen, sollten Sie ihn nicht missbrauchen. Hier sind also einige Nachteile in keiner bestimmten Reihenfolge:

  • Es ist fehleranfällig. Die manuelle Speicherzuweisung ist gefährlich und es besteht die Gefahr von Lecks. Wenn Sie nicht in der Lage sind, den Debugger oder valgrind (ein Speicherleck-Tool) zu verwenden, können Sie sich die Haare aus dem Kopf ziehen. Glücklicherweise lindern RAII-Redewendungen und kluge Hinweise dies ein wenig, aber Sie müssen mit Praktiken wie der Dreierregel und der Fünferregel vertraut sein. Es gibt eine Menge Informationen, die man aufnehmen muss, und Anfänger, die es nicht wissen oder sich nicht darum kümmern, werden in diese Falle tappen.

  • Es ist nicht erforderlich. Im Gegensatz zu Java und C #, bei denen die Verwendung des Schlüsselworts new in C++ idiomatisch ist, sollten Sie es nur bei Bedarf verwenden. Die allgemeine Redewendung lautet: Wenn Sie einen Hammer haben, sieht alles aus wie ein Nagel. Während Anfänger, die mit C++ beginnen, Angst vor Zeigern haben und gewohnheitsmäßig lernen, Stapelvariablen zu verwenden, sind es Java und C # -Programmierer start durch die Verwendung von Zeigern, ohne es zu verstehen! Das ist buchstäblich auf dem falschen Fuß abspringen. Sie müssen alles aufgeben, was Sie wissen, denn die Syntax ist eine Sache, das Erlernen der Sprache eine andere.

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

Eine Optimierung, die viele Compiler vornehmen, werden Dinge genannt elision und rückgabewertoptimierung. Diese Dinge können unnötige Kopien vermeiden, was für Objekte nützlich ist, die sehr groß sind, wie z. B. ein Vektor, der viele Elemente enthält. Normalerweise ist es üblich, Zeiger auf zu verwenden eigentum übertragen anstatt die großen Objekte zu kopieren bewegung sie herum. Dies hat zur Gründung von geführt semantik verschieben und intelligente Zeiger.

Wenn Sie Zeiger verwenden, ist dies bei (N) RVO der Fall NICHT auftreten. Es ist vorteilhafter und weniger fehleranfällig, die Vorteile von (N) RVO zu nutzen, anstatt Zeiger zurückzugeben oder weiterzugeben, wenn Sie sich Gedanken über die Optimierung machen. Fehlerlecks können auftreten, wenn der Aufrufer einer Funktion für die deleteAuswahl 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.

69
user3391320

In C++ haben Sie drei Möglichkeiten, ein Objekt zu übergeben: per Zeiger, als Referenz und als Wert. Java begrenzt Sie mit Letzterem (die einzige Ausnahme sind primitive Typen wie int, boolean usw.). Wenn Sie C++ nicht wie ein komisches Spielzeug verwenden möchten, sollten Sie den Unterschied zwischen diesen drei Möglichkeiten besser kennenlernen.

Java gibt vor, dass es kein Problem gibt wie "Wer und wann sollte dies zerstört werden?". Die Antwort lautet: Der Müllsammler, großartig und schrecklich. Trotzdem kann es keinen 100% igen Schutz gegen Speicherverluste bieten (ja, Java can leak memory ). Eigentlich gibt GC Ihnen ein falsches Sicherheitsgefühl. Je größer Ihr SUV ist, desto länger ist der Weg zum Evakuator.

Mit C++ stehen Sie dem Lebenszyklus-Management von Objekten direkt gegenüber. Nun, es gibt Mittel, um damit umzugehen ( intelligente Zeiger Familie, QObject in Qt usw.), aber keiner von ihnen kann wie GC in "Feuer und Vergessen" verwendet werden: Sie sollten immer Denken Sie an den Umgang mit dem Speicher. Sie sollten nicht nur ein Objekt zerstören, sondern dasselbe Objekt mehr als einmal zerstört wird.

Noch keine Angst? Ok: Zyklische Referenzen - handhaben Sie sie selbst, Mensch. Und denk dran: Töte jedes Objekt genau einmal, wir C++ -Laufzeiten mögen nicht diejenigen, die sich mit Leichen beschäftigen, tote lassen.

Also zurück zu deiner Frage.

Wenn Sie Ihr Objekt nach Wert, nicht nach Zeiger oder nach Verweis übergeben, kopieren Sie das Objekt (das gesamte Objekt, ob ein paar Bytes oder ein großer Datenbankspeicherauszug) - Sie sind klug genug, um letzteres zu vermeiden. t du?) jedes Mal, wenn du '=' machst. 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. Um dies allen zu zeigen, verwenden Sie diesen fantastischen Operator "->", wenn Sie auf die Mitglieder zugreifen. Oder Sie können die Kombination aus '*' und '.' Verwenden.

Wenn Sie Verweise 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 noch einmal in den Sinn zu kommen: Wenn Sie mehrere durch Kommas getrennte Variablen deklarieren, dann (auf die Zeiger achten):

  • Typ wird an alle weitergegeben
  • Der Wert/Zeiger/Referenzmodifikator 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
23
Kirill Gamazkov

Aber ich kann nicht verstehen, warum wir es so benutzen sollten.

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

Object myObject;

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 im aktuellen Threadstapel abgelegt.

Wenn Sie in den Funktionskörper schreiben:

 Object *myObject = new Object;

dann wird die Objektklasseninstanz, auf die myObject zeigt, nicht zerstört, wenn die Funktion beendet ist und die Zuweisung auf dem Heap liegt.

Wenn Sie nun Java-Programmierer sind, ist das zweite Beispiel näher an der Funktionsweise der Objektzuordnung unter Java. Diese Zeile: Object *myObject = new Object; entspricht Java: Object myObject = new Object();. Der Unterschied ist, dass myObject unter Java Müll gesammelt wird, während es unter C++ nicht freigegeben wird. Sie müssen irgendwo explizit `delete myObject; 'aufrufen. Andernfalls führen Sie Speicherlecks ein.

Seit C++ 11 können Sie sichere Methoden für dynamische Zuordnungen verwenden: new Object, indem 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 häufig in Containern gespeichert, wie z. B. Karten oder Vektoren. Sie verwalten automatisch die Lebensdauer Ihrer Objekte.

19
marcinj

In C++ werden Objekte, die auf dem Stack (mithilfe der Object object;-Anweisung innerhalb eines Blocks) zugewiesen werden, nur innerhalb des Gültigkeitsbereichs leben, in dem sie deklariert werden. Wenn Sie mit Object* obj = new Object() Speicher für Heap reservieren, bleiben sie solange im Heap, bis Sie delete obj aufrufen.

Ich würde ein Objekt auf Heap erstellen, wenn ich das Objekt nicht nur in dem Codeblock verwenden möchte, der es deklariert/zugewiesen hat.

Technisch handelt es sich um ein Problem der Speicherzuordnung, hier sind jedoch zwei weitere praktische Aspekte davon. __ Es hat zwei Dinge zu tun:. __ 1) Umfang, wenn Sie ein Objekt ohne Zeiger definieren, werden Sie nicht mehr in der Lage sein Wenn Sie einen Zeiger mit "new" definieren, können Sie von überall aus darauf zugreifen, wenn 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üssen 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 eine CPU-Belastung verursachen (z. B. Sie kopieren 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).

Abgesehen davon müssen Sie verstehen, dass "neu" Speicherplatz auf dem Heapspeicher reserviert, der zu einem bestimmten Zeitpunkt freigegeben werden muss. Wenn Sie "neu" nicht verwenden müssen, schlage ich vor, dass Sie eine reguläre Objektdefinition "auf dem Stapel" verwenden.

12
in need of help

Nun, die Hauptfrage ist Warum sollte ich einen Zeiger anstelle des Objekts selbst verwenden? Und meine Antwort, Sie sollten (fast) niemals einen Zeiger anstelle eines Objekts verwenden, da C++ Referenzen hat. Dies ist sicherer als Zeiger und garantiert dieselbe Leistung wie Zeiger.

Eine andere Sache, die Sie in Ihrer Frage erwähnt haben:

Object *myObject = new Object;

Wie funktioniert es? Es erstellt einen Zeiger vom Typ Object, weist Speicher für ein Objekt zu und ruft den Standardkonstruktor auf. Klingt gut, richtig? Aber eigentlich ist es nicht so gut, wenn Sie Speicher dynamisch zuweisen (verwendetes Schlüsselwort new), müssen Sie den Speicher auch manuell freigeben. Das heißt, im Code sollten Sie Folgendes haben:

delete myObject;

Dies ruft Destruktor auf und gibt Speicher frei. Es scheint einfach zu sein. 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 verwenden, da dies die Leistung etwas beeinträchtigt ist viel einfacher mit ihnen zu arbeiten.


Und jetzt ist eine Einführung vorbei und geht zurück zur Frage.

Sie können Zeiger anstelle von Objekten verwenden, um beim Übertragen von Daten zwischen Funktionen eine bessere Leistung zu erzielen.

Schauen Sie nach, Sie haben std::string (es ist auch ein Objekt) und enthält sehr viele Daten, zum Beispiel großes XML. Jetzt müssen Sie es analysieren, aber dazu haben Sie die 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 Funktionsstack. Dies dauert einige Zeit, sodass Ihre Leistung niedrig ist.
  2. void foo(std::string* xml);.__In diesem Fall übergeben Sie den Zeiger auf das Objekt, die gleiche Geschwindigkeit wie die size_t-Variable. Diese Deklaration ist jedoch fehleranfällig, da Sie den NULL-Zeiger oder einen ungültigen Zeiger übergeben können. Zeiger, die normalerweise in C verwendet werden, da es keine Referenzen gibt.
  3. void foo(std::string& xml);Wenn Sie einen Verweis übergeben, ist dies im Grunde dasselbe wie ein übergebender Zeiger, aber der Compiler führt einige Dinge aus und Sie können keinen ungültigen Verweis übergeben (tatsächlich ist es möglich, eine Situation mit einem ungültigen Verweis zu erstellen, der Compiler ist jedoch ein Trick).
  4. void foo(const std::string* xml);Here ist das gleiche wie second, nur der Zeigerwert kann nicht geändert werden.
  5. void foo(const std::string& xml);Here ist das gleiche wie Third, aber der Objektwert kann nicht geändert werden.

Was ich noch erwähnen möchte, können Sie diese 5 Möglichkeiten verwenden, um Daten unabhängig von der gewählten Zuweisungsart (mit new oder regular) weiterzugeben.


Noch etwas zu erwähnen, wenn Sie ein Objekt auf regular - Art erstellen, ordnen Sie Speicher im Stapel zu, aber während Sie es mit new erstellen, weisen Sie Heap zu. Es ist viel schneller, Stapel zuzuordnen, aber es ist ein bisschen klein für wirklich große Datenfelder. Wenn Sie große Objekte benötigen, sollten Sie Heap verwenden, da dies zu einem Stapelüberlauf führen kann. Normalerweise wird dieses Problem jedoch mit STL) gelöst container und denken Sie daran, std::string ist auch Container, einige Leute haben es vergessen :)

6
ST3

Es gibt viele Vorteile der Verwendung von Zeigern zum Objekt 

  1. Effizienz (wie Sie bereits betont haben). Das Übergeben von Objekten an Funktionen von .__ bedeutet, dass neue Objektkopien erstellt werden. 
  2. Arbeiten mit Objekten aus Fremdbibliotheken. Wenn Ihr Objekt Zu einem Code eines Drittanbieters gehört und die Autoren die Verwendung ihrer Objekte nur über Zeiger beabsichtigen (keine Kopierkonstruktoren usw.), können Sie dieses -Objekt nur mit Zeigern umgehen. Das Übergeben von Werten kann zu Problemen führen. (Probleme mit tiefen Kopien/flachen Kopien).
  3. wenn das Objekt eine Ressource besitzt und Sie möchten, dass der Besitz nicht mit anderen Objekten verglichen wird.
5
Rohit

Nehmen wir an, Sie haben class A, die class B enthalten. Wenn Sie eine Funktion von class B außerhalb von class A aufrufen möchten, erhalten Sie einfach einen Zeiger auf diese Klasse, und Sie können alles tun, was Sie möchten. Außerdem ändert sich der Kontext von class B in Ihrem class A

Aber seien Sie vorsichtig mit dynamischen Objekten

5
Quest

Dies ist ausführlich besprochen worden, aber in Java ist alles ein Zeiger. Dabei 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 je nach Speicherbedarf mischen. Leistung und Speicherauslastung sind in C++ (duh) deterministischer. 

3
cmollis
Object *myObject = new Object;

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

Object myObject;

Dadurch wird ein Objekt (myObject) vom Typ automatic (auf dem Stapel) erstellt, das automatisch gelöscht wird, wenn das Objekt (myObject) seinen Gültigkeitsbereich verlässt.

3
Palak Jain

Ein Zeiger verweist direkt auf den Speicherplatz eines Objekts. Java hat nichts davon. Java verfügt über Referenzen, die über Hashtabellen auf die Position des Objekts verweisen. Mit diesen Referenzen können Sie in Java keine Zeigerarithmetik durchführen.

Um Ihre Frage zu beantworten, ist es nur Ihre Präferenz. Ich bevorzuge die Java-ähnliche Syntax.

1
RioRicoRick

Ein Grund für die Verwendung von Zeigern ist die Schnittstelle zu C-Funktionen. Ein weiterer Grund ist, Speicher zu sparen. Zum Beispiel: Anstatt ein Objekt zu übergeben, das viele Daten enthält und einen prozessorintensiven Copy-Konstruktor an eine Funktion enthält, übergeben Sie einfach einen Zeiger auf das Objekt Eine Referenz wäre in diesem Fall besser, es sei denn, Sie verwenden ein Array im C-Stil.

0
theo2003

Ich werde einen wichtigen Anwendungsfall des Zeigers hinzufügen. Wenn Sie ein Objekt in der Basisklasse speichern, kann dies 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 über einen Zeiger verfügen.

0
user18853

In Bereichen, in denen die Speichernutzung an erster Stelle steht, sind Zeiger praktisch. Betrachten Sie beispielsweise einen Minimax-Algorithmus, bei dem Tausende von Knoten unter Verwendung einer rekursiven Routine generiert werden, und verwenden Sie sie später, um den nächstbesten Zug im Spiel auszuwerten. Die Fähigkeit, die Zuordnung aufzuheben oder zurückzusetzen (wie bei intelligenten Zeigern), reduziert den Speicherbedarf erheblich. Die Non-Pointer-Variable belegt jedoch weiterhin Speicherplatz, bis der rekursive Aufruf einen Wert zurückgibt.

0
seccpur

Mit Zeigern

  • kann direkt mit dem Speicher sprechen. 

  • kann viele Speicherlecks eines Programms verhindern, indem Zeiger manipuliert werden.

0
lasan