Смекни!
smekni.com

Классы: копирование и присваивание (стр. 1 из 2)

Малышев Сергей Михайлович

В этой части мы продолжим начатое в статье "Элементы класса, о которых всегда необходимо помнить" обсуждение конструктора копий (copy constructor) и операции присваивания (assignment operator). Или, вернее, начнем подробное рассмотрение весьма нетривиальной проблемы, каковой на самом деле является копирование и присваивание в классах.

Эти два элемента вполне заслужили отдельного рассмотрения. Создание программ на C++ без понимания внутренней сущности этих функций-членов сродни бегу на марафонскую дистанцию без тренировки (возможно, это не самое удачное сравнение, проще говоря, эти функции очень важны).

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

Что означает <создать копию>? Как один из вариантов, это означает присваивание значений элементов одного объекта элементам другого. Этот ответ, однако, далеко не полон. C++ - это язык, который практически не ограничивает выбор пути реализации программы. И способ создания копий объектов - не исключение из этого правила.

Иногда для копирования классов достаточно просто привести один объект в то же состояние, что и другой. Это весьма просто, и мы увидим, как это делается. Однако, если вашему приложению требуются другие методы копирования, C++ не станет создавать их за вас, хотя, если вы не напишете эти функции, компилятор сделает это сам. Правда, результат при этом может существенно отличаться от того, что вам бы хотелось.

В серии этих статей мы рассмотрим все аспекты этого вопроса, по разделам:

Понятие копирования;

Копирование буквальное и развернутое;

Когда выполняется копирование;

Разница между копированием и присваиванием

Положение в классах

Блокирование копирования и присваивания

Реализация копирования через присваивание

Копирование и присваивание в производных классах

Понятие копирования

Здесь мы поговорим об одном из аспектов внутреннего функционирования программ, написанных на C++ - о копировании. Копирование в программах на C++ происходит, прямо или косвенно, буквально на каждом шагу. Причем, не всегда с первого взгляда очевидно, где происходит копирование, а где - нет.

Мы начнем с рассмотрения син-таксиса интересующего нас предмета, а затем попробуем углубиться в его осмысление.

Определение конструктора копий.

Конструктор копий используется для создания новых объектов из уже суще-ствующих. Это означает, что, так же как для других конструкторов, новый объект еще не существует к моменту его вызова. Однако только конструкто-ру копий объект передается как аргумент по ссылке. Итак, синтаксис кон-структора копий прост. Конструктор копий произвольного класса X выгля-дит так:

Х(const X&) ; // конструктор копий класса Х

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

Одно из основных правил: если аргумент не должен изменяться, то его следует передавать как константу. В то же время, если аргумент не описан как константа, то нельзя копировать объекты-константы. Переменный объект всегда можно передать как постоянный аргумент, но не наоборот.

Вторая часть объявления аргумента, X, проста: копируется объект того же самого типа. Аргумент в целом читается как "постоянная ссылка на X". Ссылка существенна по нескольким соображениям. В первую очередь пото-му, что при передаче адреса объекта не создается копия вызывающего объ-екта (в отличие от передачи аргумента по значению).

Если вам чудится здесь какой-то подвох, то будьте внимательны. Работа конструктора копий - создание ранее не существовавшего объекта из уже существующего, а передача по значению (без использования операции получения адреса) требует создания копии аргумента, значит мы получаем бесконечную рекурсию.

Точнее: при передаче объекта по значению создает-ся его копия, если это произойдет в конструкторе копий, то он будет вызы-вать сам себя, пока не исчерпает все ресурсы системы.

Вот несколько правил, которым надо следовать при объявлениях конструк-тора копий:

Имя функции точно совпадает с именем класса.

Аргумент объявляется постоянным, что позволяет принимать как по-стоянные, так и переменные аргументы.

Тип аргумента является типом класса.

Аргумент передается по ссылке, т. е. с помощью операции получения адреса.

Написание конструктора копий является чрезвычайно ответственным поступком. Явное определение конструктора копий вызывает изменения в работе программы. Пока мы не пытались переопределить конструктор копий, исправно работал конструктор, порождаемый компилятором автоматически.

Этот конструктор создавал "фотографические" копии объектов, то есть копировал значения абсолютно всех данных-членов, в том числе и ненулевые значения указателей, представляющие адреса динамических областей памяти.

С момента появления переопределённой версии конструктора копий, вся работа по реализации алгоритмов копирования возлагается на программиста. Впрочем, заставить конструктор копий копировать объекты совсем несложно. Создадим класс, описывающий точку на плоскости.

class POINT

{

public:

POINT() { X=0; Y=0; } //конструктор по умолчанию

POINT(int a, int b) { X=a; Y=b; } //еще конструктор

POINT(const POINT& Pixel) { X=Pixel.X; Y=Pixel.Y; } //конструктор

//копирования

int X; //координаты точки

int Y;

};

Как видим, этот конструктор копий просто копирует значения координат. В принципе, если бы мы его не определили, то компилятор создал бы его сам, причем в этом случае он делал бы то же самое. Но конструктор, создающий подобные копии объектов, скорее всего, окажется непригодным для работы с объектами, содержащими в качестве членов указатели или ссылки.

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

В переопределяемом конструкторе копий (а в классе он может быть только один) можно реализовывать разнообразные алгоритмы распределения памяти. Здесь всё зависит от программиста.

Итак, обычно, конструктор копий, созданный компилятором, удовлетворителен при следующих обстоятельствах:

Среди членов класса нет указателей (*).

Среди членов класса нет ссылок (&).

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

Вот пример:

class X

{

public:

Х(); // конструктор по умолчанию

virtual ~X(); // виртуальный деструктор

// Конструктор копии и операция присваивания не определены

// намеренно. Класс содержит только данные, размещаемые

// в стеке, поэтому предопределенных конструктора копий

//и операции присваивания достаточно.

private:

int data;

char moreData;

float no_Pointers;

};

Если хотя бы одно из названных условий не выполняется, то следует опре-делить как конструктор копий, так и операцию присваивания.

Определение операции присваивания

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

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

Для произвольного класса X мы имеем следующий синтаксис операции присваивания:

X& operator=(const X&); // синтаксис операции присваивания для

// произвольного класса

Присваивание - это операция, значит мы должны использовать ключевое слово operator и соответствующий символ операции. Так как C++ допускает цепочки присваивания а = b = с = d;

// C++ допускает последовательные присваивания,

// так что это свойство надо сохранить,

то необходимо возвращать ссылку на объект; в противном случае цепочка прервется.

Итак, оператор-функция принимает постоянную ссылку, а возвращает ссылку на объект. Использование ключевого слова const позволяет функции работать как с постоянными объектами, так и с переменными.

Определяя новый класс, если вы решили объявить операцию присваивания, следуйте следующим рекомендациям:

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

Она принимает постоянную ссылку на объект типа того же класса.

Она возвращает ссылку на объект типа того же класса.

Проверка на присваивание самому себе.

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

POINT Pix;

Pix = Pix; // присваивание самому себе

Это самый тривиальный случай, он хорош для приведения примера, не более. В реальных программах такого обычно не бывает и эта ошибка, как правило, принимает далеко не столь очевидные обличия.

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