Почему книги говорят: "компилятор выделяет пространство для переменных в памяти"?

Почему книги говорят: "компилятор выделяет пространство для переменных в памяти". Разве это не исполняемый файл? Я имею в виду, например, если я пишу следующую программу,

#include <iostream>
using namespace std;

int main()
{
   int foo = 0;
   cout<<foo;
   return 0;
}

и скомпилировать его и получить исполняемый файл (пусть это будет program.exe), теперь, если я запустил program.exe, этот исполняемый файл сам будет командовать для выделения некоторого пространства для переменной foo. Не так ли? Пожалуйста, объясните, почему книги продолжают говорить: "компилятор сделает это... сделайте это", тогда как на самом деле это скомпилированный исполняемый файл.

Добавляя к этому вопросу другой связанный вопрос, почему sizeof называется оператором времени компиляции? Разве это не оператор времени выполнения?

Ответ 1

Когда мы нанимаем архитектора для проектирования дома, он определяет размер комнат и т.д. и информирует рабочих (рабочих) об этом. Рабочие выполняют работу соответственно. Но все-таки мы бы сказали: "Архитектор устроил дом таким образом" и не "Работник сделал дом таким образом".

Работник просто выполняет шаги, определенные архитектором. Компилятор на самом деле выполняет всю работу по проверке и определению того, сколько памяти будет выделено и т.д. Во время выполнения, а затем последуют эти инструкции.

Ответ 2

Технически акт создания самого пространства выполняется во время выполнения, однако компилятор должен выяснить, сколько места зарезервировано в стеке в вашем случае для вашей переменной foo.

Компилятор знает размер типа int и поэтому может генерировать правильную инструкцию ассемблера, которая зарезервирует достаточно места в стеке, чтобы позволить foo жить там.

Если вы посмотрите на созданный ниже ассемблер (используя MSVC2012) для программы, которую вы показали, я прокомментировал некоторые из них, чтобы показать вам, что происходит:

#include "stdafx.h"
#include <iostream>
using namespace std;

int main()
{
//Setup stack frame for main by storing the stack pointer from the calling function and 
//reserving space for local variables and storing commonly used registers on the stack
002E4390  push        ebp  
002E4391  mov         ebp,esp  
// reserve space for local variables, which is 204 bytes here, no idea why so much.
// this is where the compiler calculated the size of your foo and added that to whatever else needs to be stored on the stack. Subtract from stack pointer (esp) because stack grows downward.  
002E4393  sub         esp,0CCh  
002E4399  push        ebx  
002E439A  push        esi  
002E439B  push        edi  
002E439C  lea         edi,[ebp-0CCh]  // load effective address of [ebp-0CCh], which I suspect would be your foo variable into edi register
002E43A2  mov         ecx,33h  
002E43A7  mov         eax,0CCCCCCCCh  
002E43AC  rep stos    dword ptr es:[edi]  //fill block of memory at es:[edi] with stuff  
   int foo;
   return 0;
002E43AE  xor         eax,eax  //set eax to zero for return value
}
// restore everything back to how it was before main was called
    002E43B0  pop         edi  
    002E43B1  pop         esi  
    002E43B2  pop         ebx  
    002E43B3  mov         esp,ebp  
    002E43B5  pop         ebp  
    002E43B6  ret  

Ответ 3

Это просто свободное использование терминологии. Конечно, компилятор не выделяет память для программы. Более точное описание заключается в том, что он сообщает во время выполнения, сколько памяти выделяется при запуске программы.

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

О том, что эти книги говорят о распределении переменных, размер которых известен во время компиляции, в отличие от динамического распределения cin >> x; int * y = new[x];, где размер неизвестен.

Ответ 4

В нем говорится, что компилятор выделяет пространство для переменных в памяти, потому что в противном случае вам нужно выделять (и бесплатную!) память с помощью new/malloc и т.д.

Ответ 5

Конечно, компилятор не "выделяет пространство для переменных". Компилятор генерирует код, который выделяет пространство для переменных в памяти.

т.е. если у вас есть

int foo;
foo = 1;

в исходном коде, компилятор может генерировать код типа

int* fooPtr = allocate sizeof(int)
*fooPtr = 1;

В архитектуре x86 обычно, что allocate вещь будет одной командой сборки:

sub esp, 4    ; allocate 4 == sizeof(int) bytes on stack
              ; now the value of "esp" is equal to the address of "foo",
              ; i.e. it "fooPtr"
mov [esp], 1  ; *fooPtr = 1

Если у вас несколько локальных переменных, компилятор упакует их в структуру и распределяет их вместе:

int foo;
int bar;
bar = 1;

будет скомпилирован как

struct Variables { int foo; int bar; };
Variables* v = allocate sizeof(Variables);
v->bar = 1;

или

sub esp, 4+4       ; allocate sizeof(Variables) on stack
mov [esp + 4], 1   ; where 4 is offsetof(Variables, bar)

Ответ 6

Компилятор генерирует машинные инструкции и определяет, какие локальные переменные адреса памяти будут занимать. Каждой локальной переменной присваивается адрес относительно вершины стека, например, foo будет считаться адресом памяти stack_pointer. Если у вас есть переменная foo2, она будет размещена по адресу stack_pointer + 4, где 4 - размер int.

При доступе к локальной переменной foo компилятор заменит адрес, хранящийся в stack_pointer. Аппаратное обеспечение имеет специальный регистр stack_pointer, который всегда указывает на верхнюю часть текущего стека.

Компилятор знает, какой размер имеет каждая переменная, потому что он отвечает за поиск объявлений struct или class и выясняет, как он выложен в памяти. Таким образом, sizeof известно во время компиляции и рассматривается как постоянное выражение. Известно, что такие примитивные типы, как int, имеют определенный размер, например sizeof(int) составляет 4.

Ответ 7

Предложите вам прочитать компиляцию. Сосредоточьтесь на фазе хранения, ваш запрос будет разрешен.

После того, как программа скомпилирует свой преобразованный код объекта, который является кодом языка ассемблера. каждая строка языковой программы высокого уровня переводится на многие шаги ассемблерного языка. Переведенная программа помещается в ассемблер, который выполняется.  Фаза назначения STORAGE в конструкции компилятора преобразуется в таблицу операций машины (инструкции MOT на уровне сборки). Это место, где выполняется распределение пространства для переменных/регистров. В С++ есть ключевое слово модификатора регистра.