В Си скобки действуют как кадр стека?

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: деструкторы будут называться. Это ничего не говорит о памяти, которую занимали эти объекты.
Donal Fellows
9
Стандарт C указывает, что время жизни автоматических переменных, объявленных в блоке, продлевается только до конца выполнения блока. Таким образом , по существу эти автоматические переменные действительно получить «уничтожены» в конце блока.
Кафе
3
@KristopherJohnson: Если у метода есть два отдельных блока, каждый из которых объявляет массив размером 1 Кбайт, и третий блок, который вызывает вложенный метод, компилятор может свободно использовать одну и ту же память для обоих массивов и / или разместить массив в самой мелкой части стека и переместите указатель стека выше этого, вызывая вложенный метод. Такое поведение может уменьшить глубину стека, требуемую для вызова функции, на 2K.
суперкат
39

Время, в течение которого переменная фактически занимает память, очевидно зависит от компилятора (и многие компиляторы не корректируют указатель стека, когда внутренние блоки вводятся и выходятся внутри функций).

Однако тесно связанный, но, возможно, более интересный вопрос заключается в том, разрешено ли программе получать доступ к этому внутреннему объекту вне внутренней области (но внутри содержащей функции), то есть:

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

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

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

   return;
}

(Другими словами: разрешено ли компилятору освобождать d, даже если на практике большинство этого не делает?).

Ответ в том , что компилятор будет разрешено освобождать d, и доступ к p[0]которой комментарий указывает на это неопределенное поведение (программа не имеет права доступа на внутренний объект за пределами внутреннего объема). Соответствующая часть стандарта C - 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 [] не будет в стеке до конца процедуры. Но alloca () отличается.

Изменить: Кристофер Джонсон (и Саймон и Дэниел) правы , и мой первоначальный ответ был неправильным . С 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

Они могут. Они не могут. Ответ, который, я думаю, вам действительно нужен: никогда не предполагайте ничего. Современные компиляторы делают все виды архитектуры и волшебства, связанного с реализацией. Напишите свой код просто и разборчиво для людей, и пусть компилятор сделает хорошие вещи. Если вы пытаетесь создать код вокруг компилятора, то вам нужны проблемы, и проблема, с которой вы обычно сталкиваетесь в таких ситуациях, обычно ужасно тонкая и ее трудно диагностировать.

user19666
источник
1

Ваша переменная dобычно не выталкивается из стека. Фигурные скобки не обозначают кадр стека. В противном случае вы не сможете сделать что-то вроде этого:

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

Если фигурные скобки вызвали истинный push / pop стека (как вызов функции), то приведенный выше код не будет компилироваться, потому что код внутри фигурных скобок не сможет получить доступ к переменной, varкоторая находится за пределами фигурных скобок (точно так же, как вложенные элементы). функция не может напрямую обращаться к переменным в вызывающей функции). Мы знаем, что это не так.

Фигурные скобки просто используются для определения объема. Компилятор будет обрабатывать любой доступ к «внутренней» переменной снаружи вложенных фигурных скобок как недопустимый, и он может повторно использовать эту память для чего-то другого (это зависит от реализации). Тем не менее, он не может быть извлечен из стека, пока не завершится функция включения.

Обновление: вот что говорит C-спецификация . Относительно объектов с автоматическим сроком хранения (раздел 6.4.2):

Для объекта, у которого нет типа массива переменной длины, его время жизни продолжается от входа в блок, с которым он связан, до тех пор, пока выполнение этого блока не закончится.

В этом же разделе термин «время жизни» определяется как (выделено мной):

Время жизни объекта - это часть выполнения программы, в течение которой для него гарантированно хранится память . Объект существует, имеет постоянный адрес и сохраняет свое последнее сохраненное значение в течение всего срока его службы. Если на объект ссылаются вне его времени жизни, поведение не определено.

Ключевое слово здесь, конечно, «гарантировано». Как только вы покидаете область действия внутреннего набора фигурных скобок, время жизни массива заканчивается. Хранилище может выделяться или не выделяться для него (ваш компилятор может повторно использовать пространство для чего-то другого), но любые попытки доступа к массиву вызывают неопределенное поведение и приводят к непредсказуемым результатам.

В спецификации C нет понятия стековых фреймов. Он говорит только о том, как будет вести себя результирующая программа, и оставляет детали реализации компилятору (в конце концов, реализация будет выглядеть совершенно иначе на CPU без стека, чем на CPU с аппаратным стеком). В спецификации C нет ничего, что указывало бы, где кадр стека закончится или не закончится. Единственный реальный способ узнать это - скомпилировать код на вашем конкретном компиляторе / платформе и изучить получившуюся сборку. Текущий набор параметров оптимизации вашего компилятора, вероятно, также сыграет в этом свою роль.

Если вы хотите убедиться, что массив dбольше не потребляет память во время работы вашего кода, вы можете либо преобразовать код в фигурных скобках в отдельную функцию, либо явно mallocи freeв память вместо использования автоматического хранения.

ВТА
источник
1
«Если фигурные скобки вызвали push / pop в стеке, то приведенный выше код не будет компилироваться, потому что код внутри фигурных скобок не сможет получить доступ к переменной var, которая находится за пределами фигурных скобок» - это просто неверно. Компилятор всегда может запомнить расстояние от указателя стека / фрейма и использовать его для ссылки на внешние переменные. Кроме того , см ответ Иосифа на примере фигурные скобки , которые делают причиной в стек PUSH / поп - музыки.
Георгий
@ george- Поведение, которое вы описываете, а также пример Джозефа, зависит от компилятора и платформы, которые вы используете. Например, компиляция одного и того же кода для цели MIPS дает совершенно разные результаты. Я говорил чисто с точки зрения спецификации C (поскольку в OP не указан компилятор или цель). Я отредактирую ответ и добавлю больше подробностей.
ВТА
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

Таким образом, вы не можете быть уверены, что происходит.

Ми-он
источник