Пример поэлементного суммирования двух массивов. Класс 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=(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(const 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;

  // ошибки нет, если есть собственная операторна функция, которая перегружает оператор присваивания =,
  // и операторной функции DoubleArray operator=(const DoubleArray&) выполняется выделение памяти
  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. А это значит, что для корректной работы класса нужно включить собственный разработанный конструктор копирования.

Недостаток побитового копирования. Указатели obj1.p и obj2.p указывают на один участок памяти

Рисунок 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++. Конструктор копирования в классе. Указатели obj1.p и obj2.p указывают на разные участки памяти

Рисунок 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, что не сильно затратно (копирование занимает минимум времени). Поэтому, в данном примере в тело класса не включен конструктор перемещения и оператор перемещения. Работа этих функций подробно описывается в другой теме.

 


Связанные темы