C++
Specjalne funkcje członka
Szukaj…
Wirtualne i chronione niszczyciele
Klasa przeznaczona do dziedziczenia nazywa się klasą podstawową. Należy zachować ostrożność przy specjalnych funkcjach członka takiej klasy.
Klasa zaprojektowana do użycia polimorficznie w czasie wykonywania (poprzez wskaźnik do klasy bazowej) powinna zadeklarować virtual
destruktor. Pozwala to na prawidłowe zniszczenie pochodnych części obiektu, nawet gdy obiekt jest niszczony przez wskaźnik do klasy podstawowej.
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
Jeśli klasa nie musi być polimorficzna, ale nadal musi umożliwiać dziedziczenie interfejsu, użyj nie-wirtualnego protected
destruktora.
class NonPolymorphicBase {
public:
// some methods
protected:
~NonPolymorphicBase() = default; // note: non-virtual
private:
// etc.
};
Takiej klasy nigdy nie można zniszczyć wskaźnikiem, unikając cichych wycieków z powodu krojenia.
Ta technika dotyczy zwłaszcza klas zaprojektowanych jako private
klasy podstawowe. Taką klasę można wykorzystać do enkapsulacji niektórych typowych szczegółów implementacji, zapewniając jednocześnie virtual
metody jako punkty dostosowywania. Tego rodzaju klasy nigdy nie należy używać polimorficznie, a protected
destruktor pomaga udokumentować to wymaganie bezpośrednio w kodzie.
Wreszcie niektóre klasy mogą wymagać, aby nigdy nie były używane jako klasa podstawowa. W takim przypadku klasę można oznaczyć jako final
. Normalny nie-wirtualny publiczny niszczyciel jest w tym przypadku w porządku.
class FinalClass final { // marked final here
public:
~FinalClass() = default;
private:
// etc.
};
Domniemane przenoszenie i kopiowanie
Pamiętaj, że zadeklarowanie destruktora uniemożliwia kompilatorowi generowanie niejawnych konstruktorów ruchu i operatorów przypisania ruchu. Jeśli deklarujesz destruktor, pamiętaj o dodaniu odpowiednich definicji dla operacji przenoszenia.
Ponadto zadeklarowanie operacji przenoszenia spowoduje pominięcie generowania operacji kopiowania, dlatego należy je również dodać (jeśli obiekty tej klasy muszą mieć semantykę kopiowania).
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;
};
Kopiuj i zamień
Jeśli piszesz klasę, która zarządza zasobami, musisz zaimplementować wszystkie specjalne funkcje składowe (patrz Zasada Trzech / Pięciu / Zera ). Najbardziej bezpośrednim podejściem do pisania konstruktora kopiowania i operatora przypisania byłoby:
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;
}
Ale to podejście ma pewne problemy. To nie silną gwarancję wyjątek - jeśli new[]
rzuca, mamy już usunięte z zasobów należących do this
i nie można odzyskać. Powielamy wiele logiki konstrukcji kopii w przypisaniu kopii. I musimy pamiętać o sprawdzeniu samodzielnego przypisania, które zwykle po prostu nakłada się na operację kopiowania, ale nadal jest krytyczne.
Aby spełnić silną gwarancję wyjątku i uniknąć duplikacji kodu (podwoić więc z operatorem przypisania ruchu), możemy użyć idiomu kopiowania i zamiany:
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;
}
};
Dlaczego to działa? Zastanów się, co się stanie, gdy będziemy mieli
person p1 = ...;
person p2 = ...;
p1 = p2;
Najpierw kopiujemy-konstruujemy rhs
z p2
(którego nie musieliśmy tutaj kopiować). Jeśli ta operacja zostanie rzucona, nic nie robimy w operator=
a p1
pozostaje nietknięte. Następnie zamieniamy członków między *this
i rhs
, a następnie rhs
wychodzi poza zakres. Kiedy operator=
, że niejawnie czyści oryginalnych zasobów this
(przez destructor, które nie muszą powielać). Samoprzypisanie również działa - jest mniej wydajne przy kopiowaniu i zamianie (wymaga dodatkowej alokacji i dezalokacji), ale jeśli jest to mało prawdopodobny scenariusz, nie spowalniamy typowego przypadku użycia, aby to uwzględnić.
Powyższe sformułowanie działa tak, jak jest już w przypadku przypisania do ruchu.
p1 = std::move(p2);
Tutaj przenosimy-konstruujemy rhs
z p2
, a cała reszta jest równie ważna. Jeśli klasa jest ruchoma, ale nie można jej skopiować, nie ma potrzeby usuwania przypisania kopii, ponieważ ten operator przypisania będzie po prostu źle sformułowany z powodu usuniętego konstruktora kopiowania.
Domyślny konstruktor
Domyślny konstruktor to typ konstruktora, który nie wymaga parametrów po wywołaniu. Jego nazwa pochodzi od typu, który konstruuje i jest jego funkcją składową (podobnie jak wszystkie konstruktory).
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
Innym sposobem spełnienia wymogu „brak parametrów” jest podanie przez programistę wartości domyślnych dla wszystkich parametrów:
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
W niektórych okolicznościach (tj. Programista nie dostarcza konstruktorów i nie ma innych warunków dyskwalifikujących), kompilator domyślnie udostępnia pustego domyślnego konstruktora:
class C{
std::string s; // note: members need to be default constructible themselves
};
C c1; // will succeed -- C has an implicitly defined default constructor
Posiadanie innego rodzaju konstruktora jest jednym z warunków dyskwalifikujących wspomnianych wcześniej:
class C{
int i;
public:
C( int i ) : i(i){}
};
C c1; // Compile ERROR: C has no (implicitly defined) default constructor
Aby zapobiec domyślnemu tworzeniu domyślnego konstruktora, powszechną techniką jest zadeklarowanie go jako private
(bez definicji). Intencją jest spowodowanie błędu kompilacji, gdy ktoś próbuje użyć konstruktora (powoduje to albo błąd dostępu do prywatnego, albo błąd linkera, w zależności od kompilatora).
Aby mieć pewność, że zdefiniowany jest domyślny konstruktor (funkcjonalnie podobny do domyślnego), programista może napisać pusty.
W C ++ 11 programista może również użyć słowa kluczowego delete
aby uniemożliwić kompilatorowi dostarczenie domyślnego konstruktora.
class C{
int i;
public:
// default constructor is explicitly deleted
C() = delete;
};
C c1; // Compile ERROR: C has its default constructor deleted
Ponadto deweloper może również wyraźnie powiedzieć, że chce, aby kompilator udostępnił domyślny konstruktor.
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
Możesz ustalić, czy typ ma domyślny konstruktor (czy jest typem pierwotnym), używając std::is_default_constructible
z <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
W C ++ 11 nadal można używać niefunkcjonalnej wersji std::is_default_constructible
:
cout << boolalpha << is_default_constructible<C1>::value << endl; // prints true
Burzyciel
Destruktor to funkcja bez argumentów, która jest wywoływana, gdy obiekt zdefiniowany przez użytkownika ma zostać zniszczony. Jego nazwa pochodzi od typu, który niszczy z prefiksem ~
.
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
W większości przypadków (tj. Użytkownik nie dostarcza destruktora i nie ma innych warunków dyskwalifikujących), kompilator domyślnie zapewnia destruktor domyślny:
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
}
W C ++ 11 programista może zastąpić to zachowanie, uniemożliwiając kompilatorowi dostarczenie domyślnego destruktora.
class C{
int m;
public:
~C() = delete; // does NOT have implicit destructor
};
void f{
C c1;
} // Compile ERROR: C has no destructor
Ponadto deweloper może również wyraźnie powiedzieć, że chce, aby kompilator udostępnił domyślny destruktor.
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
Możesz ustalić, czy typ ma destruktor (czy jest typem pierwotnym), używając std::is_destructible
z <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