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

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

Данная тема имеет общие толкования со следующими темами:


Содержание



1. Понятие побитового копирования

Побитовое копирование – это способ получения копии объекта класса путем копирования каждого бита (байта) конкретного объекта (экземпляра) класса. Побитовое копирование используется в случаях, когда из объекта-источника нужно получить идентичный ему объект-назначение.

 

2. В каких случаях вызывается побитовое копирование объектов?

Побитовое копирование объектов вызывается в следующих случаях:

  • если в классе нет явным образом заданного конструктора копирования, который обеспечивает создание нового объекта из существующего. В этом случае компилятор автоматически генерирует собственный неявный конструктор копирования. Этот конструктор копирования реализует создание копии объекта путем побитового копирования;
  • в операторной функции operator=(), которая осуществляет копирование одного объекта в другой. В этом случае компилятор также автоматически генерирует собственную неявную операторную функцию, которая делает полную копию объекта.

Однако, побитовое копирование, которое реализовано в компиляторе, может привести к невидимым ошибкам программирования в случае если в классе память выделяется динамически.

 

3. Какие функции класса используют побитовое копирование объектов?

Побитовое копирование объектов используют две функции класса:

  • неявный конструктор копирования который генерируется компилятором автоматически в случае отсутствия явно заданного конструктора копирования. Более подробно о конструкторе копирования описывается здесь;
  • неявная операторная функция operator=(), которая генерируется компилятором автоматически в случае отсутствия явно заданной операторной функции. Подробный пример использования операторной функции operator=() описывается здесь.

 

4. Для каких классов обязательно реализовывать собственные явным образом определенные конструктор копирования и оператор копирования?

Недостатки побитового копирования проявляются для классов, в которых память для переменных выделяется динамически. Во избежание этих недостатков, в класс нужно включить собственный явным образом заданный конструктор копирования а также оператор копирования.

Если в классе память не выделяется динамически (нет указателей), то явным образом заданный конструктор копирования и оператор копирования operator=() реализовывать необязательно.

 

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

Если в классе память выделяется динамически и нет явным образом заданного конструктора копирования, то побитовое копирование приводит к следующим проблемам.

1. При копировании типа

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)
  {
    // 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) не генерируется. Таким образом, недостатки побитового копирования были устранены.

 


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