Я знаю, что компилятор C ++ создает конструктор копирования для класса. В каком случае нам нужно написать определяемый пользователем конструктор копирования? Вы можете привести несколько примеров?
Я знаю, что компилятор C ++ создает конструктор копирования для класса. В каком случае нам нужно написать определяемый пользователем конструктор копирования? Вы можете привести несколько примеров?
Ответы:
Конструктор копирования, созданный компилятором, выполняет поэлементное копирование. Иногда этого недостаточно. Например:
class Class {
public:
Class( const char* str );
~Class();
private:
char* stored;
};
Class::Class( const char* str )
{
stored = new char[srtlen( str ) + 1 ];
strcpy( stored, str );
}
Class::~Class()
{
delete[] stored;
}
в этом случае поэлементное копирование stored
члена не будет дублировать буфер (будет скопирован только указатель), поэтому первая уничтожаемая копия, совместно использующая буфер, вызовет delete[]
успешно, а вторая будет работать с неопределенным поведением. Вам нужен конструктор копии с глубоким копированием (а также оператор присваивания).
Class::Class( const Class& another )
{
stored = new char[strlen(another.stored) + 1];
strcpy( stored, another.stored );
}
void Class::operator = ( const Class& another )
{
char* temp = new char[strlen(another.stored) + 1];
strcpy( temp, another.stored);
delete[] stored;
stored = temp;
}
delete stored[];
и я считаю, что она должна бытьdelete [] stored;
std::string
. Общая идея состоит в том, что только служебные классы, которые управляют ресурсами, должны перегружать «большую тройку», а все остальные классы должны просто использовать эти служебные классы, устраняя необходимость определять какой-либо из «большой тройки».
Меня немного раздражает, что правило Rule of Five
не процитировано.
Это правило очень простое:
Правило пяти :
всякий раз, когда вы пишете один из деструктора, конструктора копирования, оператора присваивания копирования, конструктора перемещения или оператора присваивания перемещения, вам, вероятно, потребуется написать остальные четыре.
Но есть более общее правило, которому вы должны следовать, которое вытекает из необходимости писать безопасный код исключений:
Каждый ресурс должен управляться выделенным объектом
Здесь @sharptooth
код все еще (в основном) в порядке, однако, если бы он добавил второй атрибут в свой класс, этого бы не произошло. Рассмотрим следующий класс:
class Erroneous
{
public:
Erroneous();
// ... others
private:
Foo* mFoo;
Bar* mBar;
};
Erroneous::Erroneous(): mFoo(new Foo()), mBar(new Bar()) {}
Что будет, если new Bar
бросит? Как удалить объект, на который указывает mFoo
? Есть решения (на уровне функций попробуй / поймай ...), они просто не масштабируются.
Правильный способ справиться с ситуацией - использовать правильные классы вместо необработанных указателей.
class Righteous
{
public:
private:
std::unique_ptr<Foo> mFoo;
std::unique_ptr<Bar> mBar;
};
С той же реализацией конструктора (или фактически с использованием make_unique
) у меня теперь есть безопасность исключений бесплатно !!! Разве это не интересно? И, что самое главное, мне больше не нужно беспокоиться о правильном деструкторе! Мне нужно написать свое собственное Copy Constructor
и Assignment Operator
хотя, потому unique_ptr
что не определяет эти операции ... но здесь это не имеет значения;)
И поэтому sharptooth
снова посетил класс пользователя:
class Class
{
public:
Class(char const* str): mData(str) {}
private:
std::string mData;
};
Не знаю, как вы, но мне легче;)
Я могу вспомнить из своей практики и подумать о следующих случаях, когда приходится иметь дело с явным объявлением / определением конструктора копирования. Я сгруппировал дела в две категории
Я помещаю в этот раздел случаи, когда объявление / определение конструктора копирования необходимо для правильной работы программ, использующих этот тип.
Прочитав этот раздел, вы узнаете о нескольких подводных камнях, позволяющих компилятору самостоятельно сгенерировать конструктор копирования. Поэтому, как заметил в своем ответе seand , всегда безопасно отключить возможность копирования для нового класса и намеренно включить ее позже, когда это действительно необходимо.
Объявите частный копирующий конструктор и не предоставляйте для него реализацию (чтобы сборка завершилась ошибкой на этапе связывания, даже если объекты этого типа копируются в собственную область видимости класса или его друзьями).
Объявите конструктор-копию с помощью =delete
at end.
Это наиболее понятный случай и фактически единственный, упомянутый в других ответах. shaprtooth была покрыта его довольно хорошо. Я только хочу добавить, что глубоко копируемые ресурсы, которые должны принадлежать исключительно объекту, могут применяться к любому типу ресурсов, из которых динамически выделяемая память - это только один вид. При необходимости глубокое копирование объекта может также потребовать
Рассмотрим класс, в котором все объекты - независимо от того, как они были созданы - ДОЛЖНЫ быть каким-то образом зарегистрированы. Некоторые примеры:
Самый простой пример: ведение общего количества существующих на данный момент объектов. Регистрация объекта - это просто увеличение статического счетчика.
Более сложным примером является одноэлементный реестр, в котором хранятся ссылки на все существующие объекты этого типа (так что уведомления могут быть доставлены им всем).
Умные указатели с подсчетом ссылок можно рассматривать как особый случай в этой категории: новый указатель «регистрирует» себя в общем ресурсе, а не в глобальном реестре.
Такая операция саморегистрации должна выполняться ЛЮБЫМ конструктором данного типа, и конструктор копирования не является исключением.
Некоторые объекты могут иметь нетривиальную внутреннюю структуру с прямыми перекрестными ссылками между их различными подобъектами (фактически, для запуска этого случая достаточно только одной такой внутренней перекрестной ссылки). Предоставленный компилятором конструктор копирования нарушит внутренние внутриобъектные ассоциации, преобразовав их в межобъектные ассоциации.
Пример:
struct MarriedMan;
struct MarriedWoman;
struct MarriedMan {
// ...
MarriedWoman* wife; // association
};
struct MarriedWoman {
// ...
MarriedMan* husband; // association
};
struct MarriedCouple {
MarriedWoman wife; // aggregation
MarriedMan husband; // aggregation
MarriedCouple() {
wife.husband = &husband;
husband.wife = &wife;
}
};
MarriedCouple couple1; // couple1.wife and couple1.husband are spouses
MarriedCouple couple2(couple1);
// Are couple2.wife and couple2.husband indeed spouses?
// Why does couple2.wife say that she is married to couple1.husband?
// Why does couple2.husband say that he is married to couple1.wife?
Там может быть классы , где объекты являются безопасными для копирования в то время как в некотором состоянии (например , по умолчанию , возведенное состояние) и не безопасно копировать иначе. Если мы хотим разрешить копирование объектов, которые можно безопасно копировать, тогда - при программировании в целях защиты - нам нужна проверка во время выполнения в определяемом пользователем конструкторе копирования.
Иногда класс, который должен быть копируемым, объединяет некопируемые подобъекты. Обычно это происходит для объектов с ненаблюдаемым состоянием (этот случай более подробно обсуждается в разделе «Оптимизация» ниже). Компилятор просто помогает распознать этот случай.
Класс, который должен быть копируемым, может агрегировать подобъект квазикопируемого типа. Квазикопируемый тип не предоставляет конструктор копирования в строгом смысле слова, но имеет другой конструктор, который позволяет создавать концептуальную копию объекта. Причина создания квазикопируемого типа заключается в том, что нет полного соглашения о семантике копирования типа.
Например, возвращаясь к случаю саморегистрации объекта, мы можем утверждать, что могут быть ситуации, когда объект должен быть зарегистрирован в глобальном диспетчере объектов только в том случае, если это полностью автономный объект. Если это подобъект другого объекта, то ответственность за управление им лежит на содержащем его объекте.
Или должно поддерживаться как мелкое, так и глубокое копирование (ни одно из них не используется по умолчанию).
Затем окончательное решение остается за пользователями этого типа - при копировании объектов они должны явно указать (с помощью дополнительных аргументов) предполагаемый метод копирования.
В случае небезопасного подхода к программированию также возможно наличие как обычного конструктора копирования, так и конструктора квазикопирования. Это может быть оправдано, если в подавляющем большинстве случаев следует применять единственный метод копирования, тогда как в редких, но хорошо понятных ситуациях следует использовать альтернативные методы копирования. Тогда компилятор не будет жаловаться на то, что он не может неявно определить конструктор копирования; ответственность за запоминание и проверку того, следует ли копировать подобъект этого типа с помощью конструктора квазикопирования, будет исключительной ответственностью пользователей.
В редких случаях подмножество наблюдаемого состояния объекта может составлять (или считаться) неотъемлемой частью идентичности объекта и не должно передаваться на другие объекты (хотя это может быть несколько спорным).
Примеры:
UID объекта (но этот также относится к случаю "саморегистрации", описанному выше, поскольку идентификатор должен быть получен в процессе саморегистрации).
История объекта (например, стек Undo / Redo) в случае, когда новый объект не должен наследовать историю исходного объекта, а вместо этого должен начинаться с одного элемента истории « Скопировано в <TIME> из <OTHER_OBJECT_ID> ».
В таких случаях конструктор копирования должен пропустить копирование соответствующих подобъектов.
Подпись предоставленного компилятором конструктора копирования зависит от того, какие конструкторы копирования доступны для подобъектов. Если хотя бы один подобъект не имеет реального конструктора копии (принимающего исходный объект по постоянной ссылке), но вместо этого имеет изменяющийся копирующий конструктор (принимающий исходный объект по непостоянной ссылке), то у компилятора не будет выбора. но неявно объявить, а затем определить изменяющийся копирующий конструктор.
А что, если «мутирующий» конструктор-копию типа подобъекта фактически не изменяет исходный объект (и был просто написан программистом, который не знает о const
ключевом слове)? Если мы не можем исправить этот код, добавив недостающий const
, то другой вариант - объявить наш собственный определяемый пользователем конструктор копирования с правильной подписью и совершить грех обращения к файлу const_cast
.
Контейнер COW, который дал прямые ссылки на свои внутренние данные, ДОЛЖЕН быть глубоко скопирован во время создания, иначе он может вести себя как дескриптор подсчета ссылок.
Хотя COW - это метод оптимизации, эта логика в конструкторе копирования имеет решающее значение для его правильной реализации. Вот почему я разместил этот случай здесь, а не в разделе «Оптимизация», куда мы идем дальше.
В следующих случаях вам может потребоваться / необходимо определить собственный конструктор копирования из соображений оптимизации:
Рассмотрим контейнер, который поддерживает операции удаления элементов, но может сделать это, просто пометив удаленный элемент как удаленный, а затем повторно использовать его слот. Когда создается копия такого контейнера, может иметь смысл сжать уцелевшие данные, а не сохранять «удаленные» слоты как есть.
Объект может содержать данные, которые не являются частью его наблюдаемого состояния. Обычно это кэшированные / запомненные данные, накопленные за время существования объекта, чтобы ускорить некоторые медленные операции запроса, выполняемые объектом. Можно безопасно пропустить копирование этих данных, поскольку они будут пересчитаны, когда (и если!) Будут выполнены соответствующие операции. Копирование этих данных может быть неоправданным, так как оно может быть быстро признано недействительным, если наблюдаемое состояние объекта (из которого извлекаются кэшированные данные) изменено операциями изменения (и если мы не собираемся изменять объект, почему мы создаем глубокий копировать тогда?)
Эта оптимизация оправдана только в том случае, если вспомогательные данные велики по сравнению с данными, представляющими наблюдаемое состояние.
C ++ позволяет отключить неявное копирование, объявив конструктор копирования explicit
. Тогда объекты этого класса не могут быть переданы в функции и / или возвращены из функций по значению. Этот трюк можно использовать для типа, который кажется легковесным, но действительно очень дорогим для копирования (хотя сделать его квазикопируемым может быть лучшим выбором).
В C ++ 03 для объявления конструктора копирования также требовалось его определение (конечно, если вы намеревались его использовать). Следовательно, использование такого конструктора копирования просто из обсуждаемой проблемы означало, что вам нужно было написать тот же код, который компилятор автоматически сгенерирует для вас.
Стандарты C ++ 11 и более новые позволяют объявлять специальные функции-члены (конструкторы по умолчанию и конструкторы копирования, оператор присваивания копии и деструктор) с явным запросом на использование реализации по умолчанию (просто завершите объявление с помощью
=default
).
Этот ответ можно улучшить следующим образом:
- Добавить еще пример кода
- Проиллюстрируйте случай "Объекты с внутренними перекрестными ссылками"
- Добавьте ссылки
Если у вас есть класс с динамически выделяемым содержимым. Например, вы сохраняете название книги как символ * и устанавливаете заголовок с новым, копировать не будет.
Вам нужно будет написать конструктор копирования, который делает, title = new char[length+1]
а затем strcpy(title, titleIn)
. Конструктор копирования просто сделает «неглубокую» копию.
Конструктор копирования вызывается, когда объект либо передается по значению, возвращается по значению, либо явно копируется. Если конструктора копирования нет, C ++ создает конструктор копирования по умолчанию, который делает неглубокую копию. Если у объекта нет указателей на динамически выделяемую память, подойдет неглубокая копия.
Рассмотрим ниже фрагмент кода:
class base{
int a, *p;
public:
base(){
p = new int;
}
void SetData(int, int);
void ShowData();
base(const base& old_ref){
//No coding present.
}
};
void base :: ShowData(){
cout<<this->a<<" "<<*(this->p)<<endl;
}
void base :: SetData(int a, int b){
this->a = a;
*(this->p) = b;
}
int main(void)
{
base b1;
b1.SetData(2, 3);
b1.ShowData();
base b2 = b1; //!! Copy constructor called.
b2.ShowData();
return 0;
}
Output:
2 3 //b1.ShowData();
1996774332 1205913761 //b2.ShowData();
b2.ShowData();
дает нежелательный вывод, потому что существует определенный пользователем конструктор копирования, созданный без кода, написанного для явного копирования данных. Таким образом, компилятор не создает то же самое.
Просто подумал о том, чтобы поделиться этими знаниями со всеми вами, хотя большинство из вас это уже знают.
Ура ... Удачного кодирования !!!