Язык программирования C++. Пятое издание - Стенли Липпман
Шрифт:
Интервал:
Закладка:
Определять все эти функции не обязательно: вполне можно определить один или два из них, не определяя все. Эти функции можно считать модулями. Если нужен один, не обязательно определять их все.
Классы, нуждающиеся в деструкторах, нуждаются в копировании и присвоенииВот эмпирическое правило, используемое при принятии решения о необходимости определения в классе собственных версий функций-членов управления копированием: сначала следует решить, нужен ли классу деструктор. Зачастую потребность в деструкторе более очевидна, чем потребность в операторе присвоения или конструкторе копий. Если класс нуждается в деструкторе, он почти наверняка нуждается также в конструкторе копий и операторе присвоения копии.
Используемый в упражнениях класс HasPtr отлично подойдет для примера (см. раздел 13.1.1). Этот класс резервирует динамическую память в конструкторе. Синтезируемый деструктор не будет удалять указатель-член. Поэтому данный класс должен определить деструктор для освобождения памяти, зарезервированной конструктором.
Хоть это и не очевидно, но согласно эмпирическому правилу класс HasPtr нуждается также в конструкторе копий и операторе присвоения копии.
Давайте посмотрим, что было бы, если бы у класса HasPtr был деструктор и синтезируемые версии конструктора копий и оператора присвоения копии:
class HasPtr {
public:
HasPtr(const std::string &s = std::string()):
ps(new std::string(s)), i(0) { }
~HasPtr() { delete ps; }
// ошибка: HasPtr нуждается в конструкторе копий и операторе
// присвоения копии
// другие члены, как прежде
};
В этой версии класса зарезервированная в конструкторе память будет освобождена при удалении объекта класса HasPtr. К сожалению, здесь есть серьезная ошибка! Данная версия класса использует синтезируемые версии операторов копирования и присвоения. Эти функции копируют указатели-члены, а значит, несколько объектов класса HasPtr смогут указывать на ту же область памяти:
HasPtr f(HasPtr hp) // HasPtr передан по значению, поэтому он
// копируется
{
HasPtr ret = hp; // копирует данный HasPtr
// обработка ret
return ret; // ret и hp удаляются
}
Когда функция f() завершает работу, объекты hp и ret удаляются и деструктор класса HasPtr выполняется для каждого из них. Этот деструктор удалит указатель-член и в объекте ret, и в объекте hp. Но эти объекты содержат одинаковое значение указателя. Код удалит тот же указатель дважды, что является серьезной ошибкой (см. раздел 12.1.2) с непредсказуемыми результатами.
Кроме того, вызывающая сторона функции f() может все еще использовать переданный ей объект:
HasPtr p("some values");
f(p); // по завершении f() память, на которую указывает p.ps,
// освобождается
HasPtr q(p); // теперь и p, и q указывают на недопустимую память!
Память, на которую указывает указатель p (и q), больше недопустима. Она была возвращена операционной системе, когда был удален объект hp (или ret)!
Если класс нуждается в деструкторе, он почти наверняка нуждается также в операторе присвоения копии и конструкторе копий.
Классы, нуждающиеся в копировании, нуждаются также в присвоении, и наоборотХотя большинству классов требуется определить все функции-члены управления копированием (или ни один из них), у некоторых классов есть необходимость только в копировании или присвоении объектов, но нет никакой необходимости в деструкторе.
В качестве примера рассмотрим класс, присваивающий каждому своему объекту уникальный последовательный номер. Такому классу нужен конструктор копий для создания нового уникального последовательного номера для создаваемого объекта. Этот конструктор копировал бы все остальные переменные-члены заданного объекта. Класс нуждался бы также в собственном операторе присвоения копии, чтобы избежать присвоения объекту слева последовательного номера. Однако у этого класса не было бы никакой потребности в деструкторе.
Этот пример иллюстрирует второе эмпирическому правило: если класс нуждается в конструкторе копий, то он почти наверняка нуждается в операторе присвоения копии, и наоборот, — если класс нуждается в операторе присвоения, то он почти наверняка нуждается также в конструкторе копий. Однако нужда в конструкторе копий или операторе присвоения копии не означает потребности в деструкторе.
Упражнения раздела 13.1.4Упражнение 13.14. Предположим, что класс numbered имеет стандартный конструктор, создающий уникальный последовательный номер для каждого объекта, который хранится в переменной-члене mysn. Класс numbered использует синтезируемые функции-члены управления копированием и имеет следующую функцию:
void f(numbered s) { cout << s.mysn << endl; }
Какой вывод создаст следующий код?
numbered a, b = a, с = b;
f(a); f(b); f(c);
Упражнение 13.15. Предположим, что у класса numbered есть конструктор копий, создающий новый последовательный номер. Изменит ли это вывод вызовов в предыдущем упражнении? Если да, то почему? Какой вывод получится?
Упражнение 13.16. Что если параметром функции f() будет const numbered&? Это изменяет вывод? Если да, то почему? Какой вывод получится?
Упражнение 13.17. Напишите версии класса numbered и функции f(), соответствующие трем предыдущим упражнениям, и проверьте правильность предсказания вывода.
13.1.5. Использование спецификатора = default
Используя спецификатор = default, можно явно указать компилятору на необходимость создать синтезируемые версии функций-членов управления копированием (см. раздел 7.1.4):
class Sales_data {
public:
// управление копированием; версии по умолчанию
Sales_data() = default;
Sales_data(const Sales_data&) = default;
Sales_data& operator=(const Sales_data &);
~Sales_data() = default;
// другие члены как прежде
};
Sales_data& Sales_data::operator=(const Sales_data&) = default;
Когда в объявлении функции-члена в теле класса использован спецификатор = default, синтезируемая функция неявно становится встраиваемой (как и любая другая функция-член, определенная в теле класса). Если синтезируемая функция-член класса не должна быть встраиваемой функцией, можно добавить часть = default в ее определение, как это было сделано в определении оператора присвоения копии.
Спецификатор = default можно использовать только для тех функций-членов, у которых есть синтезируемая версия (т.е. стандартный конструктор или функция-член управления копированием).
13.1.6. Предотвращение копирования
Большинство классов должно определить (явно или неявно) стандартный конструктор, конструктор копий и оператор присвоения копии.
Хотя большинство классов должно определять (и, как правило, определяет) конструктор копий и оператор присвоения копии, у некоторых классов нет реальной необходимости в этих функциях. В таких случаях класс должен быть определен так, чтобы предотвращать копирование и присвоение. Например, классы iostream предотвращают копирование, чтобы не позволять нескольким объектам писать или читать из того же буфера ввода-вывода. Казалось бы, предотвратить копирование можно, и не определяя функции-члены управления копированием. Но эта стратегия не сработает: если класс не определит эти функции, то компилятор синтезирует их сам.
Определение функции как удаленнойПо новому стандарту можно предотвратить копирование, определив конструктор копий и оператор присвоения копии как удаленные функции (deleted function). Удаленной называется функция, которая была объявлена, но не может использована никаким другим способом. Чтобы определить функцию как удаленную, за списком ее параметров следует расположить часть = delete:
struct NoCopy {
NoCopy() = default; // использовать синтезируемый стандартный
// конструктор
NoCopy(const NoCopy&) = delete; // без копирования
NoCopy &operator=(const NoCopy&) = delete; // без присвоения
~NoCopy() = default; // используйте синтезируемый деструктор