25

Что такое Правило трёх?

14

Проблема с копированием объектов в C++:

  1. Что означает копирование объекта?
  2. Что такое конструктор копирования и оператор присваивания копии?
  3. Когда мне нужно объявлять их самостоятельно?
  4. Как я могу предотвратить копирование своих объектов?

5 ответ(ов)

1

Закон "большой тройки" описан выше.

Простой пример на понятном языке, какую проблему он решает:

Необычный деструктор

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

Вы можете подумать, что на этом работа выполнена.

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

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

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

Оператор присваивания и конструктор копирования

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

Это означает, что новый и старый объекты будут указывать на одну и ту же область памяти, и когда вы измените ее в одном объекте, она изменится и в другом. Если один из объектов удалит эту память, другой продолжит пытаться ее использовать — это может привести к серьезным ошибкам.

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

0

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

MyClass x(a, b);
MyClass y(c, d);
x = y; // Это будет неглубокое копирование, если оператор присваивания не предоставлен

Если в классе MyClass только примитивные типы, то стандартный оператор присваивания будет работать корректно. Однако если в классе есть указатели или объекты, у которых нет операторов присваивания, результат может оказаться непредсказуемым. Таким образом, можно сказать, что если в деструкторе класса есть что-то, что нужно удалить, возможно, потребуется оператор глубокого копирования. Это означает, что нам следует реализовать конструктор копирования и оператор присваивания.

0

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

Предположим, что мы работаем в объектно-ориентированном языке программирования. Это означает, что у нас есть выделенные участки памяти, которые представляют переменные примитивных типов (таких как int, char, byte) или классы, определенные нами, составленные из наших собственных типов и примитивов. Давайте рассмотрим простой пример на классе Car:

class Car {
private String sPrintColor;
private String sModel;
private String sMake;

public void changePaint(String newColor) {
   this.sPrintColor = newColor;
}

public Car(String model, String make, String color) { // Конструктор
   this.sPrintColor = color;
   this.sModel = model;
   this.sMake = make;
}

public Car(Car other) { // Конструктор копирования
   this.sPrintColor = other.sPrintColor;
   this.sModel = other.sModel;
   this.sMake = other.sMake;
}

public Car assign(Car other) { // Оператор присваивания
   if (this != other) {
      this.sPrintColor = other.sPrintColor;
      this.sModel = other.sModel;
      this.sMake = other.sMake;
   }
   return this;
}
}

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

Car car1 = new Car("mustang", "ford", "red");
Car car2 = new Car(car1); // Вызов конструктора копирования
car2.changePaint("green");
// Теперь car2 стал зеленым, но car1 все еще красный.

Теперь представим, что объект car2 каким-то образом связан с памятью объекта car1. Это скорее всего ошибка, но тем не менее так работают поверхностные копии. В этом случае car2 ссылается на ту же область памяти, что и car1.

Car car1 = new Car("ford", "mustang", "red");
Car car2 = car1; // car2 ссылается на car1
car2.changePaint("green"); // car1 тоже стал зеленым

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

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

Что такое конструктор копирования и оператор присваивания?

Конструктор копирования вызывается, когда вы создаете новый объект на основе существующего, например Car car2 = car1;. Оператор присваивания срабатывает, когда вы присваиваете один объект другому уже существующему объекту, как в car2 = car1;.

Когда мне нужно объявлять их самостоятельно?

Если вы не разрабатываете код для общего пользования или промышленного применения, вам нужно объявлять конструктор копирования и оператор присваивания только тогда, когда это необходимо. Однако следует быть внимательным к поведению вашего языка программирования по умолчанию, если вы не реализовали эти функции самостоятельно.

Как я могу предотвратить копирование своих объектов?

Можно сделать все функции, позволяющие выделять память для объекта, приватными. Если вы действительно не хотите, чтобы ваши объекты копировались, можно просто выбросить исключение в этих функциях или также сделать их публичными, чтобы информировать других разработчиков.

0

Когда мне нужно объявлять их самостоятельно?

Правило трех гласит, что если вы объявляете любой из следующих элементов:

  1. Конструктор копирования
  2. Оператор присваивания копированием
  3. Деструктор

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

  • любое управление ресурсами при одной операции копирования, вероятно, требуется и при другой операции копирования, и
  • деструктор класса также будет иметь отношение к управлению ресурсом (обычно его освобождению). Классическим ресурсом, которым нужно управлять, была память, и поэтому все классы стандартной библиотеки, которые управляют памятью (например, контейнеры STL, выполняющие динамическое управление памятью), объявляют «большую тройку»: как операции копирования, так и деструктор.

Одним из последствий Правила трех является то, что наличие пользовательского деструктора указывает на то, что простое побитное копирование вряд ли будет уместным для операций копирования в классе. Это, в свою очередь, подсказывает, что если класс объявляет деструктор, операции копирования, скорее всего, не должны генерироваться автоматически, поскольку они не выполнит необходимое действие. Во время принятия C98 важность этой логики не была полностью осознана, поэтому в C98 наличие пользовательского деструктора не влияло на готовность компиляторов генерировать операции копирования. Это происходит и в C++11, но только потому, что ограничение условий, при которых операции копирования генерируются, разрушит слишком много устаревшего кода.

Как я могу предотвратить копирование моих объектов?

Объявите конструктор копирования и оператор присваивания копированием как спецификаторы доступа private.

class MemoryBlock
{
public:
// код здесь

private:
MemoryBlock(const MemoryBlock& other)
{
   cout<<"конструктор копирования"<<endl;
}

// Оператор присваивания копированием.
MemoryBlock& operator=(const MemoryBlock& other)
{
 return *this;
}
};

int main()
{
   MemoryBlock a;
   MemoryBlock b(a);
}

В C++11 и позднее вы также можете объявить конструктор копирования и оператор присваивания как удаленные:

class MemoryBlock
{
public:
MemoryBlock(const MemoryBlock& other) = delete;

// Оператор присваивания копированием.
MemoryBlock& operator=(const MemoryBlock& other) = delete;
};

int main()
{
   MemoryBlock a;
   MemoryBlock b(a);
}
0

Многие из существующих ответов уже упоминают конструктор копирования, оператор присваивания и деструктор. Однако в стандарте C++11 и позже была введена семантика перемещения, что расширяет этот концепт уже не ограничиваясь тремя пунктами.

Недавно Михаэль Клесс провел лекцию, в которой затрагивается эта тема: http://channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class.

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь