Menú de navegaciónMenú
Categorías

La mejor forma de Aprender Programación online y en español www.campusmvp.es

Modelos de gestión de memoria IV: Contador de referencias automático

contadorCon el artículo de hoy, finalizamos la serie sobre cómo se gestiona la memoria en todo lenguaje de programación. Es decir cuándo, cómo y quién libera los objetos que se van creando durante la ejecución del programa. De las cuatro grandes técnicas de gestión de la memoria ya hemos visto la gestión manual, garbage collector y el contador de referencias manual, tan solo nos queda por hablar de la casuística del contador de referencias automático.

Contador de referencias automático

Esa es la cuarta y última técnica que vamos a ver. En esencia es un contador de referencias, salvo que incrementar el contador o disminuirlo se realiza automáticamente. Las normas son muy simples:

  • Cuando se crea un objeto y se asigna a una variable (un puntero en el caso de Objective-C o una referencia en Swift, aunque usaremos el término referencias a partir de ahora) el contador de referencias del objeto pasa a valer uno.
  • Cuando se copia una referencia a otra el objeto no se clona y el contador de referencias:
    • Se incrementa automáticamente si la referencia de destino NO se declaró como débil.
    • No se incrementa si la referencia de destino es débil.
  • Cuando una referencia sale de ámbito o se asigna a nil (o null o el equivalente del lenguaje), el contador de referencias:
    • Disminuye automáticamente si la referencia NO es débil.
    • No disminuye automáticamente si la referencia es débil.
  • Cuando se modifica el valor de una referencia (deja de apuntar a un objeto para apuntar a otro):
    • El contador de referencias del objeto antiguo disminuye automáticamente y el contador de referencias del objeto nuevo se incrementa automáticamente si la referencia no es débil.
    • No se modifica ningún contador de referencias en el caso de referencias débiles.

A nivel de desarrollador, usar ARC (Automatic Reference Counting) es casi idéntico a programar con un lenguaje con Garbage Collector con la salvedad de que se debe saber qué referencias deben declararse como débiles. La diferencia de ARC al respecto del garbage collector es que es totalmente predecible y que el rendimiento es superior. Un tema a puntualizar es que no es necesario soporte explícito del runtime para gestionar automáticamente el contador de referencias: en el caso de Swift y Objective-C es el propio compilador el que añade las llamadas correspondientes para incrementar o disminuir el contador de referencias en base a las reglas anteriores. De hecho la introducción de ARC en Objective-C no supuso una modificación del runtime asociado.

Los contadores de referencias (manuales y automáticos) requieren pues un mínimo soporte del runtime. En el caso de ARC, con tal de que el desarrollador sea cuidadoso sobre qué referencias deben ser fuertes y cuáles débiles, los memory leaks se minimizan mucho.

Smart pointers

Para terminar este artículo veremos una implementación de lo qué es un contador de referencias automático, pero sin soporte del runtime . Por supuesto nuestro protagonista es el lenguaje con el que abríamos esta serie de posts C++. Y lo que vamos a ver ahora se conoce con el nombre genérico de RAII (Resource Acquisition Is Initialization). Tan solo comentar que RAII va más allá de la gestión de memoria y abarca en general la gestión de cualquier recurso. Nosotros nos vamos a centrar en lo que se conoce con el nombre de smart pointers y por supuesto es la forma actual de gestionar la memoria en C++. La implementación de los smart pointers no requirió ninguna extensión al lenguaje ni tampoco ningún soporte adicional en C++, aunque hace uso extensivo de capacidades avanzadas del lenguaje (templates y sobrecarga de operadores). Aunque ya existía antes en ciertas librerías, a partir del estándar C+11 forma parte integral del lenguaje.

Contar cómo funciona internamente la implementación de los smart pointers en C++ queda fuera del alcance de este post y requiere ciertos conocimientos de cómo trabaja el propio lenguaje, así que vamos a ver cómo funciona a nivel del desarrollador:

void drink(Beer* beer) {}

int _tmain(int argc, _TCHAR* argv[])
{
	auto myBeer = std::make_shared();
	myBeer->name = "Voll Damm";
	auto myBeer2 = myBeer;
	drink(myBeer.get());
	return 0;
}

En este ejemplo la variable myBeer no es un Beer* (puntero a Beer) si no un shared_ptr<Beer>. Además la variable myBeer es un objeto que está en la pila, no en el heap. En este caso se construye a través de make_shared que crea un shared_ptr<T> para un tipo T determinado. Aunque hay otras maneras de obtener un shared_ptr a partir de un objeto. Dicho shared_ptr contiene el objeto T creado y un contador de referencias asociado, con el valor inicial de uno. A partir de aquí empieza “la magia” de shared_ptr. En la segunda línea de _tmain accedemos a la propiedad name de myBeer como si ese fuese un puntero normal, porque shared_ptr sobrecarga el operador –> para hacernos creer que es un puntero tradicional. En la tercera línea asignamos myBeer a myBeer2. Eso hace que, automáticamente, el contador de referencias sea 2 (ocurre porque shared_ptr sobrecarga el operador de asignación). El punto donde la abstracción fuga, y lo hace a conciencia, es cuando pasamos el shared_ptr a funciones que esperan un puntero tradicional. En este caso debemos usar el método get(). La fuga se produce para aclarar que la función drink no participa del nuevo sistema de gestión de memoria y que por lo tanto “puede” causar problemas. Es decir, drink podría hacer “delete beer” y se destruiría el puntero beer, contenido en el shared_ptr . Cuando se termina la función _tmain, las variables myBeer y myBeer2 salen de ámbito, y como son objetos que están en la pila se destruyen automáticamente. El destructor de shared_ptr lo que hace es disminuir el contador de referencias asociado y si llega a cero, destruye el puntero contenido en él. A nivel de grafo de objetos tenemos lo siguiente:

image

Los objetos mybeer y mybeer2 son los dos shared_ptr y son objetos que están en la pila. Luego existen dos objetos más (el contador de referencias y el propio objeto Beer) que están en el heap y que son gestionados por la clase shared_ptr. Es decir, realmente un shared_ptr es un objeto que está en la pila y que contiene dos punteros: uno al contador de referencias y otro al propio objeto que está encapsulando. El hecho de que esté en la pila y por lo tanto cada objeto se destruya cuando sale de ámbito es la base de RAII. Cuando se destruye cada objeto shared_ptr, el contador se reduce y si llega a cero, se destruye el objeto asociado. A todos los efectos tenemos el mismo comportamiento que en el caso del ARC de Apple.

Además de shared_ptr existen dos tipos más de smart pointers: weak_ptr y unique_ptr. La clase weak_ptr juega el mismo rol que los punteros débiles (es decir si mybeer2 fuese un weak_ptr el contador de referencias no se hubiese incrementado). Por último unique_ptr no comparte nunca el objeto encapsulado (no puede asignarse un unique_ptr a otro y así tener dos unique_ptr que apunten al mismo objeto).

El uso de smart pointers supone un paso adelante enorme respecto a la gestión de memoria manual pura que ofrece C++ y a pesar de que tiene sus casuísticas es, sin duda, la manera recomendada de manejar la memoria en C++ hoy en día.

Con este artículo terminamos el recorrido por los cuatro modelos de gestión de memoria, esperando que las características y los ejemplos vistos en cada uno de ellos te resulten de utilidad.

Eduard Tomás Eduard es ingeniero informático, atesora muchos años de experiencia como desarrollador y ha sido galardonado como MVP por Microsoft en diez ocasiones. Colabora con la comunidad impartiendo charlas en formato webcast y en eventos de distintos grupos de usuarios. Es Certified Kubernetes Application Developer. Ver todos los posts de Eduard Tomás
Archivado en: Lenguajes y plataformas

Boletín campusMVP.es

Solo cosas útiles. Una vez al mes.

🚀 Únete a miles de desarrolladores

DATE DE ALTA

x No me interesa | x Ya soy suscriptor

La mejor formación online para desarrolladores como tú

Agregar comentario

Los datos anteriores se utilizarán exclusivamente para permitirte hacer el comentario y, si lo seleccionas, notificarte de nuevos comentarios en este artículo, pero no se procesarán ni se utilizarán para ningún otro propósito. Lee nuestra política de privacidad.