Как на самом деле работает автоматическое распределение памяти в C ++? - PullRequest
10 голосов
/ 23 октября 2009

В C ++, при условии отсутствия оптимизации, у следующих двух программ будет одинаковый машинный код выделения памяти?

int main()
{     
    int i;
    int *p;
}

int main()
{
    int *p = new int;
    delete p;
}

Ответы [ 5 ]

23 голосов
/ 23 октября 2009

Чтобы лучше понять, что происходит, давайте представим, что у нас есть только очень примитивная операционная система, работающая на 16-битном процессоре, которая может запускать только один процесс за раз. Это означает, что одновременно может работать только одна программа. Кроме того, давайте представим, что все прерывания отключены.

В нашем процессоре есть конструкция, называемая стеком. Стек является логической конструкцией, навязываемой физической памяти. Допустим, наша оперативная память существует в адресах E000 - FFFF. Это означает, что наша работающая программа может использовать эту память любым удобным для нас способом. Давайте представим, что наша операционная система говорит, что E000 - EFFF - это стек, а F000 - FFFF - это куча.

Стек поддерживается аппаратными средствами и машинными инструкциями. Там действительно не так много, что нам нужно сделать, чтобы поддерживать его. Все, что нам (или нашей ОС) нужно сделать, это убедиться, что мы установили правильный адрес для начала стека. Указатель стека - это физический объект, находящийся в аппаратном обеспечении (процессоре) и управляемый инструкциями процессора. В этом случае наш указатель стека будет установлен в EFFF (при условии, что стек увеличивается BACKWARDS, что довольно распространено, -). В скомпилированном языке, таком как C, когда вы вызываете функцию, она помещает любые аргументы, которые вы передали функции в стек. Каждый аргумент имеет определенный размер. int обычно 16 или 32 бита, char обычно 8 бит и т. д. Давайте представим, что в нашей системе int и int * имеют 16 бит. Для каждого аргумента указатель стека DECREMENTED (-) на sizeof (аргумент), и аргумент копируется в стек. Затем любые переменные, которые вы объявили в области видимости, помещаются в стек таким же образом, но их значения не инициализируются.

Давайте еще раз рассмотрим два примера, аналогичных вашим двум.

int hello(int eeep)
{
    int i;
    int *p;
}

В нашей 16-битной системе происходит следующее: 1) толкнуть ушко на стек. Это означает, что мы уменьшаем указатель стека до EFFD (потому что sizeof (int) равно 2), а затем фактически копируем eeep в адрес EFFE (текущее значение нашего указателя стека, минус 1, потому что указатель нашего стека указывает на первое доступное место после выделения). Иногда существуют инструкции, которые могут выполнить оба действия одним махом (при условии, что вы копируете данные, которые помещаются в регистр. В противном случае вам придется вручную копировать каждый элемент типа данных в его надлежащее место в стеке - иначе это важно! ).

2) создать пространство для i. Это, вероятно, означает просто уменьшение указателя стека на EFFB.

3) создать пространство для р. Это, вероятно, означает просто уменьшение указателя стека до EFF9.

Затем наша программа запускается, помня, где живут наши переменные (eeep начинается в EFFE, i в EFFC и p в EFFA). Важно помнить, что, хотя стек и имеет значение BACKWARDS, переменные по-прежнему работают с FORWARDS (на самом деле это зависит от порядка байтов, но дело в том, что & eeep == EFFE, а не EFFF).

Когда функция закрывается, мы просто увеличиваем (++) указатель стека на 6 (потому что в стек помещены 3 «объекта», а не вида c ++, размера 2.

Теперь, ваш второй сценарий гораздо сложнее объяснить, потому что есть так много способов его реализовать, что практически невозможно объяснить в Интернете.

int hello(int eeep)
{
    int *p = malloc(sizeof(int));//C's pseudo-equivalent of new
    free(p);//C's pseudo-equivalent of delete
}

eeep и p по-прежнему помещаются и помещаются в стек, как в предыдущем примере. В этом случае, однако, мы инициализируем p результатом вызова функции. Что malloc (или new, но new делает больше в c ++. Он вызывает конструкторы, когда это уместно, и все остальное.) Делает это, он идет в этот черный ящик, называемый HEAP, и получает адрес свободной памяти. Наша операционная система будет управлять кучей для нас, но мы должны дать ей знать, когда мы хотим память и когда мы закончим с ней.

В примере, когда мы вызываем malloc (), ОС вернет блок из 2 байтов (sizeof (int) в нашей системе равно 2), предоставив нам начальный адрес этих байтов. Допустим, первый звонок дал нам адрес F000. Затем ОС отслеживает, какие адреса F000 и F001 используются в настоящее время. Когда мы вызываем free (p), ОС находит блок памяти, на который указывает p, и помечает 2 байта как неиспользуемый (потому что sizeof (звездочка p) равен 2). Если вместо этого мы выделим больше памяти, адрес F002, скорее всего, будет возвращен как начальный блок новой памяти. Обратите внимание, что сама функция malloc () является функцией. Когда p помещается в стек для вызова malloc (), p снова копируется в стек по первому открытому адресу, который имеет достаточно места в стеке, чтобы соответствовать размеру p (возможно, EFFB, потому что мы выдвинули только 2 в стеке это время размера 2, а sizeof (p) равно 2), и указатель стека снова уменьшается до EFF9, а malloc () помещает свои локальные переменные в стек, начиная с этого места. Когда malloc завершает работу, он выталкивает все свои элементы из стека и устанавливает указатель стека на то, что было до вызова. Возвращаемое значение malloc (), пустой звезды, скорее всего, будет помещено в некоторый регистр (обычно аккумулятор во многих системах) для нашего использования.

В реализации оба примера ДЕЙСТВИТЕЛЬНО не так просты. Когда вы выделяете стековую память для нового вызова функции, вы должны убедиться, что вы сохранили свое состояние (сохраните все регистры), чтобы новая функция не стирала значения постоянно. Как правило, это также включает размещение их в стеке. Таким же образом вы обычно сохраняете регистр счетчика программы, чтобы вы могли вернуться в правильное место после возврата подпрограммы. Менеджеры памяти используют свою собственную память, чтобы «запомнить», какая память выдана, а какая нет. Виртуальная память и сегментация памяти усложняют этот процесс, и алгоритмы управления памятью должны постоянно перемещать блоки (и защищать их тоже), чтобы предотвратить фрагментацию памяти (отдельная тема), и это связано с виртуальной памятью также. Второй пример - большая банка червей по сравнению с первым примером. Кроме того, выполнение нескольких процессов делает все это намного более сложным, поскольку каждый процесс имеет свой собственный стек, и доступ к куче может иметь более одного процесса (что означает, что он должен защищать себя). Кроме того, каждая архитектура процессора отличается. Некоторые архитектуры ожидают, что вы установите указатель стека на первый свободный адрес в стеке, другие ожидают, что вы укажете его на первое несвободное место.

Надеюсь, это помогло. пожалуйста, дайте мне знать.

обратите внимание, что все приведенные выше примеры предназначены для вымышленной машины, которая чрезмерно упрощена. На реальном оборудовании это становится немного более волосатым.

edit: звездочки не отображаются. я заменил их словом «звезда»


Для чего стоит, если мы используем (в основном) один и тот же код в примерах, заменяя «hello» на «example1» и «example2» соответственно, мы получим следующий вывод сборки для intel для wndows.

    .file   "test1.c"
    .text
.globl _example1
    .def    _example1;  .scl    2;  .type   32; .endef
_example1:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $8, %esp
    leave
    ret
.globl _example2
    .def    _example2;  .scl    2;  .type   32; .endef
_example2:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $8, %esp
    movl    $4, (%esp)
    call    _malloc
    movl    %eax, -4(%ebp)
    movl    -4(%ebp), %eax
    movl    %eax, (%esp)
    call    _free
    leave
    ret
    .def    _free;  .scl    3;  .type   32; .endef
    .def    _malloc;    .scl    3;  .type   32; .endef
18 голосов
/ 23 октября 2009

Нет, без оптимизации ...

int main() 
{      
    int i; 
    int *p; 
}

почти ничего не делает - просто пара инструкций для настройки указателя стека, но

int main() 
{ 
    int *p = new int; 
    delete p; 
}

выделяет блок памяти в куче, а затем освобождает его, это большая работа (я серьезно здесь - выделение кучи не тривиальная операция)

6 голосов
/ 23 октября 2009
    int i;
    int *p;

^ Распределение одного целого и одного целочисленного указателя в стеке

int *p = new int;
delete p;

^ Выделение одного целочисленного указателя на стек и блока размера целого на куче

EDIT:

Разница между сегментом стека и сегментом кучи

alt text
(источник: maxi-pedia.com )

void another_function(){
   int var1_in_other_function;   /* Stack- main-y-sr-another_function-var1_in_other_function */
   int var2_in_other_function;/* Stack- main-y-sr-another_function-var1_in_other_function-var2_in_other_function */
}
int main() {                     /* Stack- main */
   int y;                        /* Stack- main-y */
   char str;                     /* Stack- main-y-sr */
   another_function();           /*Stack- main-y-sr-another_function*/
   return 1 ;                    /* Stack- main-y-sr */ //stack will be empty after this statement                        
}

Всякий раз, когда какая-либо программа начинает выполнение, она сохраняет все свои переменные в специальной ячейке памяти, называемой Сегмент стека . Например, в случае C / C ++ первая вызываемая функция является главной. поэтому он будет помещен в стек первым. Любые переменные внутри main будут помещены в стек при выполнении программы. Теперь, когда main является первой вызванной функцией, она будет последней функцией, которая вернет любое значение (или будет извлечена из стека).

Теперь, когда вы динамически распределяете память, используя new, используется другая специальная область памяти, называемая сегментом кучи. Даже если фактические данные присутствуют в куче, указатель лежит на стеке.

2 голосов
/ 23 октября 2009

В первой программе все ваши переменные находятся в стеке. Вы не выделяете динамическую память. 'p' просто сидит в стеке, и если вы разыграете его, вы получите мусор. Во второй программе вы фактически создаете целочисленное значение в куче. 'p' фактически указывает на некоторую действительную память в этом случае. На самом деле вы можете разыменовать p и установить его в безопасное значение:

*p = 5;

Это действительно во второй программе (до удаления), а не в первой. Надеюсь, это поможет.

2 голосов
/ 23 октября 2009

Похоже, вы не знаете о стеке и куче. Ваш первый пример - просто выделение некоторой памяти в стеке, которая будет удалена, как только она выйдет из области видимости. Память в куче, полученная с помощью malloc / new, будет оставаться до тех пор, пока вы не удалите ее с помощью free / delete.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...