Menu

Pila frente a montón en C: tiempos de vida, punteros colgantes y cuándo usar cada uno

Dónde vive realmente tu información: almacenamiento automático en la pila, almacenamiento dinámico en el montón y el almacenamiento estático como tercera región, con el clásico error del puntero colgante al devolver una local y una regla para elegir.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

Cada variable de un programa de C vive en algún sitio, y dónde determina dos cosas que después no puedes cambiar: cuánto sobrevive y cuánta puedes tener. C te da tres regiones de almacenamiento, y elegir mal produce o bien un fallo o bien una fuga. Esta página las presenta y muestra el error clásico que sale de equivocarse con los tiempos de vida.

Las tres regiones

  direcciones altas
  +---------------------------+
  |  pila                     |  locales, parametros, direcciones de retorno
  |    crece hacia abajo |    |  se liberan solas al retornar
  |                      v    |
  +---------------------------+
  |        (hueco sin usar)   |
  +---------------------------+
  |                      ^    |
  |    crece hacia arriba|    |
  |  monton                   |  bloques de malloc / calloc / realloc
  +---------------------------+  solo se liberan con free()
  |  datos estaticos/globales |  globales y statics, toda la ejecucion
  +---------------------------+
  |  codigo (texto)           |  el codigo maquina, de solo lectura
  +---------------------------+
  direcciones bajas
  • El almacenamiento automático (la pila) guarda los parámetros de las funciones y las locales no static. Se reclama un bloque de pila al entrar en una función y se libera cuando retorna. El tamaño queda fijado en tiempo de compilación.
  • El almacenamiento dinámico (el montón) guarda todo lo que viene de malloc, calloc y realloc. El tamaño se decide en tiempo de ejecución; el tiempo de vida termina solo con free.
  • El almacenamiento estático guarda las globales y todo lo declarado static. Existe durante toda la ejecución del programa y se inicializa a cero antes de que main empiece.

Las direcciones del diagrama son la disposición habitual, no una garantía: el estándar describe tiempos de vida, no disposición.

El almacenamiento automático en acción

Cada llamada a demo recibe una local nueva y una table nueva. Nada se libera a mano, nada puede tener fugas, y la reserva cuesta una sola instrucción que mueve el puntero de pila. Es por esto que las locales corrientes deberían ser tu opción por defecto: son el almacenamiento más rápido y más seguro que tiene C.

La trampa es la llave de cierre. En cuanto se ejecuta, esa memoria ya no está.

El puntero colgante

Aquí está el error que todo programador de C escribe una vez:

/* ROTO: devuelve la direccion de memoria que ya no existe */
int *make_counter(void) {
    int count = 0;
    return &count;          /* count muere en esta llave */
}

int main(void) {
    int *p = make_counter();
    *p = 5;                 /* escribiendo en un marco de pila muerto */
    return 0;
}

&count era una dirección perfectamente válida mientras make_counter se ejecutaba. Al retornar, ese espacio de pila se le entrega a la siguiente función que se llame, así que p apunta ahora dentro de las variables locales de otro. Leer da basura; escribir las corrompe. GCC y Clang avisan de esta forma exacta (-Wreturn-local-addr), así que compila con los avisos activados.

El mismo error se disfraza con arrays, y ahí el aviso a menudo no se dispara:

La versión rota de esa función construiría el texto en un char buf[64] local y haría return buf;, devolviendo la dirección de un búfer que deja de existir en ese mismo instante.

Tres formas de arreglarlo

1. Quien llama suministra el búfer (mostrado arriba). Sin reserva, sin pregunta de propiedad, y el estilo más común en las bibliotecas de C. La función recibe el tamaño para poder mantenerse dentro de él.

2. Devuelve memoria del montón y di quién la libera.

El bloque del montón sobrevive a la función por diseño: ese es todo el sentido de la memoria dinámica. El costo es el comentario de propiedad y el free de quien llama.

3. Usa almacenamiento estático, cuando un único búfer compartido sea aceptable:

static dentro de una función mantiene local el ámbito de la variable mientras le da el tiempo de vida del programa, así que devolver su dirección es legal. El precio es que solo hay una: todos los que llamen la comparten, lo que hace este patrón inutilizable en código con hilos y sorprendente incluso en código de un solo hilo cuando dos llamadas mantienen el puntero a la vez.

Tamaño: dónde se acaba la pila

El espacio de pila es pequeño y fijo. El hilo principal recibe típicamente 1 MB en Windows y 8 MB en Linux; un hilo creado aparte suele recibir mucho menos. El montón está limitado por la memoria disponible del sistema.

void bad(void) {
    int huge[1000000];      /* ~4 MB de pila: probablemente revienta al entrar */
    huge[0] = 1;
}

No hay diagnóstico ni ningún NULL que comprobar: el programa simplemente muere, normalmente con un fallo de segmentación, antes de que se ejecute la primera línea del cuerpo. La versión del montón reporta el fallo como es debido:

La recursión profunda agota la pila de la misma forma, un marco a la vez: una función recursiva descontrolada es en la práctica la causa más común de un desbordamiento de pila.

Costo y localidad

Reservar en la pila es una operación aritmética sobre un registro. Reservar en el montón es una llamada de biblioteca que busca un bloque adecuado, puede tomar un cerrojo y ocasionalmente le pide más memoria al sistema operativo. En un bucle caliente esa diferencia es medible.

Los datos de la pila también son compactos y recién tocados, así que tienden a estar en caché. Los bloques del montón pueden estar dispersos. Ninguno de estos hechos debería dirigir un diseño por sí solo —la corrección del tiempo de vida va primero— pero entre dos diseños que ambos son correctos, el de la pila suele ser el más rápido.

Ver las regiones

Imprimir direcciones hace concreta la disposición. Los valores exactos difieren en cada ejecución (los sistemas modernos los aleatorizan), pero la agrupación se ve:

La global y la estática están una junto a la otra; el bloque del montón está en otro sitio; la local suele estar lejos de ambas. Convierte a void * para %p: eso es lo que exige el especificador de formato.

Cómo elegir

Usa la pila cuando:

  • el tamaño se conoce en tiempo de compilación,
  • los datos solo se necesitan dentro de esta función y de las que ella llama,
  • y es pequeño: unos pocos kilobytes, no megabytes.

Usa el montón cuando:

  • el tamaño depende de la entrada, de un archivo o de un cálculo,
  • los datos deben sobrevivir a la función que los creó,
  • o es lo bastante grande como para amenazar el límite de la pila.

Usa el estático cuando:

  • debe existir exactamente una instancia para todo el programa,
  • y compartirla entre todos los que llamen es genuinamente correcto.

La opción por defecto es la pila. Recurre al montón cuando aplique una de sus tres razones, y cuando lo hagas, sigue las reglas de propiedad de fugas de memoria para que el bloque al que le ganaste un tiempo de vida siga liberándose.

Dos errores que son imagen especular

Vale la pena nombrarlos juntos, porque son la misma pregunta sobre el tiempo de vida respondida de dos formas:

  • Puntero colgante: la memoria murió antes que el puntero. Devolver &local, o usar un puntero después de free. El programa lee o escribe almacenamiento que ahora pertenece a otra cosa.
  • Fuga de memoria: el puntero murió antes que la memoria. Perder la última referencia a un bloque de malloc. Nada se rompe de inmediato; el proceso simplemente crece.

Ambos vienen de un desajuste entre cuánto deben vivir los datos y en qué región los pusiste. Decide primero el tiempo de vida y la región se deduce sola.

Preguntas frecuentes

¿Cuál es la diferencia entre la pila y el montón en C?

La pila guarda las variables locales: el compilador les fija el tamaño, se crean al entrar en una función y se destruyen cuando retorna, y reservarlas no cuesta nada. El montón guarda los bloques de malloc: eliges el tamaño en tiempo de ejecución, el bloque sobrevive hasta que hagas free, y la reserva tiene un costo real.

¿Por qué no puedo devolver un puntero a una variable local en C?

Porque el almacenamiento de la local se libera en el momento en que la función retorna. El puntero todavía guarda esa dirección, pero la memoria pertenece ahora a la siguiente llamada a función: leerla da basura, escribirla corrompe datos ajenos. Eso es un puntero colgante. Devuelve un bloque de malloc, o haz que quien llama suministre el búfer.

¿Qué tan grande es la pila en C?

Típicamente de 1 a 8 MB para el hilo principal, y mucho menos para los hilos adicionales: lo bastante pequeña como para que int big[1000000]; como local normalmente reviente el programa al entrar. El montón está limitado por la memoria disponible del sistema, así que los datos grandes o de tamaño desconocido van ahí.

¿Cuándo debería usar el montón en lugar de la pila en C?

Tres casos: el tamaño no se conoce hasta el tiempo de ejecución, los datos deben sobrevivir a la función que los creó, o el bloque es demasiado grande para la pila (aproximadamente cualquier cosa por encima de unos cientos de kilobytes). Todo lo demás debería ser una local corriente: es más rápida y no puede tener fugas.

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR