Приклад додавання двох масивів. Клас DoubleArray. Перевантаження оператора +. Динамічне виділення пам’яті у класі. Конструктор копіювання. Недоліки побітового копіювання

Приклад додавання двох масивів. Клас DoubleArray. Перевантаження оператора +. Динамічне виділення пам’яті у класі. Конструктор копіювання. Недоліки побітового копіювання

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


Зміст


1. Текст класу DoubleArray та функції main()

Оголошується клас DoubleArray, в якому реалізовані:

  • внутрішня прихована (private) змінна-покажчик p на тип double. Ця змінна є масив чисел. Для покажчика p пам’ять буде виділятись динамічно;
  • внутрішня private-змінна size – розмір масиву;
  • конструктор без параметрів DoubleArray();
  • конструктор з одним параметром DoubleArray(int size). У цьому конструкторі пам’ять для масиву p виділяється динамічно. Розмір масиву визначається вхідним параметром size;
  • конструктор копіювання DoubleArray(const DoubleArray& A). Цей конструктор необхідний для уникнення побітового копіювання. Побітове копіювання має суттєві недоліки у випадку динамічного виділення пам’яті в класі. У конструкторі копіювання для новоствореного об’єкту пам’ять виділяється власним способом;
  • операторна функція operator+(DoubleArray&). Ця функція перевантажує оператор + таким чином, що можна додавати об’єкти класу DoubleArray між собою за формою obj1+obj2. В операторній функції масиви додаються поелементно. Якщо один масив коротше за інший, тоді елементи меншого масиву вважаються рівними 0;
  • операторна функція operator=(const DoubleArray&). Ця функція перевантажує оператор присвоєння =. Функція є необхідною для того, щоб уникнути побітового копіювання при присвоюванні об’єктів на зразок obj3=obj1+obj2 або obj3=obj2=obj1. Якщо не визначити цю функцію в класі, то буде викликаний неявний оператор присвоювання, який здійснює побітове копіювання що є неприпустимим для нашого класу;
  • деструктор класу ~DoubleArray(). У деструкторі класу виділена пам’ять звільняється;
  • функція Show(), яка відображає вміст масиву p.

Реалізація класу DoubleArray та демонстрація його роботи у функції main() наступна

#include <iostream>
#include <time.h>
using namespace std;

// динамічний масив
class DoubleArray
{
  double * p;
  int size;

public:
  // конструктори
  DoubleArray()
  {
    size = 0;
    p = nullptr;
  }

  DoubleArray(int size)
  {
    this->size = size;
    p = new double[size];
    for (int i=0; i<size; i++)
      p[i] = 0;
  }

  // деструктор
  ~DoubleArray()
  {
    if (size>0)
      delete p;
  }

  // сформувати масив випадково
  void FormArrayRandom()
  {
    srand(time(NULL));
    for (int i=0; i<size; i++)
      p[i] = rand()%10;
  }

  // сформувати масив за формулою
  void FormArray()
  {
    for (int i=0; i<size; i++)
      p[i] = i;
  }

  // методи доступу
  double GetAi(int index)
  {
    if (p!=nullptr)
    {
      if ((0<=index)&&(index<size))
        return p[index];
    }
    else
      return 0.0;
  }

  void SetAi(int index, double value)
  {
    if (p!=nullptr)
    {
      if ((index>=0)&&(index<size))
        p[index] = value;
    }
  }

  // відобразити масив
  void Show()
  {
    cout << "The Array:\n";
    for (int i=0; i<size; i++)
      cout << "p["<<i<<"] = " << p[i] << endl;
    cout << "------------------------------" << endl;
  }

  // додати конструктор копіювання, щоб коректно виконувався деструктор та ініціалізація об'єкту
  DoubleArray(const DoubleArray& A)
  {
    size = A.size;

    // виділити пам'ять по своєму
    p = new double[size];

    // заповнити значеннями
    for (int i=0; i<size; i++)
      p[i] = A.p[i];
  }

  // Додати операторну функцію, що перевантажує оператор присвоєння =
  // Необхідно для коректної роботи оператора присвоєння (уникнення побітового копіювання)
  DoubleArray operator=(const DoubleArray& A)
  {
    size = A.size;

    // виділити пам'ять для покажчиків
    p = new double[size];

    // заповнити значеннями кожен елемент масиву
    for (int i=0; i<size; i++)
      p[i] = A.p[i];

    // повернути поточний об'єкт,
    // викликається конструктор копіювання DoulbeArray(DoubleArray&)
    return *this;
  }

  // Операторна функція, що перевантажує оператор +.
  // Поелементне додавання масивів, якщо один масив коротше за інший,
  // тоді елементи меншого масиву вважаються рівними 0.
  DoubleArray operator+(DoubleArray& A)
  {
    int min = size; // розмір коротшого масиву
    int max = size; // розмір довшого масиву
    if (min>A.size) min = A.size;
    if (max<A.size) max = A.size;

    DoubleArray objA;
    objA.size = max;
    objA.p = new double[objA.size]; // виділити пам'ять

    // цикл поелементного додавання
    for (int i=0; i<max; i++)
    {
      if (i<min)
        objA.p[i] = p[i] + A.p[i];
      else
      {
        if (size<max)
          objA.p[i] = A.p[i];
        else
          objA.p[i] = p[i];
      }
    }
    return objA;
  }
};

void main()
{
  // Демонстрація - клас DoubleArray, що використовує динамічний масив елементів
  DoubleArray A(5);
  A.FormArray();
  A.Show();

  DoubleArray A2=A;

  // те саме стосується й оператора присвоєння
  DoubleArray A3;

  A.SetAi(2,55);
  A3 = A; // виклик операторної функції operator=(const DoubleArray& A)

  A3.Show();

  // Оператор присвоювання у вигляді ланцюжка
  DoubleArray A4;
  A4 = A2 = A3; // працює коректно
  cout<<"A4=A2=A3:"<<endl;
  A4.Show();

  cout<<"A2:"<<endl;
  A2.Show();

  cout<<"A3:"<<endl;
  A3.Show();

  // перевантаження оператора +
  DoubleArray A5;

  A5 = A2+A2+A3+A4;
  A5.Show();

  DoubleArray A6(3);

  A6.SetAi(0, 1.5);
  A6.SetAi(1, 2.2);
  A6.SetAi(2, 1.8);
  cout<<"A6:"<<endl;
  A6.Show();

  cout<<"A6=A5+A6:"<<endl;
  A6 = A5+A6;
  A6.Show();

  system("pause");
}

 

2. Чому необхідно використовувати конструктор копіювання у класі DoubleArray? Пояснення

Пояснимо роботу конструктора копіювання в класі DoubleArray (див. п. 1).
Якщо у класі не реалізувати власний конструктор копіювання, то буде викликатись неявний конструктор копіювання. Цей конструктор копіювання генерується компілятором автоматично у випадку якщо в класі немає розробленого власного конструктора копіювання. Неявний конструктор копіювання виконує так зване “побітове копіювання”. Для класів, що не містять динамічного виділення пам’яті (не містять покажчиків T* p) побітове копіювання працює коректно без помилок.
Однак, якщо клас містить код, що виділяє пам’ять динамічно для деякого покажчика, то побітове копіювання буде повністю копіювати значення покажчика. Тобто, при виконанні

DoubleArray obj1 = obj2;

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



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

C++. Недолік побітового копіювання

Рисунок 1. Недолік побітового копіювання у випадку, якщо клас використовує динамічне виділення пам’яті. Покажчики obj1.p та obj2.p вказують на одну й туж ділянку пам’яті

Після реалізації власного конструктора копіювання, цей конструктор буде перевизначати неявний конструктор копіювання, який генерується компілятором. У власному конструкторі копіювання потрібно реалізувати виділення пам’яті для покажчика p. Основна задача включення в клас цього конструктора – обійти побітове копіювання.
Наприклад, для класу DoubleArray конструктор копіювання може мати такий вигляд:

// додати конструктор копіювання, щоб коректно виконувався деструктор та ініціалізація об'єкту
DoubleArray(const DoubleArray& A)
{
  size = A.size;

  // виділити пам'ять для покажчика p власним способом
  p = new double[size];

  // заповнити значеннями
  for (int i=0; i<size; i++)
    p[i] = A.p[i];
}

Після додавання конструктора копіювання у тіло класу DoubleArray та виконання коду у функції main()

// функція main()
void main()
{
  // екземпляр obj1 класу DoubleArray
  DoubleArray obj1;

  // Екземпляр obj2 класу DoubleArray.
  // Викликається явно визначений конструктор копіювання,
  // у результаті obj1.p та obj2.p вказують на різні ділянки пам'яті
  DoubleArray obj2=obj1; // Виклик конструктора obj2.DoubleArray(const DoubleArray& obj1)

  // ...
};

обидва об’єкти obj1, obj2 будуть вказувати на різні ділянки пам’яті як показано на рисунку 2.

C++. Класи. Реалізація конструктора копіювання. Покажчики в об'єктах вказують на різні ділянки пам'яті

Рисунок 2. В класі реалізовано конструктор копіювання. Покажчики obj1.p та obj2.p вказують на різні ділянки пам’яті

Це призведе до коректної роботи об’єктів класів у тих випадках, де викликається конструктор копіювання.

 

3. Перевантаження операторної функції operator=(), яка перевизначає оператор присвоєння = (оператор копіювання). Чому потрібно перевантажувати? Пояснення

Для коректного присвоювання об’єктів, у класі DoubleArray крім конструктора копіювання також потрібно перевизначати оператор присвоєння =.

Як і у випадку з конструктором копіювання на необхідність використання операторної функції operator=() в класі впливають дві можливі ситуації:

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

Для класу DoubleArray при присвоюванні об’єктів

DoubleArray obj1;
DoubleArray obj2;
obj2 = obj1; // присвоєння об’єктів

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

Наш клас DoubleArray реалізує динамічне виділення пам’яті для покажчика p. Тому для класу DoubleArray обов’язково потрібно реалізовувати операторну функцію, яка перевантажує оператор присвоєння =. Наприклад, функція може бути такою:

// Додати операторну функцію, що перевантажує оператор присвоєння =
// Необхідно для коректної роботи оператора присвоєння (уникнення побітового копіювання)
DoubleArray operator=(DoubleArray& A)
{
  size = A.size;

  // виділити пам'ять для покажчиків
  p = new double[size];

  // заповнити значеннями кожен елемент масиву
  for (int i=0; i<size; i++)
    p[i] = A.p[i];

  // повернути поточний об'єкт,
  // викликається конструктор копіювання DoulbeArray(DoubleArray&)
  return *this;
}

 

4. Висновки

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

  • конструктор копіювання DoubleArray(const DoubleArray &);
  • операторну функцію копіювання operator=(const DoubleArray&), яка перевантажує оператор присвоєння =.

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

  • Поняття побітового копіювання. Проблеми, що можуть виникнути з побітовим копіюванням.

Крім того, якщо дані класу виділяються динамічно і займають великий об’єм пам’яті, то копіювання цих даних буде займати додатковий час. Щоб прискорити процес копіювання, в клас DoubleArray рекомендовано додати конструктор переміщення DoubleArray(DoubleArray&&) та оператор переміщення operator=(DoubleArray&&). Це значно прискорить час копіювання великих масивів даних. Однак, у нашому випадку, реалізовано простий динамічний масив типу double, що не є так затратно (копіювання займає малий час). Тому, у даному прикладі в тіло класу не включено конструктора переміщення та оператора переміщення. Робота цих функцій детально описується в іншій темі.

 


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