C++
Spezielle Mitgliederfunktionen
Suche…
Virtuelle und geschützte Destruktoren
Eine Klasse, die entworfen wurde, um vererbt zu werden, wird als Basisklasse bezeichnet. Bei den speziellen Memberfunktionen einer solchen Klasse ist Vorsicht geboten.
Eine Klasse, die zur Laufzeit (durch einen Zeiger auf die Basisklasse) polymorph verwendet wird, sollte den Destruktor als virtual
deklarieren. Dadurch können die abgeleiteten Teile des Objekts ordnungsgemäß zerstört werden, selbst wenn das Objekt durch einen Zeiger auf die Basisklasse zerstört wird.
class Base {
public:
virtual ~Base() = default;
private:
// data members etc.
};
class Derived : public Base { // models Is-A relationship
public:
// some methods
private:
// more data members
};
// virtual destructor in Base ensures that derived destructors
// are also called when the object is destroyed
std::unique_ptr<Base> base = std::make_unique<Derived>();
base = nullptr; // safe, doesn't leak Derived's members
Wenn die Klasse nicht polymorph sein muss, die Schnittstelle jedoch vererbt werden muss, verwenden Sie einen nicht virtuellen protected
Destruktor.
class NonPolymorphicBase {
public:
// some methods
protected:
~NonPolymorphicBase() = default; // note: non-virtual
private:
// etc.
};
Eine solche Klasse kann niemals durch einen Zeiger zerstört werden, um stille Undichtigkeiten durch Schneiden zu vermeiden.
Diese Technik gilt insbesondere für Klassen, die als private
Basisklassen konzipiert wurden. Mit einer solchen Klasse können einige allgemeine Implementierungsdetails gekapselt werden, während virtual
Methoden als Anpassungspunkte bereitgestellt werden. Diese Art von Klassen sollte niemals polymorph verwendet werden, und ein protected
Destruktor hilft, diese Anforderung direkt im Code zu dokumentieren.
Schließlich können einige Klassen verlangen, dass sie niemals als Basisklasse verwendet werden. In diesem Fall kann die Klasse als final
markiert werden. In diesem Fall ist ein normaler nicht virtueller öffentlicher Destruktor in Ordnung.
class FinalClass final { // marked final here
public:
~FinalClass() = default;
private:
// etc.
};
Implizites Verschieben und Kopieren
Beachten Sie, dass das Deklarieren eines Destruktors den Compiler daran hindert, implizite Verschiebungskonstruktoren und Verschiebungszuweisungsoperatoren zu generieren. Wenn Sie einen Destruktor deklarieren, müssen Sie auch die entsprechenden Definitionen für die Verschiebungsoperationen hinzufügen.
Durch das Deklarieren von Verschiebungsvorgängen wird außerdem die Erzeugung von Kopiervorgängen unterdrückt. Diese sollten ebenfalls hinzugefügt werden (wenn die Objekte dieser Klasse Kopiersemantik benötigen).
class Movable {
public:
virtual ~Movable() noexcept = default;
// compiler won't generate these unless we tell it to
// because we declared a destructor
Movable(Movable&&) noexcept = default;
Movable& operator=(Movable&&) noexcept = default;
// declaring move operations will suppress generation
// of copy operations unless we explicitly re-enable them
Movable(const Movable&) = default;
Movable& operator=(const Movable&) = default;
};
Kopieren und tauschen
Wenn Sie eine Klasse schreiben, die Ressourcen verwaltet, müssen Sie alle speziellen Member-Funktionen implementieren (siehe Drei- / Fünf-Regel / Null ). Der direkteste Ansatz zum Schreiben des Kopierkonstruktors und des Zuweisungsoperators wäre:
person(const person &other)
: name(new char[std::strlen(other.name) + 1])
, age(other.age)
{
std::strcpy(name, other.name);
}
person& operator=(person const& rhs) {
if (this != &other) {
delete [] name;
name = new char[std::strlen(other.name) + 1];
std::strcpy(name, other.name);
age = other.age;
}
return *this;
}
Dieser Ansatz hat jedoch einige Probleme. Die starke Ausnahmegarantie fällt nicht aus - wenn new[]
wirft, haben wir die Ressourcen, die sich im Besitz this
befinden, bereits gelöscht und können sie nicht wiederherstellen. Wir duplizieren viel von der Logik der Kopierkonstruktion bei der Kopierzuweisung. Und wir müssen uns an die Selbstzuweisungsprüfung erinnern, die den Kopiervorgang normalerweise nur belastet, aber immer noch kritisch ist.
Um die starke Ausnahmegarantie zu erfüllen und die Codevervielfältigung zu vermeiden (doppelt so mit dem nachfolgenden Verschiebungszuweisungsoperator), können wir das Copy-and-Swap-Idiom verwenden:
class person {
char* name;
int age;
public:
/* all the other functions ... */
friend void swap(person& lhs, person& rhs) {
using std::swap; // enable ADL
swap(lhs.name, rhs.name);
swap(lhs.age, rhs.age);
}
person& operator=(person rhs) {
swap(*this, rhs);
return *this;
}
};
Warum funktioniert das? Überlegen Sie, was passiert, wenn wir haben
person p1 = ...;
person p2 = ...;
p1 = p2;
Zuerst kopieren wir rhs
aus p2
(was wir hier nicht duplizieren mussten). Wenn diese Operation wirft, machen wir nichts in operator=
und p1
bleibt unangetastet. Als nächstes tauschen wir die Mitglieder zwischen *this
und rhs
, und dann geht rhs
außer Reichweite. Wenn operator=
, reinigt , dass implizit die ursprünglichen Ressourcen this
(über den destructor, die wir nicht duplizieren müssen). Selbstzuweisung funktioniert auch - mit Copy-and-Swap ist dies weniger effizient (erfordert eine zusätzliche Zuweisung und Freigabe), aber in diesem unwahrscheinlichen Fall verlangsamen wir den typischen Anwendungsfall nicht, um dies zu berücksichtigen.
Die obige Formulierung funktioniert bereits für die Bewegungszuweisung.
p1 = std::move(p2);
Hier verschieben wir rhs
von p2
, und der Rest ist genauso gültig. Wenn eine Klasse verschiebbar, aber nicht kopierbar ist, muss die Kopierzuweisung nicht gelöscht werden, da dieser Zuweisungsoperator aufgrund des gelöschten Kopierkonstruktors einfach falsch formatiert wird.
Standardkonstruktor
Ein Standardkonstruktor ist ein Typ eines Konstruktors, der beim Aufruf keine Parameter erfordert. Es ist nach dem von ihm konstruierten Typ benannt und ist eine Memberfunktion (wie alle Konstruktoren).
class C{
int i;
public:
// the default constructor definition
C()
: i(0){ // member initializer list -- initialize i to 0
// constructor function body -- can do more complex things here
}
};
C c1; // calls default constructor of C to create object c1
C c2 = C(); // calls default constructor explicitly
C c3(); // ERROR: this intuitive version is not possible due to "most vexing parse"
C c4{}; // but in C++11 {} CAN be used in a similar way
C c5[2]; // calls default constructor for both array elements
C* c6 = new C[2]; // calls default constructor for both array elements
Eine andere Möglichkeit, die Anforderung "keine Parameter" zu erfüllen, besteht darin, dass der Entwickler Standardwerte für alle Parameter bereitstellt:
class D{
int i;
int j;
public:
// also a default constructor (can be called with no parameters)
D( int i = 0, int j = 42 )
: i(i), j(j){
}
};
D d; // calls constructor of D with the provided default values for the parameters
Unter bestimmten Umständen (dh der Entwickler stellt keine Konstruktoren bereit und es gibt keine anderen disqualifizierenden Bedingungen), stellt der Compiler implizit einen leeren Standardkonstruktor bereit:
class C{
std::string s; // note: members need to be default constructible themselves
};
C c1; // will succeed -- C has an implicitly defined default constructor
Einen anderen Konstruktor-Typ zu haben, ist eine der oben genannten Disqualifizierungsbedingungen:
class C{
int i;
public:
C( int i ) : i(i){}
};
C c1; // Compile ERROR: C has no (implicitly defined) default constructor
Um die Erstellung eines impliziten Standardkonstruktors zu verhindern, ist es üblich, ihn als private
zu deklarieren (ohne Definition). Die Absicht ist, einen Kompilierungsfehler zu verursachen, wenn jemand versucht, den Konstruktor zu verwenden (dies führt je nach Compiler entweder zu einem Zugriff auf private Fehler oder zu einem Linker-Fehler).
Um sicher zu sein, dass ein Standardkonstruktor (der dem impliziten funktionell ähnlich ist) definiert ist, könnte ein Entwickler explizit einen leeren Konstruktor schreiben.
In C ++ 11 kann ein Entwickler auch das Schlüsselwort delete
, um zu verhindern, dass der Compiler einen Standardkonstruktor bereitstellt.
class C{
int i;
public:
// default constructor is explicitly deleted
C() = delete;
};
C c1; // Compile ERROR: C has its default constructor deleted
Darüber hinaus kann ein Entwickler auch explizit angeben, dass der Compiler einen Standardkonstruktor bereitstellen soll.
class C{
int i;
public:
// does have automatically generated default constructor (same as implicit one)
C() = default;
C( int i ) : i(i){}
};
C c1; // default constructed
C c2( 1 ); // constructed with the int taking constructor
Sie können mithilfe von std::is_default_constructible
aus <type_traits>
bestimmen, ob ein Typ über einen Standardkonstruktor verfügt (oder ein primitiver Typ ist):
class C1{ };
class C2{ public: C2(){} };
class C3{ public: C3(int){} };
using std::cout; using std::boolalpha; using std::endl;
using std::is_default_constructible;
cout << boolalpha << is_default_constructible<int>() << endl; // prints true
cout << boolalpha << is_default_constructible<C1>() << endl; // prints true
cout << boolalpha << is_default_constructible<C2>() << endl; // prints true
cout << boolalpha << is_default_constructible<C3>() << endl; // prints false
In C ++ 11 ist es weiterhin möglich, die Nicht-Funktionsversion von std::is_default_constructible
:
cout << boolalpha << is_default_constructible<C1>::value << endl; // prints true
Zerstörer
Ein Destruktor ist eine Funktion ohne Argumente, die aufgerufen wird, wenn ein benutzerdefiniertes Objekt zerstört werden soll. Es ist nach dem Typ benannt, den es mit einem ~
Präfix zerstört.
class C{
int* is;
string s;
public:
C()
: is( new int[10] ){
}
~C(){ // destructor definition
delete[] is;
}
};
class C_child : public C{
string s_ch;
public:
C_child(){}
~C_child(){} // child destructor
};
void f(){
C c1; // calls default constructor
C c2[2]; // calls default constructor for both elements
C* c3 = new C[2]; // calls default constructor for both array elements
C_child c_ch; // when destructed calls destructor of s_ch and of C base (and in turn s)
delete[] c3; // calls destructors on c3[0] and c3[1]
} // automatic variables are destroyed here -- i.e. c1, c2 and c_ch
In den meisten Fällen (dh ein Benutzer stellt keinen Destruktor bereit, und es gibt keine anderen disqualifizierenden Bedingungen), stellt der Compiler implizit einen Standard-Destruktor bereit:
class C{
int i;
string s;
};
void f(){
C* c1 = new C;
delete c1; // C has a destructor
}
class C{
int m;
private:
~C(){} // not public destructor!
};
class C_container{
C c;
};
void f(){
C_container* c_cont = new C_container;
delete c_cont; // Compile ERROR: C has no accessible destructor
}
In C ++ 11 kann ein Entwickler dieses Verhalten überschreiben, indem er verhindert, dass der Compiler einen Standard-Destruktor bereitstellt.
class C{
int m;
public:
~C() = delete; // does NOT have implicit destructor
};
void f{
C c1;
} // Compile ERROR: C has no destructor
Darüber hinaus kann ein Entwickler auch explizit angeben, dass der Compiler einen Standard-Destruktor bereitstellen soll.
class C{
int m;
public:
~C() = default; // saying explicitly it does have implicit/empty destructor
};
void f(){
C c1;
} // C has a destructor -- c1 properly destroyed
Sie können mit std::is_destructible
aus <type_traits>
feststellen, ob ein Typ einen Destruktor hat (oder ein primitiver Typ ist):
class C1{ };
class C2{ public: ~C2() = delete };
class C3 : public C2{ };
using std::cout; using std::boolalpha; using std::endl;
using std::is_destructible;
cout << boolalpha << is_destructible<int>() << endl; // prints true
cout << boolalpha << is_destructible<C1>() << endl; // prints true
cout << boolalpha << is_destructible<C2>() << endl; // prints false
cout << boolalpha << is_destructible<C3>() << endl; // prints false