C++. Недоліки побітового копіювання. Приклад. Необхідність використання конструктора копіювання та оператора копіювання для класів, що містять динамічне виділення пам’яті




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

Дана тема має спільні тлумачення з наступними темами:


Зміст


Пошук на інших ресурсах:

1. Поняття побітового копіювання

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

 

2. У яких випадках викликається побітове копіювання об’єктів?

Побітове копіювання викликається у наступних випадках:

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

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

 

3. Які функції класу використовують побітове копіювання об’єктів?

Побітове копіювання об’єктів використовують дві функції класу:

  • неявний конструктор копіювання який генерується компілятором автоматично у випадку відстутності явно заданого конструктора копіювання. Більш детально про конструктор копіювання описується тут;
  • неявна операторна функція operator=(), яка генерується компілятором автоматично у випадку відсутності явно заданої операторної функції. Детальний приклад використання операторної функції operator=() описується тут.

 

4. Для яких класів обов’язково реалізовувати власні явно задані конструктор копіювання та оператор копіювання?

Недоліки побітового копіювання проявляються для класів, в яких пам’ять для змінних виділяється динамічно. Для уникнення цих недоліків, у клас потрібно включити власний явно заданий конструктор копіювання а також оператор копіювання.
Якщо в класі пам’ять не виділяється динамічно (немає покажчиків), то явно заданий конструктор копіювання та оператор копіювання operator=() реалізовувати необов’язково.






 

5. Проблеми які виникають у випадку, коли в класі немає явно заданого конструктора копіювання та оператора копіювання

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

1. При наступному оголошенні об’єкту obj2

ClassName obj1;
ClassName obj2 = obj1;

обидва покажчики в об’єктах obj1, obj2 будуть вказувати на спільну ділянку пам’яті. У результаті, зміни в одному об’єкті будуть змінювати значення іншого, що є неприпустимо.

2. Клас, в якому виділяється пам’ять для покажчика обов’язково повинен її звільнити в деструкторі. Звільнення пам’яті для покажчика в деструкторі класу є нормальною практикою. Після завершення програми не залишається виділеної пам’яті, яка помилковим чином не була звільнена. Деструктор викликається при знищенні об’єкту. Якщо два об’єкти obj1 та obj2 вказують на спільну ділянку пам’яті, то ця спільна ділянка буде звільнюватись два рази (для об’єкту obj1 та об’єкту obj2). А це є помилкою: виділена один раз пам’ять може бути звільнена тільки один раз і не більше. Як результат, система згенерує виключну ситуацію.

 

6. Приклад, що демонструє недоліки побітового копіювання
6.1. Версія класу Copy, що містить помилковий код

У прикладі оголошується клас Copy, який містить помилковий код. У класі Copy оголошується покажчик на тип int. Клас містить наступні елементи:

  • змінна-покажчик на тип int;
  • конструктор з 1 параметром. У цьому конструкторі пам’ять для покажчика p виділяється динамічно;
  • метод доступу Get(), що повертає значення за покажчиком *p;
  • метод доступу Set(), який встановлює нове значення за покажчиком p. Якщо пам’ять попередньо не виділено, то вона виділяється знову.

Текст класу Copy наступний:

#include <iostream>
using namespace std;

// Недоліки побітового копіювання
class Copy
{
private:
  int * p; // покажчик на int

public:
  Copy() { p = nullptr; }

  Copy(int value)
  {
    p = new int; // виділити пам'ять
    *p = value; // заповнити значенням
  }

  int Get()
  {
    if (p != nullptr)
      return *p;
    else
      return 0;
  }

  void Set(int value)
  {
    // якщо пам'ять не виділено, то виділити пам'ять
    if (p == nullptr)
      p = new int;
    *p = value;
  }
};

void main()
{
  // Недоліки побітового копіювання
  Copy obj1(15);
  Copy obj2;

  // Виклик операторної функції operator=(), яку генерує компілятор
  // виконується побітове копіювання
  obj2 = obj1;
  cout << "After obj2=obj1" << endl;
  cout << "*(obj1.p) = " << obj1.Get() << endl;
  cout << "*(obj2.p) = " << obj2.Get() << endl;

  obj2.Set(5); // зміни в obj2 призводять до змін в obj1 - це є недолік
  cout << endl << "After obj2.Set(5)" << endl;
  cout << "*(obj1.p) = " << obj1.Get() << endl;
  cout << "*(obj2.p) = " << obj2.Get() << endl;

  obj1.Set(-20);
  cout << endl << "After obj1.Set(-20)" << endl;
  cout << "*(obj1.p) = " << obj1.Get() << endl;
  cout << "*(obj2.p) = " << obj2.Get() << endl;

  // Виклик неявного конструктор копіювання - побітове копіювання
  Copy obj3 = obj1;
  cout << endl << "obj3 = obj1" << endl;
  // зміни в obj3 призводять до змін в obj2 та obj1
  obj3.Set(50);
  cout << endl << "After obj3.Set(50)" << endl;
  cout << "*(obj1.p) = " << obj1.Get() << endl;
  cout << "*(obj2.p) = " << obj2.Get() << endl;
  cout << "*(obj3.p) = " << obj3.Get() << endl;
}

Якщо запустити програму на виконання, то вона видасть наступний результат

After obj2=obj1
*(obj1.p) = 15
*(obj2.p) = 15

After obj2.Set(5)
*(obj1.p) = 5
*(obj2.p) = 5

After obj1.Set(-20)
*(obj1.p) = -20
*(obj2.p) = -20

obj3 = obj1

After obj3.Set(50)
*(obj1.p) = 50
*(obj2.p) = 50
*(obj3.p) = 50

Як видно з результату, у класі Copy є дві помилки:

  • при отриманні копії об’єкту класу, покажчики в оригіналі та копії класу вказують на одну ділянку пам’яті, а це є помилка;
  • клас не містить деструктор ~Copy(), який звільняє пам’ять для покажчика p. Якщо в клас Copy спробувати додати деструктор, який буде звільняти пам’ять для покажчика p, то програма буде видавати виключну ситуацію. Це пов’язано з тим, що одна ділянка пам’яті буде звільнятись два рази.

 

6.2. Версія класу Copy, яка містить коректний код (виправлена версія)

Щоб виправити ситуацію у клас Copy потрібно включити такі функції:

  • конструктор копіювання. У цьому конструкторі буде формуватись копія об’єкта-параметра;
  • операторну функцію operator=(), яка також буде формувати копію об’єкта-параметра.

Після цього в клас Copy можна додати деструктор, який буде коректно звільняти пам’ять, оскільки усі об’єкти в програмі будуть вказувати на різні ділянки пам’яті.

Версія класу Copy, яка працює коректно має наступний вигляд:

// Недоліки побітового копіювання:
// виправлений варіант класу Copy
class Copy
{
private:
  int * p; // покажчик на int

public:
  Copy() { p = nullptr; }

  Copy(int value)
  {
    p = new int; // виділити пам'ять
    *p = value; // заповнити значенням
  }

  // Додано конструктор копіювання
  Copy(const Copy& obj)
  {
    p = new int;
    *p = *(obj.p);
  }

  int Get()
  {
    if (p != nullptr)
      return *p;
    else
      return 0;
  }

  void Set(int value)
  {
    // якщо пам'ять не виділено, то виділити пам'ять
    if (p == nullptr)
      p = new int;
    *p = value;
  }

  // Додано операторну функцію operator=()
  Copy& operator=(const Copy& obj)
  {

    if (this != &obj)
    {
      // 1. Виділити пам'ять, якщо вона не була попередньо виділена
      if (p == nullptr)
        p = new int;

      // 2. Скопіювати дані
        *p = *(obj.p);
    }
    // 3. Повернути поточний об'єкт
    return *this; //
  }

  // Тепер можна додати деструктор
  ~Copy()
  {
    // Звільнити попередньо виділену пам'ять
    if (p != nullptr)
      delete p;
  }
};

Функція main() залишається в попередньому варіанті. Після запуску на виконання, програма видасть наступний результат:

After obj2=obj1
*(obj1.p) = 15
*(obj2.p) = 15

After obj2.Set(5)
*(obj1.p) = 15
*(obj2.p) = 5

After obj1.Set(-20)
*(obj1.p) = -20
*(obj2.p) = 5

obj3 = obj1

After obj3.Set(50)
*(obj1.p) = -20
*(obj2.p) = 5
*(obj3.p) = 50

Як видно з результату, після додавання конструктора копіювання

// Додано конструктор копіювання
Copy(const Copy& obj)
{
    p = new int;
    *p = *(obj.p);
}

та додавання операторної функції

// Додано операторну функцію operator=()
Copy& operator=(const Copy& obj)
{
    // 1. Виділити пам'ять, якщо вона не була попередньо виділена
    if (p == nullptr)
        p = new int;

    // 2. Скопіювати дані
    *p = *(obj.p);

    // 3. Повернути поточний об'єкт
    return *this; //
}

програма працює коректно. Також працює коректно звільнення пам’яті для об’єктів obj1, obj2, obj3 у деструкторі класу

// Тепер можна додати деструктор
~Copy()
{
    // Звільнити попередньо виділену пам'ять
    if (p != nullptr)
      delete p;
}

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

 


Зв’язані теми