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

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

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

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

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

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

Копирование объектов выполняется за счет использования конструктора копирования и оператора присваивания. Конструктор копирования в качестве первого параметра (типа 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 A> Array::Array(const Array& copy)
  : size(copy.size()), data(new int[copy.size()]) 
{
   std::copy(copy.begin(),copy.end(),data);
}

Явный, т.е. не шаблонный, конструктор копирования должен также реализовывать массив (Array) на основе массива (Array).

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

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

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