Ich weiß, dass der C ++ - Compiler einen Kopierkonstruktor für eine Klasse erstellt. In welchem Fall müssen wir einen benutzerdefinierten Kopierkonstruktor schreiben? Können Sie einige Beispiele nennen?
Ich weiß, dass der C ++ - Compiler einen Kopierkonstruktor für eine Klasse erstellt. In welchem Fall müssen wir einen benutzerdefinierten Kopierkonstruktor schreiben? Können Sie einige Beispiele nennen?
Antworten:
Der vom Compiler generierte Kopierkonstruktor kopiert die Mitglieder. Manchmal reicht das nicht aus. Beispielsweise:
class Class {
public:
Class( const char* str );
~Class();
private:
char* stored;
};
Class::Class( const char* str )
{
stored = new char[srtlen( str ) + 1 ];
strcpy( stored, str );
}
Class::~Class()
{
delete[] stored;
}
In diesem Fall stored
wird der Puffer durch das kopieren des Mitglieds durch Mitglieder nicht dupliziert (nur der Zeiger wird kopiert), sodass die erste zu zerstörende Kopie, die den Puffer gemeinsam nutzt, delete[]
erfolgreich aufgerufen wird und die zweite auf undefiniertes Verhalten stößt. Sie benötigen einen Kopierkonstruktor für tiefes Kopieren (und auch einen Zuweisungsoperator).
Class::Class( const Class& another )
{
stored = new char[strlen(another.stored) + 1];
strcpy( stored, another.stored );
}
void Class::operator = ( const Class& another )
{
char* temp = new char[strlen(another.stored) + 1];
strcpy( temp, another.stored);
delete[] stored;
stored = temp;
}
delete stored[];
und ich glaube, es sollte seindelete [] stored;
std::string
. Die allgemeine Idee ist, dass nur Dienstprogrammklassen, die Ressourcen verwalten, die Big Three überladen müssen und dass alle anderen Klassen nur diese Dienstprogrammklassen verwenden sollten, sodass keine der Big Three definiert werden muss.
Ich bin ein bisschen verärgert, dass die Regel der Rule of Five
nicht zitiert wurde.
Diese Regel ist sehr einfach:
Die Fünferregel :
Wenn Sie einen der Destruktoren, Kopierkonstruktoren, Kopierzuweisungsoperatoren, Verschiebungskonstruktoren oder Verschiebungszuweisungsoperatoren schreiben, müssen Sie wahrscheinlich die anderen vier schreiben.
Es gibt jedoch eine allgemeinere Richtlinie, die Sie befolgen sollten, die sich aus der Notwendigkeit ergibt, ausnahmesicheren Code zu schreiben:
Jede Ressource sollte von einem dedizierten Objekt verwaltet werden
Hier ist @sharptooth
der Code immer noch (meistens) in Ordnung, aber wenn er seiner Klasse ein zweites Attribut hinzufügen würde, wäre dies nicht der Fall. Betrachten Sie die folgende Klasse:
class Erroneous
{
public:
Erroneous();
// ... others
private:
Foo* mFoo;
Bar* mBar;
};
Erroneous::Erroneous(): mFoo(new Foo()), mBar(new Bar()) {}
Was passiert bei new Bar
Würfen? Wie löscht man das Objekt, auf das gezeigt wird mFoo
? Es gibt Lösungen (Funktionsebene try / catch ...), sie skalieren einfach nicht.
Der richtige Weg, um mit der Situation umzugehen, besteht darin, anstelle von Rohzeigern geeignete Klassen zu verwenden.
class Righteous
{
public:
private:
std::unique_ptr<Foo> mFoo;
std::unique_ptr<Bar> mBar;
};
Mit der gleichen Konstruktorimplementierung (oder tatsächlich mit make_unique
) habe ich jetzt die Ausnahmesicherheit kostenlos !!! Ist es nicht aufregend? Und das Beste ist, ich muss mir keine Sorgen mehr um einen richtigen Zerstörer machen! Ich muss meine eigenen schreiben Copy Constructor
und Assignment Operator
zwar, weil unique_ptr
diese Operationen nicht definiert sind ... aber das spielt hier keine Rolle;)
Und deshalb hat sharptooth
die Klasse noch einmal besucht:
class Class
{
public:
Class(char const* str): mData(str) {}
private:
std::string mData;
};
Ich weiß nichts über dich, aber ich finde meine einfacher;)
Ich kann mich an meine Praxis erinnern und an die folgenden Fälle denken, in denen es darum geht, den Kopierkonstruktor explizit zu deklarieren / zu definieren. Ich habe die Fälle in zwei Kategorien eingeteilt
Ich füge in diesen Abschnitt die Fälle ein, in denen das Deklarieren / Definieren des Kopierkonstruktors für den korrekten Betrieb der Programme unter Verwendung dieses Typs erforderlich ist.
Nachdem Sie diesen Abschnitt gelesen haben, lernen Sie einige Fallstricke kennen, die es dem Compiler ermöglichen, den Kopierkonstruktor selbst zu generieren. Daher ist , wie seand in seiner bemerkte Antwort , ist es immer sicher Kopierbarkeit für eine neue Klasse auszuschalten und absichtlich es später ermöglichen , wenn es wirklich erforderlich ist .
Deklarieren Sie einen privaten Kopierkonstruktor und stellen Sie keine Implementierung dafür bereit (sodass der Build in der Verknüpfungsphase fehlschlägt, selbst wenn die Objekte dieses Typs im eigenen Bereich der Klasse oder von ihren Freunden kopiert werden).
Deklarieren Sie den Kopierkonstruktor mit =delete
am Ende.
Dies ist der am besten verstandene Fall und tatsächlich der einzige, der in den anderen Antworten erwähnt wird. Shaprtooth hat es ziemlich gut abgedeckt . Ich möchte nur hinzufügen, dass das gründliche Kopieren von Ressourcen, die ausschließlich dem Objekt gehören sollten, auf alle Arten von Ressourcen angewendet werden kann, von denen dynamisch zugewiesener Speicher nur eine Art ist. Bei Bedarf kann auch ein gründliches Kopieren eines Objekts erforderlich sein
Stellen Sie sich eine Klasse vor, in der alle Objekte - egal wie sie konstruiert wurden - irgendwie registriert sein MÜSSEN. Einige Beispiele:
Das einfachste Beispiel: Beibehalten der Gesamtzahl der aktuell vorhandenen Objekte. Bei der Objektregistrierung wird lediglich der statische Zähler erhöht.
Ein komplexeres Beispiel ist eine Singleton-Registrierung, in der Verweise auf alle vorhandenen Objekte dieses Typs gespeichert werden (damit Benachrichtigungen an alle gesendet werden können).
Referenzgezählte Smart-Zeiger können in dieser Kategorie nur als Sonderfall betrachtet werden: Der neue Zeiger "registriert" sich selbst bei der gemeinsam genutzten Ressource und nicht in einer globalen Registrierung.
Eine solche Selbstregistrierungsoperation muss von JEDEM Konstruktor des Typs ausgeführt werden, und der Kopierkonstruktor ist keine Ausnahme.
Einige Objekte haben möglicherweise eine nicht triviale interne Struktur mit direkten Querverweisen zwischen ihren verschiedenen Unterobjekten (tatsächlich reicht nur ein solcher interner Querverweis aus, um diesen Fall auszulösen). Der Compiler bereitgestellten Copykonstruktor die internen brechen intra-Objekt Verbände, um sie zu konvertieren zwischen Objekten Verbände.
Ein Beispiel:
struct MarriedMan;
struct MarriedWoman;
struct MarriedMan {
// ...
MarriedWoman* wife; // association
};
struct MarriedWoman {
// ...
MarriedMan* husband; // association
};
struct MarriedCouple {
MarriedWoman wife; // aggregation
MarriedMan husband; // aggregation
MarriedCouple() {
wife.husband = &husband;
husband.wife = &wife;
}
};
MarriedCouple couple1; // couple1.wife and couple1.husband are spouses
MarriedCouple couple2(couple1);
// Are couple2.wife and couple2.husband indeed spouses?
// Why does couple2.wife say that she is married to couple1.husband?
// Why does couple2.husband say that he is married to couple1.wife?
Es kann Klassen geben, in denen Objekte in einem bestimmten Zustand (z. B. Standard-Konstruktionszustand) sicher kopiert und ansonsten nicht sicher kopiert werden können. Wenn wir das Kopieren von sicher zu kopierenden Objekten zulassen möchten, benötigen wir - wenn wir defensiv programmieren - eine Laufzeitprüfung im benutzerdefinierten Kopierkonstruktor.
Manchmal aggregiert eine Klasse, die kopierbar sein sollte, nicht kopierbare Unterobjekte. Normalerweise geschieht dies für Objekte mit nicht beobachtbarem Zustand (dieser Fall wird im Abschnitt "Optimierung" weiter unten ausführlicher erläutert). Der Compiler hilft lediglich, diesen Fall zu erkennen.
Eine Klasse, die kopierbar sein sollte, kann ein Unterobjekt eines quasi kopierbaren Typs aggregieren. Ein quasi kopierbarer Typ stellt keinen Kopierkonstruktor im engeren Sinne bereit, verfügt jedoch über einen anderen Konstruktor, mit dem eine konzeptionelle Kopie des Objekts erstellt werden kann. Der Grund dafür, einen Typ quasi kopierbar zu machen, liegt darin, dass keine vollständige Übereinstimmung über die Kopiersemantik des Typs besteht.
Wenn wir beispielsweise den Fall der Selbstregistrierung von Objekten erneut betrachten, können wir argumentieren, dass es Situationen geben kann, in denen ein Objekt nur dann beim globalen Objektmanager registriert werden muss, wenn es sich um ein vollständiges eigenständiges Objekt handelt. Wenn es sich um ein Unterobjekt eines anderen Objekts handelt, liegt die Verantwortung für dessen Verwaltung bei dem enthaltenen Objekt.
Oder es muss sowohl flaches als auch tiefes Kopieren unterstützt werden (keines davon ist die Standardeinstellung).
Die endgültige Entscheidung bleibt dann den Benutzern dieses Typs überlassen. Beim Kopieren von Objekten müssen sie explizit (durch zusätzliche Argumente) die beabsichtigte Kopiermethode angeben.
Im Falle eines nicht defensiven Programmieransatzes ist es auch möglich, dass sowohl ein regulärer Kopierkonstruktor als auch ein Quasi-Kopierkonstruktor vorhanden sind. Dies kann gerechtfertigt sein, wenn in den allermeisten Fällen eine einzige Kopiermethode angewendet werden sollte, während in seltenen, aber gut verstandenen Situationen alternative Kopiermethoden verwendet werden sollten. Dann beschwert sich der Compiler nicht darüber, dass er den Kopierkonstruktor nicht implizit definieren kann. Es liegt in der alleinigen Verantwortung des Benutzers, sich zu merken und zu prüfen, ob ein Unterobjekt dieses Typs über einen Quasi-Kopierkonstruktor kopiert werden soll.
In seltenen Fällen kann eine Teilmenge des beobachtbaren Zustands des Objekts einen untrennbaren Teil der Identität des Objekts darstellen (oder als solcher betrachtet werden) und sollte nicht auf andere Objekte übertragbar sein (obwohl dies etwas kontrovers sein kann).
Beispiele:
Die UID des Objekts (aber diese gehört auch zum Fall "Selbstregistrierung" von oben, da die ID in einem Akt der Selbstregistrierung erhalten werden muss).
Verlauf des Objekts (z. B. des Rückgängig / Wiederherstellen-Stapels) für den Fall, dass das neue Objekt nicht den Verlauf des Quellobjekts erben darf, sondern mit einem einzelnen Verlaufselement " Kopiert um <ZEIT> von <OTHER_OBJECT_ID> " beginnt .
In solchen Fällen muss der Kopierkonstruktor das Kopieren der entsprechenden Unterobjekte überspringen.
Die Signatur des vom Compiler bereitgestellten Kopierkonstruktors hängt davon ab, welche Kopierkonstruktoren für die Unterobjekte verfügbar sind. Wenn mindestens ein Unterobjekt keinen echten Kopierkonstruktor hat (das Quellobjekt als konstante Referenz verwendet), sondern einen mutierenden Kopierkonstruktor (das Quellobjekt als nicht konstante Referenz verwendet), hat der Compiler keine Wahl aber implizit einen mutierenden Kopierkonstruktor deklarieren und dann definieren.
Was ist nun, wenn der "mutierende" Kopierkonstruktor vom Typ des Unterobjekts das Quellobjekt nicht tatsächlich mutiert (und einfach von einem Programmierer geschrieben wurde, der das const
Schlüsselwort nicht kennt )? Wenn wir diesen Code nicht durch Hinzufügen des fehlenden Codes reparieren können const
, besteht die andere Möglichkeit darin, unseren eigenen benutzerdefinierten Kopierkonstruktor mit einer korrekten Signatur zu deklarieren und die Sünde zu begehen, sich an a zu wenden const_cast
.
Ein COW-Container, der direkte Verweise auf seine internen Daten preisgegeben hat, MUSS zum Zeitpunkt der Erstellung tief kopiert werden, da er sich sonst möglicherweise als Referenzzählhandle verhält.
Obwohl COW eine Optimierungstechnik ist, ist diese Logik im Kopierkonstruktor entscheidend für die korrekte Implementierung. Deshalb habe ich diesen Fall hier und nicht im Abschnitt "Optimierung" platziert, wo wir als nächstes vorgehen.
In den folgenden Fällen möchten / müssen Sie möglicherweise Ihren eigenen Kopierkonstruktor aus Optimierungsgründen definieren:
Stellen Sie sich einen Container vor, der Elemententfernungsvorgänge unterstützt, dies jedoch möglicherweise, indem Sie das entfernte Element einfach als gelöscht markieren und seinen Steckplatz später wiederverwenden. Wenn eine Kopie eines solchen Containers erstellt wird, kann es sinnvoll sein, die überlebenden Daten zu komprimieren, anstatt die "gelöschten" Slots unverändert beizubehalten.
Ein Objekt kann Daten enthalten, die nicht Teil seines beobachtbaren Zustands sind. In der Regel handelt es sich dabei um zwischengespeicherte / gespeicherte Daten, die während der Lebensdauer des Objekts gesammelt wurden, um bestimmte langsame Abfragevorgänge zu beschleunigen, die vom Objekt ausgeführt werden. Das Kopieren dieser Daten kann sicher übersprungen werden, da sie neu berechnet werden, wenn (und wenn!) Die entsprechenden Vorgänge ausgeführt werden. Das Kopieren dieser Daten kann ungerechtfertigt sein, da es schnell ungültig werden kann, wenn der beobachtbare Zustand des Objekts (von dem die zwischengespeicherten Daten abgeleitet werden) durch Mutationsoperationen geändert wird (und wenn wir das Objekt nicht ändern wollen, warum erstellen wir eine Tiefe dann kopieren?)
Diese Optimierung ist nur gerechtfertigt, wenn die Hilfsdaten im Vergleich zu den Daten, die den beobachtbaren Zustand darstellen, groß sind.
Mit C ++ kann das implizite Kopieren deaktiviert werden, indem der Kopierkonstruktor deklariert wird explicit
. Dann können Objekte dieser Klasse nicht an Funktionen übergeben und / oder von Funktionen nach Wert zurückgegeben werden. Dieser Trick kann für einen Typ verwendet werden, der leicht zu sein scheint, aber in der Tat sehr teuer zu kopieren ist (obwohl es eine bessere Wahl sein könnte, ihn quasi kopierbar zu machen).
In C ++ 03 muss für das Deklarieren eines Kopierkonstruktors auch dieser definiert werden (natürlich, wenn Sie ihn verwenden möchten). Wenn Sie sich für einen solchen Kopierkonstruktor nur aus dem besprochenen Grund heraus entschieden haben, mussten Sie denselben Code schreiben, den der Compiler automatisch für Sie generieren würde.
C ++ 11 und neuere Standards ermöglichen das Deklarieren spezieller Elementfunktionen (Standard- und Kopierkonstruktoren, Kopierzuweisungsoperator und Destruktor) mit einer expliziten Anforderung zur Verwendung der Standardimplementierung (beenden Sie die Deklaration einfach mit
=default
).
Diese Antwort kann wie folgt verbessert werden:
- Fügen Sie weiteren Beispielcode hinzu
- Veranschaulichen Sie den Fall "Objekte mit internen Querverweisen"
- Fügen Sie einige Links hinzu
Wenn Sie eine Klasse haben, die dynamisch Inhalt zugewiesen hat. Zum Beispiel speichern Sie den Titel eines Buches als Zeichen * und setzen den Titel auf neu. Die Kopie funktioniert nicht.
Sie müssten einen Kopierkonstruktor schreiben, der dies tut title = new char[length+1]
und dann strcpy(title, titleIn)
. Der Kopierkonstruktor würde nur eine "flache" Kopie erstellen.
Der Kopierkonstruktor wird aufgerufen, wenn ein Objekt entweder als Wert übergeben, als Wert zurückgegeben oder explizit kopiert wird. Wenn kein Kopierkonstruktor vorhanden ist, erstellt c ++ einen Standardkopierkonstruktor, der eine flache Kopie erstellt. Wenn das Objekt keine Zeiger auf dynamisch zugewiesenen Speicher hat, reicht eine flache Kopie aus.
Es ist oft eine gute Idee, copy ctor und operator = zu deaktivieren, es sei denn, die Klasse benötigt es speziell. Dies kann Ineffizienzen wie das Übergeben eines Args als Wert verhindern, wenn eine Referenz beabsichtigt ist. Auch die vom Compiler generierten Methoden können ungültig sein.
Betrachten wir das folgende Code-Snippet:
class base{
int a, *p;
public:
base(){
p = new int;
}
void SetData(int, int);
void ShowData();
base(const base& old_ref){
//No coding present.
}
};
void base :: ShowData(){
cout<<this->a<<" "<<*(this->p)<<endl;
}
void base :: SetData(int a, int b){
this->a = a;
*(this->p) = b;
}
int main(void)
{
base b1;
b1.SetData(2, 3);
b1.ShowData();
base b2 = b1; //!! Copy constructor called.
b2.ShowData();
return 0;
}
Output:
2 3 //b1.ShowData();
1996774332 1205913761 //b2.ShowData();
b2.ShowData();
Gibt eine Junk-Ausgabe aus, da ein benutzerdefinierter Kopierkonstruktor erstellt wurde, für den kein Code zum expliziten Kopieren von Daten geschrieben wurde. Der Compiler erstellt also nicht dasselbe.
Ich dachte nur daran, dieses Wissen mit Ihnen allen zu teilen, obwohl die meisten von Ihnen es bereits wissen.
Prost ... Viel Spaß beim Codieren !!!