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

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

person fredoverflow    schedule 13.11.2010    source источник
comment
Прочтите всю эту цепочку и c++-faq tag wiki перед голосованием за закрытие.   -  person sbi    schedule 13.11.2010
comment
@Binary: По крайней мере, найдите время, чтобы прочитать обсуждение комментария прежде, чем вы проголосуете. Раньше текст был намного проще, но Фреда попросили расширить его. Кроме того, хотя это четыре вопроса грамматически, на самом деле это всего лишь один вопрос с несколькими аспектами. (Если вы не согласны с этим, тогда докажите свою точку зрения, ответив на каждый из этих вопросов отдельно, и позвольте нам проголосовать за результаты.)   -  person sbi    schedule 16.11.2010
comment
Фред, вот интересное дополнение к вашему ответу относительно C ++ 1x: stackoverflow.com/questions/4782757/. Как мы с этим справляемся?   -  person sbi    schedule 25.01.2011
comment
По теме: Закон большой двойки   -  person Nemanja Trifunovic    schedule 27.06.2011
comment
Имейте в виду, что, начиная с C ++ 11, я думаю, что это было обновлено до правила пяти или что-то в этом роде.   -  person paxdiablo    schedule 19.08.2015
comment
@paxdiablo, если быть точным, Правило нуля.   -  person rubenvb    schedule 25.09.2015


Ответы (8)


Вступление

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

Рассмотрим простой пример:

class person
{
    std::string name;
    int age;

public:

    person(const std::string& name, int age) : name(name), age(age)
    {
    }
};

int main()
{
    person a("Bjarne Stroustrup", 60);
    person b(a);   // What happens here?
    b = a;         // And here?
}

(Если вас озадачивает часть name(name), age(age), это называется списком инициализаторов участников.)

Специальные функции-члены

Что значит скопировать person объект? Функция main показывает два различных сценария копирования. Инициализация person b(a); выполняется конструктором копирования. Его задача - создать новый объект на основе состояния существующего объекта. Присваивание b = a выполняется оператором присваивания копии. Его работа обычно немного сложнее, потому что целевой объект уже находится в некотором допустимом состоянии, с которым нужно работать.

Поскольку мы сами не объявили ни конструктор копирования, ни оператор присваивания (ни деструктор), они неявно определены для нас. Цитата из стандарта:

Конструктор копирования [...] и оператор присваивания копии, [...] и деструктор являются специальными функциями-членами. [Примечание: Реализация неявно объявляет эти функции-члены для некоторых типов классов, если программа не объявляет их явно. Реализация неявно определяет их, если они используются. [...] конец сообщения] [n3126.pdf, раздел 12 §1]

По умолчанию копирование объекта означает копирование его членов:

Неявно определенный конструктор копирования для класса X, не являющегося объединением, выполняет поэлементное копирование своих подобъектов. [n3126.pdf раздел 12.8 §16]

Неявно определенный оператор присваивания копии для класса X, не являющегося объединением, выполняет поэлементное присваивание копий его подобъектам. [n3126.pdf раздел 12.8 §30]

Неявные определения

Неявно определенные специальные функции-члены для person выглядят следующим образом:

// 1. copy constructor
person(const person& that) : name(that.name), age(that.age)
{
}

// 2. copy assignment operator
person& operator=(const person& that)
{
    name = that.name;
    age = that.age;
    return *this;
}

// 3. destructor
~person()
{
}

Последовательное копирование - это именно то, что нам нужно в этом случае: name и age копируются, поэтому мы получаем самодостаточный, независимый person объект. Неявно определенный деструктор всегда пуст. В данном случае это тоже нормально, поскольку мы не получили никаких ресурсов в конструкторе. Деструкторы членов неявно вызываются после завершения деструктора person:

После выполнения тела деструктора и уничтожения любых автоматических объектов, размещенных в теле, деструктор для класса X вызывает деструкторы для [...] прямых членов X [n3126.pdf 12.4 §6]

Управление ресурсами

Итак, когда мы должны явно объявить эти специальные функции-члены? Когда наш класс управляет ресурсом, то есть когда объект класса отвечает за этот ресурс. Обычно это означает, что ресурс приобретается в конструкторе (или передается в конструктор) и освобождается в деструкторе.

Вернемся в прошлое, к предстандартному C ++. Не существовало такой вещи, как std::string, и программисты были влюблены в указатели. Класс person мог бы выглядеть так:

class person
{
    char* name;
    int age;

public:

    // the constructor acquires a resource:
    // in this case, dynamic memory obtained via new[]
    person(const char* the_name, int the_age)
    {
        name = new char[strlen(the_name) + 1];
        strcpy(name, the_name);
        age = the_age;
    }

    // the destructor must release this resource via delete[]
    ~person()
    {
        delete[] name;
    }
};

Даже сегодня люди по-прежнему пишут классы в этом стиле и попадают в затруднительное положение: Я втолкнул человека в вектор, и теперь у меня сумасшедшие ошибки памяти! Помните, что по умолчанию копирование объекта означает копирование его членов, но копирование члена name просто копирует указатель, не массив символов, на который он указывает! Это имеет несколько неприятных последствий:

  1. Изменения через a можно увидеть через b.
  2. После уничтожения b a.name становится висящим указателем.
  3. Если a уничтожается, удаление зависшего указателя приводит к неопределенному поведению.
  4. Поскольку присваивание не принимает во внимание то, на что name указывал перед присваиванием, рано или поздно вы получите повсюду утечки памяти.

Явные определения

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

// 1. copy constructor
person(const person& that)
{
    name = new char[strlen(that.name) + 1];
    strcpy(name, that.name);
    age = that.age;
}

// 2. copy assignment operator
person& operator=(const person& that)
{
    if (this != &that)
    {
        delete[] name;
        // This is a dangerous point in the flow of execution!
        // We have temporarily invalidated the class invariants,
        // and the next statement might throw an exception,
        // leaving the object in an invalid state :(
        name = new char[strlen(that.name) + 1];
        strcpy(name, that.name);
        age = that.age;
    }
    return *this;
}

Обратите внимание на разницу между инициализацией и назначением: мы должны удалить старое состояние перед назначением name, чтобы предотвратить утечку памяти. Также мы должны защитить себя от присвоения формы x = x. Без этой проверки delete[] name удалит массив, содержащий строку source, потому что, когда вы пишете x = x, и this->name, и that.name содержат один и тот же указатель.

Исключительная безопасность

К сожалению, это решение не сработает, если new char[...] вызовет исключение из-за нехватки памяти. Одно из возможных решений - ввести локальную переменную и изменить порядок операторов:

// 2. copy assignment operator
person& operator=(const person& that)
{
    char* local_name = new char[strlen(that.name) + 1];
    // If the above statement throws,
    // the object is still in the same state as before.
    // None of the following statements will throw an exception :)
    strcpy(local_name, that.name);
    delete[] name;
    name = local_name;
    age = that.age;
    return *this;
}

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

Некопируемые ресурсы

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

private:

    person(const person& that);
    person& operator=(const person& that);

В качестве альтернативы вы можете унаследовать от boost::noncopyable или объявить их удаленными (в C ++ 11 и выше):

person(const person& that) = delete;
person& operator=(const person& that) = delete;

Правило трех

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

Если вам нужно явно объявить деструктор, конструктор копирования или оператор присваивания копии, вам, вероятно, потребуется явно объявить все три из них.

(К сожалению, это правило не соблюдается ни стандартом C ++, ни каким-либо известным мне компилятором.)

Правило пяти

Начиная с C ++ 11, у объекта есть две дополнительные специальные функции-члены: конструктор перемещения и присваивание перемещения. Правило пяти государств также реализует эти функции.

Пример с подписями:

class person
{
    std::string name;
    int age;

public:
    person(const std::string& name, int age);        // Ctor
    person(const person &) = default;                // 1/5: Copy Ctor
    person(person &&) noexcept = default;            // 4/5: Move Ctor
    person& operator=(const person &) = default;     // 2/5: Copy Assignment
    person& operator=(person &&) noexcept = default; // 5/5: Move Assignment
    ~person() noexcept = default;                    // 3/5: Dtor
};

Правило нуля

Правило 3/5 также называется правилом 0/3/5. Нулевая часть правила гласит, что вам разрешено не писать какие-либо специальные функции-члены при создании вашего класса.

Совет

В большинстве случаев вам не нужно управлять ресурсом самостоятельно, потому что существующий класс, такой как std::string, уже делает это за вас. Просто сравните простой код, использующий элемент std::string, с запутанной и подверженной ошибкам альтернативой, использующей char*, и вы убедитесь. Пока вы держитесь подальше от необработанных элементов-указателей, правило трех вряд ли будет касаться вашего собственного кода.

person fredoverflow    schedule 13.11.2010
comment
Я бы просто изменил определение, чтобы определить или объявить частным, поскольку существуют классы (дескрипторы ресурсов, мьютексы ... мутации?), Для которых не должно существовать семантики копирования. Тогда достаточно просто деструктора, и конструктор копирования и оператор = нужно просто объявить как закрытые, чтобы избежать случайного копирования. - person Kos; 13.11.2010
comment
Фред, я бы лучше проголосовал, если (A) вы не укажете плохо реализованное назначение в копируемом коде и не добавите примечание о том, что это неправильно, и посмотрите в другом месте в мелком шрифте; либо используйте c & s в коде, либо просто пропустите реализацию всех этих членов (B), вы сократите первую половину, что имеет мало общего с RoT; (C) вы бы обсудили введение семантики перемещения и что это означает для RoT. - person sbi; 13.11.2010
comment
@sbi: этот вопрос помечен тегом c ++ - faq, поэтому такой уважаемый участник, как вы, должен не стесняться исправлять исходное сообщение (в случае, если оно содержит вводящую в заблуждение информацию), редактируя его, вместо того, чтобы просто оставлять комментарий с просьбой к автору исправить его сообщение . :-) - person Prasoon Saurav; 13.11.2010
comment
Но тогда пост надо делать Ч / Б, думаю. Мне нравится, что вы сохраняете термины в основном точными (то есть вы говорите оператор присваивания copy, и что вы не попадаете в общую ловушку, что назначение не может подразумевать копию). - person Johannes Schaub - litb; 13.11.2010
comment
Отличный пост. Не могли бы вы дать ссылку на пункт стандарта, который вы там цитируете? На случай, если кто-то захочет его найти. - person Björn Pollex; 13.11.2010
comment
@Prasoon: Я не думаю, что вырезание половины ответа будет рассматриваться как справедливое редактирование ответа, не связанного с CW. - person sbi; 13.11.2010
comment
Кроме того, я, возможно, перечитал его, но вы не упомянули, что оператор назначения копирования должен проверять личность, прежде чем что-либо делать. - person Björn Pollex; 13.11.2010
comment
@ Space_C0wb0y: На самом деле этого не должно быть. Об этом позаботится Copy-and-Swap. Вот почему здесь следует либо использовать C&S, либо не иметь каких-либо реализаций, а просто сослаться на публикацию C&S GMan. Но похоже, что Фред скрылся в тот момент, когда опубликовал свой ответ. :) - person sbi; 13.11.2010
comment
(К сожалению, это правило не применяется ни в стандарте C ++, ни в каком-либо известном мне компиляторе.) Это утверждение опровергается фразой: вам вероятно необходимо явно объявить все три из них. Наверное, это не правило. - person Lee Louviere; 27.05.2011
comment
Классы действительно обрабатывают ресурсы, необработанные указатели - не единственный такой ресурс. В общем, когда вы пишете класс, который обрабатывает ресурс, сделайте его не копируемым, если вы не пишете system. (например, если вы по какой-либо причине реализуете интеллектуальный указатель). - person CashCow; 23.02.2012
comment
Было бы здорово, если бы вы обновили свой пост для C ++ 11 (т.е. переместите конструктор / назначение) - person Alexander Malakhov; 13.09.2012
comment
По сути, проверка границ, жестко запрограммированная в std::vector, может сильно повредить вашим выступлениям. Это единственная причина, по которой вы захотите использовать указатели, но она вполне обоснована, поскольку создание быстрого кода - это почти единственная причина, по которой вы хотели бы использовать громоздкий, подробный, запутанный и слабый язык, такой как C ++, в первом. место. В этот момент вам, вероятно, понадобится вся эта хрень с назначением на месте, представленная в C ++ 11, которая заставит вас выложить около 100 строк трудоемких канонических штуковин, чтобы убедиться, что ваш код не выбрасывает мусор в память и не дает сбой. - person kuroi neko; 21.09.2014
comment
@kuroineko std::vector не проверяет границы (если вы не используете at, но зачем вам использовать это вместо operator[]?) - person Etienne de Martel; 21.09.2014
comment
@EtiennedeMartel operator[] часто выполняет проверку границ в режиме отладки, что может привести к снижению производительности; что точно то, для чего нужен режим отладки. Если кто-то жалуется, что режим отладки медленнее и безопаснее, чем режим выпуска, покажите им дорогу в психушку. - person fredoverflow; 21.09.2014
comment
@EtiennedeMartel На практике это делает Visual Studio. Я не дам двух пенсов за то, о чем спорят между собой защитники чистоты языка, это просто то, что я видел собственными глазами в (выпускном) ассемблерном коде, сгенерированном VS2013. Еще одна причина держаться подальше от C ++, если вам действительно не нужен быстрый код, и готовы преодолеть мелкие проблемы реализации 1970 года, и это после того, как вы справились с заумными языковыми конструкциями, появившимися с последними стандартными разработками. - person kuroi neko; 21.09.2014
comment
@kuroineko Я никогда не видел, чтобы Visual Studio выполняла проверки границ в режиме выпуска. В любом случае, вы всегда можете написать T * p = v.data(); или T * p = &v[0]; и работать с любимыми указателями. - person fredoverflow; 21.09.2014
comment
Я не люблю указатели. Я просто заявляю, что использование ванильных векторов может стоить вам значительного количества ресурсов процессора, в зависимости от того, какой компилятор вы используете, или даже от какой версии STL. Что касается вашего решения, оно попирает так много правил хорошей практики, что вам понадобится бык, чтобы закопать кучу мертвых котят - и он либо выйдет из строя, если вы измените размер вектора, либо будет неэффективным, поскольку адреса придется перечитывать . Что касается того, что делает VS, это не обязательная проверка per se. Он пытается привести вектор в порядок на случай, если кто-то изменил его размер. Это в строке 1621 vector STL include, если вам интересно. - person kuroi neko; 21.09.2014
comment
Можете ли вы прояснить / уточнить это утверждение (или указать мне правильное направление для указанного разъяснения): никогда не управляйте несколькими ресурсами в одном классе, это приведет только к боли. Мне кажется, что одним из основных преимуществ ООП является инкапсуляция (часто нескольких) ресурсов в один класс, так что клиентам этого класса не нужно беспокоиться об управлении этими ресурсами. - person wickstopher; 25.05.2015
comment
@wickstopher Это означает, что, например, вместо двух char* элементов данных (а также конструктора копирования, оператора присваивания и деструктора, каждый из которых управляет обоими char*s) в классе A, вы должны написать другой класс B с одним char* элементом данных (и всей сложной логикой), а затем иметь два B члена данных внутри класса A. Тогда вам вообще не нужна сложная логика в классе A. Конечно, вместо того, чтобы писать и использовать класс B, вы действительно должны просто использовать std::string внутри класса A;) Имеет смысл? - person fredoverflow; 25.05.2015
comment
Это может быть глупый вопрос, но что именно вы имеете в виду, когда говорите о ресурсах? - person solalito; 18.11.2015
comment
@solalito Все, что вы должны освободить после использования: блокировки параллелизма, дескрипторы файлов, соединения с базой данных, сетевые сокеты, память кучи ... - person fredoverflow; 18.11.2015
comment
Если я введу предложение person b=a;, будет ли вызываться конструктор копирования или будет вызван оператор присваивания копии? - person andy90; 17.01.2018
comment
Поскольку в С ++ 11 неявно определенные конструкторы-копии / присваивания-копии устарели (если задан другой), и gcc-9 может предупредить об этом: godbolt.org/z/hC54UF (предупреждение является частью -Wextra). - person chtz; 28.12.2019
comment
У меня есть дополнительный вопрос об использовании = default в вашем примере внизу, по которому я хотел бы получить дополнительную информацию. Я разместил свой вопрос здесь: stackoverflow.com/questions/63855090/. - person Gabriel Staples; 12.09.2020
comment
Что касается К сожалению, это правило не соблюдается ни стандартом C ++, ни каким-либо известным мне компилятором. В gcc есть опция Weffc++, которая может оказаться полезной. См. gcc.gnu.org/onlinedocs/gcc. -4.8.1 / gcc / - person dgruending; 28.04.2021
comment
@fredoverflow неявное определение The members' destructors are implicitly called after the person destructor is finished:, но членами являются name и age, которые относятся к типу string и типу int. существует ли деструктор для переменной типа int или string? Я думаю, что конструктор и деструктор доступны только для объектов и класса, верно? Если я ошибаюсь, пожалуйста, объясните. - person Abhishek Mane; 15.05.2021
comment
@fredoverflow, пожалуйста, ответьте, возможно, я глупо сомневаюсь, но если вы ответите, мне станет ясно. - person Abhishek Mane; 16.05.2021
comment
@AbhishekMane string - это класс, его деструктор освобождает символы в куче. Делает ли деструктор int ничего или не существует - это философский вопрос без последствий. - person fredoverflow; 17.05.2021
comment
@fredoverflow, который вы объявили name таким образом char *name, поэтому я думаю, что он не принадлежит string классу, поэтому переменная ptr типа char имеет деструктор? Я не понимаю, что вы говорите о деструкторе для переменной типа int, существует деструктор или нет? Я чувствую, что могу упустить какой-то фундаментальный базовый момент. Пожалуйста, пролей на это немного света. - person Abhishek Mane; 17.05.2021
comment
@AbhishekMane О, я думал, вы имеете в виду пример из правила пяти с std::string name;. Обычно ни char*, ни int не имеют деструктора. Но в общем коде вы можете притвориться, что они существуют: stackoverflow.com/questions/456310 - person fredoverflow; 17.05.2021
comment
@fredoverflow Спасибо, я понял. но если вы говорите как members' destructors are implicitly called, разве это не так? Что вы думаете? - person Abhishek Mane; 18.05.2021
comment
@fredoverflow stackoverflow.com/q/67772282/11862989, пожалуйста, ответьте на этот вопрос. - person Abhishek Mane; 02.06.2021

Правило трех - это практическое правило для C ++, в основном говоря

Если вашему классу нужен какой-либо из

  • конструктор копирования,
  • оператор присваивания,
  • или деструктор,

определены явно, то, вероятно, потребуются все три из них.

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

Если нет хорошей семантики для копирования ресурса, которым управляет ваш класс, рассмотрите возможность запрета копирования, объявив (не определение) конструктора копирования и оператора присваивания как private.

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

person sbi    schedule 13.11.2010
comment
Другое решение для предотвращения копирования - наследование (частное) от класса, который нельзя скопировать (например, boost::noncopyable). Это также может быть намного яснее. Думаю, здесь может помочь C ++ 0x и возможность удаления функций, но забыл синтаксис: / - person Matthieu M.; 13.11.2010
comment
@Matthieu: Да, это тоже работает. Но если noncopyable не является частью std lib, я не считаю это большим улучшением. (О, и если вы забыли синтаксис удаления, вы забыли еще больше, чем я когда-либо знал. :)) - person sbi; 13.11.2010
comment
@Daan: см. этот ответ. Однако я бы рекомендовал придерживаться Мартиньо Правило нуля. Для меня это одно из самых важных практических правил C ++, придуманных за последнее десятилетие. - person sbi; 04.06.2014
comment
Правило нуля Мартиньо теперь лучше (без очевидного захвата рекламного ПО) и находится по адресу archive.org - person Nathan Kidd; 27.06.2018

Закон большой тройки такой, как указано выше.

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

Деструктор не по умолчанию

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

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

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

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

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

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

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

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

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

person Stefan    schedule 14.05.2012
comment
Итак, если мы используем конструктор копирования, тогда копия создается, но в другом месте памяти, а если мы не используем конструктор копирования, то копия выполняется, но она указывает на то же место в памяти. это то, что вы пытаетесь сказать? Таким образом, копия без конструктора копирования означает, что новый указатель будет там, но указывает на то же место в памяти, однако, если у нас есть конструктор копирования, явно определенный пользователем, тогда у нас будет отдельный указатель, указывающий на другое место в памяти, но с данными. - person Unbreakable; 04.01.2015
comment
Извините, я ответил на это много лет назад, но моего ответа, похоже, все еще нет :-( В принципе, да - вы поняли :-) - person Stefan; 27.07.2016
comment
Как этот принцип распространяется на оператор присваивания копий? Этот ответ был бы более полезным, если бы было упомянуто 3-е в Правиле трех. - person DBedrenko; 31.10.2017
comment
@DBedrenko, вы пишете конструктор копирования, чтобы он выделял новым объектам свои собственные участки памяти ... это тот же принцип, который распространяется на оператор присваивания копии. Вы не думаете, что я это ясно дал понять? - person Stefan; 02.11.2017
comment
@Stefan Спасибо за ваш ответ, но мне это непонятно, было бы хорошо сделать его явным. Я пришел к этому вопросу, чтобы узнать назначение оператора присваивания копии, поэтому было бы хорошо выяснить, какую проблему он решает. - person DBedrenko; 02.11.2017
comment
@DBedrenko, я добавил еще немного информации. Это проясняет? - person Stefan; 02.11.2017

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

    MyClass x(a, b);
    MyClass y(c, d);
    x = y; // This is a shallow copy if assignment operator is not provided

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

person fatma.ekici    schedule 31.12.2012

Что означает копирование объекта? Есть несколько способов копирования объектов - давайте поговорим о двух типах, о которых вы, скорее всего, имеете в виду: глубокое и поверхностное копирование.

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

class Car //A very simple class just to demonstrate what these definitions mean.
//It's pseudocode C++/Javaish, I assume strings do not need to be allocated.
{
private String sPrintColor;
private String sModel;
private String sMake;

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

public Car(String model, String make, String color) //Constructor
{
   this.sPrintColor = color;
   this.sModel = model;
   this.sMake = make;
}

public ~Car() //Destructor
{
//Because we did not create any custom types, we aren't adding more code.
//Anytime your object goes out of scope / program collects garbage / etc. this guy gets called + all other related destructors.
//Since we did not use anything but strings, we have nothing additional to handle.
//The assumption is being made that the 3 strings will be handled by string's destructor and that it is being called automatically--if this were not the case you would need to do it here.
}

public Car(const Car &other) // Copy Constructor
{
   this.sPrintColor = other.sPrintColor;
   this.sModel = other.sModel;
   this.sMake = other.sMake;
}
public Car &operator =(const Car &other) // Assignment Operator
{
   if(this != &other)
   {
      this.sPrintColor = other.sPrintColor;
      this.sModel = other.sModel;
      this.sMake = other.sMake;
   }
   return *this;
}

}

Глубокая копия - это если мы объявляем объект, а затем создаем полностью отдельную копию объекта ... мы получаем 2 объекта в 2 полных наборах памяти.

Car car1 = new Car("mustang", "ford", "red");
Car car2 = car1; //Call the copy constructor
car2.changePaint("green");
//car2 is now green but car1 is still red.

А теперь сделаем что-нибудь странное. Скажем, car2 либо запрограммирован неправильно, либо преднамеренно предназначен для совместного использования фактической памяти, из которой сделан car1. (Обычно это ошибка, и в классах это обычно одеяло, под которым оно обсуждается.) Представьте, что каждый раз, когда вы спрашиваете о car2, вы действительно разрешаете указатель на пространство памяти car1 ... это более или менее то, что мелкая копия является.

//Shallow copy example
//Assume we're in C++ because it's standard behavior is to shallow copy objects if you do not have a constructor written for an operation.
//Now let's assume I do not have any code for the assignment or copy operations like I do above...with those now gone, C++ will use the default.

 Car car1 = new Car("ford", "mustang", "red"); 
 Car car2 = car1; 
 car2.changePaint("green");//car1 is also now green 
 delete car2;/*I get rid of my car which is also really your car...I told C++ to resolve 
 the address of where car2 exists and delete the memory...which is also
 the memory associated with your car.*/
 car1.changePaint("red");/*program will likely crash because this area is
 no longer allocated to the program.*/

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

Что такое конструктор копирования и оператор присваивания копии? Я уже использовал их выше. Конструктор копирования вызывается, когда вы вводите код, например Car car2 = car1;. По сути, если вы объявляете переменную и назначаете ее в одной строке, то при вызове конструктора копирования. Оператор присваивания - это то, что происходит, когда вы используете знак равенства --_ 5_. Замечание car2 не объявлено в том же заявлении. Два фрагмента кода, которые вы пишете для этих операций, вероятно, очень похожи. Фактически, в типичном шаблоне проектирования есть еще одна функция, которую вы вызываете, чтобы установить все, как только вы будете удовлетворены первоначальным копированием / назначением законным - если вы посмотрите на длинный код, который я написал, функции почти идентичны.

Когда мне нужно заявить о них самому? Если вы не пишете код, предназначенный для совместного использования или для производства, вам действительно нужно объявить их только тогда, когда они вам понадобятся. Вам действительно нужно знать, что делает ваш язык программы, если вы решили использовать его «случайно» и не сделали этого, т. Е. вы получаете компилятор по умолчанию. Например, я редко использую конструкторы копирования, но переопределения оператора присваивания очень распространены. Знаете ли вы, что вы также можете переопределить, что означают сложение, вычитание и т. Д.?

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

person user1701047    schedule 17.10.2012
comment
Вопрос был помечен как C ++. Это описание псевдокода в лучшем случае мало что дает, чтобы прояснить что-либо о четко определенном Правиле трех, а в худшем - лишь вносит путаницу. - person sehe; 12.06.2014

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

Правило трех гласит, что если вы объявите любой из

  1. конструктор копирования
  2. оператор присваивания копий
  3. деструктор

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

  • какое бы управление ресурсами ни выполнялось в одной операции копирования, вероятно, необходимо было выполнить в другой операции копирования и

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

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

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

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

class MemoryBlock
{
public:

//code here

private:
MemoryBlock(const MemoryBlock& other)
{
   cout<<"copy constructor"<<endl;
}

// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other)
{
 return *this;
}
};

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

Начиная с C ++ 11, вы также можете объявить конструктор копирования и оператор присваивания удаленными.

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

// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other) =delete
};


int main()
{
   MemoryBlock a;
   MemoryBlock b(a);
}
person Ajay yadav    schedule 12.01.2016

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

Недавно Майкл Клесс выступил с докладом на эту тему: http://channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class

person xyz    schedule 07.01.2015

Правило трех в C ++ - это фундаментальный принцип проектирования и разработки трех требований, согласно которым при наличии четкого определения в одной из следующих функций-членов программист должен определить две другие функции-члены вместе. А именно, необходимы следующие три функции-члены: деструктор, конструктор копирования, оператор присваивания копии.

Конструктор копирования в C ++ - это специальный конструктор. Он используется для создания нового объекта, который является новым объектом, эквивалентным копии существующего объекта.

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

Вот быстрые примеры:

// default constructor
My_Class a;

// copy constructor
My_Class b(a);

// copy constructor
My_Class c = a;

// copy assignment operator
b = a;
person Marcus Thornton    schedule 12.08.2014
comment
Привет, твой ответ ничего нового не добавляет. Остальные освещают тему гораздо глубже, а точнее - ваш ответ приблизительный и в некоторых местах даже неверен (а именно, здесь нет необходимости; очень вероятно, что следует). На самом деле не стоит публиковать подобные ответы на вопросы, на которые уже даны исчерпывающие ответы. Если вам нечего добавить. - person Mat; 15.08.2014
comment
Кроме того, есть четыре быстрых примера, которые так или иначе связаны с двумя из трех, которые Правило трех говорит о. Слишком много путаницы. - person anatolyg; 03.11.2014