Punteros inteligentes (I): Introducción

Última actualización de esta serie: 1 de agosto de 2020

Artículos de la serie:
  1. Introducción.
  2. Propiedad exclusiva (primera parte).
  3. Propiedad exclusiva (segunda parte).
  4. Propiedad compartida (primer parte).
  5. Propiedad compartida (segunda parte).

Introducción


Una de las principales lecciones aprendidas tras décadas de uso de C++ es la de que los punteros tradicionales no-encapsulados no deberían emplearse como agentes propietarios de los objetos a los que apuntan. 

En efecto, consideremos el uso no-encapsulado de una expresión new para alojar un objeto en el free store. Ello exige al programador el uso posterior de delete para destruir el objeto y liberar su memoria, así como la introducción de engorrosos bloques de limpieza try-cacth para evitar fugas de memoria ante la posible emisión de excepciones. Por ejemplo:

Gestión de memoria (V)

Fragmentación del free store


En la mayoría de los compiladores del lenguaje C++, el alojamiento dinámico en el espacio de almacenamiento libre (mediante expresiones newdelete) suele venir implementado en torno a las funciones malloc() y free() propias del lenguaje C. Por cada nueva petición de almacenamiento, el sistema debe realizar una búsqueda efectiva de un bloque en memoria sin utilizar de un tamaño igual o superior al solicitado. De no existir suficiente espacio en memoria, se notifica el error emitiendo por defecto una excepción de tipo std::bad_alloc. Existen múltiples algoritmos de alojamiento posibles, cada uno de los cuales posee sus ventajas y sus inconvenientes en relación a su eficiencia en la búsqueda y uso de la memoria [1,2].

Gestión de memoria (IV)

Almacenamiento libre (free store)


El lenguaje C++ permite la asignación dinámica de memoria en el sector de almacenamiento libre (free store) mediante expresiones de tipo 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 la memoria debe ser reclamada explícitamente a través de una expresión delete o, muy raramente, mediante un recolector de basura [1].

Ante una expresión

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

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

Gestión de memoria (III)

Alineación (alignment)


El proceso de alineación de datos en memoria implica el alojamiento de variables de un tipo determinado en direcciones de memoria que sean múltiplos enteros de un cierto valor L (normalmente 1, 2, 4, 8, ó 16) [1]. Concretamente, sea D la dirección en memoria de una variable cualquiera. La alineación L de dicha variable (medida en bytes) se define como la mayor potencia de 2 tal que

D mod L = 0.

Con carácter general, el compilador GCC impone la siguiente tabla de alineaciones para tipos primitivos y punteros en un sistema Linux de 64 bits:

Tamaño (bytes) Alineación (bytes)
char 1 1
short 2 2
int 4 4
long int 4 4
long long int 8 8
float 4 4
double 8 8
long double 16* 16
Puntero a cualquier tipo 8 8
* En el caso de long double, sólo 10 bytes son realmente utilizados para la representación numérica (80 bits de precisión); los 6 bytes restantes son añadidos para garantizar una alineación que sea múltiplo entero del tamaño de una palabra [2].