2010-03-01 15 views
9

Supongamos que tenemos los siguientes:¿En qué momento exacto se almacena una variable local asignada?

void print() 
{ 
    int a; // declaration 
    a = 9; 
    cout << a << endl; 
} 

int main() 
{ 
    print(); 
} 

es el almacenamiento asignado para la variable a la función de impresión momento se llama en el principal o se trata de la ejecución, cuando llega la declaración dentro de la función?

+0

Supongo que está hablando de almacenamiento en la pila, no en un registro, ¿verdad? – Ponkadoodle

+0

Sí en la pila. – Brandon

+5

riiiiight ....... ahora – Russell

Respuesta

9

Depende mucho del compilador, pero lógicamente el almacenamiento se asigna tan pronto como se declara la variable.

Considere esto simplista C++ ejemplo:

// junk.c++ 
int addtwo(int a) 
{ 
    int x = 2; 

    return a + x; 
} 

Cuando GCC compila esto, se genera el siguiente código (; comentarios mina):

.file "junk.c++" 
    .text 
.globl _Z6addtwoi 
    .type _Z6addtwoi, @function 
_Z6addtwoi: 
.LFB2: 
    pushl %ebp   ;store the old stack frame (caller's parameters and locals) 
.LCFI0: 
    movl %esp, %ebp  ;set up the base pointer for our parameters and locals 
.LCFI1: 
    subl $16, %esp  ;leave room for local variables on the stack 
.LCFI2: 
    movl $2, -4(%ebp) ;store the 2 in "x" (-4 offset from the base pointer) 
    movl -4(%ebp), %edx ;put "x" into the DX register 
    movl 8(%ebp), %eax ;put "a" (+8 offset from base pointer) into AX register 
    addl %edx, %eax  ;add the two together, storing the results in AX 
    leave     ;tear down the stack frame, no more locals or parameters 
    ret     ;exit the function, result is returned in AX by convention 
.LFE2: 
    .size _Z6addtwoi, .-_Z6addtwoi 
    .ident "GCC: (Ubuntu 4.3.3-5ubuntu4) 4.3.3" 
    .section .note.GNU-stack,"",@progbits 

Todo entre _Z6addtwoi y .LCFI2 es repetitivo código utilizado para configurar el marco de la pila (almacenar las variables de la función anterior, etc. de forma segura fuera del camino). El último "subl $ 16,% esp" es la asignación de la variable local x.

.LCFI2 es la primera parte del código de ejecución real que ha escrito. "movl $ 2, -4 (% ebp)" está poniendo el valor 2 en la variable. (Inicialización, en otras palabras.) Ahora su espacio se asigna Y se inicializa. Después de eso, carga el valor en el registro de EDX y lo sigue moviendo su parámetro, encontrado en "8 (% ebp)", en otro registro EAX. Luego agrega los dos juntos, dejando el resultado en EAX.Este es ahora el final de cualquier código que hayas escrito realmente. El resto nuevamente es solo repetitivo. Dado que GCC exige que los enteros se devuelvan en EAX, no es necesario realizar ningún trabajo para el valor de retorno. La instrucción "dejar" deshace el marco de la pila y la instrucción "ret" devuelve el control a la persona que llama.

TL; Resumen de DR: puede pensar que su espacio ha sido asignado con la primera línea de código ejecutable en su bloque (emparejado {}).


Pensé que limpiaría esto un poco con comentarios explicativos ya que esta es la respuesta seleccionada.

+0

En realidad, eso debería ser "tan pronto como la variable esté _definida_". Las declaraciones no reservan ningún almacenamiento: http://stackoverflow.com/questions/1410563/ – sbi

+0

¿Puede incluso declarar una variable local sin definirla? Y para los globales, el almacenamiento se asigna al inicio del programa. – MSalters

+0

Los valores globales inicializados se asignan * antes del inicio del programa con GCC. Acabo de haber probado esto recientemente mediante la compilación de un archivo con los siguientes definidos en el alcance global: static char c [100000000] = ""; El archivo de salida de GCC se vuelve muy grande cuando haces cosas como esa. Los compiladores más inteligentes como CL harán la asignación y la inicialización al inicio. –

3

En cuanto a la construcción de objetos:

construcción suceda en el punto de la declaración, y se llama al destructor cuando el objeto sale del ámbito.

Pero no es necesario que la construcción de los objetos y la asignación de memoria coincidan.

Al igual que la destrucción de objetos y cuando la memoria se desasigna, no es necesario que coincida.

En cuanto a cuándo es en realidad la memoria asignada a la pila:

no sé, pero se puede comprobar a través del siguiente código:

void f() 
{ 
    int y; 
    y = 0;//breakpoint here 

    int x[1000000]; 
} 

Mediante la ejecución de este código se puede ver dónde ocurre la excepción, para mí en Visual Studio 2008 ocurre al ingresar la función. Nunca llega al punto de ruptura.

+0

Sin embargo, su código no demuestra cuándo realmente asigna el espacio de pila, solo cuando ejecuta el constructor. No hay razón para que esos tengan que ser iguales. Además, no hay ninguna razón por la cual deba desasignar el espacio para c al final de ese alcance. Solo necesita ejecutar el destructor. –

+0

@Brooks Moses: Creo que hiciste este comentario al igual que lo edité para distinguir esa cosa exacta. –

+0

Sí, parece, así que te he votado. :) Escribí una respuesta complementaria para mostrar una forma diferente de demostrar esto en el código. (Por supuesto, la mejor manera es simplemente mirar el ensamblado compilado, y ver dónde incrementa el puntero de la pila, que ahora veo que alguien más ha hecho justo cuando estaba haciendo * este * comentario; ¡ja!) –

2

Esto dependerá del compilador, pero normalmente la variable int a se asignará a la pila en el momento de llamar a la función.

+0

-1 , , todos los compiladores tienen la misma semántica de asignación de memoria cuando se trata de variables locales. se asignarán en la declaración y se desasignarán cuando salga del alcance como lo dijo Brian. en el caso de una variable local múltiple, se desasignan utilizando el paradigma LIFO, donde la última memoria asignada se libera primero. – YeenFei

+3

Guau, esa es una declaración bastante loca para hacer YeenFei."todos los compiladores tienen la misma semántica de asignación de memoria cuando se trata de variables locales". Lo más probable es que sea cierto para * algunos * compiladores de bajo nivel, pero no todos. Muchos compiladores asignan almacenamiento para una variable la primera vez que se lee o se escribe. Considera 'int a; int b = 2; a = 3; ', el compilador podría asignar memoria para' a' después de 'b'. – Ponkadoodle

+2

Además, tenga en cuenta que esta es una forma muy simple de asignación; en general, todas las variables locales dentro de un alcance tienen desfases de tiempo de compilación desde un puntero guardado a la pila, y cuando se ingresa el alcance, el programa "asigna" esas variables guardando el puntero de la pila actual en algún lugar y luego volteando hacia arriba tamaño total de las variables locales. Es mucho más eficiente hacerlo todo de una vez que hacerlo incrementalmente, y significa que puede encontrar todas las variables locales con el mismo puntero variable y algunas constantes, en lugar de tener que almacenar diferentes punteros para cada una. –

2

Al menos, como las cosas se implementan normalmente, está entre los dos. Cuando llama a una función, el compilador generará un código para la llamada de función que evalúa los parámetros (si los hay) y los coloca en registros o en la pila. Luego, cuando la ejecución llegue a la entrada de la función, se asignará espacio para las variables locales en la pila y se inicializarán las locales que necesitan inicialización. En ese momento, puede haber algún código para guardar los registros que utiliza la función, mezclar los valores para colocarlos en los registros deseados y demás. El código para el cuerpo de la función comienza a ejecutarse después de eso.

3

Como complemento a la respuesta de Brian R. Bondy: es bastante fácil realizar algunos experimentos para mostrar cómo funciona esto, con un poco más de detalle que lanzar errores fuera de la pila de espacio. Considere este código:

#include<iostream> 

void foo() 
{ 
    int e; std::cout << "foo:e " << &e << std::endl; 
} 

int main() 
{ 
    int a; std::cout << "a: " << &a << std::endl; 
    foo(); 
    int b; std::cout << "b: " << &b << std::endl; 
    { 
    int c; std::cout << "c: " << &c << std::endl; 
    foo(); 
    } 
    int d; std::cout << "d: " << &d << std::endl; 
} 

Esto produce esta salida en mi máquina:

$ ./stack.exe 
a: 0x28cd30 
foo:e 0x28cd04 
b: 0x28cd2c 
c: 0x28cd24 
foo:e 0x28cd04 
d: 0x28cd28 

Dado que la pila crece hacia abajo, podemos ver el orden en el que se ponen en la pila: a, b, d, y c en ese orden, y luego las dos llamadas a foo() ponen su e en el mismo lugar las dos veces. Esto significa que se ha asignado la misma cantidad de memoria en la pila las dos veces que se llama a foo(), aunque intervienen varias declaraciones de variables (incluida una dentro de un ámbito interno). Por lo tanto, en este caso podemos concluir que toda la memoria de la pila para las variables locales en main() se asignó al principio de main() en lugar de incrementarse incrementalmente.

También puede ver que el compilador arregla las cosas para que los constructores son llamados en orden descendente pila, y los destructores son llamados en orden ascendente - todo lo que es la parte inferior construida cosa en la pila cuando se construye y cuando es destruido , pero esto no significa significa que es lo último para lo que se ha asignado espacio, o que no hay espacio actualmente no utilizado encima de la pila para las cosas que no se han construido aún (como el espacio para d cuando c o las dos encarnaciones de foo: e están construidas).

+0

@Brooks Moses: Me gusta tu configuración experimental. La salida en una de mis configuraciones (Cygwin en i386, usando g ++ 3.4.4). es ligeramente diferente. Obtuve la misma dirección para 'c' y 'd'. – Arun

+0

Gracias. Ese es un resultado interesante: supongo que es una diferencia de GCC 3.4 a 4.x; También estaba usando Cygwin en i386, pero estaba usando el compilador g ++ 4.3.2. (Una actualización muy recomendada, por cierto, pero tiene que seleccionar explícitamente gcc4 en el programa de instalación de Cygwin para obtenerla). –

Cuestiones relacionadas