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

Цією частиною ми завершимо розпочату в статтях “Елементи класу, про які завжди необхідно пам’ятати” і “Класи: копіювання і присвоювання. Частина 1, Частина 2 і Частина 3“Детальний розгляд проблеми копіювання і присвоювання в класах.

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

Блокування копіювання та привласнення

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

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

Код:






class POINT
{
  public: / / Відкриті члени
  protected: / / Захищені члени
  private: / / Закриті члени
   POINT(const POINT&);
   POINT& operator=(const POINT&); / / Блокування копіювання
};

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

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

Реалізація копіювання через присвоювання

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

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

Часто зустрічається такий варіант реалізації:

Код:






class POINT
{
  public: / / … конструктор, деструктор і інші члени класу / / Конструктор копій, реалізований через (тобто / / Неявно викликає) операцію присвоювання
    POINT(const POINT& rhs) { *this = rhs; } / / Операція присвоювання
    POINT& operator=(const POINT& rhs)
    {     
      if(this == &rhs)  return *this; / / Виконує присвоювання, розгорнуте копіювання / / Або ще що-небудь
      return *this;
    }
   private: / / Дані-члени
};

У рядку {* this = rhs;} неявно викликається операція присвоювання (дивимося в попередню статтю). Така форма виклику досить туманна і в принципі може стати причиною помилок. Раніше весь час підкреслювалося, що перевантаження операцій, як правило, відбувається неявно, тут же це правило безцеремонно порушено. Це зроблено для ясності, а також тому, що розробнику класу так чи інакше слід про це знати.

Давайте подивимося, як більш зручно реалізувати копіювання через присвоювання.

Код:






/ / Конструктор копій викликає операцію привласнення явно,  / / Точно так само, як і будь-яку іншу функцію
POINT(const POINT& rhs) { operator=(rhs); } / / Операція присвоювання
POINT& operator=(const POINT& rhs)
{     
  if(this == &rhs) return *this; / / Код присвоювання / копіювання …
  return *this;
}

Тут операція присвоювання викликається вже явно, подібно будь-якій іншій функції. У розгорнутій формі цей виклик можна представити як this-> operator = (rhs);

Випереджати виклик функції покажчиком this в принципі зайве, хоча і цілком коректно. Реалізація копіювання через присвоювання дозволяє звести розробку і налагодження двох цих функцій до однієї – операції привласнення, незалежно від подальшого розвитку та зміни вашого класу.

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

Копіювання і присвоювання в дочірніх класах

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

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

Предмет нашого обговорення – копіювання об’єктів похідних класів. Якщо для батьківського класу вже визначені копіювання і присвоювання, то весь фокус полягає в тому, щоб витягти з них максимальну користь. Прочитавши попередній розділ “Реалізація копіювання через присвоювання”, ви легко забезпечите копіювання успадкованої частини об’єкта. Треба просто використовувати його операцію присвоювання і дописати код для своєї частини класу. Розглянемо наступний клас B, похідний від класу А:

Код:






class В: public A / / Читається так: клас В відкрито виводиться з А
{
  public: В (); / / Конструктор ~ В (); / / Деструктор B (const B &); / / Конструктор копій B & operator = (const B &); / / Присвоєння
  private:
     // etc
};

З класом A все гаразд, він повністю визначений, налагоджений і працездатний. B – це клас, який визначений як нащадок A. Припустимо, що конструктор копій для B реалізований засобами привласнення класу B, тоді:

Код:






/ / Конструктор копій В :: В (const B & rhs) {operator = (rhs);} / / Хай цю роботу робить операція присвоювання / / Операція присвоювання B & В :: operator = (const B & rhs)
{
  if(this == &rhs) return *this; A :: operator = (rhs); / / копіювання спадщини, тобто класу А … / / А тут буде присвоювання членів, властивих тільки В
  return *this;
}

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

Код:






/ / Список ініціалізації в першому рядку використовується для виклику / / Конструктора копій для успадкованої з А частини В, а решта / / Частина конструктора займається копіюванням даних-членів В :: В (const B &): A (rhs)
{ / / Тут замість виклику operator = / / Присвоюються значення всім членам В
}

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

Ось, власне, і все, чим я хотів з вами поділитися.


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


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

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

Ваш отзыв

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

*

*