Ir al contenido

Memoria Dinamica: new, delete y Sus Peligros

Aprende como funciona la asignacion dinamica con new y delete, comprende los peligros de las fugas de memoria, doble liberacion y uso despues de liberar, y descubre por que el C++ moderno no deberia contener ningun new/delete crudo.

Asignacion Dinamica con new y delete

La pila es rapida pero limitada en tamano y ambito. Cuando necesitas objetos que sobrevivan al ambito que los creo, o cuyo tamano es desconocido en tiempo de compilacion, asignas en el monton (almacen libre).

new T(args) asigna memoria para un T, llama a su constructor y retorna un T*. delete p llama al destructor y libera la memoria. Para arrays: new T[n] y delete[] p. Mezclar new con delete[] o new[] con delete es comportamiento indefinido — el compilador no detectara esto.

new, delete y Sus Variantes para Arrays

Aqui esta la API de asignacion cruda. Estudiala para reconocerla en codigo legado, pero nunca escribas esto en codigo nuevo — usa punteros inteligentes (smart pointers) en su lugar.

new_delete.cpp
#include <iostream>
#include <cstring>

struct Widget {
    int id;
    Widget(int i) : id(i) { std::cout << "Widget " << id << " created\n"; }
    ~Widget() { std::cout << "Widget " << id << " destroyed\n"; }
};

int main() {
    // Single object
    Widget* w = new Widget(1);
    std::cout << "Using widget " << w->id << '\n';
    delete w;  // destructor called, memory freed

    // Array
    Widget* arr = new Widget[3]{{10}, {20}, {30}};
    for (int i = 0; i < 3; ++i)
        std::cout << arr[i].id << ' ';
    std::cout << '\n';
    delete[] arr;  // MUST use delete[] for new[]

    // UNDEFINED BEHAVIOR — don't do these:
    // delete arr;      // wrong: used delete instead of delete[]
    // delete[] w;      // wrong: used delete[] instead of delete
}

Placement new

Placement new construye un objeto en una direccion de memoria especifica sin asignar. Es usado por asignadores, pools de memoria y contenedores como std::vector internamente. Debes llamar al destructor explicitamente — delete NO debe usarse en objetos creados con placement new.

placement_new.cpp
#include <iostream>
#include <new>       // required for placement new
#include <cstddef>   // std::byte
#include <memory>    // std::align

struct Sensor {
    int id;
    double reading;
    Sensor(int i, double r) : id(i), reading(r) {
        std::cout << "Sensor " << id << " init\n";
    }
    ~Sensor() { std::cout << "Sensor " << id << " shutdown\n"; }
};

int main() {
    // Pre-allocate raw storage (aligned)
    alignas(Sensor) std::byte buffer[sizeof(Sensor)];

    // Construct in pre-allocated memory
    Sensor* s = new (buffer) Sensor(42, 3.14);
    std::cout << "Reading: " << s->reading << '\n';

    // Must destroy explicitly — do NOT use delete
    s->~Sensor();  // explicit destructor call
    // delete s;    // WRONG — memory was not allocated by new
}

Los Tres Errores Mortales de Memoria

Los new/delete crudos te exponen a tres categorias de errores:

1. Fuga de Memoria (Memory Leak) — asignar con new pero nunca llamar a delete. La memoria nunca se devuelve al sistema operativo (hasta que el proceso termina). En un servidor de larga ejecucion, esto eventualmente consume toda la RAM disponible.

2. Doble Liberacion (Double Free) — llamar a delete en el mismo puntero dos veces. El segundo delete corrompe las estructuras de datos internas del asignador del monton, frecuentemente causando fallos mucho despues en codigo no relacionado. Esto es extremadamente dificil de depurar.

3. Uso Despues de Liberar (Use-After-Free) — acceder a memoria despues de delete. La memoria puede haber sido reutilizada para una asignacion diferente, asi que podrias estar leyendo o escribiendo datos de otro objeto. Esta es una vulnerabilidad de seguridad (error de clase CVE).

Error común

Considera: f(new A(), new B()). El compilador puede evaluar los argumentos en cualquier orden y puede intercalarlos:

1. Asignar memoria para A
2. Asignar memoria para B
3. Construir A
4. Construir B

Si el paso 4 lanza una excepcion, A tiene una fuga — nadie llama a delete sobre ella. Incluso f(std::shared_ptr(new A()), std::shared_ptr(new B())) tenia este problema antes de C++17 (que garantiza que cada argumento se evalua completamente antes del siguiente). La solucion: siempre usa std::make_unique o std::make_shared — realizan la asignacion y construccion como una unica operacion segura ante excepciones.