Класи: копіювання і присвоювання. Частина 1 (исходники), Різне, Програмування, статті

У цій частині ми продовжимо розпочату в статті Елементи класу, про які завжди необхідно пам’ятати обговорення конструктора копій (copy constructor) та операції присвоювання (assignment operator). Або, вірніше, почнемо докладний розгляд вельми нетривіальним проблеми, якою насправді є копіювання і присвоювання в класах.

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

Конструктор копій служить для створення нових об’єктів з існуючих. Операція присвоювання потрібна для того, щоб зробити один існуючий об’єкт еквівалентним іншому існуючому. Що означає створити копію? Як один з варіантів, це означає присвоювання значень елементів одного об’єкту елементам іншого. Ця відповідь, однак, далеко не повний. C + + – це мова, яка практично не обмежує вибір шляхи реалізації програми. І спосіб створення копій об’єктів – не виняток з цього правила.

Іноді для копіювання класів достатньо просто навести один об’єкт в той же стан, що й інший. Це дуже просто, і ми побачимо, як це робиться. Однак якщо вашому додатком потрібні інші методи копіювання, C + + не стане створювати їх за вас, хоча, якщо ви не напишете ці функції, компілятор зробить це сам. Правда, результат при цьому може суттєво відрізнятися від того, що вам би хотілося.

В серії цих статей ми розглянемо всі аспекти цього питання, за розділами:


Поняття копіювання

Тут ми поговоримо про один з аспектів внутрішнього функціонування програм, написаних на C + + – про копіювання. Копіювання в програмах на C + + відбувається, прямо або побічно, буквально на кожному кроці. Причому, не завжди з першого погляду очевидно, де відбувається копіювання, а де ні.
Ми почнемо з розгляду синтаксису цікавить нас предмета, а потім спробуємо заглибитися в його осмислення.

Визначення конструктора копій

Конструктор копій використовується для створення нових об’єктів з уже існуючих. Це означає, що, так само як для інших конструкторів, новий об’єкт ще не існує до моменту його виклику. Однак тільки конструктору копій об’єкт передається як аргумент за посиланням. Отже, синтаксис конструктора копій простий. Конструктор копій довільного класу X виглядає так:





Х (const X &); / / конструктор копій класу Х

Так як конструктор копій – це все таки конструктор, то він повинен мати ім’я, що збігається з ім’ям класу (не забувайте з урахуванням регістру символів). Призначення конструктора копій – дублювання об’єкта-аргументу для побудови нового об’єкта. Одне з основних правил: якщо аргумент не повинен змінюватися, то його слід передавати як константу. У той же час, якщо аргумент не описаний як константа, то не можна копіювати об’єкти-константи. Змінний об’єкт завжди можна передати як постійний аргумент, але не навпаки.

Друга частина оголошення аргументу, X, проста: копіюється об’єкт того ж самого типу. Аргумент в цілому читається як “постійна посилання на X”. Посилання істотна з кількох міркувань. У першу чергу тому, що при передачі адреси об’єкта не створюється копія викликає об’єкта (на відміну від передачі аргументу за значенням). Якщо вам здається тут якийсь підступ, то будьте уважні.

Робота конструктора копій – створення раніше не існувало об’єкта з вже існуючого, а передача за значенням (без використання операції отримання адреси) вимагає створення копії аргументу, значить ми отримуємо нескінченну рекурсію. Точніше: при передачі об’єкта за значенням створюється його копія, якщо це відбудеться в конструкторі копій, то він буде викликати сам себе, поки не вичерпає всі ресурси системи.

Ось кілька правил, яких треба дотримуватися при оголошеннях конструктора копій:


Написання конструктора копій є надзвичайно відповідальним вчинком. Явна визначення конструктора копій викликає зміни в роботі програми. Поки ми не намагалися перевизначити конструктор копій, справно працював конструктор, породжуваний компілятором автоматично. Цей конструктор створював “фотографічні” копії об’єктів, тобто копіював значення абсолютно всіх даних-членів, в тому числі і ненульові значення покажчиків, що представляють адреси динамічних областей пам’яті.
З моменту появи перевизначених версії конструктора копій, вся робота по реалізації алгоритмів копіювання покладається на програміста. Втім, змусити конструктор копій копіювати об’єкти зовсім нескладно. Створимо клас, що описує точку на площині.

Код:






class POINT
{
  public: POINT () {X = 0; Y = 0;} / / конструктор за замовчуванням POINT (int a, int b) {X = a; Y = b;} / / ще конструктор POINT (const POINT & Pixel) {X = Pixel.X; Y = Pixel.Y;} / / конструктор / / Копіювання int X; / / координати точки
    int Y;
}; 


Як бачимо, цей конструктор копій просто копіює значення координат. В принципі, якщо б ми його не визначили, то компілятор створив би його сам, причому в цьому випадку він робив би те ж саме. Але конструктор, створює подібні копії об’єктів, швидше за все, виявиться непридатним для роботи з об’єктами, що містять в якості членів покажчики або посилання. Припустимо, що клас містить покажчики. Тоді адреси, містяться в покажчиках об’єкта-оригіналу і об’єкта-копії, будуть ідентичні. Це означає, що два об’єкти будуть вказувати на одну і ту ж область пам’яті, що, як правило, дуже небезпечно. Це ми розглянемо докладно трохи пізніше.
В перевизначають конструкторі копій (а в класі він може бути тільки один) можна реалізовувати різноманітні алгоритми розподілу пам’яті. Тут все залежить від програміста.
Отже, зазвичай, конструктор копій, створений компілятором, задовільний при наступних обставинах:


В цьому випадку цілком розумно використовувати конструктор, побудований компілятором. В текст визначення класу в цьому випадку корисно помістити відповідний коментар про те, що конструктор копії не визначено цілком свідомо. Взагалі кажучи, коментарі щодо розглянутих чотирьох членів доречні в кожному класі. Ось приклад:

Код:






class X
{
  public: Х (); / / конструктор за замовчуванням virtual ~ X (); / / віртуальний деструктор / / Конструктор копії і операція присвоювання не визначені / / Навмисно. Клас містить тільки дані, що розміщуються / / В стеку, тому зумовлених конструктора копій / / І операції привласнення достатньо.
  private:
   int data;
   char moreData;
   float no_Pointers;
};


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

Визначення операції привласнення

За функціональним призначенням операція присвоювання дуже схожа на конструктор копій. Принципова відмінність полягає в тому, що конструктор копій створює новий (можливо, тимчасовий) об’єкт, а операція присвоювання працює з уже створеними. Зухвалий об’єкт є лівим операндом, об’єкт-аргумент – правим.
Операція присвоювання також має відповідний синтаксис. Операція присвоювання – це функція-член і одночасно двомісна операція. Отже, в роботу залучені два об’єкти. Перший об’єкт – викликає, доступний за вказівником this, а другий – це аргумент. Як конструктор копій, так і операція присвоювання використовують як аргумент постійне посилання. Для довільного класу X ми маємо наступний синтаксис операції присвоювання:

Код:






X & operator = (const X &); / / синтаксис операції привласнення для  / / Довільного класу


Присвоєння – це операція, значить ми повинні використовувати ключове слово operator і відповідний символ операції. Так як C + + допускає ланцюжка привласнення а = b = с = d; / / C + + допускає послідовні присвоювання, / / ​​так що це властивість треба зберегти те необхідно повертати посилання на об’єкт, інакше ланцюжок перерветься. Отже, оператор-функція приймає постійну посилання, а повертає посилання на об’єкт. Використання ключового слова const дозволяє функції працювати як з постійними об’єктами, так і зі змінними.

Визначаючи новий клас, якщо ви вирішили оголосити операцію привласнення, дотримуйтесь наступних рекомендацій:


Перевірка на присвоювання самому собі

В операції присвоювання для будь-якого класу треба враховувати один важливий момент. Завжди треба перевіряти: чи не відбувається присвоювання самому собі. Воно може мати місце в тому випадку, коли об’єкт прямо або побічно викликає операцію присвоювання для себе. Пряме присвоювання може виглядати наступним чином:

Код:






POINT Pix; Pix = Pix; / / присвоювання самому собі


Це самий тривіальний випадок, він хороший для приведення прикладу, не більше. У реальних програмах такого зазвичай не буває і ця помилка, як правило, приймає далеко не настільки очевидні обличия.
Присвоєння самому собі породжує в програмі витоку пам’яті. Така ситуація може виникнути, коли два об’єкти спільно використовують певний ресурс і один з них цей ресурс звільняє. При цьому стан ресурсу стає невизначеним, але другий об’єкт продовжує на нього посилатися.
Є багато шляхів, що ведуть до виникнення цієї проблеми. Для її запобігання і слід передбачити в операції привласнення перевірку на присвоювання самому собі. Вона дуже проста і виглядає завжди абсолютно однаково:

Код:






POINT& POINT::operator=(const POINT& rhs)
{ if (this == & rhs) return * this; / / перевірка на присвоювання собі else {X = rhs.X; Y = rhs.Y;} / / те, що робить оператор корисного return * this; / / повернення посилання на об’єкт
}


Зараз ми спробуємо в ній розібратися, благо для всіх операцій присвоювання перевірка на присвоювання собі абсолютно однакова.
Оператор if (this == & rhs) перевіряє, чи не співпадає аргумент з самим об’єктом. Покажчик this містить адресу викликає об’єкта, & rhs читається як “адреса rhs”. Таким чином, порівнюються дві адреси. Якщо вони еквівалентні (==), то це один і той же об’єкт. В цьому випадку, в повній відповідності з вимогою повернення посилання на об’єкт, просто повертаємо * this (зауважте, що в кінці функції робиться то ж саме) і виходимо з функції.
Згадайте, що this – це покажчик. Значення покажчика – це адреса. Щоб отримати значення покажчика, його слід разименовивать. Разименованія покажчика виглядає так: * ptr. Покажчик this разименовивается точно так же: * this.
Помістивши ці два рядки на початку і в кінці тіла операції привласнення, ми зменшуємо ймовірність виникнення витоків пам’яті через цієї операції.
Якщо ви запам’ятали наведений тут синтаксис копіювання та привласнення і, визначаючи новий клас, відразу будете визначати і їх теж, то це вже півсправи.

Навіщо C + + вимагає визначення цих функцій-членів?

Мова C + + не надто сильно обмежує свободу програмістів в методах розробки програмного забезпечення. Зокрема, він не нав’язує вам способи копіювання та привласнення. Кількість і різноманітність ситуацій, в яких відбувається копіювання об’єктів, дивно велика. Для дуже простих об’єктів, що складаються з одного-двох елементів, витрати на копіювання незначні, але для більш складних, таких як графічний інтерфейс користувача або комплексні типи даних, які оперують з динамічною пам’яттю, витрати на копіювання істотно зростають.
По-перше, майте на увазі, що якщо ви не визначите для нового класу конструктор копій, то C + + створить його сам. Причина полягає в тому, що компілятору самому може знадобитися можливість створення копій, значить, ці дві функції повинні бути визначені. По-друге, вам може знадобитися заблокувати копіювання, або вести підрахунок посилань, або ще щось. Якщо ви не створите ці функції, то C + + створить для них версії за умовчанням.
Створювані компілятором версії обох цих функцій не завжди будуть вас задовольняти. Версії компілятора виконують буквальне, або порозрядне, копіювання. В деяких випадках це нерозумно. Не пошкодуйте часу на вивчення ситуацій, які можуть вам зустрітися при розробці програм.

Ось лише деякі з незліченної безлічі можливих ситуацій, в яких відбувається копіювання:

Код:






POINT х;  POINT у (х); / / Прямий виклик конструктора копій.  POINT х = у; / / Виглядає як присвоювання, але насправді / / Викликає конструктор копій. Чому? Див нижче.
POINT a, b; a = b; / / Виклик операції привласнення POINT Foo (); / / Повернення за значенням, викликає копіювання void Foo (POINT); / / Передача за значенням, створює копію


У всіх цих випадках виконується копіювання. В ході виконуваної компілятором оптимізації можуть з’явитися і інші варіанти. Це та область, де знання дійсно сила, здатна допомогти вам уникнути витоків пам’яті.
В операторі типу POINT х = у; не викликається операція присвоювання класу POINT, хоча на перший погляд виглядає це саме так. Причина полягає в тому, що операція присвоювання – це функція-член, а значить може бути викликана тільки для вже існуючих об’єктів, у той час як в цьому фрагменті відбувається створення нового об’єкта х.
Якщо об’єкт створюється в тому ж рядку, в якій він виступає як лівостороннього аргументу, то викликається конструктор. Рядок

Код:






Х х = у; / / виклик конструктора копій  еквівалентна рядку Х х (у); / / виклик конструктора копій / Що зовсім не те ж саме, що Х х, у; х = у; / / виклик операції привласнення


Вам слід розуміти, що ж насправді викликається, коли і чому. Це одна з тих особливостей, завдяки яким C + + важче і цікавіше, ніж С. У попередньому розділі ми прийшли до висновку, що не варто визначати операцію привласнення без конструктора копій і навпаки. Отже, напрошується висновок, що основні рекомендації для операції привласнення справедливі також і для конструктора копій.

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

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*