Классы: копирование и присваивание

Малышев Сергей Михайлович
Вэтой части мы продолжим начатое в статье «Элементы класса, о которых всегданеобходимо помнить» обсуждение конструктора копий (copy constructor) иоперации присваивания (assignment operator). Или, вернее, начнем подробноерассмотрение весьма нетривиальной проблемы, каковой на самом деле являетсякопирование и присваивание в классах.
Этидва элемента вполне заслужили отдельного рассмотрения. Создание программ на C++без понимания внутренней сущности этих функций-членов сродни бегу намарафонскую дистанцию без тренировки (возможно, это не самое удачное сравнение,проще говоря, эти функции очень важны).
Конструкторкопий служит для создания новых объектов из существующих. Операция присваиваниянужна для того, чтобы сделать один существующий объект эквивалентным другомусуществующему.
Чтоозначает ? Как один из вариантов, это означаетприсваивание значений элементов одного объекта элементам другого. Этот ответ, однако,далеко не полон. C++ — это язык, который практически не ограничивает выбор путиреализации программы. И способ создания копий объектов — не исключение из этогоправила.
Иногдадля копирования классов достаточно просто привести один объект в то жесостояние, что и другой. Это весьма просто, и мы увидим, как это делается.Однако, если вашему приложению требуются другие методы копирования, C++ не станетсоздавать их за вас, хотя, если вы не напишете эти функции, компилятор сделаетэто сам. Правда, результат при этом может существенно отличаться от того, чтовам бы хотелось.
Всерии этих статей мы рассмотрим все аспекты этого вопроса, по разделам:
Понятиекопирования;
Копированиебуквальное и развернутое;
Когдавыполняется копирование;
Разницамежду копированием и присваиванием
Положениев классах
Блокированиекопирования и присваивания
Реализациякопирования через присваивание
Копированиеи присваивание в производных классах
Понятиекопирования
Здесьмы поговорим об одном из аспектов внутреннего функционирования программ, написанныхна C++ — о копировании. Копирование в программах на C++ происходит, прямо иликосвенно, буквально на каждом шагу. Причем, не всегда с первого взглядаочевидно, где происходит копирование, а где — нет.
Мыначнем с рассмотрения син-таксиса интересующего нас предмета, а затем попробуемуглубиться в его осмысление.
Определениеконструктора копий.
Конструкторкопий используется для создания новых объектов из уже суще-ствующих. Этоозначает, что, так же как для других конструкторов, новый объект еще несуществует к моменту его вызова. Однако только конструкто-ру копий объектпередается как аргумент по ссылке. Итак, синтаксис кон-структора копий прост.Конструктор копий произвольного класса X выгля-дит так:
Х(constX&); // конструктор копий класса Х
Таккак конструктор копий — это все таки конструктор, то он должен иметь имя, совпадающеес именем класса (не забывайте — с учетом регистра символов). Назначениеконструктора копий — дублирование объекта-аргумента для построения новогообъекта.
Одноиз основных правил: если аргумент не должен изменяться, то его следуетпередавать как константу. В то же время, если аргумент не описан как константа,то нельзя копировать объекты-константы. Переменный объект всегда можно передатькак постоянный аргумент, но не наоборот.
Втораячасть объявления аргумента, X, проста: копируется объект того же самого типа.Аргумент в целом читается как «постоянная ссылка на X». Ссылкасущественна по нескольким соображениям. В первую очередь пото-му, что припередаче адреса объекта не создается копия вызывающего объ-екта (в отличие отпередачи аргумента по значению).
Есливам чудится здесь какой-то подвох, то будьте внимательны. Работа конструкторакопий — создание ранее не существовавшего объекта из уже существующего, апередача по значению (без использования операции получения адреса) требуетсоздания копии аргумента, значит мы получаем бесконечную рекурсию.
Точнее:при передаче объекта по значению создает-ся его копия, если это произойдет вконструкторе копий, то он будет вызы-вать сам себя, пока не исчерпает всересурсы системы.
Вотнесколько правил, которым надо следовать при объявлениях конструк-тора копий:
Имяфункции точно совпадает с именем класса.
Аргументобъявляется постоянным, что позволяет принимать как по-стоянные, так ипеременные аргументы.
Типаргумента является типом класса.
Аргументпередается по ссылке, т. е. с помощью операции получения адреса.
Написаниеконструктора копий является чрезвычайно ответственным поступком. Явноеопределение конструктора копий вызывает изменения в работе программы. Пока мыне пытались переопределить конструктор копий, исправно работал конструктор, порождаемыйкомпилятором автоматически.
Этотконструктор создавал «фотографические» копии объектов, то естькопировал значения абсолютно всех данных-членов, в том числе и ненулевыезначения указателей, представляющие адреса динамических областей памяти.
Смомента появления переопределённой версии конструктора копий, вся работа пореализации алгоритмов копирования возлагается на программиста. Впрочем, заставитьконструктор копий копировать объекты совсем несложно. Создадим класс, описывающийточку на плоскости.
classPOINT
{
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; } //конструктор
//копирования
intX; //координаты точки
intY;
};
Каквидим, этот конструктор копий просто копирует значения координат. В принципе, еслибы мы его не определили, то компилятор создал бы его сам, причем в этом случаеон делал бы то же самое. Но конструктор, создающий подобные копии объектов, скореевсего, окажется непригодным для работы с объектами, содержащими в качествечленов указатели или ссылки.
Предполо-жим,что класс содержит указатели. Тогда адреса, содержащиеся в указате-ляхобъекта-оригинала и объекта-копии, будут идентичны. Это означает, что дваобъекта будут указывать на одну и ту же область памяти, что, как прави-ло, оченьопасно. Это мы рассмотрим подробно несколько позднее.
Впереопределяемом конструкторе копий (а в классе он может быть только один)можно реализовывать разнообразные алгоритмы распределения памяти. Здесь всёзависит от программиста.
Итак,обычно, конструктор копий, созданный компилятором, удовлетворителен приследующих обстоятельствах:
Средичленов класса нет указателей (*).
Средичленов класса нет ссылок (&).
Вэтом случае вполне разумно использовать конструктор, построенный компилятором.В текст определения класса в этом случае полезно поместить соответст-вующийкомментарий о том, что конструктор копии не определен вполне сознательно.Вообще говоря, комментарии относи-тельно рассматриваемых четырех членов уместныв каждом классе.
Вотпример:
classX
{
public:
Х();// конструктор по умолчанию
virtual~X(); // виртуальный деструктор
//Конструктор копии и операция присваивания не определены
//намеренно. Класс содержит только данные, размещаемые
//в стеке, поэтому предопределенных конструктора копий
//иоперации присваивания достаточно.
private:
int data;
char moreData;
float no_Pointers;
};
Еслихотя бы одно из названных условий не выполняется, то следует опре-делить какконструктор копий, так и операцию присваивания.
Определениеоперации присваивания
Пофункциональному назначению операция присваивания очень похожа на конструкторкопий. Принципиальное отличие состоит в том, что конструктор копий создаетновый (возможно, временный) объект, а операция присваивания работает с ужесозданными. Вызывающий объект является левым операндом, объект-аргумент — правым.
Операцияприсваивания также имеет соответствующий синтаксис. Операция присваивания — этофункция-член и одновременно двухместная операция. Следовательно, в работувовлечены два объекта. Первый объект — вызывающий, доступный по указателю this,а второй — это аргумент. Как конструктор копий, так и операция присваиванияиспользуют в качестве аргумента постоянную ссылку.
Дляпроизвольного класса X мы имеем следующий синтаксис операции присваивания:
X&operator=(const X&); // синтаксис операции присваивания для
//произвольного класса
Присваивание- это операция, значит мы должны использовать ключевое слово operator исоответствующий символ операции. Так как C++ допускает цепочки присваивания а =b = с = d;
//C++ допускает последовательные присваивания,
//так что это свойство надо сохранить,
тонеобходимо возвращать ссылку на объект; в противном случае цепочка прервется.
Итак,оператор-функция принимает постоянную ссылку, а возвращает ссылку на объект.Использование ключевого слова const позволяет функции работать как спостоянными объектами, так и с переменными.
Определяяновый класс, если вы решили объявить операцию присваивания, следуйте следующимрекомендациям:
Операцияприсваивания должна быть членом класса.
Онапринимает постоянную ссылку на объект типа того же класса.
Онавозвращает ссылку на объект типа того же класса.
Проверкана присваивание самому себе.
Воперации присваивания для любого класса надо учитывать один важный момент.Всегда надо проверять: не происходит ли присваивания самому себе. Оно можетиметь место в том случае, когда объект прямо или косвенно вызывает операциюприсваивания для себя. Прямое присваивание может выглядеть следующим образом:
POINTPix;
Pix= Pix; // присваивание самому себе
Этосамый тривиальный случай, он хорош для приведения примера, не более. В реальныхпрограммах такого обычно не бывает и эта ошибка, как правило, принимает далеконе столь очевидные обличия.
Присваиваниесамому себе порож-дает в программе утечки памяти. Такая ситуация можетвозникнуть, когда два объекта сообща используют некоторый ресурс и один из нихэтот ресурс освобождает. При этом состояние ресурса становится неопределенным, новторой объект продолжает на него ссылаться.
Естьмного путей, ведущих к возникновению этой проблемы. Для ее предотвращения иследует предусмотреть в операции присваивания проверку на присваивание самомусебе. Она очень проста и выглядит всегда совершенно одинаково:
POINT&POINT::operator=(const POINT& rhs)
{
if(this== &rhs) return *this; // проверка на присваивание себе
else{ X=rhs.X; Y=rhs.Y; } //то, что делает оператор полезного
return*this; // возврат ссылки на объект
}
Сейчасмы попробуем в ней разобраться, благо для всех операций присваивания проверкана присваивание себе со-вершенно одинакова. Оператор if(this == &rhs)проверяет, не совпадает ли аргумент с самим объектом. Указатель this со-держитадрес вызывающего объекта, &rhs читается как «адрес rhs».
Такимобразом, сравниваются два адреса. Если они эквивалентны (==), то это один и тотже объект. В этом случае, в полном соответствии с требованием воз-врата ссылкина объект, просто возвращаем *this (заметьте, что в конце функции делается тоже самое) и выходим из функции.
Вспомните,что this — это указатель. Значение указателя — это адрес. Чтобы получитьзначение указателя, его следует разыменовывать. Разыменование указателявыглядит так: *ptr. Указатель this разыменовывается точно так же: *this.
Помещаяэти две строки в начале и в конце тела операции присваивания, мы уменьшаемвероятность возникновения утечек памяти из-за этой опера-ции. Если вы запомнилиприведенный здесь синтаксис копирования и при-сваивания и, определяя новый класс,сразу будете определять и их тоже, то это уже полдела.
ЗачемC++ требует определения этих функций-членов?
ЯзыкC++ не слишком сильно ограничивает свободу программистов в методах разработкипрограммного обеспечения. В частности, он не навязывает вам способы копированияи присваивания. Количество и разнообразие ситуаций, в которых происходиткопирование объектов, удивительно велико. Для очень простых объектов, состоящихиз одного-двух элементов, затраты на копирование незначительны, но для болеесложных, таких как графический интерфейс пользователя или комплексные типыданных, оперирующие с динамической памятью, издержки на копирование существенновозрастают.
Во-первых,имейте в виду, что если вы не определите для нового класса конструктор копий, тоC++ создаст его сам. Причина заключается в том, что компилятору самому можетпотребоваться возможность создания копий, значит, эти две функции обязаны бытьопределены.
Во-вторых,вам может потребоваться заблокировать копирование, либо вести подсчет ссылок, илиеще что-нибудь. Если вы не создадите эти функции, то C++ создаст для них версиипо умолчанию.
Создаваемыекомпилятором версии обеих этих функций не всегда будут вас удовлетворять.Версии компилятора выполняют буквальное, или поразряд-ное, копирование. Внекоторых случаях это неразумно. Не пожалейте времени на изучение ситуаций, которыемогут вам встретиться при разработке программ.
Вотлишь некоторые из бесчисленного множества возможных ситуаций, в которыхпроисходит копирование:
POINTх;
POINTу(х); // Прямой вызов конструктора копий.
POINTх = у; // Выглядит как присваивание, но на самом деле
//вызывает конструктор копий. Почему? См. ниже.
POINTa, b;
a= b; // Вызов операции присваивания
POINTFoo(); // Возврат по значению, вызывает копирование
voidFoo(POINT); // Передача по значению, создает копию
Вовсех этих случаях выполняется копирование. В ходе выполняемой компиляторомоптимизации могут появиться и другие варианты. Это та область, где знаниедействительно сила, способная помочь вам избежать утечек памяти.
Воператоре типа POINT х = у; не вызывается операция присваивания класса POINT, хотяна первый взгляд выглядит это именно так. Причина состоит в том, что операцияприсваивания — это функция-член, а значит может быть вызвана только для ужесуществующих объектов, в то время как в этом фрагменте происходит созданиенового объекта х.
Еслиобъект создается в той же строке, в которой он выступает в качествелевостороннего аргумента, то вызывается конструктор. Строка
Хх = у; // вызов конструктора копий
эквивалентнастроке
Хх(у); // вызов конструктора копий
БИКЮ, что совсем не то же самое, что
Хх, у;
х= у; // вызов операции присваивания
Вамследует понимать, что же на самом деле вызывается, когда и почему. Это одна изтех особенностей, благодаря которым C++ труднее и интерес-нее, чем С. Впредыдущем разделе мы пришли к заключению, что не стоит определять операциюприсваивания без конструктора копий и наоборот.
Следовательно,напрашивается вывод, что основные рекомендации для операции присваиваниясправедливы также и для конструктора копий.
Наэтом, пожалуй пока и остановимся. Небольшое резюме напоследок.
Есликласс содержит указатели или ссылки, то скорее всего вам придется определятьоперацию присваивания и конструктор копий для этого класса самостоятельно, неполагаясь на компилятор. В противном случае можно спокойно использо-ватьсозданные компилятором присваивание и копирование, но при этом по-лезноупомянуть об этом в комментариях к классу.
Список литературы
P.Kimmel Using Borland C++ 5 Special Edition перевод BHV- С.Петербург 1997
C++. Бархатный путь Марченко А.Л.  Центр ИнформационныхТехнологий
www.citmgu.ru
Thinking in C++, 2nd ed. Volume 1 c2000by Bruce Eckel
Дляподготовки данной работы были использованы материалы с сайта my-pc.jino.ru/