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

Матеріал з Вікіпедії — вільної енциклопедії.
Перейти до навігації Перейти до пошуку

Конструктор копіювання — особливий конструктор в мові програмування C++, який використовується для створення нових об'єктів як копії існуючого об'єкта. Першим аргументом такого коструктора є посилання (константне або ні) на об'єкт того ж типу, що й тип об'єкта який ми конструюємо, за цим параметром можуть іти інші будь-яких типів, але обов'язково із значеннями за замовчанням.

Зазвичай компілятор самостійно створює коструктор копіювання для кожного класу (відомий як уставний (англ. default) конструктор копіювання), але при потребі програміст створює конструктор копіювання, відомий як користувачевий або визначений користувачем конструктор копіювання. В таких випадках компілятор не створює його.

Користувацький конструктор копіювання здебільшого потрібен, коли об'єкт має вказівники або неспільні посилання, такі як файл, в цьому випадку деструктор і оператор присвоювання також мають бути написані (дивись Правило трьох).

Визначення[ред. | ред. код]

Копіювання об'єктів досягається через використання конструктора копіювання і оператора присвоювання. Перший параметр конструктора копіювання це посилання (можливо const або volatile) на його власний тип класу. Він може мати більше аргументів, але інші мають мати значення за замовчанням.[1] В наступному прикладі наводяться правильні конструктори для класу X:

X(const X& copyFromMe);
X(X& copyFromMe);
X(const volatile X& copyFromMe);
X(volatile X& copyFromMe);
X(const X& copyFromMe, int = 10);
X(const X& copyFromMe, double = 1.0, int = 40);
X a = X();     // в даному випадку, завдяки оптимізації, одразу створюється об'єкт а 
               // без використання конструктора копіювання,
               // хоча може бути створений тимчасовий константний об'єкт із 
               // використанням звичайного конструктора X, 
               // який потім, за допомогою конструктора копіювання, 
               // ініціалізує a як копію тимчасового об'єкта.

Розглянемо різницю між першим і другим конструкторами копіювання

X const a;
X b = a;       // вірно для X(const X& copyFromMe), але не вірно X(X& copyFromMe)
               // бо другий конструктор вимагає неконстантне посилання X&

Варіант X& використовується при потребі змінити об'єкт, що копіюється. Таке потрібно дуже рідко, але це можна побачити в стандартній бібліотеці std::auto_ptr.

X a;
X b = a;       // вірно для всіх конструкторів копіювання

Наступні конструктори копіювання не вірні через те, що copyFromMe передається не як посилання:

X(X copyFromMe);
X(const X copyFromMe);

тобто виклик такого конструктора викличе копіювання параметрів, що в свою чергу призведе до нескінченної рекурсії.

Конструктор копіювання може викликатися в наступних випадках:

  1. Коли об'єкт повертається за значенням
  2. Коли об'єкт передається в функцію за значеннам як аргумент
  3. Коли об'єкт ініціалізується за допомогою іншого об'єкта (того ж типу)
  4. Коли об'єкт створюється компілятором як тимчасовий

Всі ці випадки тотожні до:[2] T x = a;

У деяких випадках компілятор може з метою оптимізації не викликати конструктор копіюваня.

Операції[ред. | ред. код]

Об'єктові може бути присвоєне значення двома шляхами:

  • Явне присвоєння у виразі
  • Ініціалізація

Явне присвоєння у виразі[ред. | ред. код]

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

Конструктор копіювання використовується виключно для ініціалізації, і не застосовується для присвоювання де використовується оператор присвоювання натомість.

Неявний конструктор копіювання класу викликає базовий конструктор копіювання і копіює члени класу підходящим для їхнього типу чином. Якщо це тип класу, тоді відповідний конструктор копіювання викликається, якщо це скалярний тип, використовується вбудований оператор присвоювання. Насамкінець, якщо це масив, кожен елемент копіюється відповідно до його типу.[3]

Шляхом визначення користувачевого конструктора копіювання програміст може задати необхідну поведінку при копіюванні.

Приклади[ред. | ред. код]

Наступні приклади показують як працюють конструктори копіювання і навіщо вони потрібні.

Неявний конструктор копіювання[ред. | ред. код]

Розглянемо наступний приклад.

#include <iostream>

class Person {
public:
    int age;

    explicit 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;

    return 0;
}

Виведення

10 15 10
23 15 10

Як і очікувалось, timmy був скопійований в новий об'єкт, timmy_clone. І хоча вік timmy був змінений, вік timmy_clone залишився тим самим. Причина цього в тому, що це зовсім різні об'єкти.

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

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

Тобто, коли ж ми дійсно потребуємо користувачевого конструктора копіювання? Наступний розділ дасть відповідь на це питання.

Користувацький конструктор копіювання[ред. | ред. код]

Тепер, уявімо дуже простий клас динамічних масивів:

#include <iostream>

class Array
{
  public:
    int size;
    int* data;

    explicit 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)

    return 0;
}

Виведення

25 25
Segmentation fault

Через те, що ми не визначили конструктор копіювання, компілятор створив його для нас. Створений конструктор може виглядати схожим на наступний:

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

У даному разі конструктор виконав поверхове копіювання (англ. shallow copy) вказівника data. Він скопіював лише адресу первісного члена даних; тобто вони обидва використовують вказівник на одну ділянку пам'яті, нам би хотілось іншого. Коли програма досягає рядка (1), викликається деструктор копії (через автоматичне знищення об'єкта у стеку при виході з його області видимості). Деструктор масиву видаляє масив data, що використовується обома об'єктами. Після цього на рядку (2) ми намагаємось отримати доступ до неіснуючого об'єкта і записати туди! Це викликає відому помилку сегментації.

Якщо ми напишемо власний конструктор копіювання, який виконує глибоке копіювання, тоді ми позбудемось цієї проблеми.

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

Тут, ми створюємо новий масив і копіюємо вміст у нього. Тепер деструктор copy видалить лише власні дані і не зачепить дані об'єкта first. Рядок (2) відтепер не викликатиме помилку сегментації.

Замість того, щоб робити глибоке копіюваня одразу, можна використати один із способів оптимізації. Вони дозволяють безпечне спільне використання даних кількома об'єктами, таким чином зберігаючи пам'ять. Стратегія копіювання при записі робить копію даних тільки при спробі запису. Лічильник посилань слідкує за тим скільки об'єктів посилаються на дані, і знищує їх тільки тоді, коли кількість дорівнює нулю (наприклад boost::shared_ptr).

Конструктори копіювання і шаблони[ред. | ред. код]

Всупереч очікуванням, шаблонний конструктор копіювання не є користувачевим конструктором копіювання. Тож недостатньо мати:

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

(Зауважте, що тип A може бути Array.) Користувачевий, нешаблонний конструктор копіювання має бути визначений для конструювання Array на основі Array.

Явний конструктор копіювання[ред. | ред. код]

Явний коструктор копіювання визначається за допомогою ключового слова explicit. Наприклад:

explicit X(const X& copyFromMe);

Він використовується для запобігання копіюванню об'єктів під час виклику функцій або під час ініціалізації копіюванням.

    X a;
    X b = a;   // помилка
    X b(a);    // вірно

Примітки[ред. | ред. код]

  1. INCITS ISO IEC 14882-2003 12.8.2. [1] [Архівовано 8 червня 2007 у Wayback Machine.]
  2. ISO/IEC (2003). ISO/IEC 14882:2003(E): Programming Languages — C++ § 8.5 Initializers [dcl.init] para. 12
  3. INCITS ISO IEC 14882-2003 12.8.8. [2] [Архівовано 8 червня 2007 у Wayback Machine.]