У C, дужки виступають як рамка стека?


153

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

void foo() {
   int c[100];
   {
       int d[200];
   }
   //code that takes a while
   return;
}

Чи dбуде займати пам’ять під час code that takes a whileрозділу?


8
Ви маєте на увазі (1) згідно Стандарту, (2) універсальну практику серед впроваджень, або (3) поширену практику серед впроваджень?
Девід Торнлі

Відповіді:


83

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

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

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

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


9
Це не конкретна реалізація?
авакар

54
У C ++ деструктор об'єкта викликається в кінці своєї області дії. Незалежно від того, чи повертається пам'ять, - це специфічна проблема.
Крістофер Джонсон

8
@ pm100: Буде викликано деструкторів. Це нічого не говорить про пам’ять, яку ці об'єкти займали.
Стипендіати Доналу

9
Стандарт C визначає, що термін служби автоматичних змінних, оголошених у блоці, подовжується лише до тих пір, поки не завершиться виконання блоку. Таким чином , по суті ці автоматичні змінні дійсно отримати «знищені» в кінці блоку.
caf

3
@KristopherJohnson: Якщо метод мав два окремі блоки, кожен з яких оголосив масив 1 Кбайт, а третій блок, який викликав вкладений метод, компілятор буде вільний використовувати однакову пам'ять для обох масивів та / або розмістити масив. в найменшій частині стека і перемістіть вказівник стека над ним, викликаючи вкладений метод. Така поведінка може зменшити на 2K глибину стека, необхідну для виклику функції.
supercat

39

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

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

void foo() {
   int c[100];
   int *p;

   {
       int d[200];
       p = d;
   }

   /* Can I access p[0] here? */

   return;
}

(Іншими словами: чи дозволяється компілятору розміщувати місце d, навіть якщо на практиці більшість цього не робить?).

Відповідь в тому , що компілятор буде дозволено звільняти d, і доступ до p[0]якої коментар вказує на це невизначене поведінку (програма НЕ має права доступу на внутрішній об'єкт за межами внутрішнього обсягу). Відповідна частина стандарту С - 6.2.4p5:

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


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

20

Ваше запитання недостатньо чітке, щоб відповісти однозначно.

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

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

void foo()
{
  {
    int d[100];
  }
  {
    double e[20];
  }
}

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

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


6

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


3

Ні, d [] не буде на стеку до кінця рутини. Але аллока () різна.

Редагувати: Крістофер Джонсон (а також Симон та Даніель) мають рацію , і моя початкова відповідь була неправильною . З gcc 4.3.4.on CYGWIN код:

void foo(int[]);
void bar(void);
void foobar(int); 

void foobar(int flag) {
    if (flag) {
        int big[100000000];
        foo(big);
    }
    bar();
}

дає:

_foobar:
    pushl   %ebp
    movl    %esp, %ebp
    movl    $400000008, %eax
    call    __alloca
    cmpl    $0, 8(%ebp)
    je      L2
    leal    -400000000(%ebp), %eax
    movl    %eax, (%esp)
    call    _foo
L2:
    call    _bar
    leave
    ret

Живи й учись! І швидкий тест, схоже, показує, що AndreyT також правильно стосується декількох розподілів.

Додано набагато пізніше : Наведений вище тест показує, що документація gcc не зовсім вірна. Протягом багатьох років це було сказано (наголос додано):

«Простір для масиву змінної довжини звільняється , як тільки ім'я масиву в області видимості кінців


Компіляція з відключеною оптимізацією не обов'язково показує, що ви отримаєте в оптимізованому коді. У цьому випадку поведінка однакова (виділіть на початку функції та вільну лише при виході з функції): godbolt.org/g/M112AQ . Але не-cygwin gcc не викликає allocaфункції. Я дуже здивований, що cygwin gcc зробив би це. Це навіть не масив змінної довжини, тож IDK, чому ви це піднімаєте.
Пітер Кордес

2

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


1

Ваша змінна dзазвичай не вискакує зі стека. Фігурні дужки не позначають рамку стека. Інакше ви не зможете зробити щось подібне:

char var = getch();
    {
        char next_var = var + 1;
        use_variable(next_char);
    }

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

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

Оновлення: Ось, що має сказати специфікація C Щодо об'єктів з автоматичною тривалістю зберігання (розділ 6.4.2):

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

Цей же розділ визначає термін "термін експлуатації" як (моє наголос):

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

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

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

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


1
"Якщо фігурні дужки викликали натиск / поп стека, то наведений вище код не буде компілюватися, оскільки код усередині брекетів не зможе отримати доступ до змінної var, яка живе поза брекетів" - це просто не відповідає дійсності. Компілятор завжди може запам'ятати відстань від вказівника стека / кадру та використовувати його для посилання на зовнішні змінні. Також див. Відповідь Йосифа на прикладі фігурних брекетів, які викликають натиск / попс стека.
Джордж

@ george- Поведінка, яку ви описуєте, як і приклад Йосифа, залежить від компілятора та платформи, яку ви використовуєте. Наприклад, компіляція одного і того ж коду для цілі MIPS дає абсолютно різні результати. Я говорив чисто з точки зору специфікації C (оскільки в ОП не було вказано компілятора чи цілі). Я відредагую відповідь і додаю більше конкретики.
бта

0

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


3
Ніяких гарантій. Після закриття області компілятор більше не відслідковує цю пам'ять (або, принаймні, не потрібно ...), і цілком може її використовувати повторно. Ось чому дотик до пам'яті, раніше зайнятої змінною поза рамками, є невизначеним поведінкою. Остерігайтеся назальних демонів і подібних попереджень.
dmckee --- кошеня колишнього модератора

0

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

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

#include <stdio.h>
int main() {
    int* x;
    int* y;
    {
        int a;
        x = &a;
        printf("%p\n", (void*) x);
    }
    {
        int b;
        y = &b;
        printf("%p\n", (void*) y);
    }
}

Використовуючи gcc, ми отримуємо тут двічі однакову адресу: Coliro

Але якщо ми спробуємо наступний код:

#include <stdio.h>
int main() {
    int* x;
    int* y;
    {
        int a;
        x = &a;
    }
    {
        int b;
        y = &b;
    }
    printf("%p\n", (void*) x);
    printf("%p\n", (void*) y);
}

Використовуючи gcc, ми отримуємо тут дві різні адреси: Coliro

Отже, ви не можете бути впевнені в тому, що відбувається.

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