Menu

Memoria dinámica en C++: new y delete explicados

Cómo reservar memoria en tiempo de ejecución con new, liberarla con delete y evitar las fugas, los punteros colgantes y las dobles liberaciones que conlleva gestionar el heap a mano.

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

Por qué pedirías memoria en tiempo de ejecución

Hasta ahora, todas las variables que has creado han vivido en la pila (stack): su tamaño se conoce en tiempo de compilación y se destruyen automáticamente cuando termina su ámbito. Eso es rápido y seguro, pero no sirve para el caso en el que no sabes cuánta memoria necesitas hasta que el programa se está ejecutando: un búfer cuyo tamaño depende de la entrada del usuario, una estructura que debe sobrevivir a la función que la creó, o un grafo cuya forma no se conoce de antemano.

Para esos casos, C++ te permite reservar memoria del heap (también llamado almacén libre) con new, y devolverla con delete. La expresión new reserva un bloque, ejecuta el constructor y devuelve un puntero a él, construyendo directamente sobre los punteros que viste en la página anterior.

La variable p en sí vive en la pila: es solo un puntero. El int al que apunta vive en el heap y permanece vivo hasta que lo liberas con delete, sin importar cuántos ámbitos vayan y vengan.

Pila frente a heap

Esta distinción es la razón misma por la que existe new, así que vale la pena hacerla concreta.

void demo() {
    int a = 10;            // en la pila - desaparece cuando demo() retorna
    int* b = new int(10);  // 'b' en la pila, el int al que apunta en el heap
}                          // 'a' destruido; el int del heap se FUGA - nunca se libera

Diferencias clave:

  • Pila - vida automática, muy rápida, tamaño limitado (normalmente unos pocos MB), liberada por ti cuando el ámbito termina.
  • Heap - vida manual, algo más lenta, grande, y liberada solo cuando llamas a delete.

El intercambio es flexibilidad a cambio de responsabilidad: la memoria del heap vive exactamente el tiempo que quieras, pero te conviertes en el responsable de recordar liberarla.

Reservar arrays con new[]

Cuando necesitas un bloque cuya longitud se decide en tiempo de ejecución, usa la forma de array new T[n]. Devuelve un puntero al primer elemento, y lo liberas con el delete[] correspondiente.

La regla es estricta y fácil de equivocar: la memoria de new se libera con delete, y la memoria de new[] se libera con delete[]. Mezclarlas (delete arr sobre algo reservado con new[]) es comportamiento indefinido, aunque parezca funcionar en tu máquina.

Los tres errores clásicos

La gestión manual de memoria tiene un pequeño conjunto de errores que explican la mayoría de los fallos del heap. Aprende a reconocer los tres.

1. Fuga de memoria - nunca llamas a delete. El bloque queda reservado para siempre. Inofensivo una vez, fatal dentro de un bucle.

void leaky() {
    int* p = new int(5);
    // ... sin delete ...
}   // p desaparece; el int del heap ahora es inalcanzable Y no liberado

2. Puntero colgante - usas memoria después de liberarla. El puntero todavía guarda la dirección antigua, pero esa memoria ya no es tuya.

3. Doble liberación - liberas el mismo bloque dos veces. Esto corrompe la contabilidad interna del heap y normalmente provoca un fallo.

int* p = new int(1);
delete p;
delete p;   // doble liberación - comportamiento indefinido, a menudo un fallo

Poner un puntero a nullptr después de liberarlo desactiva tanto el uso colgante como la doble liberación: desreferenciar nullptr falla de inmediato (fácil de depurar), y delete nullptr es, de forma explícita, una operación segura que no hace nada.

Un ciclo realista de reservar-usar-liberar

Juntándolo todo, esta es la forma de una gestión manual correcta: reservar, usar, liberar exactamente una vez y no tocar el puntero después.

Fíjate en que delete u hace dos cosas con un tipo de clase: primero ejecuta el destructor del objeto y luego libera la memoria en crudo. Ese orden importa en cuanto tus objetos poseen recursos propios.

Un detalle sutil: si se lanza una excepción entre new y delete, el delete nunca se ejecuta y se produce una fuga. Envolver cada reserva en try/catch para manejar eso es tedioso y propenso a errores, que es precisamente el problema que resuelve la página siguiente.

Siguiente: Punteros inteligentes

Ya has visto el coste completo de gestionar la memoria a mano: cada new es una promesa de delete más adelante, y una sola liberación olvidada, duplicada o prematura es comportamiento indefinido. El C++ moderno casi nunca hace esa promesa manualmente. La página siguiente presenta los punteros inteligentes - std::unique_ptr y std::shared_ptr - objetos que poseen una reserva del heap y llaman a delete por ti automáticamente cuando salen de ámbito, convirtiendo los tres errores clásicos en cosas de las que se encargan el compilador y RAII en tu lugar.

Preguntas frecuentes

¿Cuál es la diferencia entre new y delete en C++?

new reserva memoria en el heap en tiempo de ejecución y devuelve un puntero a ella; delete libera la memoria que se reservó con new. Cada new debe emparejarse con exactamente un delete, o de lo contrario habrá una fuga de memoria. Para arrays, usa new[] con delete[].

¿Qué ocurre si olvidas llamar a delete en C++?

Se produce una fuga de memoria: el bloque del heap queda reservado durante toda la vida del programa aunque ya nada apunte a él. Una sola fuga suele ser inofensiva, pero las fugas dentro de un bucle o en un servicio de larga ejecución crecen hasta que el programa se queda sin memoria y se bloquea.

¿Debería usar new y delete directamente en C++ moderno?

Rara vez. Es preferible usar contenedores como std::vector o punteros inteligentes (std::unique_ptr, std::shared_ptr) que liberan la memoria automáticamente. Vale la pena entender new/delete en crudo porque los punteros inteligentes lo envuelven, pero en el código cotidiano es una fuente de fugas y punteros colgantes.

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR