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 reservaAutomático — basta con declararlaTú — new (o un contenedor / puntero inteligente)
Tiempo de vidaTermina cuando termina el ámbitoHasta que hagas delete (o muera su dueño)
VelocidadExtremadamente rápida (mover un puntero)Más lenta (contabilidad, buscar hueco)
TamañoPequeña — típicamente ~1–8 MBBásicamente «la RAM disponible»
¿Cuándo muere?Al salir del ámbito — garantizadoSolo si la liberas — si no: fuga
Uso típicoLocales, parámetros de funcionesDatos 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

Preguntas de entrevista sobre este tema

  1. «¿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).
  2. «¿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.
  3. «¿Qué es una fuga de memoria, en una frase?» — memoria del heap que ya no es alcanzable y nunca se liberará.
  4. «¿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.
  5. «¿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

  1. 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.
  2. 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.
  3. Profundidad de recursión: el ejemplo de Fibonacci con fib(10) es seguro; razona cuánto baja aproximadamente la pila de fib y 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.