Стек, статична та купа в C ++


160

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

Я чув, що інші мови містять "сміттєзбірник", тому вам не потрібно турбуватися про пам'ять. Що робить смітник?

Що ви могли б самостійно маніпулювати пам’яттю, що не могли б скористатися цим сміттєзбірником?

Одного разу хтось сказав мені, що з цією декларацією:

int * asafe=new int;

У мене є "покажчик на покажчик". Що це означає? Він відрізняється:

asafe=new int;

?


Колись тому було задано дуже схоже запитання: що і де стопка і купа? Є кілька дійсно хороших відповідей на це питання, які повинні пролити трохи світла на ваше.
Скотт Саад

Можливий дублікат Що і де є стек і купа?
Swati Garg

Відповіді:


223

Подібне запитання було задано, але воно не запитувало про статику.

Короткий опис того, що таке статична, купа та стека:

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

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

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

Коли ви хочете використовувати кожен з них:

  • Статистика / глобальна сфера корисна для пам’яті, яку ви знаєте, що вам завжди знадобиться, і ви знаєте, що ніколи не хочете розбиратися. (До речі, вбудовані середовища можуть вважатися такими, що мають лише статичну пам'ять ... стек і купа є частиною відомого адресного простору, спільним для третього типу пам’яті: програмного коду. Програми часто роблять динамічне розподілення поза своїми статична пам'ять, коли їм потрібні такі речі, як пов'язані списки. Але незалежно, сама статична пам'ять (буфер) сама по собі не "виділяється", а навпаки інші об'єкти виділяються із пам'яті, яку утримує буфер для цієї мети. Ви можете це зробити і в невбудованих, і в консольних іграх часто ухиляються від вбудованих динамічних механізмів пам'яті на користь жорсткого контролю процесу розподілу, використовуючи буфери заданих розмірів для всіх розподілів.)

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

  • Купи розподілу (динамічно розподілена пам'ять) корисні, коли ви хочете бути більш гнучкими, ніж вище. Часто функцію викликають у відповідь на подію (користувач натискає кнопку "створити вікно"). Відповідна відповідь може вимагати виділення нового об'єкта (нового об'єкта Box), який повинен триматися довгий час після закінчення функції, тому він не може бути в стеці. Але ви не знаєте, скільки вікон ви хотіли б на початку програми, тому це не може бути статичним.

Збір сміття

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

Збір сміття - чудовий механізм, коли продуктивність не є величезною проблемою. Я чую, що GC стають все кращими і складнішими, але факт полягає в тому, що, можливо, ви будете змушені прийняти штрафну ефективність (залежно від випадку використання). І якщо ти лінивий, він все одно може не працювати належним чином. У найкращі часи збирачі сміття усвідомлюють, що пам’ять згасає, коли зрозуміє, що на неї більше немає посилань (див. Підрахунок посилань). Але, якщо у вас є об'єкт, який посилається на себе (можливо, посилаючись на інший об'єкт, який посилається назад), то підрахунок посилань поодинці не вказуватиме на те, що пам'ять можна видалити. У цьому випадку ГК потрібно переглянути весь контрольний суп і з'ясувати, чи є якісь острови, на які посилаються лише вони самі. Я, напевно, думаю, що це операція O (n ^ 2), але що б це не було, це може стати поганим, якщо ви взагалі переймаєтесь продуктивністю. (Редагувати: Мартін Б. зазначає, що це O (n) для досить ефективних алгоритмів. Це все ще занадто O (n), якщо ви переймаєтесь продуктивністю і можете постійно розміщуватися без збирання сміття.)

Особисто, коли я чую, як люди кажуть, що на C ++ немає збирання сміття, мій розум відзначає це як особливість C ++, але я, мабуть, у меншості. Напевно, людям найважче дізнатися про програмування на C та C ++ - це покажчики та як правильно керувати їх динамічним розподілом пам'яті. Деякі інші мови, як-от Python, були б жахливими без GC, тому я думаю, що це зводиться до того, що ви хочете отримати з мови. Якщо ви хочете отримати надійну продуктивність, то C ++ без збирання сміття - це єдине, про що я можу придумати цю сторону Fortran. Якщо ви хочете зручності користування та навчальних коліс (щоб уберегти вас від збоїв, не вимагаючи, щоб ви навчились «правильному» керуванню пам’яттю), виберіть щось із GC. Навіть якщо ви знаєте, як добре керувати пам'яттю, це заощадить ваш час, який ви можете витратити на оптимізацію іншого коду. Насправді вже не так багато покарання за продуктивність, але якщо вам справді потрібні надійні показники (і здатність точно знати, що відбувається, коли під кришками), тоді я б дотримувався C ++. Є причина, що кожен основний ігровий движок, про який я коли-небудь чув, знаходиться на C ++ (якщо не на С або на зборах). Python та інші добре підходять для створення сценаріїв, але це не головний ігровий механізм.


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

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

7
Ваше зневажливе ставлення до вивезення сміття було трохи менш корисним.
P тато

9
Часто збір сміття в наш час краще, ніж пам'ять, що звільняється вручну, тому що це відбувається, коли мало що потрібно зробити, на відміну від звільнення пам’яті, що може статися прямо тоді, коли виступ можна використовувати інакше.
Георг Шоллі

3
Лише невеликий коментар - збирання сміття не має складності O (n ^ 2) (що, справді, може бути згубним для продуктивності). Час, витрачений на один цикл вивезення сміття, пропорційний розміру купи - див. Hpl.hp.com/personal/Hans_Boehm/gc/complexity.html .
Мартін Б

54

Далі, звичайно, все не зовсім точно. Візьміть його з зерном солі, коли прочитаєте :)

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


Автоматична тривалість зберігання

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

if(some condition) {
    int a[3]; // array a has automatic storage duration
    fill_it(a);
    print_it(a);
}

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


Статична тривалість зберігання

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

// static storage duration. in global namespace scope
string globalA; 
int main() {
    foo();
    foo();
}

void foo() {
    // static storage duration. in local scope
    static string localA;
    localA += "ab"
    cout << localA;
}

Програма друкує ababab, оскільки localAне руйнується після виходу з її блоку. Можна сказати, що об'єкти, які мають локальну область, починаються протягом життя, коли контроль досягає їх визначення . Бо localAце відбувається, коли введено тіло функції. Для об'єктів із області імен час життя починається при запуску програми . Те саме стосується статичних об'єктів сфери дії класу:

class A {
    static string classScopeA;
};

string A::classScopeA;

A a, b; &a.classScopeA == &b.classScopeA == &A::classScopeA;

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


Динамічна тривалість зберігання

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

int main() {
    // the object that s points to has dynamic storage 
    // duration
    string *s = new string;
    // pass a pointer pointing to the object around. 
    // the object itself isn't touched
    foo(s);
    delete s;
}

void foo(string *s) {
    cout << s->size();
}

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

int main() {
    shared_ptr<string> s(new string);
    foo(s);
}

void foo(shared_ptr<string> s) {
    cout << s->size();
}

Не потрібно дбати про виклик видалення: Спільний ptr робить це за вас, якщо останній вказівник, на який посилається об'єкт, виходить за межі області. Сам спільний ptr має тривалість автоматичного зберігання. Таким чином, його термін експлуатації автоматично керується, що дозволяє перевірити, чи слід видаляти вказаний на динамічний об'єкт у своєму деструкторі. Для посилання на shared_ptr див. Документи для підвищення: http://www.boost.org/doc/libs/1_37_0/libs/smart_ptr/shared_ptr.htm


39

Це було сказано детально, так само, як "коротка відповідь":

  • статична змінна (клас)
    час життя = тривалість програми (1)
    видимість = визначається модифікаторами доступу (приватний / захищений / загальнодоступний)

  • статична змінна (глобальний обсяг)
    час життя = тривалість виконання програми (1)
    видимість = одиниця компіляції, в яку інстанціюється (2)


  • життя змінної купи = визначена вами (нова для видалення)
    видимість = визначена вами (що б ви не призначили вказівник)

  • стек змінна
    видимість = від декларації, поки область не закінчиться
    протягом життя = від декларації, поки не буде оголошено область застосування


(1) точніше: від ініціалізації до деініціалізації компіляційного блоку (тобто файла C / C ++). Порядок ініціалізації одиниць компіляції не визначений стандартом.

(2) Остерігайтеся: якщо ви інстанціюєте статичну змінну в заголовку, кожен елемент компіляції отримує власну копію.


5

Я впевнений, що хтось із педантів невдовзі придумає кращу відповідь, але головна відмінність - швидкість та розмір.

Стек

Різко швидше виділити. Це робиться в O (1), оскільки він виділяється при встановленні кадру стека, так що він по суті вільний. Недолік полягає в тому, що якщо у вас не вистачає місця у стеці, ви будете звільнені. Ви можете налаштувати розмір стека, але для IIRC вам належить ~ 2 Мб. Крім того, як тільки ви вийдете з функції, все в стеку буде очищено. Тому може бути проблематично посилатися на це пізніше. (Покажчики на стек виділених об'єктів призводять до помилок.)

Купи

Різко повільніше виділяти. Але у вас є ГБ, з яким можна грати, і на це вказуйте.

Збирач сміття

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


3

Які проблеми статичного і стекового?

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

Проблема з розподілом на "стек" полягає в тому, що розподіл знищується, як тільки повертається підпрограма, яка виконує розподіл.

Я міг би написати всю програму без виділення змінних у купу?

Можливо, але не нетривіальна, звичайна, велика програма (але так звані "вбудовані" програми можуть писатися без купи, використовуючи підмножину C ++).

Що робить сміттєзбірник?

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

Збирачі сміття - не звичайна особливість програмування на C ++.

Що ви могли б самостійно маніпулювати пам’яттю, що не могли б скористатися цим сміттєзбірником?

Вивчіть механізми C ++ для детермінованої оперативної пам'яті:

  • "статичний": ніколи не розміщений
  • 'stack': як тільки змінна "виходить з поля"
  • 'heap': коли вказівник видаляється (явно видаляється програмою або неявно видаляється в межах якоїсь або іншої підпрограми)

1

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

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


1

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


0

Перевага GC в одних ситуаціях - роздратування в інших; опора на GC спонукає не багато про це думати. Теоретично чекає, поки період "простоює" або поки він абсолютно не повинен, коли він вкраде пропускну здатність і спричинить затримку відповіді у вашому додатку.

Але вам не потрібно «не думати про це». Як і у всьому іншому в багатопотокових додатках, коли ви можете поступатися, ви можете поступатися. Так, наприклад, в .Net, можна запросити GC; роблячи це, замість менш частого тривалого запуску GC, ви можете мати більш часті коротші запущені GC та розподілити затримку, пов’язану з цією накладною витратою.

Але це перемагає первинну привабливість GC, яка, здається, "спонукає не потрібно багато про це думати, тому що це авто-мат-ic".

Якщо ви вперше потрапили до програмування до того, як GC набув поширення, і вам було комфортно з malloc / free та new / delete, то, можливо, ви навіть виявите, що GC трохи дратує і / або є недовірливим (як це може бути недовірно " оптимізація ", яка має картату історію.) Багато додатків переносять випадкові затримки. Але для програм, які не мають, де випадкова затримка є менш прийнятною, загальною реакцією є ухилення від середовищ GC та переміщення у напрямку суто некерованого коду (або, не дай бог, давно вмираючого мистецтва, мови складання.)

Тут у мене був деякий час літній студент, стажист, розумна дитина, яку відлучили в GC; він був настільки прихильний до нагляду GC, що навіть при програмуванні в некерованому C / C ++ він відмовився переслідувати модель malloc / free new / delete, оскільки, цитуйте, "вам не слід було робити цього на сучасній мові програмування". А ти знаєш? Що стосується крихітних коротких запущених додатків, ви дійсно можете піти з цього, але не для тривалих застосунків.


0

Стек - це пам'ять, виділена компілятором, коли коли-небудь ми компілюємо програму, за замовчуванням компілятор виділяє деяку пам’ять з ОС (ми можемо змінити налаштування з налаштувань компілятора у вашому IDE), а ОС - та, яка дає вам пам'ять, це залежить. у багатьох доступних пам’яті в системі та багатьох інших речах, а пам'ять про стек виділяється, коли ми оголошуємо змінну, яку вони копіюють (посилаються на формали), ці змінні натискають на стек, вони слідують деяким умовам іменування за замовчуванням його CDECL у Visual studios наприклад: позначення інфіксації: c = a + b; штовхання стека виконується праворуч вліво PUSHING, b до стека, оператор, a до стека та результат цих i, ec до стеку. Заздалегідь позначення: = + cab Тут усі змінні переміщуються на стек 1-го (справа наліво), після чого виконується операція. Ця пам'ять, виділена компілятором, фіксована. Отже, припускаємо, що 1МБ пам’яті виділено нашому додатку, давайте скажемо, що змінні використовували 700 кб пам’яті (всі локальні змінні висуваються на стек, якщо вони не динамічно розподіляються), тому решта 324 кб пам’яті виділяється у купу. І цей стек має менший час життя, коли область функції закінчується, ці стеки очищаються.

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