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

Є два види копіювання: буквальне копіювання (shallow copy) – те, яке зазвичай пропонується компілятором, якщо ви не визначите цю операцію самі (тобто автоматично сформовані компілятором конструктор копій і операція присвоювання), і розгорнуте копіювання (deep copy – або глибоке копіювання).

Буквальне копіювання

Буквальне копіювання – це просте побітно (або порозрядне) копіювання. Воно означає, що кількість і стан всіх бітів одного об’єкта абсолютно точно відтворюється у другому. Наприклад, точно копіюються 32 розряду цілого числа; точно так же буквально відтворюються і 32 біта вказівника (наприклад char *, хоча зрозуміло, що в різних системах і в різних моделях пам’яті і розмірність покажчика та розмірність цілого числа може бути зовсім інший).

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

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

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

Легко здогадатися, що витоку пам’яті ні до чого доброго не приводять. Коли один об’єкт звільняє пам’ять (наприклад при своєму знищення), яку також використовує й інший об’єкт (або декілька об’єктів), то в другому об’єкті залишаються ненульові покажчики (вони-то все ще вказують на ту область пам’яті!). Коли справа доходить до видалення цього другого об’єкта, до його членам-вказівниками зазвичай застосовується операція delete. Виклик delete для покажчика «в нікуди» призводить до непередбачуваних, але зазвичай до дуже руйнівних наслідків. Наприклад, пам’ять, яку в цьому випадку нібито «звільнив» об’єкт, вже могла бути розподілена знову для потреб зовсім іншого об’єкта.

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

Проте, саме з цією метою цей приклад і наведено.

Код:






01 / / SMARRAY2.H – інтерфейс інтелектуального масиву
02  #ifndef _SMARRAY2_H
03  #define _SMARRAY2_H
04  class INT_ARRAY
05  { 
06 public:
07   INT_ARRAY(unsigned int sz = 100);
08   ~INT_ARRAY();    09 INT_ARRAY (const INT_ARRAY &); / / Оголошення конструктора копій 10 INT_ARRAY & operator = (const INT_ARRAY &) ;/ / Оголошення операції привласнення 11 / / Використання беззнакових цілих знімає необхідність 12 / / перевірки на негативні індекси
13   int& operator[]( unsigned int index);
14
15 private:
16   unsigned int max;
17   unsigned int dummy;
18   int *data;
19  };
20  #endif

Тут був оголошений конструктор копій, а слідом – операція присвоювання класу INT_ARRAY. Оголошення цих функцій цілком правильні. Але, як ви побачите нижче, для цього класу визначення цих функцій будуть неправильні.

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


Код:






01 / / SMARRAY2.CPP – реалізація інтелектуального масиву
02  #include <mem.h>
03  #include [i]
04  #include “smarray2.h”
05 06 / / Конструктор – Додає одиницю до розміру масиву для зберігання 07 / / фіктивного значення на випадок використання неприпустимого індексу.
08  INT_ARRAY::INT_ARRAY(unsigned int sz)
09  {
10    max=sz;
11    dummy=sz+1;
12    data=new int[sz+1]; 13 / / Якщо new повертає допустимий блок пам’яті, 14 / / тоді data не нуль. Функція memset ініціалізує 15 / / цей блок пам’яті значенням 0.
16    if(data) memset( data, 0, dummy);
17    else max = -1;
18  } 19 / / деструктор
20  INT_ARRAY::~INT_ARRAY()
21  { 22 delete [] data; / / звільнення масиву 23 data = 0; / / установка вказівника в 0 дозволяє перевіряти 24 / / його неприпустимість
25  } / / – Попередження – / / Цей клас використовує буквальне копіювання. / / Не користуйтеся їм в реальних програмах. / / Цей конструктор копій демонструє варіант / / “Буквального” конструктора, що генерується компілятором. / / Тут не тільки два покажчика посилаються на один і той же / / Блок пам’яті, а й, крім того, не звільняється / / Старий блок. / / Конструктор копій
26  INT_ARRAY::INT_ARRAY(const INT_ARRAY& rhs)
27  {
28    this->max = rhs.max;
29    this->dummy = rhs. dummy; 30 this-> data = rhs.data; / / ПОМИЛКА: буквальне копіювання
31  } / / Оператор присвоювання / / Попередження – І тут та сама помилка
32  INT_ARRAY& INT_ARRAY::operator=(const INT_ARRAY& rhs)
33  {
34    if(this == &rhs) return *this;
35    this->max = rhs.max;
36    this->dummy = rhs.dummy;
37    this->data = rhs.data;
38    return *this;
39  } / / Дуже коротко, але допустимість індексу перевіряється. / / Цей вид проблем – чудовий кандидат для / / Обробки виняткових ситуацій.
40  int& INT_ARRAY::operator[](unsigned int index)
41  {
42    return index < max ? data[index] : data[dummy];
43  }
44  void main(void)
45  {
46    INT_ARRAY ouch; / / Створюється штучна область видимості, / / Щоб деструктор startMeUp був викликаний першим
47    {
48      INT_ARRAY StartMeUp(10);
49      for(unsigned int k = 0; k<10; k++) 50 StartMeUp [k] = k; / / тут працює operator [] 51 ouch = StartMeUp; / / Виклик “ПОГАНИЙ” операції присвоювання 52 / / Демострація того факту, що ‘ouch’ і 53 / / ‘startMeUp’ вказують на один і той же блок пам’яті
54      for(unsigned int k = 0; k<10; k++) cout << ouch[k] << endl;
55    }
56  }

Отже, тут (рядки 26-31) ми визначили конструктор копій, що виконує буквальне копіювання. Саме так функціонує конструктор копій, який компілятор буде генерувати за замовчуванням, якщо ви не заблокуєте копіювання (як це зробити, ми розглянемо пізніше) або не визначите правильно реалізований конструктор копій.

Тут же визначена і настільки ж погана оператор-функція присвоювання (рядки 32-39). Як і конструктор копій, вона не звільняє пам’ять, виділену вказівником викликає об’єкта, не перерозподіляє пам’ять і не копіює всі елементи масиву. Все, що вона робить – це присвоює вказівниками посилання на один і той же фрагмент пам’яті.

Нарешті у функції main () для об’єкта StartMeUp створюється штучна область видимості (рядок 47 і 55) так, що його деструктор викликається раніше, ніж деструктор об’єкта ouch. Елементам об’єкта StartMeUp присвоюються значення від 0 до 9. Далі відбувається виклик операції привласнення, отже в обох об’єктах елементи будуть мати значення від 0 до 9.

Деструктор об’єкта StartMeUp викликається першим і звільняє пам’ять, на яку вказує член класу int * data. В правильно певному класі це не повинно зачіпати інші об’єкти того ж типу. Цей клас, проте, використовує буквальне копіювання там, де потрібно розгорнуте. В результаті виходить, що покажчик data об’єкта ouch після цього посилається невідомо куди. Або, відомо куди, але там вже невідомо що …

Так що для класу, подібного INT_ARRAY, доведеться виконувати розгорнутий копіювання або створити спеціальний механізм для підрахунку числа посилань data, щоб знати, коли його можна безболісно видалити. А тепер давайте подивимося, що таке розгорнуте копіювання і як його слід визначати.

Розгорнуте копіювання

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

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

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

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

Код:






35 / / Конструктор копій з розгорнутим копіюванням
36 INT_ARRAY::INT_ARRAY(const INT_ARRAY& rhs)
37 { 38 delete [] data; / / звільняємо пам’ять
39   max = rhs.max;
40   dummy = rhs.dummy; 41 data = new int [dummy]; / / виділяємо новий блок
42   for( unsigned int j = 0; j<duinmy; j++) 43 data [j] = rhs.data [j]; / / копіюємо дані
44 } 45 / / Операція присвоювання з розгорнутим копіюванням
46 INT_ARRAY& INT_ARRAY::operator=( const INT_ARRAY&rhs)
47 {
48   if(this == &rhs) return *this; 49 / / Зверніть увагу, що код ідентичний тому, / / Що використовується в конструкторі копій
50    delete [] data;
51    max = rhs.max;
52    dummy = rhs.dummy;
53    data = new int [dummy];
54    for( unsigned int j = 0; j<dummy; j++)
55      data[j] = rhs.data [j];
56    return *this;
57  }

Зверніть увагу, що рядки 38-43 конструктора копій і рядки 50-55 операції привласнення ідентичні: звільняється область пам’яті, що адресується вказівником data викликає об’єкта, потім виділяється нова область пам’яті і в неї копіюються значення з пам’яті об’єкта аргументу.

Конструктор копій і операція присвоювання переробленого класу не стануть тепер причиною втрат пам’яті.

На цьому, мабуть, зробимо паузу до наступного разу. Невелике резюме наостанок. Для розробки конструкторів копій та операцій присвоювання можна сформулювати наступні рекомендації:


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

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


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


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

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

Ваш отзыв

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

*

*