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
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).
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
:
a_stack
vom Typ A
auf dem stack erstellen, befinden sich das Objekt und seine Mitglieder (offensichtlich) auch auf dem stack .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:
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.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
.foo
automatisch erstellt, indem der implizite Standardkonstruktor von Foo
aufgerufen wird. Dadurch wird eine integer
erstellt, aber nicht initialisiert (es wird eine Zufallszahl haben)!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)?Es gibt verschiedene Anwendungsfälle für die Verwendung eines Zeigers auf ein Mitglied:
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!
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)
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:
Vorteile, das Mitglied als Objekt zu haben:
Diese Frage könnte endlos diskutiert werden, aber die Grundlagen sind:
Wenn MyOtherClass kein Zeiger ist:
Wenn MyOtherClass ein Zeiger ist:
NULL
sein, die in Ihrem Kontext eine Bedeutung haben und Speicherplatz sparen kannIch 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.
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.
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
).
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.
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).