Я знаю, що компілятор 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
? Є рішення (функціональний рівень try / catch ...), вони просто не масштабуються.
Правильний спосіб вирішити ситуацію - використовувати правильні класи замість сирих покажчиків.
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;
};
Я не знаю про вас, але мені моє легше;)
Я можу згадати свою практику і подумати про наступні випадки, коли доводиться мати справу з явним декларуванням / визначенням конструктора копіювання. Я згрупував справи у дві категорії
У цьому розділі я розміщую випадки, коли декларування / визначення конструктора копіювання необхідно для коректної роботи програм, що використовують цей тип.
Прочитавши цей розділ, ви дізнаєтесь про кілька підводних каменів, що дозволяють компілятору самостійно генерувати конструктор копіювання. Тому, як зазначив у відповіді Сінд , завжди можна безпечно вимкнути можливість копіювання для нового класу та навмисно увімкнути його пізніше, коли це дійсно потрібно.
Оголосіть приватний конструктор копій і не надайте реалізацію для нього (так що збірка не вдається на етапі зв’язування, навіть якщо об’єкти цього типу копіюються у власній області дії класу або його друзями).
Оголосіть конструктор копіювання з =delete
кінцем.
Це найкраще зрозумілий випадок і насправді єдиний, згаданий в інших відповідях. 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 об'єкта (але цей також належить до справи "самореєстрації" зверху, оскільки ідентифікатор повинен бути отриманий в акті самореєстрації).
Історія об'єкта (наприклад, стек "Скасувати / Повторити") у випадку, коли новий об'єкт не повинен успадковувати історію вихідного об'єкта, а натомість починати з одного елемента історії " Скопійовано о <TIME> з <OTHER_OBJECT_ID> ".
У таких випадках конструктор копіювання повинен пропустити копіювання відповідних під-об'єктів.
Підпис конструктора копіювання, наданого компілятором, залежить від того, які конструктори копій доступні для під-об'єктів. Якщо принаймні в одному під-об'єкті немає реального конструктора копіювання (беручи вихідний об'єкт за постійним посиланням), а замість цього має мутуючий конструктор копіювання (беручи вихідний об'єкт за непостійним посиланням), тоді компілятор не матиме вибору але неявно оголосити, а потім визначити мутуючий конструктор копіювання.
А що, якщо "мутуючий" конструктор копій типу під-об'єкта насправді не мутує вихідний об'єкт (а був просто написаний програмістом, який не знає про const
ключове слово)? Якщо ми не можемо виправити цей код, додавши відсутній const
, тоді інший варіант - оголосити власний конструктор копіювання, визначений користувачем, правильним підписом і здійснити гріх звернення до a 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();
дає непотрібні результати, оскільки існує визначений користувачем конструктор копіювання, створений без коду, написаного для явного копіювання даних. Отже, компілятор не створює те саме.
Просто подумав поділитися цими знаннями з усіма, хоча більшість з вас це вже знає.
Вітаємо ... Щасливого кодування !!!