- Що означає копіювання об'єкта ?
- Що таке конструктор копій та оператор призначення копії ?
- Коли мені потрібно оголосити їх самостійно?
- Як я можу запобігти їх копіювання?
Відповіді:
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
члена просто копіює покажчик, а не масив символів, на який він вказує! Це має кілька неприємних наслідків:
a
можна спостерігати через b
.b
знищення a.name
- це звисаючий покажчик.a
його знищити, видалення звисаючого покажчика призводить до невизначеної поведінки .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
буде видалено масив, що містить рядок джерела , тому що, коли ви пишете 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, об’єкт має 2 додаткові функції спеціального члена: конструктор переміщення та призначення переміщення. Правило п'яти держав здійснювати і ці функції.
Приклад із підписами:
class person
{
std::string name;
int age;
public:
person(const std::string& name, int age); // Ctor
person(const person &) = default; // Copy Ctor
person(person &&) noexcept = default; // Move Ctor
person& operator=(const person &) = default; // Copy Assignment
person& operator=(person &&) noexcept = default; // Move Assignment
~person() noexcept = default; // Dtor
};
Правило 3/5 також називається правилом 0/3/5. Нульова частина правила вказує, що ви можете не писати жодної функції спеціального члена під час створення свого класу.
Більшу частину часу вам не потрібно самостійно керувати ресурсом, тому що існуючий клас на зразок std::string
вже робить це за вас. Просто порівняйте простий код за допомогою std::string
члена з перекрученою та схильною до помилок альтернативою з використанням a, char*
і ви повинні переконатися. Поки ви тримаєтесь подалі від необроблених членів вказівника, правило трьох навряд чи стосуватиметься вашого власного коду.
Правило трьох є правилом для C ++, в основному говорять ,
Якщо ваш клас потребує будь-якого з
- конструктор копіювання ,
- оператор присвоювання ,
- або деструктора ,
визначено експліцитно, то, ймовірно, знадобляться всі троє .
Причини цього полягають у тому, що всі троє зазвичай використовуються для управління ресурсом, і якщо ваш клас управляє ресурсом, йому зазвичай потрібно керувати копіюванням, а також звільненням.
Якщо немає хорошої семантики для копіювання ресурсу, яким керує ваш клас, то розгляньте можливість заборонити копіювання, оголосивши (не визначаючи ) конструктор копій та оператор присвоєння як private
.
(Зауважте, що наступна нова версія стандарту C ++ (а це C ++ 11) додає семантику переміщення до C ++, що, ймовірно, змінить Правило 3. Але я знаю надто мало про це, щоб написати розділ C ++ 11 про правило трьох.)
boost::noncopyable
). Це також може бути набагато зрозуміліше. Я думаю, що C ++ 0x і можливість "видалити" функції можуть тут допомогти, але забув синтаксис: /
noncopyable
вона не є частиною std lib, я не вважаю це великим вдосконаленням. (О, і якщо ви забули синтаксис видалення, ви забули mor ethan, що я коли-небудь знав. :)
)
Закон великої трійки визначений вище.
Простий приклад простої англійської мови про проблему, яку вона вирішує:
Неруйнівний деструктор
Ви виділили пам'ять у своєму конструкторі, і тому вам потрібно написати деструктор, щоб видалити його. Інакше ви спричинить витік пам'яті.
Ви можете подумати, що це робота зроблена.
Проблема буде в тому випадку, якщо копія зроблена з вашого об’єкта, то копія буде вказувати на ту саму пам'ять, що і вихідний об'єкт.
Після того, як один з них видаляє пам'ять у своєму деструкторі, інший матиме вказівник на недійсну пам'ять (це називається звисаючим вказівником), коли він намагатиметься використовувати її, речі стають волохатими.
Тому ви пишете конструктор копій, щоб він виділяв новим об’єктам власні фрагменти пам'яті для знищення.
Оператор призначення та конструктор копій
Ви виділили пам'ять у своєму конструкторі вказівнику вашого класу. Коли ви копіюєте об'єкт цього класу, оператор присвоєння за замовчуванням та конструктор копій копіюють значення цього вказівника на новий об'єкт.
Це означає, що новий об'єкт і старий об'єкт будуть вказувати на один і той же фрагмент пам'яті, тому при зміні його в одному об'єкт він буде змінений і на інший об'єкт. Якщо один об'єкт видаляє цю пам'ять, інший буде продовжувати спробувати використовувати його - eek.
Для вирішення цього питання ви пишете власну версію конструктора копіювання та оператора присвоєння. Ваші версії виділяють окрему пам'ять новим об'єктам і копіюють значення, на які вказує перший вказівник, а не його адресу.
В основному, якщо у вас є деструктор (не деструктор за замовчуванням), це означає, що визначений вами клас має деякий розподіл пам'яті. Припустимо, що клас використовується зовні за допомогою якогось коду клієнта чи ви.
MyClass x(a, b);
MyClass y(c, d);
x = y; // This is a shallow copy if assignment operator is not provided
Якщо у MyClass є лише деякі примітивні типізовані члени, оператор присвоєння за замовчуванням працював би, але якщо у нього є деякі члени вказівника та об'єкти, які не мають операторів призначення, результат був би непередбачуваним. Тому ми можемо сказати, що якщо деструктор класу є чим видалити, нам може знадобитися оператор глибокої копії, що означає, що ми повинні забезпечити конструктор копій та оператор присвоєння.
Що означає копіювання об'єкта? Існує кілька способів копіювання об'єктів - поговоримо про два види, які ви, швидше за все, маєте на увазі - глибока копія та неглибока копія.
Оскільки ми є об'єктно-орієнтованою мовою (або принаймні припускаємо це), скажімо, у вас виділений фрагмент пам'яті. Оскільки це є мовою ОО, ми можемо легко посилатися на фрагменти пам’яті, які ми виділяємо, оскільки вони, як правило, примітивні змінні (ints, символи, байти) або класи, які ми визначили, створені з наших власних типів та примітивів. Тож скажімо, у нас такий клас автомобіля:
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;
Essential, якщо ви оголошуєте змінну і призначаєте її в одному рядку, саме тоді викликається конструктор копіювання. Оператор присвоєння - це те, що відбувається, коли ви використовуєте знак рівності-- car2 = car1;
. Зауважтеcar2
не оголошується в одній заяві. Два фрагменти коду, які ви пишете для цих операцій, ймовірно, дуже схожі. Насправді типова модель дизайну має ще одну функцію, яку ви закликаєте встановити все, як тільки ви задоволені, початкова копія / призначення є законною - якщо ви подивитесь на написаний вами кодекс, функції майже однакові.
Коли мені потрібно оголосити їх самостійно? Якщо ви не пишете код, який потрібно поділитись або якийсь спосіб виготовлення, вам потрібно оголосити їх лише тоді, коли вони вам потрібні. Вам потрібно знати, що робить ваша програмна мова, якщо ви вирішили використовувати її "випадково", а не зробили її - тобто ви отримаєте компілятор за замовчуванням. Наприклад, я рідко використовую конструктори копій, але скасування операторів присвоєння дуже часто. Чи знаєте ви, що ви можете перекрити, що означають також додавання, віднімання тощо?
Як я можу запобігти їх копіювання? Переосмислити всі способи, яким вам дозволяється виділити пам'ять для вашого об'єкта приватною функцією, є розумним початком. Якщо ви дійсно не хочете, щоб люди їх копіювали, ви можете оприлюднити їх і попередити програміста, кинувши виняток, а також не скопіювавши об'єкт.
Коли мені потрібно оголосити їх самостійно?
Правило трьох зазначає, що якщо ви оголосите будь-яке з
тоді слід оголосити всіх трьох. Це випливало із спостереження, що необхідність перейняти значення операції копіювання майже завжди випливала з класу, який здійснює якесь управління ресурсами, і що майже завжди передбачає, що
що б управління ресурсами не робилося в одній операції з копіюванням, можливо, це було потрібно зробити в іншій операції копіювання та
деструктор класу також братиме участь в управлінні ресурсом (зазвичай його випускають). Класичним ресурсом, яким слід керувати, була пам’ять, і саме тому всі класи Стандартної бібліотеки, що управляють пам'яттю (наприклад, контейнери 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);
}
Багато з існуючих відповідей вже стосуються конструктора копіювання, оператора присвоєння та деструктора. Однак у пост C ++ 11 введення семантичного руху може розширити це за межі 3.
Нещодавно Майкл Клайсз виступив із доповіддю, яка стосується цієї теми: http://channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class
Правило трьох в C ++ є основоположним принципом розробки та розробки трьох вимог, що якщо є чітке визначення в одній з наступних функцій-членів, програміст повинен визначати інші два члени функції. А саме такі три функції-члена незамінні: деструктор, конструктор копій, оператор присвоєння копії.
Конструктор копіювання в C ++ - це спеціальний конструктор. Він використовується для побудови нового об'єкта, який є новим об'єктом, еквівалентним копії існуючого об'єкта.
Оператор присвоєння копій - це спеціальний оператор присвоєння, який зазвичай використовується для вказівки існуючого об'єкта іншим об'єктам одного типу.
Є короткі приклади:
// default constructor
My_Class a;
// copy constructor
My_Class b(a);
// copy constructor
My_Class c = a;
// copy assignment operator
b = a;
c++-faq
теги вікі , перш ніж голосувати , щоб закрити .