Обычно операторы присвоения объединяют действия деструктора и конструктора копий. Подобно деструктору, оператор присвоения освобождает ресурсы левого операнда. Подобно конструктору копий, оператор присвоения копирует данные из правого операнда. Однако критически важно, чтобы эти действия осуществлялись в правильной последовательности, даже если объект присваивается сам себе. Кроме того, по возможности следует писать собственные операторы присвоения так, чтобы они оставляли левый операнд в корректном состоянии, иначе произойдет исключение (см. раздел 5.6.2).

В данном случае можно отработать случай присвоения самому себе (и сделать код устойчивым к исключению), осуществляя сначала копирование правого операнда. После копирования освобождается левый операнд и указатель модифицируется так, чтобы он указывал на вновь зарезервированную строку:

HasPtr& HasPtr::operator=(const HasPtr &rhs) {

 auto newp = new string(*rhs.ps); // скопировать строку

 delete ps; // освободить прежнюю память

 ps = newp; // копировать данные из rhs в этот объект

 i = rhs.i;

 return *this; // возвратить этот объект

};

В этом операторе присвоения, безусловно, сначала выполняется работа конструктора: инициализатор newp идентичен инициализатору ps в конструкторе копий класса HasPtr. Затем, как в деструкторе, удаляется строка, на которую в настоящее время указывает указатель ps. Остается только скопировать указатель на недавно созданную строку и значение типа int из rhs в этот объект.

Ключевая концепция. Операторы присвоения

Создавая оператор присвоения, следует учитывать два момента.

• Операторы присвоения должны работать правильно, если объект присваивается сам себе.

• Большинство операторов присвоения делят работу с деструктором и конструктором копий.

Шаблон разработки оператора присвоения подразумевает сначала копирование правого операнда в локальный временный объект. После копирования вполне безопасно удалить существующие члены левого операнда. Как только левый операнд будет освобожден, копировать данные из временного объекта в переменные-члены левого операнда.

Для иллюстрации важности принятия мер против присвоения самому себе рассмотрим, что случилось бы, выгляди оператор присвоения так:

// НЕПРАВИЛЬНЫЙ способ написания оператора присвоения!

HasPtr&

HasPtr::operator=(const HasPtr &rhs) {

 delete ps; // освобождает строку, на которую указывает этот объект

 // если rhs и *this - тот же объект, произойдет копирование удаленной

 // памяти!

 ps = new string(*(rhs.ps));

 i = rhs.i;

 return *this;

}

Если rhs и этот объект совпадают, удаление ps освободит строку, на которую указывают и *this, и rhs. При попытке копирования *(rhs.ps) в операторе new этот указатель указывает уже на недопустимую область памяти. Результат непредсказуем.

Для операторов присвоения критически важно работать правильно, даже если объект присваивается сам себе. Проще всего обеспечить это, скопировав правый операнд перед удалением левого.

Упражнения раздела 13.2.1

Упражнение 13.23. Сравните функции-члены управления копированием, написанные для решения упражнений предыдущего раздела, с кодом, представленным здесь. Убедитесь, что понимаете различия, если таковые вообще есть, между вашим кодом и приведенным в книге.

Упражнение 13.24. Что будет, если в версии класса HasPtr данного раздела не определен деструктор? Что если не определен конструктор копий?

Перейти на страницу:

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