Передача в функции и возвращение объекта
Объекты можно передавать в функции в качестве аргументов точно так же, как передаются данные других типов. Всем этим мы уже пользовались, когда реализовывали методы класса_3d, в котором в качестве параметров методов projection(...), operator +(...) и operator = (...) передавали объект типа _3d.
Следует помнить, что С++ методом передачи параметров, по умолчанию является передача объектов по значению. Это означает, что внутри функции создается копия объекта - аргумента, и эта копия, а не сам объект, используется функцией. Следовательно, изменения копии объекта внутри функции не влияют на сам объект.
При передаче объекта в функцию появляется новый объект. Когда работа функции, которой был передан объект, завершается, то удаляется копия аргумента. И вот на что здесь следует обратить внимание. Как формируется копия объекта и вызывается ли деструктор объекта, когда удаляется его копия? То, что вызывается деструктор копии, наверное, понятно, поскольку объект (копия объекта, передаваемого в качестве параметра) выходит из области видимости. Но давайте вспомним, что объект внутри функции - это побитная копия передаваемого объекта, а это значит, что если объект содержит в себе, например, некоторый указатель на динамически выделенную область памяти, то при копировании создается объект, указывающий на ту же область памяти. И как только вызывается деструктор копии, где, как правило, принято высвобождать память, то высвобождается область памяти, на которую указывал объект-"оригинал", что приводит к разрушению исходного объекта.
class ClassName
{
public:
ClassName ()
{
cout << 'Работа конструктора \n';
}
~ClassName ()
{
cout << 'Работа деструктора \n';
}
};
void f (ClassName o)
{
cout << 'Работа функции f \n';
}
main()
{
ClassName c1;
f (c1);
}
Эта программа выполнит следующее
Работа конструктора
Работа функции f
Работа деструктора
Работа деструктора
Конструктор вызывается только один раз. Это происходит при создании с1. Однако деструктор срабатывает дважды: один раз для копии o, второй раз для самого объекта c1. Тот факт, что деструктор вызывается дважды, может стать потенциальным источником проблем, например, для объектов, деструктор которых высвобождает динамически выделенную область памяти.
Похожая проблема возникает и при использовании объекта в качестве возвращаемого значения.
Для того чтобы функция могла возвращать объект, нужно: во-первых, объявить функцию так, чтобы ее возвращаемое значение имело тип класса, во-вторых, возвращать объект с помощью обычного оператора return. Однако если возвращаемый объект содержит деструктор, то в этом случае возникают похожие проблемы, связанные с "неожиданным" разрушением объекта.
class ClassName {
public:
ClassName ()
{
cout << 'Работа конструктора \n';
}
~ClassName ()
{
cout << 'Работа деструктора \n';
}
};
ClassName f()
{
ClassName obj;
cout << 'Работа функции f \n';
return obj;
}
main()
{
ClassName c1;
c1 = f();
}
Эта программа выполнит следующее
Работа конструктора
Работа конструктора
Работа функции f
Работа деструктора
Работа деструктора
Работа деструктора
Конструктор вызывается два раза: для с1 и obj. Однако деструкторов здесь три. Как же так? Понятно, что один деструктор разрушает с1, еще один - obj. "Лишний" вызов деструктора (второй по счету) вызывается для так называемого временного объекта, который является копией возвращаемого объекта. Формируется эта копия, когда функция возвращает объект. После того, как функция возвратила свое значение, выполняется деструктор временного объекта. Понятно, что если деструктор, например, высвобождает динамически выделенную память, то разрушение временного объекта приведет к разрушению возвращаемого объекта.
Одним из способов обойти такого рода проблемы является создание особого типа конструкторов, - конструкторов копирования. Конструктор копирования или конструктор копии позволяет точно определить порядок создания копии объекта.
Любой конструктор копирования имеет следующую форму:
имя_класса (const имя_класса & obj)
{
... // тело конструктора
}
Здесь obj - это ссылка на объект или адрес объекта. (Подробнее использование ссылок рассмотрим немного позднее.) Конструктор копирования вызывается всякий раз, когда создается копия объекта. Мы уже рассмотрели два таких случая. Во-первых, при передаче объекта в качестве параметра функции. Во-вторых, при создании временного объекта тогда, когда функция в качестве возвращаемого значения использует объект. Есть еще один случай, когда полезен конструктор копирования, - это инициализация одного объекта другим.
class ClassName
{
public:
ClassName ()
{
cout << 'Работа конструктора \n';
}
ClassName (const ClassName& obj)
{
cout << 'Работа конструктора копирования\n';
}
~ClassName ()
{
cout << 'Работа деструктора \n';
}
};
main()
{
ClassName c1; // вызов конструктора
ClassName c2 = c1; // вызов конструктора копирования
}
Замечание: конструктор копирования не влияет на операцию присваивания.
Теперь, когда есть конструктор копирования, можно смело передавать объекты в качестве параметров функций и возвращать объекты. При этом количество вызовов конструкторов будет совпадать с количеством вызовов деструкторов, а поскольку процесс образования копий теперь стал контролируемым, существенно снизилась вероятность неожиданного разрушения объекта.
Помимо создания конструктора копирования есть другой, более элегантный и более быстрый способ организации взаимодействия между функцией и программой, передающей объект. Этот способ связан с передачей функцией не самого объекта, а его адреса.