Gestión de memoria (IV)

Lecturas de la serie
Sobre este artículo
Tiempo estimado de lectura: 5 minutos
Nivel: Básico
Última actualización: 11 de mayo, 2026
1. Almacenamiento libre (free store)
El lenguaje C++ permite el alojamiento dinámico de memoria en el sector de almacenamiento libre (free store) mediante expresiones new. En contraste con el alojamiento de variables locales en la pila del usuario, el tiempo de vida de los objetos alojados dinámicamente no se encuentra limitado al ámbito en que fueron creados, de forma que el objeto debe ser destruido y la memoria debe ser reclamada explícitamente a través de una expresión delete o, muy raramente, mediante un recolector de basura (garbage collector).
Sobra decir que la recolección de basura es ajena a la filosofía de C++, que prioriza el control determinista de los recursos mediante destructores y la técnica RAII. Puede consultarse [1] por interés histórico.
Ante una expresión

   X* p = new X{/* argumentos */};

el código generado por el compilador resulta ser básicamente equivalente a:

   X* p;    {       // alojamos sizeof(X) bytes en el free store       // (posible lanzamiento de std::bad_alloc bajo fallo de alojamiento):       void* loc = operator new(sizeof(X));                                   // (A)       try {          // construimos el objeto X en el espacio alojado:          ::new(loc) X{/* argumentos */};                                     // (B)          p = static_cast<X*>(loc); // asignamos el puntero si la construcción resultó exitosa       }       catch (...) { // si el constructor lanza una excepción...          operator delete(loc); // desalojamos la memoria y                      (C)          throw;                // relanzamos la excepción                       (D)       }    }

Observemos la separación estricta entre la asignación del espacio de memoria y la inicialización del objeto en el mismo. Se invoca, primeramente, al operador operator new con el fin de asignar un bloque de memoria convenientemente alineado de (como mínimo) sizeof(X) bytes de extensión (véase (A)). Por defecto, de no existir suficiente espacio en memoria, se notificará el error mediante el lanzamiento de una excepción de tipo std::bad_alloc. Por contra, si la operación tiene éxito, la función retornará un puntero no nulo al bloque de memoria asignado. Es en dicha dirección donde se construirá el objeto de tipo X (B). La inicialización del objeto se encierra en un bloque try con el fin de que, de emitirse una excepción durante su construcción, ésta sea capturada y manejada en un bloque catch; ello permite liberar la memoria preasignada en (A) antes de relanzar la excepción (ver C y D). Para más información, puede visitarse el enlace [2].
Una expresión de la forma X* p = new X, siendo X una clase definida por el programador, deberá inicializar al objeto invocando a su constructor por defecto X::X() (o realizar una inicialización de agregado en el caso de agregados). Por contra, de tratarse de un tipo fundamental/primitivo, el valor sufrirá una inicialización por defecto (default-initialization), permaneciendo con un valor indeterminado (y su lectura sería comportamiento indefinido) salvo que se solicite de forma explícita su inicialización a cero mediante la inclusión de un par de llaves adicionales en la expresión, como en int* p = new int{}.
De existir versiones específicas para operator new y operator delete en la clase X (implementadas como funciones miembro públicas y estáticas; véase el cuadro de código inferior), serán éstas las invocadas en los puntos (A) y (C). Su inclusión se enmarca en el uso de técnicas avanzadas de gestión de memoria, detección de errores o control del uso del free store [3, 4]. De requerirse el empleo de las funciones de alojamiento en el espacio de nombres global, las versiones proporcionadas en la clase pueden ser omitidas con sentencias de la forma X* p = ::new X.

   class X {    public:       // ...       // funciones miembro implícitamente estáticas:       void* operator new(std::size_t n);       void operator delete(void* p) noexcept;       void* operator new[](std::size_t n);       void operator delete[](void* p) noexcept;    };    

Por su parte, X* p = new X[n]{} aloja un array de n objetos en el free store y equivale esencialmente a:

   X* p;    {       auto loc = static_cast<X*>(operator new[](n*sizeof(X)));       auto i = std::size_t{};       try {          for (; i < n; ++i) {             ::new(static_cast<void*>(loc + i)) X{};  // (A) }          p = loc;       }       catch (...) {          for (auto j = std::size_t{}; j < i; ++j) {           (loc + j)->~X();                       // (B) }          operator delete[](loc);                   // (C)          throw;                                    // (D)       }    }

De lanzarse una excepción durante la construcción de uno cualquiera de los objetos del array (A), todos los objetos construidos satisfactoriamente con antelación a éste serían destruidos invocándose al destructor de la clase X (B). A continuación, se procedería a liberar la memoria asignada inicialmente par el array (C) y se reemitiría la excepción (D).
Al hacerse uso de una expresión X* p = new X[n] (sin llaves finales) con una clase X definida por el programador, los  n objetos son inicializados mediante una llamada al constructor por defecto X::X(), salvo en el caso de agregados, donde tiene lugar una inicialización de agregado. En contraste, de trabajarse con un tipo fundamental/primitivo, los elementos permanecen indeterminados, salvo que se solicite de forma explícita su inicialización a cero mediante la inclusión de un par de llaves adicionales en la expresión, como en int* p = new int[n]{}.
Observemos que la variable de retorno en una expresión new es un puntero al tipo de objeto construido, proporcionando su dirección en memoria. Este puntero suele ser una variable local con duración de almacenamiento automática, por lo que dejará de existir al final del ámbito en que fue creada. No ocurrirá así con el objeto referenciado, que seguirá almacenado en el free store hasta ser destruido (y su espacio en memoria desalojado) explícitamente mediante una expresión delete. Esta propiedad puede dar lugar a fugas de memoria (memory leaks) indeseadas, como en el siguiente ejemplo:

   {       X* p = new X{};       // ...           } // el puntero sale fuera de ámbito      // pero el objeto apuntado por él permanece en el free store (memory leak)

Este tipo de fugas deben evitarse delegando la gestión de la memoria en objetos con semántica de valor que implementen la técnica RAII. Esto incluye el uso de contenedores de la biblioteca estándar como std::vector, o el empleo de punteros inteligentes (std::unique_ptr, std::shared_ptr). Estas herramientas aseguran la liberación automática del recurso y permiten, cuando es necesario, la transferencia segura de su propiedad mediante operaciones de movimiento (std::move).

Una expresión de tipo delete como la siguiente

   X* p = new X{};    // ...    delete p;

es equivalente a:

   X* p = new X{};    // ...    if (p != nullptr) {       p->~X();            // llamada al destructor de X     operator delete(p); // desalojo del espacio en memoria    }

Es decir, si el puntero es no-nulo, se invoca al destructor de la clase para el objeto referenciado y se libera la memoria ocupada por éste. Notemos en este caso (y también en ejemplos anteriores) la importancia de que el destructor de la clase X no emita excepciones de ningún tipo. En el ejemplo considerado, la razón es evidente: de lanzarse una excepción desde el destructor, el operador delete (responsable de la liberación del bloque de memoria ocupado por el objeto) no será invocado, produciéndose una laguna de memoria.
Desde C++11, los destructores son noexcept por defecto. Si un destructor lanzara una excepción, especialmente durante el desenredo de la pila (stack unwinding), el programa invocaría inmediatamente a std::terminate().
Análogamente,

   X* p = new X[n]{};    // ...    delete[] p;

equivale a:

   X* p = new X[n]{};    // ...    if (p != nullptr) {       for (auto i = std::size_t{}; i < n; ++i) {          (p + n - i -1)->~X(); }       operator delete[](p);    }

Observemos que no es necesario especificar el tamaño del array (en el código, n) para desalojarlo a través del operador delete[]. Dicho tamaño es almacenado junto al propio objeto en memoria dinámica, o bien mediante la gestión de una tabla asociativa entre punteros y tamaños en memoria (consúltese la referencia [5] para más información al respecto).
Referencias bibliográficas
  1. Boehm-Demers-Weiser garbage collector – https://www.hboehm.info/gc/
  2. isocpp.org C++ FAQ – https://isocpp.org/wiki/faq/freestore-mgmt#new-doesnt-leak-if-ctor-throws
  3. Meyers S., 'Effective C++ Third Edition'. Addison Wesley, 2005.
  4. Dickheiser M. J., 'C++ for Game Programmers', Second Edition. Charles River Media (2006).
  5. isocpp.org C++ FAQ – https://isocpp.org/wiki/faq/freestore-mgmt#num-elems-in-new-array
  6. cppreference – new expression – https://en.cppreference.com/w/cpp/language/new

Comentarios