Stack vs heap en C++: dónde viven de verdad tus variables
Respuesta corta: las variables locales normales viven en la pila (stack) — se reservan en nanosegundos y se destruyen solas al salir del ámbito. El heap es para lo que debe sobrevivir a su ámbito o cuyo tamaño solo se conoce en ejecución: más grande y flexible, pero se reserva explícitamente y lo liberas tú.
Mira correr la diferencia de tiempo de vida
La función de abajo tiene una variable local, y el programa tiene una global. Llama a la función tres veces y fíjate en qué recuerda cada una:
#include <iostream>
using namespace std;
int counter = 0; // global: vive mientras viva el programa
void bump() {
int local = 0; // pila: se recrea en cada llamada
local++;
counter++;
cout << "local=" << local << " counter=" << counter << endl;
}
int main() {
bump();
bump();
bump();
return 0;
}
local imprime 1 las tres veces — nace, vive y muere dentro de una sola llamada a bump(), en la pila. counter conserva su valor entre llamadas porque su tiempo de vida es todo el programa. El tiempo de vida es el corazón de la distinción pila/heap.
Lado a lado
| Pila (stack) | Heap | |
|---|---|---|
| Quién reserva | Automático — basta con declararla | Tú — new (o un contenedor / puntero inteligente) |
| Tiempo de vida | Termina cuando termina el ámbito | Hasta que hagas delete (o muera su dueño) |
| Velocidad | Extremadamente rápida (mover un puntero) | Más lenta (contabilidad, buscar hueco) |
| Tamaño | Pequeña — típicamente ~1–8 MB | Básicamente «la RAM disponible» |
| ¿Cuándo muere? | Al salir del ámbito — garantizado | Solo si la liberas — si no: fuga |
| Uso típico | Locales, parámetros de funciones | Datos grandes, de tamaño variable en ejecución, objetos compartidos |
Reservar en el heap: new y delete
(Nuestro motor en el navegador todavía no soporta new/delete — prueba este en tu compilador de escritorio.)
int main() {
int* p = new int(42); // reserva en el heap; p guarda la dirección
cout << *p << endl; // 42
delete p; // devuelve la memoria — es tu trabajo, no del ámbito
return 0;
}
Cada new debe tener exactamente un delete. Si te falta el delete → fuga de memoria (memoria que ya no puedes alcanzar ni reutilizar). Si haces delete dos veces, o sigues usando el puntero después de liberarlo (un puntero colgante) → comportamiento indefinido.
La respuesta moderna: casi nunca llamas a new/delete a mano
El superpoder de C++ para este problema es RAII — Resource Acquisition Is Initialization: ata el tiempo de vida del recurso al ámbito de un objeto, y el ámbito garantiza la limpieza. Ya has usado RAII sin saber el nombre: un std::vector gestiona su array del heap automáticamente. Para objetos sueltos, ese mismo trabajo lo hacen los punteros inteligentes:
#include <memory>
int main() {
auto p = std::make_unique<int>(42); // memoria del heap, con dueño: p
cout << *p << endl; // 42
return 0; // p muere aquí — la memoria se libera sola
}
Regla práctica para código moderno: pila por defecto; heap a través de un dueño (vector, string, unique_ptr) — casi nunca new/delete crudos.
Dos fallos famosos
- Desbordamiento de pila: la pila es pequeña. Una recursión que nunca llega a su caso base (o arrays locales enormes como
int big[10000000]) la agota y el programa se cae. De ahí viene literalmente el nombre del sitio Stack Overflow. - Devolver un puntero a una local:
int* f() { int x = 5; return &x; }—xmuere en la llave de cierre; quien llama recibe la dirección de un cadáver. Comportamiento indefinido, y el bug de tiempo de vida más clásico de las entrevistas de C++.
Preguntas de entrevista sobre este tema
- «¿Qué vive en la pila y qué en el heap?» — locales y parámetros frente a datos reservados explícitamente (y lo que vector y string gestionan por ti por debajo).
- «¿Por qué reservar en la pila es rápido?» — es mover un único puntero de pila; reservar en el heap exige contabilidad y buscar espacio libre.
- «¿Qué es una fuga de memoria, en una frase?» — memoria del heap que ya no es alcanzable y nunca se liberará.
- «¿Qué es RAII?» — atar el tiempo de vida de un recurso al ámbito de un objeto para que la limpieza sea automática; así funcionan vector, string, fstream y los punteros inteligentes.
- «¿Por qué está mal devolver una referencia o un puntero a una variable local?» — la pregunta de tiempo de vida con la que empieza este artículo, convertida en arma.
Practica en el editor
- Modifica la demo de tiempo de vida: añade una segunda global y una segunda local; predice las seis líneas de salida antes de ejecutar.
- Escribe
int* roto() { int x = 7; return &x; }— no lo ejecutes (¡comportamiento indefinido!); en su lugar, explica en una línea de comentario por qué un compilador de escritorio te avisa. - Profundidad de recursión: el ejemplo de Fibonacci con
fib(10)es seguro; razona cuánto baja aproximadamente la pila defiby por quéfib(100000)sería un desbordamiento de pila.
Relacionado: pasar por valor, puntero o referencia, qué es int main(), 15 preguntas de entrevista para juniors.