Un puntero inteligente es un idioma de indirección de memoria que se usa ampliamente cuando se programa en lenguajes de alto nivel como C++ , Rust , etc. Por regla general, se implementa como una clase especializada (generalmente parametrizada ), que imita la interfaz de un puntero regular y agrega la nueva funcionalidad necesaria (por ejemplo, verificación de límites en el acceso o limpieza de memoria ) [1] .
Por lo general, el propósito principal del uso de punteros inteligentes es encapsular el manejo dinámico de la memoria de tal manera que las propiedades y el comportamiento de los punteros inteligentes imiten las propiedades y el comportamiento de los punteros regulares. Al mismo tiempo, son responsables de la liberación oportuna y precisa de los recursos asignados, lo que simplifica el desarrollo del código y el proceso de depuración, eliminando las fugas de memoria y la aparición de enlaces colgantes [2] .
Estos se usan comúnmente con objetos que tienen operaciones especiales "aumentar el recuento de referencias" ( AddRef()en COM ) y "reducir el recuento de referencias" ( Release()en COM). La mayoría de las veces, dichos objetos se heredan de una clase o interfaz especial (por ejemplo, IUnknownen COM).
Cuando aparece una nueva referencia a un objeto, se llama a la operación “aumentar el número de referencias”, y cuando se destruye, se llama a la operación “disminuir el número de referencias”. Si, como resultado de la operación "reducir referencias", el número de referencias a un objeto se vuelve cero, entonces el objeto se elimina.
Esta técnica se llama conteo automático de referencias . Hace coincidir el número de punteros que almacenan la dirección del objeto con el número de referencias almacenadas en el objeto, y cuando este número llega a cero, hace que el objeto se elimine. Sus ventajas son una confiabilidad relativamente alta, velocidad y facilidad de implementación en C++ . La desventaja es que se vuelve más difícil de usar en caso de referencias circulares (la necesidad de usar "referencias débiles").
Hay dos tipos de punteros de este tipo: con almacenamiento de contador dentro del objeto y con almacenamiento de contador fuera.
La opción más simple es almacenar el contador dentro de un objeto administrado. En COM , los objetos contados por referencia se implementan de la siguiente manera:
Implementado de la misma manera boost::intrusive_ptr.
Los std::shared_ptrcontadores de referencia se almacenan fuera del objeto, en una estructura de datos especial. Este puntero inteligente tiene el doble de tamaño que uno estándar (tiene dos campos, uno apunta a la contraestructura y el segundo al objeto gestionado). Este diseño permite:
Dado que la estructura del contador es pequeña, se puede asignar, por ejemplo, a través del grupo de objetos .
Supongamos que hay dos objetos y cada uno de ellos tiene un puntero propietario. Al puntero en el primer objeto se le asigna la dirección del segundo objeto, y el puntero en el segundo es la dirección del primer objeto. Si ahora a todos los punteros externos (es decir, no almacenados dentro de estos objetos) a dos objetos dados se les asignan nuevos valores, entonces los punteros dentro de los objetos aún se poseerán entre sí y permanecerán en la memoria. Como resultado, habrá una situación en la que no se podrá acceder a los objetos, es decir, una pérdida de memoria .
El problema de las referencias circulares se resuelve ya sea mediante el diseño adecuado de las estructuras de datos, o mediante el uso de la recolección de basura , o mediante el uso de dos tipos de referencias: fuertes (propietarias) y débiles (no propietarias, por ejemplo std::weak_ptr).
A menudo, los punteros de propiedad compartida son demasiado grandes y "pesados" para las tareas del programador: por ejemplo, debe crear un objeto de uno de los tipos N, poseerlo, acceder a sus funciones virtuales de vez en cuando y luego eliminarlo correctamente. Para hacer esto, use el "hermano pequeño", un indicador de propiedad exclusiva.
Dichos punteros al asignar un nuevo valor o eliminarse eliminan el objeto. La asignación de punteros de propiedad única solo es posible con la destrucción de uno de los punteros; por lo tanto, nunca habrá una situación en la que dos punteros posean el mismo objeto.
Su desventaja es la dificultad de pasar un objeto fuera del alcance del puntero.
En la mayoría de los casos, si hay una función que se ocupa de una matriz, se escribe una de dos cosas:
clasificación vacía ( tamaño_t tamaño , int * datos ); // puntero + tamaño void sort ( std :: vector < int >& data ); // estructura de memoria especificaEl primero excluye la verificación automática de rango. El segundo limita la aplicabilidad std::vectorde 's, y no puede ordenar, por ejemplo, una cadena de una matriz o parte de otra vector's.
Por lo tanto, en las bibliotecas desarrolladas para funciones que usan los búferes de memoria de otras personas, usan tipos de datos "ligeros" como
plantilla < claseT > _ estructura Buf1d { datos T * ; tamaño_t tamaño ; Buf1d ( estándar :: vector < T > & vec ); T & operador []( tamaño_t i ); };A menudo se usa para cadenas: analizar , ejecutar un editor de texto y otras tareas específicas necesitan sus propias estructuras de datos que son más rápidas que los métodos estándar de manipulación de cadenas.