Чому програмісти на C ++ мінімізують використання "нового"?


873

Я натрапив на запитання про переповнення стека Витік пам'яті з std :: string при використанні std :: list <std :: string> , і один із коментарів говорить про це:

Перестань newтак сильно користуватися. Я не бачу жодної причини, коли ти використовував нове де завгодно. Ви можете створювати об'єкти за значенням в C ++, і це одна з величезних переваг використання мови.
Не потрібно все виділяти на купу.
Перестаньте думати, як програміст Java .

Я не дуже впевнений, що він має на увазі під цим.

Чому об’єкти повинні створюватися за значенням у C ++ якомога частіше, і яка різниця це має внутрішнє?
Я неправильно трактував відповідь?

Відповіді:


1037

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

Стек

Стек завжди виділяє пам'ять послідовно. Це можна зробити, тому що вимагає, щоб ви звільнили пам'ять у зворотному порядку (First-In, Last-Out: FILO). Це методика розподілу пам'яті для локальних змінних у багатьох мовах програмування. Це дуже, дуже швидко, оскільки воно вимагає мінімальної бухгалтерії, а наступна адреса для виділення неявна.

У C ++ це називається автоматичним зберіганням, оскільки зберігання заявляється автоматично в кінці області дії. Як тільки виконання поточного блоку коду (з обмеженим використанням {}) завершено, пам'ять для всіх змінних у цьому блоці автоматично збирається. Це також момент, коли деструктори викликають для очищення ресурсів.

Купи

Купа забезпечує більш гнучкий режим розподілу пам'яті. Бухгалтерський облік складніший, а розподіл - повільніший. Оскільки немає неявної точки випуску, ви повинні звільнити пам'ять вручну, використовуючи deleteабо delete[]( freeна C). Однак відсутність неявної точки випуску є ключем до гнучкості купи.

Причини використання динамічного розподілу

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

Дві основні причини використання динамічного розподілу:

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

  • Ви хочете виділити пам'ять, яка зберігатиметься після виходу з поточного блоку. Наприклад, ви можете написати функцію, string readfile(string path)яка повертає вміст файлу. У цьому випадку, навіть якщо стек міг вмістити весь вміст файлу, ви не змогли повернутися з функції і зберегти виділений блок пам'яті.

Чому динамічний розподіл часто не потрібен

У C ++ є акуратна конструкція, яка називається деструктором . Цей механізм дозволяє керувати ресурсами, вирівнюючи термін експлуатації ресурсу з терміном служби змінної. Ця методика називається RAII і є відмітною точкою C ++. Він "загортає" ресурси в об'єкти. std::stringє ідеальним прикладом. Цей фрагмент:

int main ( int argc, char* argv[] )
{
    std::string program(argv[0]);
}

фактично виділяє змінний об'єм пам'яті. std::stringОб'єкт виділяє пам'ять , використовуючи купу і звільняє його в деструкції. У цьому випадку вам не потрібно було керувати будь-якими ресурсами вручну, і все ж отримали переваги динамічного розподілу пам'яті.

Зокрема, це означає, що в цьому фрагменті:

int main ( int argc, char* argv[] )
{
    std::string * program = new std::string(argv[0]);  // Bad!
    delete program;
}

відбувається непотрібне динамічне розподілення пам'яті. Програма вимагає більшого набору тексту (!) Та вводить ризик забути розмістити пам'ять. Це робиться без видимої вигоди.

Чому слід використовувати автоматичне зберігання якомога частіше

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

  • швидше набирати;
  • швидше при бігу;
  • менш схильний до витоку пам'яті / ресурсів.

Бонусні бали

У посиланому питанні є додаткові занепокоєння. Зокрема, наступний клас:

class Line {
public:
    Line();
    ~Line();
    std::string* mString;
};

Line::Line() {
    mString = new std::string("foo_bar");
}

Line::~Line() {
    delete mString;
}

Насправді набагато більш ризиковано використовувати ніж наступне:

class Line {
public:
    Line();
    std::string mString;
};

Line::Line() {
    mString = "foo_bar";
    // note: there is a cleaner way to write this.
}

Причина в тому, що std::stringправильно визначається конструктор копій. Розглянемо наступну програму:

int main ()
{
    Line l1;
    Line l2 = l1;
}

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

Інші примітки

Широке використання RAII вважається найкращою практикою застосування C ++ через усі вищевикладені причини. Однак є додаткова вигода, яка не відразу очевидна. В основному, це краще, ніж сума його частин. Весь механізм складається . Це ваги.

Якщо ви використовуєте Lineклас як будівельний блок:

 class Table
 {
      Line borders[4];
 };

Тоді

 int main ()
 {
     Table table;
 }

виділяє чотири std::stringекземпляри, чотири Lineекземпляри, один Tableекземпляр і весь вміст рядка, і все звільняється автоматично .


55
+1 для згадування про RAII наприкінці, але має бути щось про винятки та розмотування стека.
Tobu

7
@Tobu: Так, але ця посада вже досить довга, і я хотів би тримати її досить зосередженою на питанні ОП. Я закінчу писати повідомлення в блозі чи щось таке і посилатись на нього звідси.
Андре Карон

15
Було б чудовим доповненням, щоб згадати про мінус розподілу стеків (принаймні, до C ++ 1x) - вам часто потрібно копіювати речі без потреби, якщо ви не обережні. наприклад, Monsterвипльовує Treasureдо , Worldколи він помирає. За своїм Die()методом він додає скарб у світ. Його потрібно використовувати world->Add(new Treasure(/*...*/))в інших, щоб зберегти скарб після того, як він помре. Альтернативи - це shared_ptr(може бути надмірний), auto_ptr(поганий семантичний для передачі права власності), передається за значенням (марнотратний) та move+ unique_ptr(ще широко не реалізований).
kizzx2

7
Те, що ви сказали про локальні змінні, виділені стеком, може бути трохи оманливим. "Стек" відноситься до стеку викликів, який зберігає кадри стека . Саме ці кадри зберігаються в LIFO. Локальні змінні для конкретного кадру розподіляються так, ніби вони є членами структури.
someguy

7
@someguy: Дійсно, пояснення не є ідеальним. Реалізація має свободу у своїй політиці розподілу. Однак змінні повинні бути ініціалізовані та знищені LIFO, тому аналогія має місце. Я не думаю, що ця робота ще більше ускладнює відповідь.
Андре Карон

171

Тому що стек швидший і герметичний

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


18
"потрібно виділити простір лише одну інструкцію" - о, нісенітниця. Звичайно, для додавання до покажчика стека потрібна лише одна інструкція, але якщо клас має якусь цікаву внутрішню структуру, буде набагато більше, ніж додавання до покажчика стека. Не менш справедливо сказати, що в Java не потрібно ніяких інструкцій виділяти простір, оскільки компілятор буде керувати посиланнями під час компіляції.
Чарлі Мартін

31
@Charlie правильний. Автоматичні змінні швидкі, а точніше було би надійніше.
Олівер Чарльворт

28
@Charlie: Внутрішні класи класу потрібно налаштувати в будь-якому випадку. Порівняння проводиться щодо розподілу необхідного простору.
Олівер Чарльворт

51
кашель int x; return &x;
peterchen

16
швидко так. Але точно не дурень. Ніщо не є надійним. Ви можете отримати StackOverflow :)
rxantos

107

Причина, чому складна.

По-перше, C ++ не збирає сміття. Тому для кожного нового має бути відповідне видалення. Якщо не вдалося вставити це видалення, то у вас витік пам'яті. Тепер для простого випадку, як це:

std::string *someString = new std::string(...);
//Do stuff
delete someString;

Це просто. Але що станеться, якщо "Робити речі" кидає виняток? На жаль: витік пам'яті. Що станеться, якщо "Виконувати речі" returnрано? На жаль: витік пам'яті.

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

Або ви можете просто зробити це:

std::string someString(...);
//Do stuff

Ні delete. Об'єкт був створений на "стеці", і він буде знищений, як тільки вийде за межі поля. Ви навіть можете повернути об'єкт, перенісши таким чином його вміст у функцію виклику. Ви можете передавати об'єкт функціям (як правило, як посилання або const-посилання:. void SomeFunc(std::string &iCanModifyThis, const std::string &iCantModifyThis)І так далі).

Усі без newі delete. Немає сумнівів, хто належить пам’яті чи хто несе відповідальність за її видалення. Якщо ти зробиш:

std::string someString(...);
std::string otherString;
otherString = someString;

Зрозуміло , що otherStringмає копію даних про someString. Це не вказівник; це окремий об’єкт. У них може бути однаковий вміст, але ви можете змінити один, не впливаючи на інший:

someString += "More text.";
if(otherString == someString) { /*Will never get here */ }

Бачите ідею?


1
У цій примітці ... Якщо об'єкт динамічно розподіляється в main(), існує протягом тривалості програми, його неможливо легко створити на стеці через ситуацію, і вказівники на нього передаються будь-яким функціям, які потребують доступу до нього , може це спричинить витік у разі збоїв програми, чи це було б безпечно? Я б припустив останнє, оскільки ОС, що розподіляє всю пам'ять програми, повинна і логічно розподіляти її, але я не хочу нічого припускати, коли це стосується new.
Час Джастіна - Поновіть Моніку

4
@JustinTime Вам не потрібно турбуватися про звільнення пам’яті динамічно виділених об’єктів, які мають залишитися протягом життя програми. Коли програма виконується, ОС створює для неї атлас фізичної пам'яті або віртуальної пам'яті. Кожна адреса в просторі віртуальної пам'яті відображається на адресу фізичної пам'яті, і коли програма виходить, все те, що відображено у її віртуальній пам'яті, звільняється. Отже, поки програма повністю завершується, вам не потрібно турбуватися про те, що виділена пам'ять ніколи не буде видалена.
Ейман Аль-Еріані

75

Об'єкти, створені, newповинні бути врешті-решт, deleteщоб вони не просочилися. Деструктор не буде викликаний, пам'ять не буде звільнена, весь біт. Оскільки у C ++ немає збирання сміття, це проблема.

Об'єкти, створені за значенням (тобто в стеці), автоматично гинуть, коли вони виходять за межі області. Виклик деструктора вставляється компілятором, і пам'ять автоматично звільняється після повернення функції.

Розумні покажчики подобаються unique_ptr, shared_ptrвирішити звисають посилальні проблеми, але вони вимагають кодування дисципліни та інші потенційні проблеми (copyability, посилання на петлю і т.д.).

Крім того, у сильно багатопотокових сценаріях newє точка суперечки між потоками; може бути вплив на продуктивність для надмірного використання new. Створення об'єкта стека за визначенням є потоком локальним, оскільки кожен потік має свій стек.

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


9
+1. Re "Об'єкти, створені, newповинні бути врешті-решт, deleteщоб вони не просочилися." - що ще гірше, ви new[]повинні відповідати delete[], і ви отримуєте невизначене поведінку, якщо ви delete new[]-ed пам'ять або delete[] new-ed пам'ять - дуже мало компіляторів попереджають про це (деякі інструменти, такі як Cppcheck, коли вони можуть).
Тоні Делрой

3
@TonyDelroy Є ситуації, коли компілятор не може цього попередити. Якщо функція повертає вказівник, її можна створити, якщо new (один елемент) або new [].
fbafelipe

32
  • C ++ не використовує жодного менеджера пам'яті самостійно. Інші мови, такі як C #, Java мають збирач сміття для обробки пам'яті
  • Реалізації C ++ зазвичай використовують підпрограми операційної системи для розподілу пам'яті, і занадто багато нового / видалення могло б фрагментувати наявну пам'ять
  • З будь-яким додатком, якщо пам'ять часто використовується, доцільно попередньо виділити її та звільнити, коли цього не потрібно.
  • Неправильне управління пам’яттю може призвести до витоку пам’яті, і це насправді важко відстежити. Тому використання об'єктів стека в межах функції є перевіреною методикою
  • Недоліком використання об'єктів стека є те, що він створює кілька копій об'єктів при поверненні, переході до функцій тощо. Однак розумні компілятори добре знають ці ситуації, і вони були оптимізовані для роботи
  • Це дуже нудно в C ++, якщо пам'ять, яка виділяється та звільняється в двох різних місцях. Відповідальність за випуск завжди є питанням, і ми здебільшого покладаємось на деякі загальнодоступні покажчики, об'єкти стека (максимально можливі) та такі методи, як auto_ptr (об'єкти RAII)
  • Найкраще те, що ви контролюєте пам’ять, а найгірше - ви не матимете ніякого контролю над пам’яттю, якщо ми будемо використовувати неправильне управління пам’яттю для програми. Аварії, спричинені пошкодженнями пам’яті, найнебезпечніші і важко відстежувати.

5
Насправді, будь-яка мова, яка виділяє пам'ять, має менеджер пам'яті, включаючи c. Більшість просто дуже прості, тобто int * x = malloc (4); int * y = malloc (4); ... перший виклик виділить пам'ять, він же запитає os для пам'яті, (як правило, в шматках 1k / 4k), щоб другий виклик насправді не виділив пам'ять, а надав вам частинку останнього фрагменту, виділеного на нього. IMO, сміттєзбірники не є менеджерами пам’яті, оскільки він обробляє лише автоматичне переміщення пам’яті. Щоб називатися менеджером пам'яті, він повинен не тільки керувати розмовою, але і розподілом пам'яті.
Рахлі

1
Локальні змінні використовують стек, тому компілятор не передає дзвінок malloc()або його друзям для виділення необхідної пам'яті. Однак стек не може випустити жоден елемент у стеку, єдиний спосіб, коли коли-небудь звільняється пам'ять стека, - це відкручування зверху стека.
Мікко Ранталайнен

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

22

Я бачу, що пропущено кілька важливих причин зробити якомога менше нових:

Оператор newмає недетермінований час виконання

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

Оператор new- це неявна синхронізація потоку

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

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


2
І те й інше можна уникнути, використовуючи розміщення нового / видалення та розподілення пам'яті перед рукою. Або ви можете самостійно виділити / звільнити пам'ять, а потім викликати конструктор / деструктор. Саме так зазвичай працює std :: vector.
rxantos

1
@rxantos Будь ласка, прочитайте ОП, це питання стосується уникнення зайвих виділень пам'яті. Також немає видалення місця розташування.
Емілі Л.

@Emily Це те, що означав ОП, я думаю:void * someAddress = ...; delete (T*)someAddress
xryl669

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

1
@mikkorantalainen це технічно правда, але в умовах низької пам’яті всі ставки все одно вимикаються на продуктивність wrt, оскільки ви натискаєте на диск, тому нічого не можете зробити. Це ні в якому разі не визнає пораду уникати нових дзвінків, коли це розумно робити.
Емілі Л.

21

Pre-C ++ 17:

Тому що він схильний до тонких протікань, навіть якщо результат ви загорнете в розумний покажчик .

Розглянемо "обережного" користувача, який пам'ятає загортати об'єкти в розумні покажчики:

foo(shared_ptr<T1>(new T1()), shared_ptr<T2>(new T2()));

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

Рішення: використання make_shared.

Post-C ++ 17:

Це більше не є проблемою: C ++ 17 накладає обмеження на порядок цих операцій, в цьому випадку гарантуючи, що за кожним викликом new()слід негайно слідувати побудовою відповідного інтелектуального вказівника, без будь-якої іншої операції між ними. Це означає, що до моменту виклику другого new()гарантується, що перший об’єкт вже загорнутий у його розумний вказівник, таким чином запобігаючи будь-яким витокам у разі викидання виключення.

Більш детальне пояснення нового порядку оцінювання, запровадженого C ++ 17, було надано Баррі в іншій відповіді .

Завдяки @Remy Lebeau за вказівку на те, що це все ще проблема під C ++ 17 (хоча і менше): shared_ptrконструктор може не виділити свій блок управління і кинути, і в цьому випадку вказівник, переданий на нього, не видаляється.

Рішення: використання make_shared.


5
Інше рішення: Ніколи динамічно не виділяйте більше одного об'єкта на рядок.
Сурма

3
@Antimin: Так, набагато більш заманливо виділити більше одного об'єкта, коли ви вже виділили один, порівняно з тим, коли ви не виділили жодного.
користувач541686

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

2
Навіть у випадку після C ++ 17 витік все ж може відбутися, якщо це newвдасться, і тоді подальша shared_ptrконструкція завершиться невдачею. std::make_shared()вирішив би і це
Ремі Лебо

1
@Mehrdad відповідний shared_ptrконструктор виділяє пам'ять для блоку управління, який зберігає загальний покажчик і делетер, так що так, теоретично він може викинути помилку пам'яті. Лише конструктори копіювання, переміщення та зшивання не кидаються. make_sharedвиділяє спільний об'єкт всередині самого блоку управління, тож є лише 1 розподіл замість 2.
Ремі Лебо

17

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

Найпростіший спосіб зробити це - створити об'єкт в автоматичному сховищі, тому C ++ знає його знищити, коли він виходить за межі області:

 {
    File foo = File("foo.dat");

    // do things

 }

Тепер зауважте, що коли ви падаєте з цього блоку після закінчення бренда, fooце не входить у рамки. C ++ автоматично зателефонує своєму dtor. На відміну від Java, вам не потрібно чекати, коли GC знайде її.

Ти писав

 {
     File * foo = new File("foo.dat");

ви хочете, щоб це явно було узгоджено

     delete foo;
  }

а ще краще, виділіть свій File *як "розумний вказівник". Якщо ви не будете обережні, це може призвести до протікання.

Сама відповідь дає помилкове припущення, що якщо ви не користуєтеся, newто не виділяєте на купу; насправді в C ++ ви цього не знаєте. Щонайбільше, ви знаєте, що невеликий об’єм пам’яті, скажімо, один вказівник, безумовно, виділяється на стеці. Однак врахуйте, чи реалізація File щось подібне

  class File {
    private:
      FileImpl * fd;
    public:
      File(String fn){ fd = new FileImpl(fn);}

то FileImplвсе одно буде виділено на стек.

І так, вам краще бути обов'язково

     ~File(){ delete fd ; }

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


4
Слід поглянути на код у згаданому питанні. У цьому коді безумовно багато помилок.
Андре Карон

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

5
Справедлива точка. Але зазвичай застосовуються загальні правила, щоб уникнути загальних підводних каменів. Невже це була слабкість людей чи ні, управління пам'яттю є досить складним, щоб гарантувати таке загальне правило! :)
Robben_Ford_Fan_boy

9
@Charlie: коментар не говорить, що ви ніколи не повинні використовувати new. У ньому сказано, що якщо у вас є вибір між динамічним розподілом та автоматичним зберіганням, використовуйте автоматичне зберігання.
Андре Карон

8
@Charlie: у використанні немає нічого поганого new, але якщо ти використовуєш delete, ти робиш це неправильно!
Матьє М.

16

new()не слід використовувати якомога менше . Її слід використовувати якомога обережніше . І його слід вживати так часто, як це потрібно, продиктовано прагматизмом.

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

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


9
"якщо термін експлуатації вашого об'єкта повинен вийти за межі поточного діапазону, то новий () - це правильна відповідь" ... чому б не повернути перевагу за значенням або прийняти змінну, що охоплює абонент, за допомогою не- constref або pointer ...?
Тоні Делрой

2
@Tony: Так, так! Я радий почути, що хтось виступає за посилання. Вони були створені для запобігання цієї проблеми.
Натан Осман

@TonyD ... або об'єднати їх: повернути розумний покажчик за значенням. Таким чином, абонент і в багатьох випадках (тобто там, де make_shared/_uniqueце можливо), користувачеві ніколи не потрібно newабо delete. Ця відповідь пропускає реальні моменти: (A) C ++ надає такі речі, як RVO, семантика переміщення та вихідні параметри - що часто означає, що обробка об'єкта та розширення терміну експлуатації шляхом повернення динамічно розподіленої пам'яті стає непотрібним і недбалим. (B) Навіть у ситуаціях, коли необхідний динамічний розподіл, stdlib забезпечує обгортки RAII, які позбавляють користувача від потворних внутрішніх деталей.
підкреслюй_d

14

При використанні нових об'єктів виділяються в купу. Зазвичай він використовується, коли ви очікуєте розширення. Коли ви оголошуєте об'єкт, наприклад,

Class var;

його поміщають на стек.

Вам завжди доведеться викликати знищення на об'єкті, який ви розмістили на купі з новим. Це відкриває можливість витоку пам'яті. Об'єкти, розміщені на стеці, не схильні до протікання пам'яті!


2
+1 «[купа] зазвичай використовується , коли ви очікуєте розширення» - як додаток до std::stringабо std::map, так, проникливістю. Моя початкова реакція полягала в тому, що "але також дуже часто від'єднується термін експлуатації об'єкта від області створення коду", але справді повернення за значенням або прийняття значень, нанесених номером абонента, за нереференційним constабо покажчиком, для цього краще, за винятком випадків, коли "розширення" задіяне теж. Хоча деякі інші звуки використовують, як заводські методи ....
Тоні Делрой

12

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

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

Однак ще краще, щоб уникнути проблеми взагалі. Якщо ви можете поставити його на стек, тоді зробіть це.


Ви завжди можете виділити досить великий об'єм пам'яті, а потім використовувати місце розташування new / delete, якщо швидкість є проблемою.
rxantos

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

10

Я думаю, що плакат мав на меті сказати, You do not have to allocate everything on theheapа не те stack.

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


10

Я схильний не погоджуватися з ідеєю використання нового «занадто багато». Хоча використання оригінальних плакатів нових із системними класами трохи смішно. ( int *i; i = new int[9999];«насправді?» int i[9999];набагато зрозуміліше.) Я думаю, що саме це діставало коза коментера.

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

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


зазвичай нове / видалене буде використовувати його, коли ви не знаєте, доки розмір масиву. Звичайно std :: вектор приховує для вас нове / видалення. Ви все ще використовуєте їх, але через std :: vector. Тому в даний час він буде використовуватися, коли ви не знаєте розмір масиву і хочете чомусь уникнути накладних витрат std :: vector (Що мало, але все ще існує).
rxantos

When you're working with your own classes/objects... у вас часто немає підстав для цього! Невелика частка Q в деталях дизайну контейнерів кваліфікованими кодерами. На відміну від цього, пригнічуюча частка полягає в плутанині новачків, які не знають, що існує stdlib - або їм активно надаються жахливі завдання на «курсах програмування», де вихователь вимагає, щоб вони безцільно винаходили колесо - перш ніж вони навіть зможуть дізналися, що таке колесо і чому воно працює. Сприяючи більш абстрактному розподілу, C ++ може врятувати нас від нескінченного "segfault з пов'язаним списком" C; будь ласка, давайте нехай це .
підкреслюйте_d

" використання оригінальних плакатів нового класу в системних класах є дещо смішним. ( int *i; i = new int[9999];насправді? int i[9999];набагато зрозуміліше.)" Так, зрозуміліше, але для того, щоб грати в захисника диявола, тип не обов'язково є поганим аргументом. Маючи 9999 елементів, я можу уявити, що щільна вбудована система не має достатнього стека для 9999 елементів: 9999x4 байт становить ~ 40 кБ, х8 ~ 80 кБ. Отже, таким системам, можливо, доведеться використовувати динамічне розподіл, якщо припустити, що вони реалізують його за допомогою альтернативної пам'яті. Однак це може тільки виправдати динамічне розподіл, ні new; vectorб реальне виправлення в цьому випадку
underscore_d

Погодьтеся з @underscore_d - це не такий чудовий приклад. Я б не додав 40 000 або 80 000 байтів до свого стеку просто так. Я б, мабуть, виділив їх на купі ( std::make_unique<int[]>()звичайно).
einpoklum

3

Дві причини:

  1. У цьому випадку це зайве. Ви ускладнюєте свій код непотрібно.
  2. Він виділяє простір на купі, і це означає, що вам доведеться пам’ятати про deleteце пізніше, інакше це спричинить витік пам’яті.

2

newє новим goto.

Згадаймо, чому gotoце так знущається: хоча це потужний низькорівневий інструмент для контролю потоку, люди часто використовували його непотрібно складними способами, які ускладнювали дотримання коду. Крім того, найбільш корисні та найпростіші для читання шаблони були закодовані у структурованих операторах програмування (наприклад, forабо while); остаточний ефект полягає в тому, що код, де gotoє відповідний спосіб, є досить рідкісним, якщо ви спокушаєтесь писати goto, ви, ймовірно, робите речі погано (якщо ви справді не знаєте, що робите).

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

Я б навіть заперечував, що newце гірше, ніж gotoчерез необхідність пари newта deleteтверджень.

Мовляв goto, якщо вам колись здається, що вам потрібно користуватися new, ви, ймовірно, робите речі погано - особливо якщо ви робите це поза межами впровадження класу, метою якого в житті є інкапсуляція будь-яких динамічних виділень, які вам потрібно зробити.


І я додав би: "Ви в основному просто не потрібні".
einpoklum

1

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

Інший сценарій - бібліотека, яку ми використовуємо, забезпечує семантику значень і робить динамічне розподілення непотрібним. Std::stringє хорошим прикладом.

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

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


4
Це безглузда відповідь. For object oriented code, using a pointer [...] is a must: нісенітниця . Якщо ви знецінюєте "OO", посилаючись лише на невелику підмножину, поліморфізм - також нісенітниця: посилання також працюють. [pointer] means use new to create it beforehand: особливо нісенітниця : посилання чи покажчики можуть бути використані для автоматично виділених об'єктів та використовуваних поліморфно; дивись на мене . [typical OO code] use new a lot: можливо, в якійсь старій книзі, але кого це хвилює? Будь-які розпливчасті сучасні C ++ уникнення new/ нераціональні покажчики, коли це можливо - і жодним чином не є OO, роблячи це
underscore_d

1

Ще один пункт до всіх вищезазначених правильних відповідей, це залежить від того, яким типом програмування ти займаєшся. Наприклад, ядро, що розвивається в Windows -> Стек сильно обмежений, і ви, можливо, не зможете приймати помилки сторінки, як в режимі користувача.

У таких середовищах нові або C-подібні виклики API є кращими та навіть необхідними.

Звичайно, це лише виняток із правила.


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