Упражнение 13.13. Наилучший способ изучения функций-членов управления копированием и конструкторов — это определить простой класс с этими функциями-членами, каждая из которых выводит свое имя:

struct X {

 X() {std::cout << "X()" << std::endl;}

 X(const X&) {std::cout << "X(const X&)" << std::endl;}

};

Добавьте в структуру X оператор присвоения копии и деструктор, а затем напишите программу, использующую объекты класса X различными способами: передайте их как ссылочный и не ссылочный параметры; динамически зарезервируйте их; поместите в контейнеры и т.д. Изучайте вывод, пока не начнете хорошо понимать, когда и почему используется каждая функция-член управления копированием. По мере чтения вывода помните, что компилятор может обойти вызовы конструктора копий.

<p><image l:href="#reader.png"/>13.1.4. Правило три/пять</p>

Как уже упоминалось, существуют три базовых функции, контролирующих копирование объектов класса: конструктор копий, оператор присвоения копии и деструктор. Кроме того, как будет продемонстрировано в разделе 13.6, по новому стандарту класс может также определить конструктор перемещения и оператор присваивания при перемещении.

Определять все эти функции не обязательно: вполне можно определить один или два из них, не определяя все. Эти функции можно считать модулями. Если нужен один, не обязательно определять их все.

Классы, нуждающиеся в деструкторах, нуждаются в копировании и присвоении

Вот эмпирическое правило, используемое при принятии решения о необходимости определения в классе собственных версий функций-членов управления копированием: сначала следует решить, нужен ли классу деструктор. Зачастую потребность в деструкторе более очевидна, чем потребность в операторе присвоения или конструкторе копий. Если класс нуждается в деструкторе, он почти наверняка нуждается также в конструкторе копий и операторе присвоения копии.

Используемый в упражнениях класс 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 указывают на недопустимую память!

Память, на которую указывает указатель pq), больше недопустима. Она была возвращена операционной системе, когда был удален объект hp (или ret)!

Если класс нуждается в деструкторе, он почти наверняка нуждается также в операторе присвоения копии и конструкторе копий.

Классы, нуждающиеся в копировании, нуждаются также в присвоении, и наоборот
Перейти на страницу:

Похожие книги