C++
Specialmedlemfunktioner
Sök…
Virtuella och skyddade destruktorer
En klass designad för att ärva-från kallas en basklass. Man bör vara försiktig med de speciella medlemsfunktionerna i en sådan klass.
En klass utformad för att användas polymorfiskt vid körning (genom en pekare till basklassen) bör förklara förstöraren som virtual
. Detta tillåter att de härledda delarna av objektet förstörs ordentligt, även när objektet förstörs genom en pekare till basklassen.
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
Om klassen inte behöver vara polymorf, men fortfarande behöver tillåta att gränssnittet ärvs, använd en icke-virtuell protected
destruktor.
class NonPolymorphicBase {
public:
// some methods
protected:
~NonPolymorphicBase() = default; // note: non-virtual
private:
// etc.
};
En sådan klass kan aldrig förstöras genom en pekare och undvika tyst läckage på grund av skivning.
Denna teknik gäller särskilt klasser designade för att vara private
basklasser. En sådan klass kan användas för att kapsla in vissa vanliga implementeringsdetaljer, samtidigt som de tillhandahåller virtual
metoder som anpassningspunkter. Denna typ av klass bör aldrig användas polymorfiskt, och en protected
förstörare hjälper till att dokumentera detta krav direkt i koden.
Slutligen kan vissa klasser kräva att de aldrig används som basklass. I detta fall kan klassen markeras som final
. En vanlig icke-virtuell offentlig förstörare är bra i detta fall.
class FinalClass final { // marked final here
public:
~FinalClass() = default;
private:
// etc.
};
Implicit Flytta och kopiera
Kom ihåg att förklara en förstörare hindrar kompilatorn från att generera implicita flyttkonstruktörer och flytta uppdragsoperatörer. Om du förklarar en förstörare, kom ihåg att också lägga till lämpliga definitioner för flyttoperationerna.
Dessutom kommer att förklara flyttningsoperationer att undertrycka genereringen av kopieringsoperationer, så dessa bör också läggas till (om objektet i denna klass måste ha kopisemantik).
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;
};
Kopiera och byt
Om du skriver en klass som hanterar resurser måste du implementera alla specialmedlemfunktioner (se regel om tre / fem / noll ). Det mest direkta sättet att skriva kopieringskonstruktören och uppdragsoperatören skulle vara:
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;
}
Men detta tillvägagångssätt har vissa problem. Det misslyckas med den starka undantagsgarantin - om new[]
kastar, har vi redan rensat resurserna som ägs av this
och kan inte återhämta sig. Vi duplicerar mycket av logiken för kopieringskonstruktion i kopieringsuppdrag. Och vi måste komma ihåg självtilldelningskontrollen, som vanligtvis bara lägger omkostnader för kopieringen, men som fortfarande är kritisk.
För att tillfredsställa den starka undantagsgarantin och undvika kodduplicering (dubbelt så med den efterföljande operatören för flyttilldelning) kan vi använda kopierings- och swap-formen:
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;
}
};
Varför fungerar det här? Tänk på vad som händer när vi har det
person p1 = ...;
person p2 = ...;
p1 = p2;
Först kopierar vi rhs
från p2
(som vi inte behövde kopiera här). Om den operationen kastar, gör vi ingenting i operator=
och p1
förblir orört. Därefter byter vi medlemmarna mellan *this
och rhs
, och sedan går rhs
utanför räckvidden. När operator=
rensar det implicit de ursprungliga resurserna för this
(via destruktorn, som vi inte behövde kopiera). Självtilldelning fungerar också - det är mindre effektivt med kopiering och byte (innebär en extra allokering och omlokalisering), men om det är det osannolika scenariot drar vi inte ner det typiska fallet för att redogöra för det.
Ovanstående formulering fungerar som den redan är för flytttilldelning.
p1 = std::move(p2);
Här flyttar vi-Construct rhs
från p2
, och resten är lika giltiga. Om en klass är rörlig men inte kopierbar, finns det inget behov av att ta bort kopieringsuppdraget, eftersom denna tilldelningsoperatör helt enkelt blir felformad på grund av den borttagna kopikonstruktören.
Standardkonstruktör
En standardkonstruktör är en typ av konstruktör som inte kräver några parametrar när den anropas. Den är uppkallad efter den typ den konstruerar och är en medlemsfunktion för den (som alla konstruktörer är).
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
Ett annat sätt att uppfylla kravet på "inga parametrar" är att utvecklaren tillhandahåller standardvärden för alla parametrar:
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
Under vissa omständigheter (dvs. utvecklaren tillhandahåller inga konstruktörer och det finns inga andra diskvalificerande villkor) ger kompilatorn implicit en tom standardkonstruktör:
class C{
std::string s; // note: members need to be default constructible themselves
};
C c1; // will succeed -- C has an implicitly defined default constructor
Att ha någon annan typ av konstruktör är ett av de diskvalificerande villkoren som nämnts tidigare:
class C{
int i;
public:
C( int i ) : i(i){}
};
C c1; // Compile ERROR: C has no (implicitly defined) default constructor
För att förhindra implicit standardkonstruktörskapning är en vanlig teknik att förklara den som private
(utan definition). Avsikten är att orsaka ett kompileringsfel när någon försöker använda konstruktören (detta leder antingen till ett Access to private error eller ett linker-fel, beroende på kompilatorn).
För att vara säker på att en standardkonstruktör (funktionellt liknar den implicita) är definierad, kan en utvecklare skriva ett tomt uttryckligt.
I C ++ 11 kan en utvecklare också använda sökordet delete
att förhindra kompilatorn från att tillhandahålla en standardkonstruktör.
class C{
int i;
public:
// default constructor is explicitly deleted
C() = delete;
};
C c1; // Compile ERROR: C has its default constructor deleted
Dessutom kan en utvecklare också vara uttrycklig för att vilja att kompilatorn ska tillhandahålla en standardkonstruktör.
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
Du kan bestämma om en typ har en standardkonstruktör (eller är en primitiv typ) med std::is_default_constructible
från <type_traits>
:
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
I C ++ 11 är det fortfarande möjligt att använda den icke-funktorversionen av std::is_default_constructible
:
cout << boolalpha << is_default_constructible<C1>::value << endl; // prints true
destructor
En destruktor är en funktion utan argument som kallas när ett användardefinierat objekt är på väg att förstöras. Den är uppkallad efter den typ som den förstör med ett ~
prefix.
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
Under de flesta omständigheter (dvs. en användare tillhandahåller ingen förstörare och det finns inga andra diskvalificerande villkor) ger kompilatorn en standarddestruktor implicit:
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
}
I C ++ 11 kan en utvecklare åsidosätta detta beteende genom att förhindra kompilatorn från att tillhandahålla en standardförstörare.
class C{
int m;
public:
~C() = delete; // does NOT have implicit destructor
};
void f{
C c1;
} // Compile ERROR: C has no destructor
Dessutom kan en utvecklare också vara uttrycklig för att vilja att kompilatorn ska tillhandahålla en standardförstörare.
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
Du kan bestämma om en typ har en förstörare (eller är en primitiv typ) med std::is_destructible
från <type_traits>
:
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