it-swarm.com.de

Klassenmitglieder, die Objekte sind - Zeiger oder nicht? C++

Wenn ich eine Klasse MyClass erstelle und ein privates Mitglied MyOtherClass sagt, ist es besser, MyOtherClass als Zeiger zu definieren oder nicht? Was bedeutet es auch, wenn es kein Hinweis darauf ist, wo es gespeichert ist? Wird das Objekt erstellt, wenn die Klasse erstellt wird?

Ich habe festgestellt, dass die Beispiele in QT normalerweise Klassenmitglieder als Zeiger deklarieren, wenn es sich um Klassen handelt.

Grüße

Kennzeichen

35
Mark

Wenn ich eine Klasse MyClass erstelle und ein privates Mitglied MyOtherClass sagt, ist es besser, MyOtherClass als Zeiger zu definieren oder nicht?

sie sollten es im Allgemeinen als Wert in Ihrer Klasse deklarieren. es wird lokal sein, es wird weniger Chancen für Fehler geben, weniger Zuweisungen - letztendlich weniger Dinge, die schief gehen könnten, und der Compiler kann immer wissen, dass es sich um einen bestimmten Offset handelt, also ... es hilft bei der Optimierung und der binären Reduzierung bei einer wenige Ebenen. Es wird einige Fälle geben, in denen Sie wissen, dass Sie sich mit Zeigern befassen müssen (d. h. polymorph, gemeinsam genutzt, Neuzuweisung erforderlich). Es ist in der Regel am besten, einen Zeiger nur dann zu verwenden, wenn dies erforderlich ist - insbesondere, wenn er privat/gekapselt ist.

Was bedeutet es auch, wenn es kein Hinweis darauf ist, wo es gespeichert ist?

die Adresse wird in der Nähe von (oder gleich) sein. this - gcc (zum Beispiel) verfügt über einige erweiterte Optionen zum Speichern von Klassendaten (Größen, vtables, Offsets).

Wird das Objekt erstellt, wenn die Klasse erstellt wird?

yes - Die Größe von MyClass wächst um sizeof (MyOtherClass) oder um mehr, wenn der Compiler sie neu ausrichtet (z. B. zu ihrer natürlichen Ausrichtung).

29
justin

Wo ist Ihr Mitglied gespeichert?

Schauen Sie sich dieses Beispiel an:

struct Foo { int m; };
struct A {
  Foo foo;
};
struct B {
  Foo *foo;
  B() : foo(new Foo()) { } // ctor: allocate Foo on heap
  ~B() { delete foo; } // dtor: Don't forget this!
};

void bar() {
  A a_stack; // a_stack is on stack
             // a_stack.foo is on stack too
  A* a_heap = new A(); // a_heap is on stack (it's a pointer)
                       // *a_heap (the pointee) is on heap
                       // a_heap->foo is on heap
  B b_stack; // b_stack is on stack
             // b_stack.foo is on stack
             // *b_stack.foo is on heap
  B* b_heap = new B(); // b_heap is on stack
                       // *b_heap is on heap
                       // b_heap->foo is on heap
                       // *(b_heap->foo is on heap
  delete a_heap;
  delete b_heap;
  // B::~B() will delete b_heap->foo!
} 

Wir definieren zwei Klassen A und B. A speichert ein öffentliches Mitglied foo vom Typ Foo. B hat ein Mitglied foo vom Typ pointer to Foo.

Wie ist die Situation für A:

  • Wenn Sie eine Variable a_stack vom Typ A auf dem stack erstellen, befinden sich das Objekt und seine Mitglieder (offensichtlich) auch auf dem stack .
  • Wenn Sie einen Zeiger auf eine A wie im obigen Beispiel a_heap erstellen, befindet sich nur die Zeigervariable auf dem stack ; Alles andere (das Objekt und seine Mitglieder) befindet sich auf dem Heap .

Wie sieht die Situation bei B aus:

  • sie erstellen B auf dem Stapel : Dann befinden sich sowohl das Objekt als auch sein Mitglied foo auf dem Stapel , aber das Objekt, auf das foo zeigt (das Spitzenelement), befindet sich auf dem Haufen . Kurz gesagt: b_stack.foo (der Zeiger) befindet sich auf dem Stapel, aber *b_stack.foo (der Spitzenreiter) befindet sich auf dem Haufen.
  • sie erstellen einen Zeiger auf eine B mit dem Namen b_heap: b_heap (der Zeiger) befindet sich auf dem Stapel, *b_heap (der Pointee) befindet sich auf dem Heap sowie auf dem Member b_heap->foo und *b_heap->foo.

Wird das Objekt automatisch erstellt?

  • Im Fall von A: Ja wird foo automatisch erstellt, indem der implizite Standardkonstruktor von Foo aufgerufen wird. Dadurch wird eine integer erstellt, aber nicht initialisiert (es wird eine Zufallszahl haben)!
  • Im Fall von B: Wenn Sie ctor und dtor weglassen, wird foo (der Zeiger) ebenfalls erstellt und mit einer Zufallszahl initialisiert, was bedeutet, dass er auf eine zufällige Position auf dem Heap verweist. Beachten Sie aber, dass der Zeiger existiert! Beachten Sie auch, dass der implizite Standardkonstruktor foo nichts zuweist. Sie müssen dies tun explizit . Aus diesem Grund benötigen Sie normalerweise einen expliziten Konstruktor und einen zugehörigen Destruktor , um die Spitze Ihres Mitgliedszeigers zuzuweisen und zu löschen. Vergessen Sie nicht Kopiersemantik : Was passiert mit der Spitze, wenn Sie das Objekt kopieren (durch Kopierkonstruktion oder Zuweisung)?

Was hat das alles zu bedeuten?

Es gibt verschiedene Anwendungsfälle für die Verwendung eines Zeigers auf ein Mitglied:

  • Zeigen auf ein Objekt, das Sie nicht besitzen. Angenommen, Ihre Klasse benötigt Zugriff auf eine riesige Datenstruktur, deren Kopieren sehr kostspielig ist. Dann könnten Sie einfach einen Zeiger auf diese Datenstruktur speichern. Beachten Sie, dass in diesem Fall creation und deletion der Datenstruktur außerhalb des Bereichs Ihrer Klasse liegt. Jemand anderes muss aufpassen.
  • Erhöht die Kompilierungszeit, da in Ihrer Header-Datei der Pointee nicht definiert werden muss.
  • Etwas fortgeschrittener; Wenn Ihre Klasse einen Zeiger auf eine andere Klasse hat, in der alle privaten Mitglieder gespeichert sind, das "Pimpl - Idiom": http://c2.com/cgi/wiki?PimplIdiom , werfen Sie auch einen Blick auf Sutter, H. (2000) ): Außergewöhnliches C++, p. 99-119
  • Und einige andere, schauen Sie sich die anderen Antworten an

Rat

Seien Sie besonders vorsichtig, wenn Ihre Mitglieder Zeiger sind und Sie sie besitzen. Sie müssen geeignete Konstruktoren und Destruktoren schreiben und über Kopierkonstruktoren und Zuweisungsoperatoren nachdenken. Was passiert mit der Spitze, wenn Sie das Objekt kopieren? Normalerweise müssen Sie auch die Spitze kopieren!

21
WolfgangP

In C++ sind Zeiger Objekte für sich. Sie sind nicht wirklich an das gebunden, worauf sie zeigen, und es gibt keine besondere Wechselwirkung zwischen einem Zeiger und seiner Spitze (ist das ein Wort?)

Wenn Sie einen Zeiger erstellen, erstellen Sie einen Zeiger und sonst nichts . Sie erstellen kein Objekt, auf das es möglicherweise verweist oder nicht. Wenn ein Zeiger den Gültigkeitsbereich verlässt, ist das Objekt, auf das verwiesen wird, nicht betroffen. Ein Zeiger beeinflusst in keiner Weise die Lebensdauer dessen, worauf er zeigt.

Im Allgemeinen sollten Sie also nicht standardmäßig Zeiger verwenden. Wenn Ihre Klasse ein anderes Objekt enthält, sollte dieses andere Objekt kein Zeiger sein.

Wenn Ihre Klasse jedoch etwas über ein anderes Objekt weiß, ist ein Zeiger möglicherweise eine gute Möglichkeit, es darzustellen (da mehrere Instanzen Ihrer Klasse dann auf dieselbe Instanz verweisen können, ohne den Besitz der Klasse zu übernehmen und ohne deren Kontrolle Lebenszeit)

17
jalf

Die gängige Weisheit in C++ ist, die Verwendung von (nackten) Zeigern so weit wie möglich zu vermeiden. Insbesondere nackte Zeiger, die auf dynamisch zugewiesenen Speicher verweisen.

Der Grund dafür ist, dass Zeiger das Schreiben robuster Klassen erschweren, insbesondere wenn Sie auch die Möglichkeit in Betracht ziehen müssen, Ausnahmen auszulösen.

Einige Vorteile des Zeigers:

  • Das untergeordnete Objekt (MyOtherClass) kann eine andere Lebensdauer haben als das übergeordnete Objekt (MyClass).
  • Das Objekt kann möglicherweise von mehreren MyClass-Objekten (oder anderen Objekten) gemeinsam genutzt werden.
  • Beim Kompilieren der Header-Datei für MyClass muss der Compiler nicht unbedingt die Definition von MyOtherClass kennen. Sie müssen den Header nicht einschließen, wodurch die Kompilierzeiten verkürzt werden.
  • Verkleinert MyClass. Dies kann für die Leistung wichtig sein, wenn Ihr Code häufig MyClass-Objekte kopiert. Sie können einfach den MyOtherClass-Zeiger kopieren und eine Art Referenzzählsystem implementieren.

Vorteile, das Mitglied als Objekt zu haben:

  • Sie müssen keinen expliziten Code schreiben, um das Objekt zu erstellen und zu zerstören. Es ist einfacher und weniger fehleranfällig.
  • Effizientere Speicherverwaltung, da nur ein Speicherblock anstelle von zwei reserviert werden muss.
  • Das Implementieren von Zuweisungsoperatoren, Konstruktoren zum Kopieren/Verschieben usw. ist viel einfacher.
  • Intuitiver
3
Timo

Diese Frage könnte endlos diskutiert werden, aber die Grundlagen sind:

Wenn MyOtherClass kein Zeiger ist:

  • Die Erstellung und Zerstörung von MyOtherClass erfolgt automatisch, wodurch Fehler reduziert werden können.
  • Der von MyOtherClass verwendete Speicher ist lokal für MyClassInstance, wodurch die Leistung verbessert werden könnte.

Wenn MyOtherClass ein Zeiger ist:

  • Die Erstellung und Zerstörung von MyOtherClass liegt in Ihrer Verantwortung
  • MyOtherClass kann eine NULL sein, die in Ihrem Kontext eine Bedeutung haben und Speicherplatz sparen kann
  • Zwei Instanzen von MyClass können dieselbe MyOtherClass verwenden
3
Drew Dormann

Ich befolge die folgende Regel: Wenn das Mitgliedsobjekt mit dem einkapselnden Objekt lebt und stirbt, verwende keine Zeiger. Sie benötigen einen Zeiger, wenn das Mitgliedsobjekt das Kapselungsobjekt aus irgendeinem Grund überleben muss. Kommt auf die jeweilige Aufgabe an.

Normalerweise verwenden Sie einen Zeiger, wenn das Mitgliedsobjekt Ihnen übergeben und nicht von Ihnen erstellt wurde. Dann müssen Sie es normalerweise auch nicht zerstören.

3
chvor

Wenn Sie das MyOtherClass-Objekt zu einem Mitglied Ihrer MyClass machen:

size of MyClass = size of MyClass + size of MyOtherClass

Wenn Sie das MyOtherClass-Objekt als Zeigermitglied Ihrer MyClass festlegen:

size of MyClass = size of MyClass + size of any pointer on your system

Möglicherweise möchten Sie MyOtherClass als Zeigermember behalten, da Sie dadurch die Flexibilität haben, auf eine andere Klasse zu verweisen, die von dieser abgeleitet ist. Grundsätzlich hilft Ihnen Dynamice Polymorphismus zu implementieren.

1
Alok Save

Es hängt davon ab, ob... :-)

Wenn Sie Zeiger verwenden, um einen class A zu sagen, müssen Sie das Objekt vom Typ A erstellen, z. im Konstruktor Ihrer Klasse

 m_pA = new A();

Vergessen Sie außerdem nicht, das Objekt im Destruktor zu zerstören, oder es liegt ein Speicherverlust vor:

delete m_pA; 
m_pA = NULL;

Stattdessen ist es einfacher, ein Objekt vom Typ A in Ihrer Klasse zusammenzufassen, und Sie können nicht vergessen, es zu zerstören, da dies am Ende der Lebensdauer Ihres Objekts automatisch erfolgt.

Andererseits hat ein Zeiger folgende Vorteile:

  • Wenn Ihr Objekt auf dem Stapel zugeordnet ist und Typ A viel Speicher belegt, wird dies nicht vom Stapel, sondern vom Heap zugeordnet.

  • Sie können Ihr A-Objekt später erstellen (z. B. in einer Methode Create) oder es früher zerstören (in der Methode Close).

1
ur.

Ein Vorteil der übergeordneten Klasse, die die Beziehung zu einem Mitgliedsobjekt als Zeiger (std :: auto_ptr) auf das Mitgliedsobjekt beibehält, besteht darin, dass Sie das Objekt weiter deklarieren können, anstatt die Headerdatei des Objekts einschließen zu müssen.

Dadurch werden die Klassen zur Erstellungszeit entkoppelt, sodass die Header-Klasse des Mitgliedsobjekts geändert werden kann, ohne dass alle Clients der übergeordneten Klasse erneut kompiliert werden, obwohl sie wahrscheinlich nicht auf die Funktionen des Mitgliedsobjekts zugreifen.

Wenn Sie einen auto_ptr verwenden, müssen Sie sich nur um die Konstruktion kümmern, die Sie normalerweise in der Initialisierungsliste durchführen können. Die Zerstörung zusammen mit dem übergeordneten Objekt wird durch auto_ptr garantiert.

1
andreas buykx

Das Einfache ist, Ihre Mitglieder als Objekte zu deklarieren. Auf diese Weise müssen Sie sich nicht um die Erstellung, Zerstörung und Zuordnung von Kopien kümmern. Dies wird alles automatisch erledigt.

Es gibt jedoch immer noch einige Fälle, in denen Sie Zeiger benötigen. Schließlich enthalten verwaltete Sprachen (wie C # oder Java) Elementobjekte tatsächlich anhand von Zeigern.

Der naheliegendste Fall ist, wenn das aufzubewahrende Objekt polymorph ist. In Qt gehören die meisten Objekte, wie Sie bereits betont haben, zu einer großen Hierarchie von polymorphen Klassen, und das Festhalten an Zeigern ist obligatorisch, da Sie nicht im Voraus wissen, welche Größe das Mitgliedsobjekt haben wird.

Bitte beachten Sie in diesem Fall einige häufige Fallstricke, insbesondere wenn Sie mit generischen Klassen arbeiten. Ausnahmesicherheit ist ein großes Anliegen:

struct Foo
{
    Foo() 
    {
        bar_ = new Bar();
        baz_ = new Baz(); // If this line throw, bar_ is never reclaimed
                          // See copy constructor for a workaround
    }

    Foo(Foo const& x)
    {
        bar_ = x.bar_.clone();
        try { baz_ = x.baz_.clone(); }
        catch (...) { delete bar_; throw; }
    }

    // Copy and swap idiom is perfect for this.
    // It yields exception safe operator= if the copy constructor
    // is exception safe.
    void swap(Foo& x) throw()
    { std::swap(bar_, x.bar_); std::swap(baz_, x.baz_); }

    Foo& operator=(Foo x) { x.swap(*this); return *this; }

private:
    Bar* bar_;
    Baz* baz_;
};

Wie Sie sehen, ist es ziemlich umständlich, ausnahmesichere Konstruktoren in Gegenwart von Zeigern zu haben. Sie sollten sich RAII und intelligente Zeiger ansehen (es gibt viele Ressourcen hier und anderswo im Web).

0
Alexandre C.