Чому використання «нового» викликає протікання пам’яті?


131

Спочатку я засвоїв C #, а зараз я починаю з C ++. Як я розумію, оператор newв C ++ не схожий на той, що знаходиться в C #.

Чи можете ви пояснити причину витоку пам’яті в цьому прикладі коду?

class A { ... };
struct B { ... };

A *object1 = new A();
B object2 = *(new B());

Відповіді:


464

Що відбувається

Коли ви пишете, T t;ви створюєте об'єкт типу Tз автоматичною тривалістю зберігання . Він буде очищений автоматично, коли він вийде із сфери застосування.

Коли ви пишете, new T()ви створюєте об'єкт типу Tз динамічною тривалістю зберігання . Він не очиститься автоматично.

новий без очищення

Вам потрібно передати вказівник на нього delete, щоб очистити його:

новинка з видаленням

Однак ваш другий приклад є гіршим: ви скасовуєте покажчик і робите копію об'єкта. Таким чином ви втрачаєте вказівник на створений за допомогою об'єкта new, тому ви ніколи не можете його видалити, навіть якщо хочете!

новинка з дереф

Що ти повинен робити

Вам слід віддати перевагу тривалості автоматичного зберігання. Потрібен новий об’єкт, просто напишіть:

A a; // a new object of type A
B b; // a new object of type B

Якщо вам потрібна динамічна тривалість зберігання, збережіть вказівник на виділений об'єкт в об'єкті автоматичної тривалості зберігання, який автоматично видаляє його.

template <typename T>
class automatic_pointer {
public:
    automatic_pointer(T* pointer) : pointer(pointer) {}

    // destructor: gets called upon cleanup
    // in this case, we want to use delete
    ~automatic_pointer() { delete pointer; }

    // emulate pointers!
    // with this we can write *p
    T& operator*() const { return *pointer; }
    // and with this we can write p->f()
    T* operator->() const { return pointer; }

private:
    T* pointer;

    // for this example, I'll just forbid copies
    // a smarter class could deal with this some other way
    automatic_pointer(automatic_pointer const&);
    automatic_pointer& operator=(automatic_pointer const&);
};

automatic_pointer<A> a(new A()); // acts like a pointer, but deletes automatically
automatic_pointer<B> b(new B()); // acts like a pointer, but deletes automatically

новинка з автоматичним покажчиком

Це загальна ідіома, яка йде не надто описовим іменем RAII ( Придбання ресурсів - ініціалізація ). Коли ви придбаєте ресурс, який потребує очищення, ви вставляєте його в об'єкт автоматичної тривалості зберігання, тому вам не потрібно турбуватися про його очищення. Це стосується будь-якого ресурсу, будь то пам'ять, відкриті файли, мережеві з'єднання чи все, що вам здається.

Ця automatic_pointerріч вже існує в різних формах, я просто надавав її, щоб навести приклад. Дуже схожий клас існує в стандартній бібліотеці під назвою std::unique_ptr.

Існує також старий (до-С ++ 11) з назвою, auto_ptrале він тепер застарілий, оскільки він має дивну поведінку копіювання.

А потім є ще розумніші приклади, наприклад std::shared_ptr, що дозволяє декілька покажчиків на один і той же об'єкт і очищає його лише тоді, коли останній вказівник знищений.


4
@ user1131997: радий, що ти поставив це ще одне питання. Як бачите, це не дуже просто пояснити в коментарях :)
Р. Мартіньо Фернандес

@ R.MartinhoFernandes: відмінна відповідь. Лише одне питання. Чому ви використовували return за посиланням у функції оператора * ()?
Деструктор

@ Запізнення у відповідь деструктора: D. Повернення за посиланням дозволяє змінювати покажчик, так що ви можете робити, наприклад, так *p += 2, як це було б із звичайним вказівником. Якби він не повернувся за посиланням, він би не імітував нормальну поведінку вказівника, що є тут наміром.
Р. Мартіньо Фернандес

Дуже дякую за пораду "зберігати вказівник на виділений об'єкт у об'єкті автоматичної тривалості зберігання, який автоматично видаляє його". Якби існував спосіб вимагати від кодерів вивчити цей шаблон, перш ніж вони зможуть скласти будь-який C ++!
Енді

35

Покрокове пояснення:

// creates a new object on the heap:
new B()
// dereferences the object
*(new B())
// calls the copy constructor of B on the object
B object2 = *(new B());

Таким чином, до кінця цього у вас на купі є об’єкт без вказівника на нього, тому видалити його неможливо.

Інший зразок:

A *object1 = new A();

це витік пам'яті, лише якщо ви забудете deleteвиділену пам'ять:

delete object1;

У C ++ є об'єкти з автоматичним зберіганням, створені на стеку, які автоматично утилізуються, та об’єкти з динамічним зберіганням, на купі, яку ви виділите newта зобов'язані звільнити себеdelete . (це все грубо кажучи)

Подумайте, що ви повинні мати deleteдля кожного об'єкта, виділеного з new.

EDIT

Подумайте про це, object2не повинно бути витоку пам'яті.

Наступний код - це просто зрозуміти, це погана ідея, ніколи не подобається такий код:

class B
{
public:
    B() {};   //default constructor
    B(const B& other) //copy constructor, this will be called
                      //on the line B object2 = *(new B())
    {
        delete &other;
    }
}

У цьому випадку, оскільки otherпередається через посилання, це буде саме той об'єкт, на який вказуєnew B() . Тому отримання його адреси &otherта видалення вказівника звільнить пам'ять.

Але я не можу наголосити на цьому досить, не робіть цього. Тут просто зробити висновок.


2
Я думав так само: ми можемо зламати його, щоб не протікати, але ви не хотіли б цього робити. object1 також не повинен просочуватися, оскільки його конструктор може приєднатись до якоїсь структури даних, яка видалить її в якийсь момент.
CashCow

2
Завжди просто так спокусливо написати ті відповіді "це можна зробити, але не робити"! :-) Я знаю почуття
Кос

11

Дано два "об'єкти":

obj a;
obj b;

Вони не будуть займати одне і те ж місце в пам'яті. Іншими словами,&a != &b

Призначення значення одного іншому не змінить їх місцезнаходження, але воно змінить їх вміст:

obj a;
obj b = a;
//a == b, but &a != &b

Інтуїтивно, вказівники "об'єкти" працюють так само:

obj *a;
obj *b = a;
//a == b, but &a != &b

Тепер давайте розглянемо ваш приклад:

A *object1 = new A();

Це привласнення значення new A()до object1. Значення - вказівник, значення object1 == new A(), але &object1 != &(new A()). (Зауважте, що цей приклад недійсний код, він лише для пояснення)

Оскільки значення вказівника збережено, ми можемо звільнити пам'ять, на яку він вказує: delete object1;Через наше правило, це поводиться так само, як delete (new A());і у нього немає витоку.


Для другого прикладу ви копіюєте об'єкт із загостреним об'єктом. Значення - це вміст цього об'єкта, а не власне вказівник. Як і в будь-якому іншому випадку, &object2 != &*(new A()).

B object2 = *(new B());

Ми втратили вказівник на виділену пам’ять, і тому не можемо його звільнити. delete &object2;може здатися, що це спрацювало б, але тому &object2 != &*(new A()), що це не рівнозначно delete (new A())і недійсно.


9

У C # та Java ви використовуєте new, щоб створити екземпляр будь-якого класу, і потім вам не потрібно буде турбуватися про його знищення пізніше.

У C ++ також є ключове слово "new", яке створює об'єкт, але на відміну від Java або C #, це не єдиний спосіб створити об'єкт.

C ++ має два механізми для створення об'єкта:

  • автоматичний
  • динамічний

За допомогою автоматичного створення ви створюєте об'єкт у обширному середовищі: - у функції або - як член класу (або структури).

У функції ви створили би її так:

int func()
{
   A a;
   B b( 1, 2 );
}

У межах класу ви зазвичай створюєте його таким чином:

class A
{
  B b;
public:
  A();
};    

A::A() :
 b( 1, 2 )
{
}

У першому випадку об'єкти руйнуються автоматично при виході блоку області. Це може бути функція або блок області в межах функції.

В останньому випадку об'єкт b знищується разом із екземпляром A, в якому він є членом.

Об'єкти виділяються новими, коли вам потрібно контролювати термін експлуатації об'єкта, а потім для його знищення потрібне видалення. Завдяки техніці, відомій як RAII, ви дбаєте про видалення об'єкта в точці, яку ви створюєте, вводячи його в автоматичний об'єкт, і чекаєте, коли деструктор цього автоматичного об'єкта набере чинності.

Одним із таких об'єктів є shared_ptr, який викликатиме логіку "делетер", але лише тоді, коли всі екземпляри shared_ptr, які діляться об'єктом, будуть знищені.

Загалом, хоча ваш код може мати багато дзвінків до нових, у вас повинні бути обмежені виклики для видалення, і завжди слід переконатися, що вони викликаються від деструкторів або об'єктів "видалення", які розміщуються в смарт-покажчики.

Ваші деструктори також ніколи не повинні кидати винятків.

Якщо ви це зробите, у вас буде мало витоків пам'яті.


4
Там більше automaticі dynamic. Там також static.
Mooing Duck

9
B object2 = *(new B());

Ця лінія є причиною протікання. Давайте трохи розберемо це.

object2 - змінна типу B, що зберігається за адресою 1 сказати (так, я тут вибираю довільні числа). З правого боку ви попросили новий B або вказівник на об’єкт типу B. Програма з радістю передає вам це і призначає ваш новий B на адресу 2, а також створює вказівник на адресу 3. Тепер, єдиний спосіб отримати доступ до даних за адресою 2 - це через вказівник на адресу 3. Далі ви перенаправляєте покажчик за допомогою* для отримання даних, на які вказує вказівник (дані за адресою 2). Це фактично створює копію цих даних і призначає їх об'єкту2, призначеному в адресі 1. Пам'ятайте, що це КОПІЯ, а не оригінал.

Тепер ось проблема:

Ви ніколи насправді не зберігали цей покажчик де-небудь, де можете ним користуватися! Після завершення цього завдання вказівник (пам'ять в адресі3, за допомогою якого ви отримували доступ до адреси2) виходить за межі та за межами Вашого досяжності! Ви більше не можете зателефонувати видалити на ньому, і тому не можете очистити пам'ять у адресі2. Те, що вам залишилося, - це копія даних з адреси2 в адресу1. Дві ті самі речі, що сидять у пам’яті. Один, до якого ви можете отримати доступ, інший ви не можете (тому що ви втратили шлях до нього). Ось чому це витік пам’яті.

Я б запропонував прийти з вашого C # фону, щоб ви багато читали про те, як працюють покажчики на C ++. Вони є актуальною темою і можуть зайняти певний час, але їх використання буде для вас неоціненним.


8

Якщо це полегшує ситуацію, вважайте, що пам'ять комп’ютера - як готель, а програми - це клієнти, які наймають номери, коли вони потребують.

Те, як працює цей готель, полягає в тому, що ви бронюєте номер та повідомляєте порти, коли ви їдете.

Якщо ви програмуєте кімнату і виїжджаєте, не повідомляючи портьє, портитель подумає, що кімната все ще використовується, і ніхто не дозволить користуватися нею. У цьому випадку відбувається витік приміщення.

Якщо ваша програма виділяє пам'ять і не видаляє її (вона просто припиняє її використання), комп'ютер вважає, що пам'ять все ще використовується, і не дозволить нікому іншим користуватися нею. Це витік пам'яті.

Це не точна аналогія, але це може допомогти.


5
Мені дуже подобається ця аналогія, її не досконала, але це, безумовно, хороший спосіб пояснити витоки пам’яті людям, які в ній новачки!
АдамМ

1
Я використав це в інтерв'ю старшого інженера Bloomberg в Лондоні, щоб пояснити витоки пам’яті дівчині HR. Я пережив це інтерв'ю, тому що мені вдалося фактично пояснити витоки пам'яті (і проблеми з нанизуванням) непрограмістам таким чином, як вона зрозуміла.
Стефан

7

Під час створення object2ви створюєте копію створеного вами об’єкта за допомогою нового, але ви також втрачаєте вказівник (ніколи не присвоєний) (тому пізніше його неможливо видалити). Щоб цього уникнути, вам доведеться зробити object2посилання.


3
Неймовірно погана практика приймати адресу посилання для видалення об'єкта. Використовуйте розумний вказівник.
Том Вітток

3
Неймовірно погана практика, так? Як ви думаєте, що розумні покажчики використовують поза кадром?
Сліпий

3
Розумні покажчики @Blindy (принаймні пристойно реалізовані) використовують вказівники безпосередньо.
Лучіан Григоре

2
Ну, якщо бути чесним, вся ідея не така велика, чи не так? Насправді я навіть не впевнений, де модель, яка була б випробувана в ОП, була б корисною.
Маріо

7

Ну, ви створюєте витік пам'яті, якщо ви в якийсь момент не звільняєте пам'ять, яку ви виділили за допомогою newоператора, передаючи вказівник на цю пам'ять доdelete оператору.

У ваших двох випадках вище:

A *object1 = new A();

Тут ви не використовуєте deleteдля звільнення пам’яті, тому якщо і коли ваш object1покажчик вийде за межі області, у вас з’явиться витік пам’яті, оскільки ви втратили вказівник і тому не можете використовуватиdelete оператором.

І ось тут

B object2 = *(new B());

ви відкидаєте вказівник, повернутий назад new B(), і тому ніколи не можете передавати цей вказівник deleteдля звільнення пам'яті. Звідси ще один витік пам'яті.


7

Саме ця лінія одразу просочується:

B object2 = *(new B());

Тут ви створюєте новий Bоб’єкт на купі, потім створюєте копію на стеку. Той, що виділився на купі, більше не може бути доступний, а отже, і витік.

Цей рядок не одразу протікає:

A *object1 = new A();

Там буде текти , якщо ви ніколи не deleted , object1хоча.


4
Будь-ласка, не використовуйте купу / стек при поясненні динамічного / автоматичного зберігання.
Паббі

2
@Pubby чому не використовувати? Через динамічне / автоматичне зберігання завжди купа, а не стек? І тому немає необхідності докладно розповідати про стек / купу, я прав?

4
@ user1131997 Heap / stack - це деталі реалізації. Про них важливо знати, але це питання не має значення.
Паббі

2
Хм, я хотів би окремо відповісти на нього, тобто такий же, як і мій, але замінюючи купу / стек тим, що вважаєте найкращим. Мені було б цікаво дізнатися, як ви вважаєте за краще пояснити це.
mattjgalloway
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.