Конструктор копирования (Tkuvmjrtmkj tkhnjkfgunx)

Перейти к навигации Перейти к поиску

Конструктором копирования (англ. 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&).
               // Для того чтобы создать объект a компилятор создаст временный объект класса 
               // X и после этого с помощью конструктора копирования создаст объект a.
               // Копирование временных объектов требует наличия const типа.

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

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();

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

Неявный конструктор копирования класса вызывает конструкторы копирования базовых классов и создаёт побитовые копии членов класса. Если членом класса является класс, то вызывается его конструктор копирования. Если это скалярный тип (POD-тип в Си++), то используется встроенный оператор присваивания. И наконец, если это массив, то каждый элемент массива копируется соответствующим их типу образом.[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).

Конструкторы копирования и шаблоны

[править | править код]

Шаблонный конструктор не является конструктором копирования[источник не указан 2757 дней].

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] Архивная копия от 8 июня 2007 на Wayback Machine
  2. INCITS ISO IEC 14882-2003 12.8.8. [2] Архивная копия от 8 июня 2007 на Wayback Machine