Функция может возвращать объект в точку вызова. В качестве примера рассмотрим программу:
#include <iostream.h>
class myclass
{
int i;
public:
void set_i(int n) { i=n; }
int get_i() {return i;}
};
myclass f(); // возвращение объекта типа myclass
int main()
{
myclass c1;
c1 = f();
cout << c1.get_i() << "\n";
return 0;
}
myclass f()
{
myclass x;
x.set_i(1);
return x;
}
Когда функция возвращает объект, автоматически создается временный объект, содержащий возвращаемое значение. Именно этот объект фактически возвращается функцией. После того, как значение возвращено, этот объект уничтожается. Уничтожение временного объекта может вызывать неожиданные побочные эффекты в некоторых ситуациях. Например, если возвращаемый функцией объект имеет деструктор, освобождающий динамически зарезервированную память, то эта память будет освобождена даже в том случае, когда объект, получающий возвращаемое значение, будет продолжать использовать ее. Перегрузка оператора присваивания и определение конструктора копирования позволяют преодолеть эту проблему.
Если два объекта имеют один и тот же тип, то можно присваивать один объект другому. Это означает, что данные объекта с правой стороны равенства будут скопированы в данные объекта с левой стороны равенства. Например, следующая программа выводит значение 99:
#include <iostream.h>
class myclass
{
int i;
public:
void set_i(int n) { i=n; }
int get_i() { return i; }
};
int main()
{
myclass ob1, ob2;
ob1.set_i(99);
ob2 = ob1; // присвоение данных ob1 объекту ob2
cout << "this is ob2's i: " << ob2. get_i();
return 0;
}
По умолчанию все данные одного объекта присваиваются другому путем побитового копирования. Однако возможно перегрузить оператор присваивания и определить некоторые другие процедуры присваивания.
По умолчанию при инициализации одного объекта другим C++ выполняет побитовое копирование. Это означает, что точная копия инициализирующего объекта создается в целевом объекте. Хотя в большинстве случаев такой способ инициализации объекта является вполне приемлемым, имеются случаи, когда побитовое копирование не может использоваться. Например, такая ситуация имеет место, когда объект выделяет память при своем создании. Рассмотрим в качестве примера два объекта А и Вкласса ClassType, выделяющего память при создании объекта. Положим, что объект Ауже существует. Это означает, что объект Ауже выделил память. Далее предположим, что Аиспользовался для инициализации объекта В,как показано ниже:
ClassType В = А;
Если в данном случае используется побитовое копирование, то В станет точной копией А. Это означает, что В будет использовать тот же самый участок выделенной памяти, что и А, вместо того, чтобы выделить свой собственный. Ясно, что такая ситуация нежелательна. Например, если класс ClassType включает в себя деструктор, освобождающий память, то тогда одна и та же память будет освобождаться дважды при уничтожении объектов А и В.
Проблема того же типа может возникнуть еще в двух случаях. Первый из них возникает, когда копия объекта создается при передаче в функцию объекта в качестве аргумента. Второй случай возникает, когда временный объект создается функцией, возвращающей объект в качестве своего значения.
Для решения подобных проблем язык C++ позволяет создать конструктор копирования, который используется компилятором, когда один объект инициализирует другой. При наличии конструктора копирования побитовое копирование не выполняется. Общая форма конструктора копирования имеет вид:
имя_класса(const имя_класса &оbj)
{
тело конструктора
}
Здесь obj является ссылкой на объект в правой части инициализации. Конструктор копирования может иметь также дополнительные параметры, если для них определены значения по умолчанию. Однако в любом случае первым параметром должна быть ссылка на объект, выполняющий инициализацию.
Инициализация возникает в трех случаях: когда один объект инициализирует другой, когда копия объекта передается в функцию и когда создается временный объект (обычно он служит возвращаемым значением). Например, любая из следующих инструкций вызывает инициализацию:
myclass x = у; // инициализация
F(x); // передача параметра
У = F1(); // получение временного объекта
Ниже приведен пример, где необходим явный конструктор копирования. Эта программа создает очень простой «безопасный» тип массива целых чисел, предотвращающий вывод за границы массива. Память для каждого массива выделяется с использованием оператора new и в каждом объекте поддерживается работа с указателем на выделенную память.
#include <iostream.h>
#include <stdlib.h>
class array
{
int *p;
int size;
public:
array(int s) { p = new int[s]; if(!p) exit(1); size = s; }
~array() { delete [] p; }
array(const array &a);
void put(int i, int j) { if(i>=0 && i<size) p[i] = j; }
// создание другого массива и инициализация его значениями num
array x(num); // вызов конструктора копирования
for(i=0; i<10; i++) cout << x.get(i);
return 0;
}
Когда объект num используется для инициализации х, то вызывается конструктор копирования, выделяющий новую память, адрес которой помещается в х.р, а затем содержание массива num копируется в массив объекта х. Таким образом, объекты х и num содержат массивы с одинаковыми значениями, но каждый массив независим от другого и располагается в своей области памяти. Если бы конструктор копирования не был создан, то использовалась бы инициализация по умолчанию путем побитового копирования, так что массивы х и num разделяли бы между собой одну и ту же область памяти.
Конструктор копирования вызывается только для инициализации. Например, следующие инструкции не содержат вызова конструктора копирования:
array a(10);
array b(10);
b = а; // не вызывает конструктор копирования
В данном случае b=а выполняет операцию присваивания. Если оператор = не перегружен, то будет сделана побитовая копия. Поэтому в определенных случаях требуется перегрузить оператор = в дополнение к созданию конструктора копирования (перегрузка функций и операторов рассматривается в следующей главе).