Конструктор копирования
Конструктор копирования.
Прежде чем обсуждать "загадочный" конструктор копирования, давайте поговорим о простых истинах. Итак.
Передача объекта в функцию.
Объекты класса можно передавать в функции в качестве аргументов точно так же, как передаются данные других типов. Однако, следует помнить, что в языках С и С++ методом передачи параметров, по умолчанию является передача объектов по значению. Это означает, что внутри функции создается копия объекта - аргумента, и эта копия, а не сам объект, используется функцией. Следовательно, изменения копии объекта внутри функции не влияют на сам объект.
Вот несколько утверждений, которые характеризуют действия, происходящие с объектом в этом случае:
При передаче объекта в функцию появляется новый объект. Когда работа функции, которой был передан объект, завершается, то удаляется копия аргумента.
Когда удаляется копия объекта, вызывается деструктор копии, поскольку эта копия выходит из своей области видимости.
Объект внутри функции - это побитовая копия передаваемого объекта, а это значит, что если объект содержит в себе, например, некоторый указатель на динамически выделенную область памяти, то при копировании создается объект, указывающий на ту же область памяти. И как только вызывается деструктор копии, где, как правило, принято высвобождать память, то высвобождается область памяти, на которую указывал объект-"оригинал", что приводит к разрушению исходного объекта.
# include <iostream> using namespace std; class ClassName { public: ClassName () { cout << "ClassName!!!\n"; } ~ClassName () { cout << "~ClassName!!!\n"; } }; void f (ClassName o) { cout << "Function f!!!\n"; } void main() { ClassName c1; f(c1); } Результат работы программы: ClassName!!! Function f!!! ~ClassName!!! ~ClassName!!! |
Конструктор вызывается только один раз. Это происходит при создании с1. Однако деструктор срабатывает дважды: один раз для копии o, второй раз для самого объекта c1. Тот факт, что деструктор вызывается дважды, может стать потенциальным источником проблем, например, (как уже говорилось) для объектов, деструктор которых высвобождает динамически выделенную область памяти.
Возврат объекта из функции.
Похожая проблема возникает и при использовании объекта в качестве возвращаемого значения.
Для того чтобы функция могла возвращать объект, нужно: во-первых, объявить функцию так, чтобы ее возвращаемое значение имело тип класса, во-вторых, возвращать объект с помощью обычного оператора return. Однако если возвращаемый объект содержит деструктор, то в этом случае возникают проблемы, связанные с "неожиданным" разрушением объекта.
# include <iostream> using namespace std; class ClassName { public: ClassName () { cout << "ClassName!!!\n"; } ~ClassName () { cout << "~ClassName!!!\n"; } }; ClassName f() { ClassName obj; cout << "Function f\n"; return obj; } void main() { ClassName c1; f(); } Результат работы программы: ClassName!!! ClassName!!! Function f ~ClassName!!! ~ClassName!!! ~ClassName!!! |
Конструктор вызывается два раза: для с1 и obj. Однако деструкторов здесь три. Как же так? Ясно, что один деструктор разрушает с1, еще один - obj. "Лишний" вызов деструктора (второй по счету) вызывается для так называемого временного объекта, который является копией возвращаемого объекта. Формируется эта копия, когда функция возвращает объект. После того, как функция возвратила свое значение, выполняется деструктор временного объекта. Естественно, что если деструктор, например, высвобождает динамически выделенную память, то разрушение временного объекта приведет к разрушению возвращаемого объекта.
Инициализация одного объекта другим при создании.
В программировании есть еще один случай побитового копирования - инициализация одного объекта другим при создании:
# include <iostream> using namespace std; class ClassName { public: ClassName () { cout << "ClassName!!!\n"; } ~ClassName () { cout << "~ClassName!!!\n"; } }; void main() { ClassName c1; // Вот он!!! Момент побитового копирования. ClassName c2=c1; } Результат работы программы: ClassName!!! ~ClassName!!! ~ClassName!!! |
Конструктор вызывается один раз: для с1. Для с2 конструктор не срабатывает. Однако деструктор срабатывает для обоих объектов. А, поскольку, с2 является точной копией с1, деструктор, высвобождающий динамически выделенную память, вызывается дважды для одного и того же фрагмента этой памяти. Это неминуемо приведет к ошибке.
Решение проблемы.
Одним из способов обойти такого рода проблемы является создание особого типа конструкторов, - конструкторов копирования. Конструктор копирования или конструктор копии позволяет точно определить порядок создания копии объекта. Любой конструктор копирования имеет следующую форму:
имя_класса (const имя_класса & obj) { ... // тело конструктора } |
Здесь obj - это ссылка на объект или адрес объекта. Конструктор копирования вызывается всякий раз, когда создается копия объекта. Таким образом, в конструкторе копирования можно выделить "свою" память под новый объект.
# include <iostream> using namespace std; class ClassName { public: ClassName () { cout << "ClassName!!!\n"; } ClassName (ClassName&obj){ cout << "Copy ClassName!!!\n"; } ~ClassName () { cout << "~ClassName!!!\n"; } }; void f(ClassName o){ cout<<"Function f!!!\n"; } ClassName r(){ ClassName o; cout<<"Function r!!!\n"; return o; } void main() { // инициализация одного объекта другим ClassName c1; ClassName c2=c1; // передача объекта в функцию ClassName a; f(a); //возврат объекта из функции r();
} Результат работы программы: // создался объект с1 ClassName!!! // инициализация объекта с2 объектом с1 Copy ClassName!!! // создался объект а ClassName!!! // передача а в функцию по значению // создалась копия о Copy ClassName!!! // отработала функция f Function f!!! // уничтожилась копия o ~ClassName!!! // создался объект o // внутри функции r ClassName!!! // отработала функция r Function r!!! // возврат из функции // создалась копия объекта о Copy ClassName!!! // уничтожился объект o ~ClassName!!! // уничтожилась его копия ~ClassName!!! // уничтожился объект а ~ClassName!!! // уничтожился объект с2 ~ClassName!!! // уничтожился объект с1 ~ClassName!!! |
Примечание: Конструктор копирования не влияет на операцию присваивания вида A=B. Здесь также срабатывает побитовое копирование, однако эту проблему решают в С++ иначе.
Теперь, когда есть конструктор копирования, можно смело передавать объекты в качестве параметров функций и возвращать объекты. При этом количество вызовов конструкторов будет совпадать с количеством вызовов деструкторов, а поскольку процесс образования копий теперь стал контролируемым, существенно снизилась вероятность неожиданного разрушения объекта.
Примечание: Кстати!!! Помимо создания конструктора копирования есть другой способ организации взаимодействия между функцией и программой, передающей объект. Этот способ - передача объекта по ссылке или по указателю.
Конструктор копирования