c++ übergeben - Warum sollte ich einen Zeiger anstelle des Objekts selbst verwenden?




11 Answers

Es ist sehr bedauerlich, dass Sie die dynamische Zuweisung so oft sehen. Das zeigt nur, wie viele schlechte C ++ - Programmierer es gibt.

In gewissem Sinne haben Sie zwei Fragen zu einem zusammengefasst. Die erste ist, wann sollten wir die dynamische Zuweisung (mit new ) verwenden? Die zweite ist, wann sollten wir Zeiger verwenden?

Die wichtige Botschaft, die Sie mit nach Hause nehmen, ist, dass Sie immer das geeignete Werkzeug für den Job verwenden sollten . In fast allen Situationen gibt es etwas passenderes und sichereres als die manuelle dynamische Zuweisung und / oder die Verwendung roher Zeiger.

Dynamische Zuordnung

In Ihrer Frage haben Sie zwei Möglichkeiten zum Erstellen eines Objekts demonstriert. Der Hauptunterschied ist die Speicherdauer des Objekts. Beim Ausführen von Object myObject; Innerhalb eines Blocks wird das Objekt mit der automatischen Speicherdauer erstellt. Dies bedeutet, dass es automatisch gelöscht wird, wenn es den Gültigkeitsbereich verlässt. Wenn Sie ein new Object() , hat das Objekt eine dynamische Speicherdauer, was bedeutet, dass es so lange am Leben bleibt, bis Sie es explizit delete . Sie sollten die dynamische Speicherdauer nur dann verwenden, wenn Sie sie benötigen. Das heißt, Sie sollten immer Objekte mit automatischer Speicherdauer erstellen, wenn Sie können .

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

  1. Sie benötigen das Objekt, um den aktuellen Gültigkeitsbereich zu überstehen - das bestimmte Objekt an diesem bestimmten Speicherort, keine Kopie davon. Wenn Sie mit dem Kopieren / Verschieben des Objekts einverstanden sind (die meiste Zeit sollte es sein), sollten Sie ein automatisches Objekt bevorzugen.
  2. Sie müssen viel Speicher reservieren , was den Stack leicht füllen kann. Es wäre schön, wenn wir uns damit nicht beschäftigen müssten (die meiste Zeit sollte es nicht sein), da es wirklich außerhalb von C ++ liegt, aber leider müssen wir uns mit der Realität der Systeme auseinandersetzen entwickeln sich für.

Wenn Sie die dynamische Zuordnung unbedingt benötigen, sollten Sie sie in einen Smart-Zeiger oder einen anderen Typ RAII , der RAII ausführt (wie die Standardcontainer). Smart Pointer bieten Ownership-Semantiken von dynamisch zugewiesenen Objekten. Schauen Sie sich zum Beispiel std::unique_ptr und std::shared_ptr an. Wenn Sie sie entsprechend verwenden, können Sie fast vollständig vermeiden, eine eigene Speicherverwaltung durchzuführen (siehe die Null-Regel ).

Zeiger

Es gibt jedoch andere allgemeine Verwendungen für rohe Zeiger, die über die dynamische Zuweisung hinausgehen, aber die meisten haben Alternativen, die Sie bevorzugen sollten. Wie zuvor, bevorzugen Sie immer die Alternativen, es sei denn, Sie benötigen wirklich Zeiger .

  1. Sie benötigen Referenzsemantik . Manchmal möchten Sie ein Objekt mithilfe eines Zeigers übergeben (unabhängig davon, wie es zugewiesen wurde), da die Funktion, an die Sie es übergeben, Zugriff auf das betreffende Objekt haben soll (keine Kopie davon). In den meisten Situationen sollten Sie Referenztypen jedoch den Zeigern vorziehen, da dies speziell für sie gedacht ist. Beachten Sie, dass es nicht unbedingt darum geht, die Lebensdauer des Objekts über den aktuellen Bereich hinaus zu verlängern, wie in obiger Situation 1. Wenn Sie eine Kopie des Objekts übergeben möchten, benötigen Sie wie zuvor keine Referenzsemantik.

  2. Sie brauchen Polymorphie . Sie können Funktionen nur polymorph (dh gemäß dem dynamischen Typ eines Objekts) über einen Zeiger oder eine Referenz auf das Objekt aufrufen. Wenn das 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 nullptr , dass ein nullptr übergeben wird, wenn das Objekt ausgelassen wird. Wenn es sich um ein Argument handelt, sollten Sie lieber Standardargumente oder Funktionsüberladungen verwenden. Andernfalls sollten Sie lieber einen Typ verwenden, der dieses Verhalten kapselt, z. B. std::optional (eingeführt in C ++ 17 - mit früheren C ++ - Standards, verwenden Sie boost::optional ).

  4. Sie möchten die Übersetzungseinheiten entkoppeln, um die Übersetzungszeit zu verbessern . Die nützliche Eigenschaft eines Zeigers ist, dass Sie nur eine Vorwärtsdeklaration des angegebenen Typs benötigen (um das Objekt tatsächlich zu verwenden, benötigen Sie eine Definition). Dadurch können Sie Teile Ihres Kompilierungsprozesses entkoppeln, was die Kompilierungszeit erheblich verbessern kann. Siehe das Pimpl-Idiom .

  5. Sie müssen eine Schnittstelle mit einer C-Bibliothek oder einer C-style-Bibliothek herstellen. An diesem Punkt müssen Sie rohe Zeiger verwenden. Das Beste, was Sie tun können, ist sicherzustellen, dass Sie Ihre rohen Zeiger erst im letzten möglichen Moment loslassen. Sie können einen rohen Zeiger von einem intelligenten Zeiger abrufen, z. B. indem Sie die Funktion get member verwenden. Wenn eine Bibliothek eine Zuweisung für Sie vornimmt, von der erwartet wird, dass Sie die Zuordnung über ein Handle aufheben, können Sie das Handle häufig in einem intelligenten Zeiger mit einem benutzerdefinierten Löschelement umbrechen, das das Objekt entsprechend freigibt.

pointer auf

Ich komme aus einem Java-Hintergrund und habe angefangen, mit Objekten in C ++ zu arbeiten. Aber eine Sache, die mir auffiel, ist, dass Leute oft Zeiger auf Objekte anstatt auf die Objekte selbst verwenden, zum Beispiel diese Deklaration:

Object *myObject = new Object;

eher, als:

Object myObject;

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

myObject.testFunc();

wir müssen schreiben:

myObject->testFunc();

Aber ich kann nicht herausfinden, warum wir es so machen sollten. Ich würde annehmen, dass es mit Effizienz und Geschwindigkeit zu tun hat, da wir direkten Zugriff auf die Speicheradresse haben. Habe ich recht?




Es gibt viele ausgezeichnete Antworten auf diese Frage, einschließlich der wichtigen Anwendungsfälle von Vorwärtsdeklarationen, Polymorphismus usw., aber ich fühle, 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, die die zwei Sprachen vergleicht:

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ächstliegende Ä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...

Der beste Weg, darüber nachzudenken, ist, dass - mehr oder weniger - Java (implizit) Zeiger auf Objekte handhabt, während C ++ entweder Zeiger auf Objekte oder die Objekte selbst behandeln kann. Es gibt 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, das Verwenden von Zeigern ist NICHT notwendigerweise entweder 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, wo sie leben werden, haben.

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




Vorwort

Java ist nichts gegen C ++, im Gegensatz zum Hype. Die Java-Hype-Maschine möchte, dass Sie glauben, dass, weil Java C ++ ähnliche Syntax hat, die Sprachen ähnlich sind. Nichts kann weiter von der Wahrheit entfernt sein. Diese Fehlinformation ist einer der Gründe, warum Java-Programmierer nach C ++ gehen und Java-ähnliche Syntax verwenden, ohne die Implikationen ihres Codes zu verstehen.

Weiter gehen wir

Aber ich kann nicht herausfinden, warum wir es so machen sollten. Ich würde annehmen, dass es mit Effizienz und Geschwindigkeit zu tun hat, da wir direkten Zugriff auf die Speicheradresse haben. Habe ich recht?

Im Gegenteil, eigentlich. Der Heap ist viel langsamer als der Stack, da der Stack im Vergleich zum Heap sehr einfach ist. Bei automatischen Speichervariablen (auch Stack-Variablen genannt) werden ihre Destruktoren aufgerufen, sobald sie den Gültigkeitsbereich verlassen haben. Beispielsweise:

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

Wenn Sie jedoch einen dynamisch zugewiesenen Zeiger verwenden, muss sein Destruktor manuell aufgerufen werden. delete ruft diesen Destruktor für dich auf.

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

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

Vorteile der dynamischen Zuordnung

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

Eines der ersten Probleme, mit denen viele C ++ - Programmierer konfrontiert sind, besteht darin, dass sie, wenn sie willkürliche Eingaben von Benutzern akzeptieren, nur eine feste Größe für eine Stapelvariable zuweisen können. Sie können die Größe von Arrays auch nicht ändern. Beispielsweise:

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

Natürlich, wenn Sie stattdessen eine std::string verwenden, ändert sich std::string intern selbst, so dass dies kein Problem sein sollte. Aber im Grunde ist die Lösung dieses Problems die dynamische Zuweisung. Sie können dynamischen Speicher basierend auf der Eingabe 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 Erweiterungen von GCC widerspiegelt. 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 / zugewiesen werden, während der Stack eine Beschränkung hat.

2. Arrays sind keine Zeiger

Wie ist das ein Vorteil, den Sie fragen? Die Antwort wird klar, wenn Sie die Verwirrung / Mythos hinter Arrays und Zeigern verstehen. Es wird allgemein angenommen, dass sie gleich sind, aber nicht. Dieser Mythos rührt von der Tatsache her, dass Zeiger wie Arrays subskribiert werden können und weil Arrays in einer Funktionsdeklaration auf der obersten Ebene zu Zeigern zerfallen. Sobald ein Array jedoch zu einem Zeiger abfällt, verliert der Zeiger seine sizeof Information. So gibt sizeof(pointer) die Größe des Zeigers in Bytes an, was normalerweise 8 Bytes auf einem 64-Bit-System ist.

Sie können Arrays nicht zuweisen, sondern nur initialisieren. Beispielsweise:

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, weil die Unterscheidung zwischen Zeigern und Arrays in Java und C # Hand-waved ist, verstehen Anfänger den Unterschied nicht.

3. Polymorphismus

Java und C # verfügen über Funktionen, mit denen Sie Objekte als weitere Objekte behandeln können, z. B. mit dem Schlüsselwort as . Wenn jemand also ein Entity Objekt als ein Player Objekt behandeln möchte, könnte man Player player = Entity as Player; Dies ist sehr nützlich, wenn Sie Funktionen in einem homogenen Container aufrufen möchten, der nur für einen bestimmten Typ gelten soll. 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();
}

Also, wenn nur Dreiecke eine Rotate-Funktion hätten, wäre es ein Compilerfehler, wenn Sie versuchen würden, ihn für alle Objekte der Klasse aufzurufen. Mit dynamic_cast können Sie das Schlüsselwort as simulieren. Um klar zu sein, wenn eine Umwandlung fehlschlägt, wird ein ungültiger Zeiger zurückgegeben. So !test ist im Wesentlichen eine Abkürzung für die Überprüfung, ob der test NULL ist oder ein ungültiger Zeiger, was bedeutet, dass die Umwandlung fehlgeschlagen ist.

Vorteile automatischer Variablen

Nachdem Sie all die großartigen Dinge gesehen haben, die die dynamische Allokation bewirken kann, werden Sie sich wahrscheinlich fragen, warum nicht jemand die dynamische Zuweisung die ganze Zeit benutzen würde? Ich habe dir schon einen Grund gesagt, der Haufen ist langsam. Und wenn Sie nicht diese ganze Erinnerung brauchen, sollten Sie sie nicht missbrauchen. Also hier sind einige Nachteile in keiner bestimmten Reihenfolge:

  • Es ist fehleranfällig. Manuelle Speicherzuweisung ist gefährlich und Sie sind anfällig für Lecks. Wenn Sie den Debugger oder valgrind (ein Speicherleck-Tool) nicht beherrschen, können Sie sich die Haare aus dem Kopf reißen. Glücklicherweise lindern RAII-Idiome und intelligente Zeiger das ein wenig, aber Sie müssen mit Praktiken wie der Regel der Drei und der Regel der Fünf vertraut sein. Es ist eine Menge Informationen, und Anfänger, die es entweder nicht wissen oder nicht, werden in diese Falle geraten.

  • Es ist nicht erforderlich. Im Gegensatz zu Java und C #, wo es idiomatisch ist, das new Schlüsselwort überall zu verwenden, sollten Sie es in C ++ nur bei Bedarf verwenden. Der übliche Satz lautet, alles sieht wie ein Nagel aus, wenn du einen Hammer hast. Während Anfänger, die mit C ++ beginnen, Angst vor Zeigern haben und lernen, Stack-Variablen aus Gewohnheit zu verwenden, beginnen Java und C # -Programmierer mit Zeigern, ohne sie zu verstehen! Das geht buchstäblich auf den falschen Fuß. Sie müssen alles aufgeben, was Sie wissen, denn Syntax ist eine Sache, das Erlernen der Sprache ist eine andere Sache.

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

Eine Optimierung, die viele Compiler machen, sind Dinge namens Elision und Return-Value-Optimierung . Diese Dinge können unnötige Kopien vermeiden, was nützlich für sehr große Objekte ist, wie zum Beispiel einen Vektor, der viele Elemente enthält. Normalerweise ist es üblich, Zeiger zu verwenden, um das Eigentum zu übertragen, anstatt die großen Objekte zu kopieren, um sie zu verschieben . Dies hat zur Einführung von Bewegungssemantik und Smartpointern 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 weiterzugeben, wenn Sie sich Sorgen um die Optimierung machen. Fehlerlecks können auftreten, wenn der Aufrufer einer Funktion für das delete 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 Stack-Variablen, weil es einfacher und besser ist.




In C ++ werden Objekte, die auf dem Stapel zugeordnet sind (mit Object object; Anweisung innerhalb eines Blocks) nur innerhalb des Gültigkeitsbereichs ausgeführt, in dem sie deklariert sind. Wenn der Codeblock die Ausführung beendet, wird das deklarierte Objekt zerstört. Wenn Sie jedoch mit Hilfe von Object* obj = new Object() Speicher auf dem Heap Object* obj = new Object() , leben sie weiterhin im Heap, bis Sie delete obj .

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




Technisch ist es eine Speicherzuteilung, aber hier sind zwei weitere praktische Aspekte. Es hat mit zwei Dingen zu tun: 1) Scope, wenn Sie ein Objekt ohne einen Zeiger definieren, können Sie nicht mehr darauf zugreifen, nachdem der Codeblock definiert wurde, während Sie einen Zeiger mit "neu" definieren can access it from anywhere you have a pointer to this memory until you call "delete" on the same pointer. 2) If you want to pass arguments to a function you want to pass a pointer or a reference in order to be more efficient. When you pass an Object then the object is copied, if this is an object that uses a lot of memory this might be CPU consuming (eg you copy a vector full of data). When you pass a pointer all you pass is one int (depending of implementation but most of them are one int).

Other than that you need to understand that "new" allocates memory on the heap that needs to be freed at some point. When you don't have to use "new" I suggest you use a regular object definition "on the stack".




There are many benefits of using pointers to object -

  1. Efficiency (as you already pointed out). Passing objects to functions mean creating new copies of object.
  2. Working with objects from third party libraries. If your object belongs to a third party code and the authors intend the usage of their objects through pointers only (no copy constructors etc) the only way you can pass around this object is using pointers. Passing by value may cause issues. (Deep copy / shallow copy issues).
  3. if the object owns a resource and you want that the ownership should not be sahred with other objects.



This is has been discussed at length, but in Java everything is a pointer. It makes no distinction between stack and heap allocations (all objects are allocated on the heap), so you don't realize you're using pointers. In C++, you can mix the two, depending on your memory requirements. Performance and memory usage is more deterministic in C++ (duh).




You shouldn't . People (many people, sadly) write it out of ignorance.

Sometimes dynamic allocation has its place but, in the examples you give, it is wrong .

If you want to think about efficiency, then this is worse , because it introduces indirection for no good reason. This sort of programming is slower and more error-prone .




"Necessity is the mother of invention." The most of important difference that I would like to point out is the outcome of my own experience of coding. Sometimes you need to pass objects to functions . In that case if your object is of a very big class then passing it as an object will copy its state (which you might not want ..AND CAN BE BIG OVERHEAD) thus resulting in overhead of copying object .while pointer is fixed 4 byte size (assuming 32 bit).Other reasons are already mentioned above...




One reason for using pointers is to interface with C functions. Another reason is to save memory; for example: instead of passing an object which contains a lot of data and has a processor-intensive copy-constructor to a function, just pass a pointer to the object, saving memory and speed especially if you're in a loop, however a reference would be better in that case, unless you're using an C-style array.




I will include one important use case of pointer. When you are storing some object in the base class, but it could be polymorphic.

Class Base1 {
};

Class Derived1 : public Base1 {
};


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

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

So in this case you can't declare bObj as an direct object, you have to have pointer.




Related