2009-10-23 19 views
9

En C++, suponiendo que no hay optimización, ¿los dos programas siguientes terminan con el mismo código de máquina de asignación de memoria?¿Cómo funciona realmente la asignación automática de memoria en C++?

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

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

¿Cómo pueden tener la misma asignación de memoria? El primero asigna un tamaño para un int (en la pila) y un puntero (en la pila). El segundo asigna un puntero en la pila y espacio para un int en el montón. – bobbyalex

+1

Básicamente mi pregunta es "En tiempo de ejecución, ¿cuál es la diferencia entre la pila y el montón?" – Tarquila

+2

Pruebe aquí: http://stackoverflow.com/questions/79923/what-and-where-are-the-stack-and-heap – Alex

Respuesta

17

No, sin optimización ...

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

no hace casi nada - sólo un par de instrucciones para ajustar el puntero de pila, pero

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

asigna un bloque de memoria en montón y luego lo libera, eso es un montón de trabajo (lo digo en serio aquí - la asignación de heap no es una operación trivial).

+4

+1, la asignación de montón invoca mucha información de almacenamiento, solo para hablar sobre la sobrecarga de memoria. –

2

Parece que no sabes sobre la pila y el montón. Su primer ejemplo es simplemente asignar algo de memoria en la pila que se eliminará tan pronto como salga del alcance. La memoria en el montón que se obtiene con malloc/new se mantendrá hasta que la elimines mediante free/delete.

+1

Cuando se "elimina tan pronto como sale del alcance" - ¿el compilador ingresa el código para hacer eso? – Tarquila

+0

El compilador maneja la pila y los marcos de pila requeridos para las reglas de alcance, así que de alguna manera ... pero 'eliminar' algo en la pila es trivial ya que solo implica decrementar un puntero de pila, y no es como la asignación de montón y desasignación. – workmad3

+0

Sí, mueve el puntero de la pila hacia atrás. La pila tiene un tamaño fijo (o al menos puedes pretender que es). – gnud

2

En el primer programa, todas sus variables residen en la pila. No está asignando ninguna memoria dinámica. 'p' solo está en la pila y si la desreferencia obtendrá basura. En el segundo programa, en realidad está creando un valor entero en el montón. 'p' en realidad está apuntando a una memoria válida en este caso. En realidad podría eliminar la referencia P y configurarlo para que algo significativo con seguridad:

*p = 5; 

Eso es válido en el segundo programa (antes de la eliminación), no el primero. Espero que ayude.

6
int i; 
    int *p; 

^asignación de un número entero y un puntero número entero en la pila

int *p = new int; 
delete p; 

^asignación de un puntero de número entero en la pila y de bloques del tamaño de número entero en el montón

EDIT:

Diferencia entre segmento de pila y segmento de montón

alt text http://www.maxi-pedia.com/web_files/images/HeapAndStack.png

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       
} 

Cada vez que un programa se inicia la ejecución que almacena todas sus variables, en especial ubicación de memoria memoy llamada segmento de pila. Por ejemplo, en el caso de C/C++, la primera función llamada es main. entonces se pondrá en la pila primero. Cualquier variable dentro de main se pondrá en la pila a medida que el programa se ejecute. Ahora, como main es la primera función llamada, será la última función para devolver cualquier valor (o saldrá de la pila).

Ahora cuando dinámicamente asigna memoria usando new se usa otra ubicación de memoria especial llamada segmento Heap. Incluso si los datos reales están presentes en el puntero del montón se encuentra en la pila.

+0

Eso es útil. En su diagrama, dibujó el montón con flechas: supongo que quiere decir que puede haber cualquier cantidad de distancia (memoria que no pertenece al programa) entre la pila y el montón. – bobobobo

+0

A cada proceso se le asignan 4GB de memoria virtual, por lo que la memoria se distribuirá entre los segmentos – Xinus

23

Para comprender mejor lo que está sucediendo, imaginemos que solo tenemos un sistema operativo muy primitivo que se ejecuta en un procesador de 16 bits que puede ejecutar solo un proceso a la vez. Es decir: solo se puede ejecutar un programa a la vez. Además, imaginemos que todas las interrupciones están deshabilitadas.

Hay un constructo en nuestro procesador llamado pila. La pila es una construcción lógica impuesta en la memoria física. Digamos que nuestra RAM existe en las direcciones E000 a FFFF. Esto significa que nuestro programa en ejecución puede usar esta memoria como queramos. Imaginemos que nuestro sistema operativo dice que E000 a EFFF es la pila, y F000 a FFFF es el montón.

La pila se mantiene mediante el hardware y las instrucciones de la máquina. Realmente no hay mucho que hacer para mantenerlo. Todo lo que (o nuestro sistema operativo) debemos hacer es asegurarnos de establecer una dirección adecuada para el inicio de la pila. El puntero de la pila es una entidad física que reside en el hardware (procesador) y se gestiona mediante instrucciones del procesador. En este caso, nuestro puntero de pila se establecerá en EFFF (suponiendo que la pila crezca HACIA ATRÁS, que es bastante común, -). Con un lenguaje compilado como C, cuando llamas a una función, empuja cualquier argumento que hayas pasado a la función en la pila. Cada argumento tiene un cierto tamaño. int es usualmente 16 o 32 bits, char es usualmente 8 bits, etc. Imaginemos que en nuestro sistema, int e int * son 16 bits. Para cada argumento, el puntero de la pila es DISMINUIDO (-) por sizeof (argumento), y el argumento se copia en la pila. Entonces, cualquier variable que haya declarado en el alcance se inserta en la pila de la misma manera, pero sus valores no se inicializan.

Consideremos dos ejemplos similares a los dos ejemplos.

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

¿Qué sucede aquí en nuestro sistema de 16 bits es la siguiente: 1) empuje eeep en la pila. Esto significa que disminuimos el puntero de pila a EFFD (porque sizeof (int) es 2) y luego copiamos eeep para direccionar EFFE (el valor actual de nuestro puntero de pila, menos 1 porque nuestro puntero de pila apunta al primer punto que está disponible después de la asignación). A veces hay instrucciones que pueden hacer ambas cosas de una sola vez (suponiendo que está copiando datos que encajan en un registro. De lo contrario, tendría que copiar manualmente cada elemento de un tipo de datos en su lugar correcto en la pila, ¡el orden es importante!)

2) crear espacio para i. Esto probablemente significa simplemente disminuir el puntero de la pila a EFFB.

3) crear espacio para p. Esto probablemente significa simplemente disminuir el puntero de la pila a EFF9.

Luego se ejecuta nuestro programa, recordando dónde viven nuestras variables (eeep comienza en EFFE, i en EFFC, y p en EFFA). Lo importante que hay que recordar es que, aunque la pila cuenta BACKWARDS, las variables aún funcionan FORWARDS (esto depende de endianness, pero el punto es que & eeep == EFFE, no EFFF).

Cuando la función se cierra, simplemente incremento (++) el puntero de pila en un 6, (porque 3 "objetos", no el C++ tipo, de tamaño 2 han sido empujados a la pila.

Ahora, el segundo escenario es mucho más difícil de explicar porque hay muchos métodos para llevarla a cabo que es casi imposible de explicar en el Internet.

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

eeep yp todavía se empujan y se asignan en la pila como en el anterior ejemplo. En este caso, sin embargo, inicializamos p al resultado de una llamada a función. ¿Qué malloc (o nuevo, pero nuevo hace más en C++) llama a los contras? tructors cuando sea apropiado, y todo lo demás.) lo hace se va a esta caja negra llamada HEAP y obtiene una dirección de memoria libre. Nuestro sistema operativo administrará el montón para nosotros, pero tenemos que dejarlo saber cuando queremos memoria y cuando hayamos terminado con ella.

En el ejemplo, cuando llamamos a malloc(), el sistema operativo devolverá un bloque de 2 bytes (sizeof (int) en nuestro sistema es 2) al proporcionarnos la dirección de inicio de estos bytes. Digamos que la primera llamada nos dio la dirección F000. El sistema operativo entonces realiza un seguimiento de que las direcciones F000 y F001 están actualmente en uso. Cuando llamamos a free (p), el sistema operativo encuentra el bloque de memoria que p apunta y marca 2 bytes como no utilizado (porque sizeof (estrella p) es 2). Si, en cambio, asignamos más memoria, la dirección F002 probablemente se devolverá como el bloque de inicio de la nueva memoria. Tenga en cuenta que malloc() en sí mismo es una función. Cuando p se inserta en la pila para la llamada de malloc(), la p se copia en la pila nuevamente en la primera dirección abierta que tiene suficiente espacio en la pila para ajustarse al tamaño de p (probablemente EFFB, porque solo presionamos 2 cosas en la pila esta vez de tamaño 2, y sizeof (p) es 2), y el puntero de la pila se decrementa nuevamente a EFF9, y malloc() colocará sus variables locales en la pila comenzando en esta ubicación. Cuando Malloc termina, saca todos sus elementos de la pila y establece el puntero de la pila como era antes de que se llamara. El valor de retorno de malloc(), una estrella vacía, probablemente se colocará en algún registro (generalmente el acumulador en muchos sistemas) para nuestro uso.

En la implementación, ambos ejemplos REALMENTE no son así de simples. Cuando asigna memoria de pila, para una nueva llamada de función, debe asegurarse de guardar su estado (guardar todos los registros) para que la nueva función no borre los valores de forma permanente. Esto generalmente implica empujarlos en la pila, también. De la misma manera, generalmente guardará el registro del contador del programa para que pueda regresar al lugar correcto después de que la subrutina regrese. Los administradores de memoria usan memoria propia para "recordar" qué memoria se ha entregado y qué no. La memoria virtual y la segmentación de la memoria complican aún más este proceso, y los algoritmos de administración de memoria deben mover bloques continuamente (y protegerlos también) para evitar la fragmentación de la memoria (un tema completo), y esto se relaciona con la memoria virtual también. El segundo ejemplo realmente es una gran lata de gusanos en comparación con el primer ejemplo. Además, ejecutar múltiples procesos hace que todo esto sea mucho más complicado, ya que cada proceso tiene su propia pila, y se puede acceder al montón mediante más de un proceso (lo que significa que debe protegerse). Además, cada arquitectura de procesador es diferente. Algunas arquitecturas esperarán que configure el puntero de la pila en la primera dirección libre en la pila, mientras que otros esperarán que lo apunte al primer lugar no libre.

Espero que esto haya ayudado. Por favor hagamelo saber.

aviso, todos los ejemplos anteriores son para una máquina ficticia que está demasiado simplificada. En hardware real, esto se pone un poco más peludo.

editar: los asteriscos no aparecen. i los reemplazó con la palabra "estrella"


Por lo que vale la pena, si utilizamos (en su mayoría) el mismo código en los ejemplos, en sustitución de "hola" con "ejemplo1" y "example2", respectivamente, se obtener el siguiente resultado de ensamblaje para intel on 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 
+0

como se puede esperar con algo tan tedioso, sigo encontrando pequeños errores. por favor coméntelos y lo actualizaré si considero que es un buen punto. –

+0

FF002 => F002. Muy buena entrada! Creo que podría dejarlo más claro con algunos diagramas ascii de los diseños de memoria de los que habla, pero eso sería mucho más trabajo. :) – Bill

+0

esto es ahora una wiki comunitaria, por lo que si alguien quisiera agregar los diagramas (¡buena sugerencia!), Siéntase libre de hacerlo. –

Cuestiones relacionadas