Коли я повинен використовувати нове ключове слово в C ++?


272

Я недовго використовую C ++, і мені цікаво про нове ключове слово. Просто я повинен ним користуватися, чи ні?

1) За допомогою нового ключового слова ...

MyClass* myClass = new MyClass();
myClass->MyField = "Hello world!";

2) Без нового ключового слова ...

MyClass myClass;
myClass.MyField = "Hello world!";

З точки зору впровадження, вони не здаються такими різними (але я впевнений, що вони є) ... Однак моя основна мова - це C #, і, звичайно, 1-й метод - це те, до чого я звик.

Складність полягає в тому, що метод 1 важче використовувати з класами std C ++.

Який метод я повинен використовувати?

Оновлення 1:

Нещодавно я використовував нове ключове слово для купи пам'яті (або вільної пам'яті ) для великого масиву , який виходить з області видимості (тобто повертається з функції). Якщо раніше я використовував стек, який спричинив пошкодження половини елементів поза межами області, перехід на використання купи гарантував, що елементи знаходяться в такті. Так!

Оновлення 2:

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

Foobar *foobar = new Foobar();
delete foobar; // TODO: Move this to the right place.

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


6
Коротка відповідь - використовуйте коротку версію, коли зможете піти з нею. :)
jalf

11
Краща техніка, ніж завжди писати відповідне видалення - використовуйте контейнери STL та смарт-покажчики типу std::vectorта std::shared_ptr. Вони завершують дзвінки до вас newі deleteвам, тому ви ще менше шансів на витік пам'яті. Запитайте себе, наприклад: чи завжди ви пам'ятаєте, щоб deleteскрізь помістити відповідне місце, де викид може бути кинутий? Поставити deleteруку в руки складніше, ніж ви могли подумати.
AshleysBrain

@nbolton Re: ОНОВЛЕННЯ 1 - Одне з найкрасивих речей щодо C ++ - це те, що вона дозволяє зберігати визначені користувачем типи на стеці, тоді як зібрані сміття мов, таких як C #, змушують зберігати дані у купі . Зберігання даних у купі витрачає більше ресурсів, ніж зберігання даних на стеку , тому вам слід віддати перевагу стеку перед купою , за винятком випадків, коли вашому UDT потрібен великий об'єм пам'яті для зберігання його даних. (Це також означає, що об'єкти передаються за значенням за замовчуванням). Кращим рішенням вашої проблеми буде передача масиву функції за посиланням .
Чарльз Аддіс

Відповіді:


303

Спосіб 1 (з використанням new)

  • Виділяє пам'ять для об'єкта у вільному магазині (це часто те саме, що і купа )
  • Вимагає, щоб ви явно deleteоб’єкт пізніше. (Якщо ви не видалите його, ви можете створити витік пам'яті)
  • Пам'ять залишається виділеною, поки ви deleteїї не зробите . (тобто ви можете returnоб'єкт, який ви створили за допомогою new)
  • Приклад у запитанні просочить пам'ять, якщо вказівник не буде deleted; і його завжди слід видалити , незалежно від того, який шлях управління здійснено, або якщо будуть викинуті винятки.

Спосіб 2 (не використовується new)

  • Виділяє пам’ять для об'єкта в стеку (куди йдуть всі локальні змінні) Загалом для стека доступно менше пам'яті; якщо ви виділите занадто багато об'єктів, ви ризикуєте переповнювати стек.
  • deleteПізніше вам це не знадобиться .
  • Пам'ять більше не виділяється, коли вона виходить за межі сфери. (тобто не слід returnвказувати на об’єкт у стеці)

Що стосується того, який з них використовувати; ви вибираєте метод, який найкраще підходить вам, враховуючи вищезазначені обмеження.

Деякі прості випадки:

  • Якщо ви не хочете турбуватися про дзвінки delete, (і потенціал викликати витоки пам’яті ) вам не слід користуватися new.
  • Якщо ви хочете повернути вказівник на ваш об'єкт з функції, ви повинні використовувати new

4
Один нітпік - я вважаю, що новий оператор виділяє пам'ять з "безкоштовного магазину", тоді як malloc виділяє з "купи". Це не гарантується тим самим, хоча на практиці вони зазвичай є. Дивіться gotw.ca/gotw/009.htm .
Фред Ларсон

4
Я думаю, що ваша відповідь могла б бути зрозумілішою, на чому використовувати. (99% часу вибір простий. Використовуйте метод 2, на об'єкті обгортки, який викликає new / delete у конструкторі / деструкторі)
jalf

4
@jalf: Метод 2 - це той, який не використовує нове: - / У будь-якому випадку, багато разів кодування буде набагато простішим (наприклад, обробка випадків помилок), використовуючи метод 2 (той, що не має нового)
Daniel LeCheminant

Ще одна нітрик ... Вам слід зробити більш очевидним, що перший приклад Ніка просочує пам'ять, тоді як другий - ні, навіть за винятком винятків.
Арафангіон

4
@Fred, Arafangion: Дякую за ваше розуміння; Я включив ваші коментарі у відповідь.
Daniel LeCheminant

118

Існує важлива різниця між ними.

Все, що не виділено, newповодиться так само, як типи значень у C # (і люди часто кажуть, що ці об’єкти виділяються на стеку, що, мабуть, є найбільш поширеним / очевидним випадком, але не завжди відповідає дійсності. Точніше, об'єкти, виділені без використання, newмають автоматичне зберігання тривалість Все, що виділяється з new, розподіляється на купі, і вказівник на нього повертається, точно так само, як еталонні типи в C #.

Все, що виділяється на стеку, має мати постійний розмір, визначений під час компіляції (компілятор повинен правильно встановити покажчик стека, або якщо об'єкт є членом іншого класу, він повинен регулювати розмір цього іншого класу) . Ось чому масиви в C # є типовими типами. Вони повинні бути, тому що, використовуючи еталонні типи, ми можемо вирішити під час виконання, скільки пам’яті запитувати. І те саме стосується і тут. Тільки масиви з постійним розміром (розмір, який можна визначити під час компіляції) можуть бути виділені з автоматичною тривалістю зберігання (у стеці). Масиви з динамічним розміром потрібно розподіляти по купі, зателефонувавши new.

(І на цьому зупиняється будь-яка схожість на C #)

Тепер усе, що виділяється на стеці, має "автоматичну" тривалість зберігання (ви можете фактично оголосити змінну як auto, але це за замовчуванням, якщо не вказаний інший тип зберігання, тому ключове слово насправді не використовується на практиці, але саме тут він походить від)

Автоматична тривалість зберігання означає саме те, що воно звучить, тривалість змінної обробляється автоматично. Навпаки, все, що виділяється на купі, ви повинні видалити вручну. Ось приклад:

void foo() {
  bar b;
  bar* b2 = new bar();
}

Ця функція створює три значення, які варто врахувати:

У рядку 1 він оголошує змінну bтипу barна стеку (автоматична тривалість).

У другому рядку він оголошує barвказівник b2на стек (автоматична тривалість) і викликає новий, виділяючи barоб'єкт на купу. (динамічна тривалість)

Коли функція повернеться, відбудеться наступне: По-перше, b2виходить за межі (порядок руйнування завжди протилежний порядку побудови). Але b2це лише вказівник, тому нічого не відбувається, пам'ять, яку він займає, просто звільняється. І що важливо, пам'ять, на яку він вказує ( barекземпляр на купі) НЕ торкається. Вивільняється лише вказівник, оскільки лише вказівник мав автоматичну тривалість. По-друге, bвиходить за межі, тому оскільки він має автоматичну тривалість, його деструктор викликається, і пам'ять звільняється.

А barекземпляр на купі? Це, мабуть, все ще є. Ніхто не заважав видалити його, тому ми просочилися пам’яттю.

З цього прикладу ми бачимо, що все, що має автоматичну тривалість, гарантовано викликає свій деструктор, коли він виходить за межі області. Це корисно. Але все, що виділяється на купі, триває стільки, скільки нам потрібно, і може бути динамічно розміром, як у випадку з масивами. Це теж корисно. Ми можемо використовувати це для управління нашими розподілами пам'яті. Що робити, якщо клас Foo виділив деяку пам’ять на купі у своєму конструкторі та видалив цю пам'ять у своєму деструкторі. Тоді ми могли б отримати найкраще з обох світів, безпечне розподілення пам’яті, яке гарантовано буде звільнене знову, але без обмежень змушувати все бути на стеці.

І саме так працює більшість кодів C ++. Подивіться, наприклад, стандартну бібліотеку std::vector. Зазвичай виділяється на стеку, але може бути динамічно розміром і розміром. І це робиться, розподіляючи пам’ять на купу, як потрібно. Користувач класу ніколи цього не бачить, тому немає шансів просочити пам'ять або забути очистити те, що ви виділили.

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

Як правило, ніколи не використовуйте нові / видаляти безпосередньо з коду високого рівня. Завжди загорніть його в клас, який може керувати пам'яттю для вас, і це забезпечить його звільнення знову. (Так, з цього правила можуть бути винятки. Зокрема, розумні покажчики вимагають від вас newбезпосередньо зателефонувати та передати вказівник його конструктору, який потім переймає та забезпечує deleteправильне виклик. Але це все ще дуже важливе правило )


2
"Все, що не виділено новим, розміщується на стеці". Не в системах, над якими я працював ... загалом інтіалізовані (і uninit.) Глобальні (статичні) дані розміщуються у власних сегментах. Наприклад, сегменти .ker, .bss тощо. Педантичний, я знаю ...
День

Звичайно, ти маєш рацію. Я не дуже думав про статичні дані. Мої погані, звичайно. :)
jalf

2
Чому все, що виділяється на стеку, має мати постійний розмір?
користувач541686

Це не завжди , є кілька способів його обійти, але в загальному випадку це так, тому що це на стеці. Якщо він знаходиться у верхній частині стека, то можливо можливо змінити його розмір, але коли щось інше натисне на нього, воно "замуроване", оточене предметами з обох боків, тому його реально змінити не можна . Так, сказати, що завжди має бути фіксований розмір - це трохи спрощення, але воно передає основну ідею (і я б не рекомендував возитися з функціями C, які дозволяють вам бути занадто креативними при розподілі стеків)
jalf

14

Який метод я повинен використовувати?

Це майже ніколи не визначається вашими вподобаннями введення тексту, а контекстом. Якщо вам потрібно тримати об’єкт у кількох стеках або якщо він занадто важкий для стека, ви виділите його у вільний магазин. Крім того, оскільки ви виділяєте об'єкт, ви також несете відповідальність за звільнення пам'яті. Знайдіть deleteоператора.

Щоб полегшити тягар використання управління безкоштовними магазинами, люди винайшли такі речі, як auto_ptrі unique_ptr. Настійно рекомендую поглянути на них. Вони можуть навіть допомогти вашим питанням набору тексту ;-)


10

Якщо ви пишете на C ++, ви, ймовірно, пишете для виконання. Використання нового та безкоштовного магазину набагато повільніше, ніж використання стека (особливо при використанні потоків), тому використовуйте його лише тоді, коли вам це потрібно.

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

Також намагайтеся уникати будь-якого використання видалення. Загорніть замість цього нове в розумний покажчик. Нехай виклик смарт-вказівника видаляє для вас.

Є деякі випадки, коли розумний вказівник не розумний. Ніколи не зберігайте std :: auto_ptr <> всередині контейнера STL. Він занадто рано видалить покажчик через операції з копіювання всередині контейнера. Інший випадок, коли у вас є дійсно великий контейнер STL-покажчиків на об’єкти. boost :: shared_ptr <> матиме тону швидкісних накладних витрат, коли вона нахиляє відліку відліку вгору та вниз. Краще в цьому випадку - помістити контейнер STL в інший об'єкт і дати цьому об'єкту деструктор, який буде викликати видалення на кожному вказівнику контейнера.


10

Коротка відповідь: якщо ви новачок в C ++, вам ніколи не слід користуватисяnew або deleteсебе.

Натомість слід використовувати розумні покажчики, такі як std::unique_ptrта std::make_unique(або рідше std::shared_ptrі std::make_shared). Таким чином, вам не доведеться турбуватися майже про витоки пам'яті. І навіть якщо ви більш просунуті, найкращою практикою, як правило, буде інкапсуляція користувальницького способу, який ви використовуєте newтаdelete в невеликий клас (наприклад, спеціальний смарт-покажчик), який призначений саме для вирішення проблем із життєвим циклом.

Звичайно, за кадром, ці розумні покажчики все ще виконують динамічне розподілення та розміщення, тому код, що використовує їх, все ще матиме відповідні витрати на виконання. Інші відповіді тут висвітлювали ці питання, і про те, як приймати дизайнерські рішення про те, коли використовувати розумні покажчики, а не просто створювати об’єкти в стеку або включати їх як прямих членів об’єкта, досить добре, що я їх не повторюватиму. Але моє резюме було б: не використовуйте розумні покажчики та динамічне розподілення, поки щось не змусить вас це зробити.


цікаво подивитися, як відповідь може змінюватись з часом;)
Вовк


2

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


1

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

Однак, є кілька очевидних випадків, коли змінних стеків недостатньо.

Якщо змінна стека має великий слід пам'яті, то ви ризикуєте переповнити стек. За замовчуванням розмір стека кожного потоку становить 1 Мб для Windows. Навряд чи ви створите змінну стека розміром 1 Мб, але ви повинні мати на увазі, що використання стека є сукупним. Якщо ваша функція викликає функцію, яка викликає іншу функцію, яка викликає іншу функцію, яка ..., змінні стека у всіх цих функціях займають місце в одній стеці. Рекурсивні функції можуть швидко зіткнутися з цією проблемою, залежно від глибини рекурсії. Якщо це проблема, ви можете збільшити розмір стека (не рекомендується) або виділити змінну на купі за допомогою нового оператора (рекомендується).

Інша, більш вірогідна умова, полягає в тому, що ваша змінна повинна "жити" поза межами вашої функції. У цьому випадку ви маєте розподілити змінну на купі, щоб її можна було досягти за межами будь-якої заданої функції.


1

Ви передаєте myClass з функції чи очікуєте її існування поза цією функцією? Як казали деякі інші, справа стосується сфери, коли ви не розподіляєте її на купу. Коли ви виходите з функції, вона проходить (зрештою). Однією з класичних помилок, допущених початківцями, є спроба створити локальний об’єкт якогось класу у функції та повернути його, не виділяючи його на купу. Я пам'ятаю налагодження подібних речей ще в попередні дні, роблячи c ++.


0

Другий метод створює екземпляр у стеку разом з такими речами, як щось оголошене intта списком параметрів, які передаються у функцію.

Перший метод забезпечує місце для вказівника на стеку, який ви встановили на місце в пам'яті, де MyClassбуло виділено нове в купі - або вільному сховищі.

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


-1

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

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