Правильне використання стека та купи в C ++?


122

Я певний час програмував, але це були переважно Java та C #. Мені ніколи насправді не довелося самостійно керувати пам’яттю. Нещодавно я почав програмувати на C ++, і я трохи розгублений, коли мені потрібно зберігати речі на стеці та коли зберігати їх у купі.

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


Відповіді:


242

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

class Thingy;

Thingy* foo( ) 
{
  int a; // this int lives on the stack
  Thingy B; // this thingy lives on the stack and will be deleted when we return from foo
  Thingy *pointerToB = &B; // this points to an address on the stack
  Thingy *pointerToC = new Thingy(); // this makes a Thingy on the heap.
                                     // pointerToC contains its address.

  // this is safe: C lives on the heap and outlives foo().
  // Whoever you pass this to must remember to delete it!
  return pointerToC;

  // this is NOT SAFE: B lives on the stack and will be deleted when foo() returns. 
  // whoever uses this returned pointer will probably cause a crash!
  return pointerToB;
}

Для більш чіткого розуміння того, що таке стек, підійдіть до нього з іншого кінця - а не намагайтеся зрозуміти, що робить стек з точки зору мови високого рівня, шукайте "стек викликів" та "конвенція виклику" і дивіться, що машина дійсно робить, коли ви викликаєте функцію. Пам'ять комп’ютера - це лише ряд адрес; "heap" і "stack" - це винаходи компілятора.


7
Було б сміливо додати, що інформація з різним розміром зазвичай йде в купу. Єдині винятки, про які я знаю, - це VLA в C99 (який має обмежену підтримку) та функція alloca (), яку часто неправильно розуміють навіть програмісти на C.
Ден Олсон

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

18
Звичайно, і new / malloc () сама по собі є повільною роботою, і стек, швидше за все, знаходиться в dcache, ніж довільна лінія купи. Це реальні міркування, але, як правило, другорядні у питанні про тривалість життя.
Crashworks

1
Це правда: "Комп'ютерна пам'ять - це лише ряд адрес;" купа "та" стек "- це винаходи компіляції" ?? Я читав у багатьох місцях, що стек - це особлива область пам’яті нашого комп’ютера.
Vineeth Chitteti

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

42

Я б сказав:

Зберігайте його на стеку, якщо МОЖЕТЕ.

Зберігайте його на купі, якщо цього потрібно.

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

  • Він занадто великий - у багатопотокових програмах на 32-розрядної ОС стек має невеликий і фіксований (принаймні на час створення потоків) розмір (як правило, лише кілька мег. Це так, що ви можете створювати безліч потоків без вичерпної адреси Для 64-бітових програм або для однопотокових (все-таки Linux) програм це не є основною проблемою.За 32-бітовим Linux, однопотокові програми зазвичай використовують динамічні стеки, які можуть постійно зростати, поки не досягнуть вершини купи.
  • Вам потрібно отримати доступ до нього за межами вихідного кадру стека - це справді основна причина.

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


1
Що-небудь більше, ніж пара КБ, як правило, найкраще класти на купу. Я не знаю конкретики, але не пригадую, щоб коли-небудь працював зі стеком, який був "кількома мегами".
Ден Олсон

2
Це те, що я б не стосувався користувача на початку. Для користувача, вектори та списки, здається, виділяються на стеці, навіть якщо STL зберігає вміст у купі. Питання, здавалося, більше по лінії вирішення питання, коли явно зателефонувати новим / видалити.
Девід Родрігес - дрибес

1
Ден: Я поставив 2 гіги (Так, G як у GIGS) на стек під 32-бітовим Linux. Ліміти стеку залежать від ОС.
Mr.Ree

6
mrree: Стек Nintendo DS становить 16 кілобайт. Деякі обмеження стека залежать від апаратних засобів.
Мураха

Ant: Усі стеки залежать від апаратного забезпечення, залежать від ОС, а також залежать від компілятора.
Вільямі

24

Це тонкіше, ніж інші відповіді. Не існує абсолютного розриву між даними про стек і даними на купі, залежно від того, як ви це оголошуєте. Наприклад:

std::vector<int> v(10);

В тілі функції, що оголошує vectorна стеку (динамічний масив) з десяти цілих чисел. Але сховище, яким керує vector, не знаходиться в стеці.

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

Не так. Припустимо, функцією було:

void GetSomeNumbers(std::vector<int> &result)
{
    std::vector<int> v(10);

    // fill v with numbers

    result.swap(v);
}

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

Тому сучасний підхід C ++ полягає у тому, щоб ніколи не зберігати адресу даних купи у голих змінних місцевих покажчиків. Усі виділення купи повинні бути приховані всередині класів.

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

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

a = b;

поміняйте їх так:

a.swap(b);

тому що це набагато швидше і не кидає винятків. Єдина вимога полягає в тому, що вам не потрібно bпродовжувати утримувати одне і те ж значення (воно отримає aнатомість значення, яке буде потраплено в кошик a = b).

Мінус полягає в тому, що такий підхід змушує вас повертати значення з функцій через вихідні параметри замість фактичного поверненого значення. Але вони виправляють це в C ++ 0x з посиланнями на оцінку rvalue .

У найскладніших ситуаціях з усіх країн ви піднесуть цю ідею до загальної крайності та використовуєте інтелектуальний клас вказівника, такий, shared_ptrякий вже є в tr1. (Хоча я стверджую, що якщо вам це здається потрібним, ви, можливо, переїхали за межі приємного місця застосування Standard C ++.)


6

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


5

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

Розподіл у купі вимагає відстеження блоку пам'яті, який не є операцією постійного часу (і займає певні цикли та накладні витрати). Це може ставати повільніше, коли пам'ять стає фрагментованою та / або ви наближаєтесь до використання 100% свого адресного простору. З іншого боку, розподіл стеків - це операції постійного часу, в основному "вільні" операції.

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


І купа, і стек - це підказка віртуальної пам'яті. Час пошуку купи надзвичайно швидко порівняно з тим, що потрібно для відображення в новій пам'яті. Під 32-бітовим Linux я можу поставити 2gig на свій стек. Під Macs, я думаю, що стек важко обмежений 65Meg.
Mr.Ree

3

Стек є більш ефективним та простішим керуванням масштабованими даними.

Але купу слід використовувати для будь-якого розміру, ніж декілька КБ (це легко в C ++, просто створіть boost::scoped_ptrна стеку, щоб утримати покажчик на виділену пам'ять).

Розглянемо рекурсивний алгоритм, який постійно дзвонить у себе. Дуже важко обмежити та вгадати загальне використання стека! В той час як на купі, розподільник ( malloc()або new) може вказати поза пам'яттю, повернувши NULLабо throwing.

Джерело : Linux Kernel, стек якого не більше 8 КБ!


Для довідки щодо інших читачів: (A) Тут "слід" - це суто особиста думка користувача, яка складається в кращому випадку з 1 цитуванням та 1 сценарієм, з яким багато користувачів навряд чи можуть зіткнутися (рекурсія). Також (B) надається стандартна бібліотека std::unique_ptr, якій слід віддати перевагу будь-якій зовнішній бібліотеці, наприклад, Boost (хоча це з часом відповідає стандартам).
підкреслити_3


1

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


4
Я підозрюю, що він запитував, коли поставити речі на купу, а не як.
Стів Роу

0

На мою думку, є два вирішальні фактори

1) Scope of variable
2) Performance.

Я вважаю за краще використовувати стек у більшості випадків, але якщо вам потрібен доступ до змінної за межами області, ви можете використовувати купу.

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


0

напевно, на це відповіли досить добре. Я хотів би вказати на нижченаведені серії статей, щоб глибше зрозуміти деталі низького рівня. Алекс Дарбі має низку статей, де він виводить вас за допомогою налагоджувача. Ось частина 3 про стек. http://www.altdevblogaday.com/2011/12/14/cc-low-level-curriculum-part-3-the-stack/


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