C++
Специальные функции участников
Поиск…
Виртуальные и защищенные деструкторы
Класс, предназначенный для наследования, называется базовым классом. Следует соблюдать особые функции члена такого класса.
Класс, предназначенный для полиморфного использования во время выполнения (посредством указателя на базовый класс), должен объявить деструктор virtual
. Это позволяет соответствующим образом уничтожить производные части объекта, даже если объект уничтожен с помощью указателя на базовый класс.
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
Если класс не должен быть полиморфным, но все же необходимо разрешить его наследование, используйте не виртуальный protected
деструктор.
class NonPolymorphicBase {
public:
// some methods
protected:
~NonPolymorphicBase() = default; // note: non-virtual
private:
// etc.
};
Такой класс никогда не может быть уничтожен с помощью указателя, избегая молчащих утечек из-за нарезки.
Этот метод особенно относится к классам, предназначенным для private
базовых классов. Такой класс может быть использован для инкапсуляции некоторых общих деталей реализации, а также предоставления virtual
методов в качестве точек настройки. Этот класс не должен использоваться полиморфно, а protected
деструктор помогает документировать это требование непосредственно в коде.
Наконец, некоторые классы могут потребовать, чтобы они никогда не использовались в качестве базового класса. В этом случае класс может быть отмечен как final
. В этом случае нормальный не виртуальный публичный деструктор.
class FinalClass final { // marked final here
public:
~FinalClass() = default;
private:
// etc.
};
Неявное перемещение и копирование
Имейте в виду, что объявление деструктора запрещает компилятору создавать неявные конструкторы перемещения и переводить операторы присваивания. Если вы объявите деструктор, не забудьте также добавить соответствующие определения для операций перемещения.
Кроме того, объявление операций перемещения будет препятствовать генерации операций копирования, поэтому их также следует добавить (если для объектов этого класса требуется семантика копирования).
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;
};
Копирование и свопинг
Если вы пишете класс, который управляет ресурсами, вам необходимо реализовать все специальные функции-члены (см. Правило 3/5 / Zero ). Наиболее прямым подходом к написанию конструктора копирования и оператора присваивания будет:
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;
}
Но этот подход имеет некоторые проблемы. Это не дает надежной гарантии исключения - если new[]
бросает, мы уже очистили ресурсы, принадлежащие this
и не можем восстановить. Мы дублируем много логики построения копии при копировании. И мы должны помнить проверку самонаведения, которая обычно просто добавляет накладные расходы на операцию копирования, но по-прежнему имеет решающее значение.
Чтобы удовлетворить сильную гарантию исключения и избежать дублирования кода (в два раза с последующим оператором присваивания оператора), мы можем использовать идиому копирования и свопинга:
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;
}
};
Почему это работает? Подумайте, что произойдет, когда у нас есть
person p1 = ...;
person p2 = ...;
p1 = p2;
Во-первых, мы копируем конструкцию rhs
из p2
(которую мы не должны дублировать здесь). Если эта операция срабатывает, мы ничего не делаем в operator=
и p1
остается нетронутым. Затем мы заменяем элементы между *this
и rhs
, а затем rhs
выходит за рамки. Когда operator=
, это неявно очищает исходные ресурсы this
(через деструктор, который мы не должны дублировать). Самозадание работает тоже - оно менее эффективно с копированием и заменой (связано с дополнительным распределением и освобождением), но если это маловероятный сценарий, мы не замедляем типичный вариант использования для его учета.
Вышеприведенная формулировка работает как-уже для назначения перемещения.
p1 = std::move(p2);
Здесь мы перемещаем-строим rhs
из p2
, а все остальное справедливо. Если класс движется, но не копируется, нет необходимости удалять назначение копии, так как этот оператор присваивания будет просто плохо сформирован из-за конструктора удаленной копии.
Конструктор по умолчанию
Конструктор по умолчанию - это тип конструктора, который не требует никаких параметров при вызове. Он назван в честь того типа, который он строит, и является его функцией-членом (как и все конструкторы).
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
Другим способом удовлетворения требований «без параметров» является предоставление разработчикам значений по умолчанию для всех параметров:
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
В некоторых случаях (т.е. разработчик не предоставляет никаких конструкторов и других условий дисквалификации), компилятор неявно предоставляет пустой конструктор по умолчанию:
class C{
std::string s; // note: members need to be default constructible themselves
};
C c1; // will succeed -- C has an implicitly defined default constructor
Наличие другого типа конструктора является одним из условий дисквалификации, упомянутых ранее:
class C{
int i;
public:
C( int i ) : i(i){}
};
C c1; // Compile ERROR: C has no (implicitly defined) default constructor
Чтобы предотвратить неявное создание конструктора по умолчанию, общепринятой техникой является объявить ее как private
(без определения). Цель состоит в том, чтобы вызвать ошибку компиляции, когда кто-то пытается использовать конструктор (это либо приводит к ошибке доступа к закрытой ошибке, либо ошибке компоновщика, в зависимости от компилятора).
Чтобы убедиться, что конструктор по умолчанию (функционально подобный неявному) определен, разработчик может написать пустой ящик явно.
В C ++ 11 разработчик также может использовать ключевое слово delete
чтобы компилятор не предоставлял конструктор по умолчанию.
class C{
int i;
public:
// default constructor is explicitly deleted
C() = delete;
};
C c1; // Compile ERROR: C has its default constructor deleted
Кроме того, разработчик также может быть явно о том, что компилятор должен предоставить конструктор по умолчанию.
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
Вы можете определить, имеет ли тип конструктор по умолчанию (или является примитивным типом), используя std::is_default_constructible
из <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
В C ++ 11 все еще можно использовать версию std::is_default_constructible
для std::is_default_constructible
:
cout << boolalpha << is_default_constructible<C1>::value << endl; // prints true
Destructor
Деструктор - это функция без аргументов, которая вызывается, когда пользовательский объект будет уничтожен. Она названа в честь типа он Уничтожает с ~
префиксом.
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
В большинстве случаев (т. Е. Пользователь не предоставляет деструктора и других условий дисквалификации), компилятор предоставляет деструктор по умолчанию неявно:
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
}
В C ++ 11 разработчик может переопределить это поведение, запретив компилятору предоставлять деструктор по умолчанию.
class C{
int m;
public:
~C() = delete; // does NOT have implicit destructor
};
void f{
C c1;
} // Compile ERROR: C has no destructor
Кроме того, разработчик также может быть явно о том, что компилятор должен предоставить деструктор по умолчанию.
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
Вы можете определить, имеет ли тип деструктор (или является примитивным типом), используя std::is_destructible
из <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