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

Продовжимо розпочату в статтях “Класи: копіювання і присвоювання. Частина 1 і Частина 2“Детальний розгляд проблеми копіювання і присвоювання в класах. У цій частині ми розглянемо різницю між копіюванням та привласненням, подивимося, коли виконується копіювання, і обговоримо положення конструктора копій та операції присвоювання в класах.

Коли виконується копіювання?

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

Копіювання відбувається при передачі об’єкта за значенням, тобто коли в описі відповідного аргументу функції не використовуються операція отримання адреси (&) або разименованія (*). При поверненні об’єкта за значенням також має місце копіювання. Ось приклади обох випадків:

Код:




void Pixel (POINT arg); / / передача об’єкта за значенням POINT Draw (void); / / повернення об’єкта за значенням


POINT – в даному випадку той самий клас, що визначає точку на площині з першої частини цих статей.

Коли об’єкт передається за значенням, всі вироблені над ним маніпуляції в дійсності стосуються тільки його копії. Якщо об’єкт створюється всередині функції, вкрай бажано повертати його за значенням, а не за посиланням. Чому? Давайте подивимося:

Код:




POINT& PointMaker(void)
{ POINT * data = new POINT; / / локально розміщений об’єкт
  return *data;
}


У цьому прикладі відповідальність за знищення об’єкта data, розміщеного оператором new в купі, лягає на зовнішнє оточення функції PointMaker () в програмі, що загрожує помилками. У наступному прикладі об’єкт розміщується по-іншому, в локальному стеку функції. Подивимося, до чого це призведе.

Код:




POINT& PointMaker(void)
{
  POINT data;
  return &data;
}


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

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

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

А тепер давайте розглянемо, коли ж використовується конструктор копій, а коли операція присвоювання. Конструктор копій або операція присвоювання викликається при створенні або копіюванні вже існуючих об’єктів, а також при створенні тимчасових об’єктів. Нижче ми розглянемо всі (ну або майже все) варіанти виклику на прикладі об’єктів Y і Z класу POINT. Для початку візьмемо таку форму виклику:

Код:




POINT Y(Z);


Це прямий виклик конструктора копій класу POINT. Об’єкт Y – викликає, а об’єкт Z виступає в ролі аргументу. Оскільки синтаксис конструктора копій виглядає як

Код:




POINT:: POINT(const POINT& rhs);


то в нашому випадку покажчик this в конструкторі копій посилається на Y, а псевдонім rhs є адресою Z. У даному прикладі створюється новий об’єкт Y, так що абсолютно безсумнівно викликається конструктор копій.

У наступному прикладі також відбувається виклик конструктора копій, але вже не настільки очевидний: POINT Y = Z; Присутність в цьому рядку знака рівності змушує припустити, що тут відбувається присвоювання. Проте, крім цього, в наявності факт створення нового об’єкта класу POINT. Якби в цьому операторі використовувалася операція присвоювання, то в розгорнутому вигляді він виглядав би так:

Код:




Y.operator = (Z); / / Що неприпустимо


Об’єкт Y ще не існує, отже і тут не обійтися без конструктора.А оскільки для створення об’єкта в даному прикладі використовуються значення вже існуючого об’єкта, значить викликається конструктор є конструктором копій.

У наступному прикладі і Y, і Z вже існують і об’єкту Y за допомогою операції привласнення присвоюється значення об’єкта Z:

Код:




POINT Y, Z; Y = Z; / / виклик операції привласнення


Спробуємо проілюструвати всі ці приклади в наступній програмі. При виклику кожної з цих функцій програма друкує в файл відповідне повідомлення.

Код:




/ / COPIES.CPP – Демонстрація форм операторів / / Конструктора копій та операції присвоювання
#include <iostream.h>
#include <fstream.h> ofstream of (“output.dat”); / / тут можна подивитися результати
class X
{
  public:
    X();
    ~X () ;
    X(const X&) ;
    X operator=(const X);
    operator int() { return num; }
  private:
    int num;
}; / / Конструктор за умовчанням
X::X()
{ of << "конструктор" << endl;   num = 5; } / / Деструктор X::~X() { of << "деструктор" << endl; } / / Конструктор копій X::X(const X& rhs) { of << "конструктор копій" << endl;   num = rhs.num; } / / Операція присвоювання X& X::operator=(const X& rhs) {   if(this == &rhs) return *this; of << "операція присвоювання" << endl;   num = rhs. num;   return *this; } / / Повернення за значенням X Foo(void) {   return X(); } void main(void) {   { X Z; / / конструктор X Y = Z; / / конструктор копій } / / Виклик двох деструкторів   { X A; / / конструктор XB (A); / / конструктор копій } / / Виклик двох деструкторів   { X C, D; / / два конструктора C = D; / / операція присвоювання } / / Виклик двох деструкторів   { X E = Foo (); / / конструктор } / / Деструктор }


Парні дужки {} використані для виклику деструкторів в порядку створення об’єктів. Зазвичай так не робиться, так що цей прийом застосований тільки для того, щоб виклики конструкторів і деструкторів для зручності інтерпретації результатів слідували парами. Зразковий вид результатів відтворюють коментарі до функції main (), а що вийшло насправді, можна переглянути в файлі Output.dat.

Положення в класах

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

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

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


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


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

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

Ваш отзыв

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

*

*