Ya no necesitas buscar más en internet porque has llegado al sitio perfecto, contamos con la solución que necesitas hallar sin complicaciones.
Solución:
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, supongamos que todas las interrupciones están desactivadas.
Hay una construcción en nuestro procesador llamada pila. La pila es una construcción lógica impuesta a la memoria física. Digamos que nuestra RAM existe en las direcciones E000 a FFFF. Esto significa que nuestro programa en ejecución puede utilizar esta memoria de la forma que 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 necesitemos hacer para mantenerlo. Todo lo que nosotros (o nuestro sistema operativo) debemos hacer es asegurarnos de establecer una dirección adecuada para el inicio de la pila. El puntero de pila es una entidad física que reside en el hardware (procesador) y es administrado por las instrucciones del procesador. En este caso, nuestro puntero de pila se establecería en EFFF (asumiendo que la pila crece HACIA ATRÁS, lo cual 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 de 16 o 32 bits, char es usualmente de 8 bits, etc. Supongamos que en nuestro sistema, int e int * son de 16 bits. Para cada argumento, el puntero de la pila se DISMINUYE (-) por tamaño de (argumento), y el argumento se copia en la pila. Luego, cualquier variable que haya declarado en el alcance se inserta en la pila de la misma manera, pero sus valores no se inicializan.
Reconsideremos dos ejemplos similares a sus dos ejemplos.
int hello(int eeep)
int i;
int *p;
Lo que sucede aquí en nuestro sistema de 16 bits es lo siguiente: 1) insertar eeep en la pila. Esto significa que disminuimos el puntero de la pila a EFFD (porque sizeof (int) es 2) y luego copiamos eeep a la dirección EFFE (el valor actual de nuestro puntero de la pila, menos 1 porque nuestro puntero de la pila apunta al primer lugar que está disponible después de la asignación). A veces hay instrucciones que pueden hacer ambas cosas de una sola vez (asumiendo que está copiando datos que quepan en un registro. De lo contrario, tendría que copiar manualmente cada elemento de un tipo de datos en su lugar apropiado en la pila – ¡el orden importa! ).
2) crea espacio para i. Esto probablemente significa simplemente disminuir el puntero de la pila a EFFB.
3) crea espacio para p. Esto probablemente significa simplemente disminuir el puntero de la pila a EFF9.
Luego, nuestro programa se ejecuta, recordando dónde viven nuestras variables (eeep comienza en EFFE, i en EFFC yp en EFFA). Lo importante a recordar es que aunque la pila cuenta HACIA ATRÁS, las variables aún operan HACIA ADELANTE (esto en realidad depende de la endianidad, pero el punto es que & eeep == EFFE, no EFFF).
Cuando la función se cierra, simplemente incrementamos (++) el puntero de la pila en 6, (porque se han insertado 3 “objetos”, no del tipo c ++, de tamaño 2 en la pila.
Ahora, su segundo escenario es mucho más difícil de explicar porque hay tantos métodos para lograrlo que es casi imposible de explicar en 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 insertan y asignan en la pila como en el ejemplo anterior. En este caso, sin embargo, inicializamos p con el resultado de una llamada a la función. Lo que malloc (o new, pero new hace más en c ++. Llama a los constructores cuando es apropiado, y todo lo demás) es que va a esta caja negra llamada HEAP y obtiene una dirección de memoria libre. Nuestro sistema operativo administrará el montón por nosotros, pero tenemos que avisarle cuando queremos memoria y cuando terminamos 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) dándonos la dirección inicial de estos bytes. Digamos que la primera llamada nos dio la dirección F000. Luego, el sistema operativo realiza un seguimiento de las direcciones F000 y F001 que están actualmente en uso. Cuando llamamos a free (p), el sistema operativo encuentra el bloque de memoria al que apunta p y marca 2 bytes como no utilizados (porque sizeof (estrella p) es 2). Si, en cambio, asignamos más memoria, la dirección F002 probablemente se devolverá como el bloque inicial 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 nuevamente en la pila en la primera dirección abierta que tenga 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 reduce nuevamente a EFF9, y malloc () pondrá 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 en lo que era antes de ser llamado. El valor de retorno de malloc (), un void estrella, 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 tan 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. Por lo general, esto también implica empujarlos hacia la pila. De la misma manera, generalmente guardará el registro del contador del programa para que pueda regresar al lugar correcto después de que regrese la subrutina. Los administradores de memoria usan su propia memoria para “recordar” qué memoria se ha entregado y cuál no. La memoria virtual y la segmentación de la memoria complican aún más este proceso, y los algoritmos de gestión de la memoria deben mover continuamente los bloques (y protegerlos también) para evitar la fragmentación de la memoria (todo un tema en sí mismo), y esto se relaciona con la memoria virtual. así como. El segundo ejemplo realmente es una gran lata de gusanos comparado con el primer ejemplo. Además, ejecutar varios procesos hace que todo esto sea mucho más complicado, ya que cada proceso tiene su propia pila y más de un proceso puede acceder al montón (lo que significa que debe protegerse a sí mismo). Además, cada arquitectura de procesador es diferente. Algunas arquitecturas esperarán que establezca el puntero de la pila en la primera dirección libre en la pila, otras esperarán que lo apunte al primer lugar no libre.
Espero que esto haya ayudado. Por favor hagamelo saber.
Tenga en cuenta que todos los ejemplos anteriores son para una máquina ficticia que está demasiado simplificada. En hardware real, esto se vuelve un poco más complicado.
editar: los asteriscos no aparecen. los reemplacé con la palabra “estrella”
Por lo que vale, si usamos (principalmente) el mismo código en los ejemplos, reemplazando “hola” con “ejemplo1” y “ejemplo2”, respectivamente, obtenemos la siguiente salida ensamblada para intel en 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
No, sin optimización …
int main()
int i;
int *p;
no hace casi nada, solo un par de instrucciones para ajustar el puntero de la pila, pero
int main()
int *p = new int;
delete p;
asigna un bloque de memoria en el montón y luego lo libera, eso es mucho trabajo (lo digo en serio, la asignación del montón no es una operación trivial).
int i;
int *p;
^ Asignación de un entero y un puntero de entero en la pila
int *p = new int;
delete p;
^ Asignación de un puntero entero en la pila y bloque del tamaño del entero en el montón
EDITAR:
Diferencia entre segmento de pila y segmento de montón
(fuente: 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
Siempre que un programa comienza a ejecutarse, almacena todas sus variables en una ubicación especial de memoria llamada Segmento de pila. Por ejemplo, en el caso de C / C ++, la primera función llamada es main. por lo que se colocará primero en la pila. Cualquier variable dentro de main se colocará en la pila a medida que se ejecute el programa. Ahora, como principal es la primera función llamada, será la última función en devolver cualquier valor (O se extraerá de la pila).
Ahora, cuando asigna memoria dinámicamente usando new
se utiliza otra ubicación de memoria especial llamada segmento de montón. Incluso si los datos reales están presentes en el puntero del montón, se encuentran en la pila.