Конструктор копирования

Материал из Википедии — свободной энциклопедии
Перейти к: навигация, поиск

Конструктором копирования (англ. copy constructor) называется специальный конструктор в языке программирования C++ и в некоторых других языках программирования, например, Java, применяемый для создания нового объекта как копии уже существующего. Такой конструктор принимает как минимум один аргумент: ссылку на копируемый объект.

Обычно компилятор автоматически создает конструктор копирования для каждого класса (известные как неявные конструкторы копирования, то есть конструкторы копирования, заданные неявным образом), но в некоторых случаях программист создает конструктор копирования, называемый в таком случае явным конструктором копирования (или «конструктором копирования, заданным явным образом»). В подобных случаях компилятор не создает неявные конструкторы.

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

Определение[править | править вики-текст]

Копирование объектов выполняется за счёт использования конструктора копирования и оператора присваивания. Конструктор копирования в качестве первого параметра (типа const или volatile) принимает ссылку на собственный тип класса. Кроме этого параметра, он может иметь еще дополнительные параметры, при условии, что для таких дополнительных параметров заданы значения по умолчанию.[1] Следующий пример демонстрирует корректные конструкторы копирования для класса X:

X(const X&);
X(X&);
X(const volatile X&);
X(volatile X&);
X(const X&, int = 10);
X(const X&, double = 1.0, int = 40);

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

X a = X();     // корректно, если есть X(const &X) и не корректно, если есть X(X&)
               // так как второй вариант не поддерживает переменные

Другое отличие между ними довольно наглядно:

const X a;
X b = a;       // корректно, если есть X(const X&) и не корректно, если есть X(X&)
               // так как второй не поддерживает тип const X&

Вид X& конструктора копирования используется когда необходимо изменить копируемый объект. Это довольно редкая ситуация, но она предусмотрена в стандартной библиотеке вызовом std::auto_ptr. Ссылка должна реализовывать:

X a;
X b = a;       // корректно, если определен любой из конструкторов копирования
               // с момента передачи ссылки

Следующие конструкторы копирования (или постоянные конструкторы) некорректны:

X(X);
X(const X);

так как вызов этих конструкторов потребует очередного копирования, что приведет к бесконечному рекурсивному вызову (то есть бесконечный цикл).

Существует четыре случая вызова конструктора копирования:

  1. Когда объект является возвращаемым значением
  2. Когда объект передается (функции) по значению в качестве аргумента
  3. Когда объект конструируется на основе другого объекта (того же класса)
  4. Когда компилятор генерирует временный объект (как в первом и втором случаях выше; как явное преобразование и т. д.)

Операции[править | править вики-текст]

Объекту может быть присвоено значение при помощи одного из двух способов:

  • Явное присваивание в выражении
  • Инициализация

Явное присваивание в выражении[править | править вики-текст]

Object A;
Object B;
A = B;       // транслируется как Object::operator=(const Object&), таким образом вызывается A.operator=(B)

Инициализация[править | править вики-текст]

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

a. При помощи объявления

Object B = A; // транслируется как Object::Object(const Object&)

b. При помощи аргументов функции

type function (Object a);

c. При помощи возвращаемого значения функции

Object a = function();

Конструктор копирования используется только в последнем случае (инициализации) и не используется вместо присваивания (то есть там, где используется оператор присваивания).

Неявный конструктор копирования класса вызывает базовые конструкторы копирования и копии их членов, соответствующие их типу. Если это тип класса, то вызывается конструктор копирования. Если это скалярный тип, то используется встроенный оператор присваивания. И наконец, если это массив, то каждый элемент копируется соответствующим их типу образом.[2]

Применением явного конструктора копирования программист может определить дальнейшие действия после копирования объекта.

Примеры[править | править вики-текст]

Следующие примеры иллюстрируют работу конструкторов копирования и их необходимость.

Неявный конструктор копирования[править | править вики-текст]

#include <iostream>
 
class Person
{
    public:
        int age;
        Person(int age) : age(age) {}
};
 
int main()
{
    Person timmy(10);
    Person sally(15);
 
    Person timmy_clone = timmy;
 
    std::cout << timmy.age << " " << sally.age << " " << timmy_clone.age << std::endl;
 
    timmy.age = 23;
 
    std::cout << timmy.age << " " << sally.age << " " << timmy_clone.age << std::endl;
}

Результат

10 15 10
23 15 10

Как и ожидалось, timmy скопировался в новый объект timmy_clone. При изменении возраста (age) timmy, у timmy_clone возраст не менялся: объекты полностью независимы.

Компилятор сгенерировал для нас конструктор копирования, который может быть записан примерно так:

Person(Person const& copy)
  : age(copy.age) {}

Явный конструктор копирования[править | править вики-текст]

Следующий пример показывает один простой класс динамических массивов:

#include <iostream>
 
class Array
{
  public:
    int size;
    int* data;
 
    Array(int size)
        : size(size), data(new int[size]) {}
 
    ~Array() 
    {
        delete[] data;
    }
};
 
int main()
{
    Array first(20);
    first.data[0] = 25;
 
    {
        Array copy = first;
 
        std::cout << first.data[0] << " " << copy.data[0] << std::endl;
 
    }    // (1)
 
    first.data[0] = 10;    // (2)
}

Результат

25 25
Segmentation fault

Здесь компилятор сгенерировал конструктор копирования автоматически. Этот конструктор выглядит примерно так:

Array(Array const& copy)
  : size(copy.size), data(copy.data) {}

Проблема, связанная с этим конструктором, заключается в том, что он выполняет простое копирование указателя data. Он только копирует адрес, но не сами данные. И когда программа доходит до строчки (1), вызывается деструктор copy (объекты в стеке уничтожаются автоматически при достижении их границ). Как видно, деструктор Array удаляет массив data, поэтому когда он удаляет данные copy, он также удаляет данные first. Строка (2) теперь получает неправильные данные и записывает их. Это и приводит к знаменитой ошибке сегментации (segmentation fault).

В случае собственного конструктора копирования, выполняющего глубокое копирование, этой проблемы не возникнет:

Array(Array const& copy)
  : size(copy.size), data(new int[copy.size]) 
{
    std::copy(copy.data, copy.data + copy.size, data);    // #include <algorithm> для std::copy
}

Здесь создаётся новый массив int и содержимое копируется в него. Теперь деструктор copy только удалит его данные и не тронет данные first. Строка (2) больше не вызывает ошибку сегментации.

Вместо выполнения глубокого копирования можно использовать несколько оптимизирующих стратегий. Это позволит безопасным способом разрешить доступ к данным для нескольких объектов, тем самым экономя память. Стратегия копирование при записи создает копию данных только когда их записывает. Счетчик ссылок содержит счетчик количества объектов ссылающихся на данные и удаляет его только тогда, когда счетчик доходит до нуля (например, boost::shared_ptr).

Конструкторы копирования и шаблоны[править | править вики-текст]

Шаблонный конструктор не является конструктором копирования.

template <typename T> Array::Array(const T& copy)
  : size(copy.size()), data(new int[copy.size()]) 
{
   std::copy(copy.begin(),copy.end(),data);
}

Этот конструктор не будет использоваться, если Т это тип Array.

Array arr(5);
Array arr2(arr); /* здесь будет вызван, или нешаблонный конструктор копирования, или, в случае его отсутствия, конструктор копирования, определённый по умолчанию */

См. также[править | править вики-текст]

Примечания[править | править вики-текст]

  1. INCITS ISO IEC 14882-2003 12.8.2. [1]
  2. INCITS ISO IEC 14882-2003 12.8.8. [2]