Недостатки побитового копирования. Пример. Необходимость использования конструктора копирования и оператора копирования для классов, содержащих динамическое выделение памяти
Данная тема имеет общие толкования со следующими темами:
- Конструктор копирования. Примеры использования. Передача объекта класса в функцию. Возврат объекта класса из функции
- Класс DoubleArray. Пример поэлементного суммирования двух массивов. Перегрузка оператора +. Динамическое выделение памяти под массив. Конструктор копирования. Недостатки побитового копирования
Содержание
- 1. Понятие побитового копирования
- 2. В каких случаях вызывается побитовое копирование объектов?
- 3. Какие функции класса используют побитовое копирование объектов?
- 4. Для каких классов обязательно реализовывать собственные явным образом определенные конструктор копирования и оператор копирования?
- 5. Проблемы которые возникают в случае, если в классе нет явным образом заданного конструктора копирования и оператора копирования
- 6. Пример, который демонстрирует недостатки побитового копирования
- Связанные темы
Поиск на других ресурсах:
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) { 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) не генерируется. Таким образом, недостатки побитового копирования были устранены.
⇑
Связанные темы
- Конструктор копирования. Примеры использования. Передача объекта класса в функцию. Возврат объекта класса из функции
- Класс DoubleArray. Пример поэлементного суммирования двух массивов. Перегрузка оператора +. Динамическое выделение памяти под массив. Конструктор копирования. Недостатки побитового копирования
⇑