Disponible la nueva versión "donationware" 7.3 de OrganiZATOR
Descubre un nuevo concepto en el manejo de la información.
La mejor ayuda para sobrevivir en la moderna jungla de datos la tienes aquí.

Curso C++

[Home]  [Inicio]  [Índice]


4.13.3  Objetos-puntero

§1  Preámbulo

En el capítulo dedicado a punteros ( 4.2) hemos visto que estos objetos están definidos de forma nativa en el lenguaje (en el mismo sentido en que lo están los tipos int o float), y cómo pueden definirse punteros a cualquier tipo de entidad incluyendo funciones. Sin embargo, existen ocasiones en que resulta útil construir tipos abstractos que funcionen como punteros. Nos referimos a clases cuyas instancias puedan ser utilizadas como si fuesen punteros a otros objetos.

Observe el lector que no nos referimos a clases cuyos miembros sean punteros, caso este del que hemos visto múltiples ejemplos a lo largo de este tutorial, sino a objetos que puedan ser utilizados como punteros. Observe también que decimos: "utilizados como punteros". La razón es que evidentemente, desde el punto de vista formal tales objetos no son punteros, sino del tipo definido por la clase. Lo aclararemos con un ejemplo; supongamos:

int x = 10;

int* pint = &x;

cout << *pint;     // -> 10

En este caso *pint es equivalente a x, y el tipo de pint es puntero-a-int (int*). Lo que deseamos es una clase ClaseP tal que:

class Clase1;     // una clase cualquiera

Clase1 obj;       // un objeto

class ClaseP;     // una clase-puntero

ClaseP opt;       // objeto-puntero

...        // algún mecanismo para que opt pueda señalar a obj
*opt;              // debe ser equivalente a obj


Pretendemos que las instancias de ClaseP puedan ser utilizadas como punteros, pero observe que en estos casos el tipo de obj es Clase1, y el de opt es ClaseP (no puntero-a-Clase1).

§2  Objetos-puntero

A estas entidades las denominaremos objetos-puntero, y queremos aplicarles el álgebra de los punteros-a-clase ( 4.2.1f). Es decir, suponiendo que x  y fun sean respectivamente una propiedad y un método de obj, deberíamos poder escribir:

(*opt).x;       // Ok. equivalente a obj.x

opt->x;         // Ídem.

opt->fun()      // Ok. invocación de obj.fun

También deben estar permitidas las operaciones de asignación con estos objetos:

ClaseP opt2 = opt;

opt2->fun();   // Ok. invocación de obj.fun

§2.1  Ejemplo

A continuación se muestra un ejemplo de lo que podría ser el diseño de una de estas clases-puntero. Para evidenciar el paralelismo de comportamiento con los punteros-a-clase primitivos, se define también uno de estos punteros. A continuación comprobamos como las operaciones que pueden hacerse con uno de ellos pueden realizarse también con las instancias de la nueva clase.

#include <iostream>   // Ejemplo-1 objeto-puntero
using namespace std;

struct Cls {          // una clase cualquiera
  int x;
  void fe() { cout << "Cls x = " << x << endl; }

  Cls(int n=1) : x(n) {}
};
Cls* eptr;            // puntero-a-clase tradicional

class ClsPtr {        // clase puntero-a-Ext
  public:
  Cls* ptr;                          // L.13:
  Cls operator*() { return *ptr; }   // L.14
  Cls* operator->() { return ptr; }  // L.15
  ClsPtr() { ptr = NULL; }           // constructor por defecto
  ClsPtr(Cls& obj) { ptr = &obj; }   // constructor
};

int main() {          // =================
  Cls obj;            // M.1: Una instancia de Cls
  eptr = &obj;        // M.2:
  (*eptr).fe();       // M.3:
  eptr->fe();
  cout << "Valor Cls x = " << (*eptr).x << endl;

  ClsPtr op1(obj);    // M.7:
  (*op1).fe();        // M.8:
  op1->fe();
  cout << "Valor Cls x = " << (*op1).x << endl;

  ClsPtr op2 = op1;   // M.12
  cout << "Valor Cls x = " << (*op2).x << endl;

  return 0;
}

Salidas:

Cls x = 1
Cls x = 1
Valor Cls x = 1
Cls x = 1
Cls x = 1
Valor Cls x = 1
Valor Cls x = 1

Comentario:

La clase Cls (todos sus miembros son públicos) con una propiedad y un método, sirve de testigo. A continuación definimos eptr; un puntero tradicional a Cls que servirá de término de comparación.

La clase-puntero ClsPtr  es diseñada ex profeso para que sus objetos sirvan como punteros-a-Cls. El secreto estriba principalmente en el miembro ptr (L.13) que es un puntero-a-Cls. Para utilizar con estos objetos los operadores de indirección * ( 4.9.11) y selector indirecto de miembro -> ( 4.9.16), ha sido necesario sobrecargarlos para los miembros de la clase (sentencias L.14 y L.15). El diseño se completa con sendos constructores. El primero es por defecto; el segundo acepta una referencia-a-Cls y es utilizado para iniciar el puntero ptr del objeto creado con el valor correspondiente.

Las sentencias M.1 y M.2 se limitan a crear una instancia obj de la clase-testigo y a iniciar el puntero eptr con la dirección del objeto. M.3 y M.4 invocan el método fe() de obj utilizando diversas notaciones. Finalmente M.5 muestra el valor de la propiedad x utilizando el operador de indirección * y el selector directo de miembro . ( 4.9.16).  Este grupo de sentencias es responsable de las tres primeras salidas.

El grupo M.7-M.10, responsables de las salidas 4 a 6, es análogo al anterior, pero utilizando un objeto-puntero op1. Observe que las sentencias M.8-M.10 son paralelas a M.3-M.5. Sin embargo, la inicialización del objeto op1 no puede realizarse igual que con eptr. En efecto, un intento de construir una sentencia paralela a M.2:

ClsPtr op1 = &obj;       // M.7bis Error!!

provocaría un error de compilación: Cannot convert 'Cls *' to 'ClsPtr' in...  La razón es que evidentemente, el tipo del Rvalue; puntero-a-Cls (Cls*) no puede ser convertido al tipo del Lvalue (ClsPtr). Por esta razón, la inicialización de este tipo de objetos es encomendada al constructor, al que se pasa el argumento adecuado.

Finalmente, en M.12 se crea una nueva instancia de ClsPtr utilizando el constructor por defecto. A continuación, el operador de asignación proporcionado por defecto por el compilador, inicia el nuevo objeto con los valores del anterior. La sentencia M.13, responsable de la última salida, es comprobación de que esta asignación miembro a miembro es suficiente en este caso.

§3  Un diseño más general

El diseño anterior adolece de un inconveniente: la clase-puntero, debe ser construida específicamente para servir de puntero a objetos de otra clase (en nuestro ejemplo, ClsPtr solo sirve para señalar objetos de tipo Cls).  Sin embargo, es posible un diseño más general recurriendo al mecanismo de plantillas:

template<class T> class ClPt {     // clase-puntero genérica
  public:
  T* ptr;
  T operator*() { return *ptr; }
  T* operator->() { return ptr; }
  ClPt() { ptr = NULL; }           // constructor
  ClPt(T& obj) { ptr = &obj; }    // constructor
};

Este diseño permite que la clase ClPt pueda ser utilizada como puntero a objetos de cualquier tipo. Por ejemplo, siguiendo con el supuesto anterior:

ClPt<Cls> op1(obj);    // instanciar un objeto-puntero
op1->fe();             // -> Cls x = 1
cout << (*op1).x;      // -> 1

§4  Algunos refinamientos adicionales

Es frecuente que los objetos-puntero sean utilizados para acceder objetos creados con new ( 4.9.20), para lo que se recurre a una invocación implícita o explícita del constructor con los argumentos adecuados.  Por ejemplo:

ClPt<Cls> op1(new Cls);                // invocación implícita

ClPt<Cls> op1 = ClPt<Cls>(new Cls);    // invocación explícita


Las sentencias anteriores nos obligan a definir un nuevo constructor de ClPt que acepte un puntero-a-T como argumento, ya que este es el valor devuelto por new T. En consecuencia el nuevo diseño queda como sigue:

template<class T> class ClPt {    // clase- puntero genérica
  public:
  T* ptr;
  T operator*() { return *ptr; }
  T* operator->() { return ptr; }
  ClPt() { ptr = NULL; }          // constructor
  ClPt(T& obj) { ptr = &obj; }    // constructor

  ClPt(T* pt) { ptr = pt; }       // constructor
};


§4.1
  El hecho de que los objetos creados con new sean referenciados mediante un objeto-puntero, en vez de un puntero básico, puede resolver uno de los principales problemas de los objetos persistentes: que un olvido o circunstancia imprevista origine una pérdida de memoria porque no se invoque la destrucción con delete. Es la situación del siguiente esquema:

func () {

  Cls* eptr = new Cls;

  ...

}

Si olvidamos liberar el espacio previamente asignado con new, al salir eptr de ámbito, tendremos una pérdida permanente de memoria en el montón. Sin embargo, una construcción semejante con un objeto-puntero podría evitarla:

func () {

  ClPt<Cls> op1(new Cls);

  ...

}

Para ello, aprovechando que el compilador invoca automáticamente el destructor del objeto op1 al salir este de ámbito [1], añadimos a la clase-puntero un destructor explícito que sea capaz de destruir el objeto referenciado:

~ClPt() { delete ptr; }    // destructor


A continuación se muestra un ejemplo compilable, junto con las salidas obtenidas. Observe que en el destructor hemos incluido un mecanismo de control para el caso de que el objeto sea un puntero nulo:

#include <iostream>  // Ejemplo-2 objeto-puntero
using namespace std;

struct Cls {         // una clase cualquiera
  int x;
};

template<class T> class ClPt { // clase puntero generica
  public:
  T* ptr; // L.13:
  T operator*() { return *ptr; }
  T* operator->() { return ptr; }
  ClPt() { ptr = NULL; }      // constructor
  ClPt(T& obj) { ptr = &obj; } // constructor
  ClPt(T* pt) { ptr = pt; }   // constructor
  ~ClPt() {                    // destructor
    if (ptr != NULL) {
      delete ptr;
      cout << "Destruido objeto persistente" << endl;
    }
    else
      cout << "Puntero nulo. No destruir objeto" << endl;
  }
};

int main() {         // =================
  {
    ClPt<Cls> op1(new Cls);
    ClPt<Cls> op2;           // M.3:
  }
  return 0;
}

Salida:

Puntero nulo. No destruir objeto
Destruido objeto persistente

Comentario:  Observe que los objetos son destruidos en orden inverso al de creación.

§4.2  Incluir la propiedad del objeto

El diseño anterior adolece todavía de un grave problema: varios objetos-puntero pueden señalar a la misma entidad; al salir de ámbito el primero la destruye, pero el intento de los demás de destruir un objeto inexistente producirá un error de runtime. Para comprobarlo basta modificar la sentencia M.3 del ejemplo anterior:

ClPt<Cls> op2 = op1;   // M.3bis:

Esta modificación produce un error fatal al ejecutar el programa. La salida de ámbito de op2 destruye el objeto creado con new y recupera la memoria asignada, pero el intento posterior de hacer lo mismo por parte de op1 produce un error.

Una forma de evitar este inconveniente, es conseguir que distintos objetos-puntero no puedan señalar simultáneamente a una misma entidad; para esto se utiliza el concepto de "propiedad": si un objeto-puntero señala a un objeto detenta la "propiedad" del mismo, y ningún otro puede señalarlo. Cualquier intento de reasignación origina la pérdida de propiedad del primero en favor del segundo [2].

La forma práctica de conseguirlo es añadir a la clase-puntero un constructor-copia ( 4.11.2d4) que realice el trabajo correspondiente. En nuestro caso tendría el siguiente aspecto:

ClPt(ClPt& obj) {             // constructor-copia
  if (obj.ptr == NULL) {
    ptr = NULL;
  }
  else {
    ptr = obj.ptr;
    obj.ptr = NULL;
  }
}

Podemos comprobar que si el objeto copiado es un puntero nulo, el nuevo puntero también lo es nulo, y ninguno detenta ninguna propiedad. Sería el caso de las sentencias:

ClPt<Cls> op1;
ClPt<Cls> op2 = op1;

En cambio, si el objeto copiado es propietario de un objeto, lo perdería en favor del segundo transformándose en un puntero nulo. Sería el caso de:

ClPt<Cls> op1(new Cls);
ClPt<Cls> op2 = op1;



Generalmente, los objetos-puntero se relacionan con el control de recursos ( 4.1.5a), de forma que el mecanismo de traspaso de propiedad se conoce también como transferencia de recursos ("resource transfer"), ya que los objetos involucrados representan recursos que deben ser adquiridos y posteriormente desechados durante la ejecución del programa.

A continuación se muestra el nuevo diseño en condiciones reales, junto con el resultado obtenido con las sentencias que antes producían error:

#include <iostream>  // Ejemplo-3 objeto-puntero
using namespace std;

struct Cls {         // una clase cualquiera
  int x;
};

template<class T> class ClPt {  // clase puntero generica
  public:
  T* ptr; // L.13:
  T operator*() { return *ptr; }
  T* operator->() { return ptr; }
  ClPt() { ptr = NULL; }        // constructor por defecto
  ClPt(T& obj) { ptr = &obj; }  // constructor
  ClPt(T* pt) { ptr = pt; }     // constructor
  ClPt(ClPt& obj) {             // constructor-copia
    if (obj.ptr == NULL) {
      ptr = NULL;
    }
    else {
      ptr = obj.ptr;
      obj.ptr = NULL;
    }
  }
  ~ClPt() {                    // destructor
    if (ptr != NULL) {
      delete ptr;
      cout << "Destruido objeto persistente\n";
    }
    else cout << "Puntero nulo. No destruir objeto\n";
  }
};

int main() {         // =================
  {
    ClPt<Cls> op1(new Cls);
    ClPt<Cls> op2 = op1;
  }
  return 0;
}

Salida:

Destruido objeto persistente
Puntero nulo. No destruir objeto

Comentario:

Aunque la destrucción de objetos sigue realizándose en orden inverso al de creación, ahora op2 señala a un objeto, mientras que op1 ha pasado a ser un puntero nulo al perder la propiedad del objeto creado con new.


§4.3  Aunque el diseño anterior garantiza la propiedad del puntero para los objetos de nueva creación, todavía pueden presentarse duplicidades si utilizamos el operador de asignación proporcionado por el compilador. Podemos comprobarlo incluyendo una sentencia de asignación después de M3:

  {
    ClPt<Cls> op1(new Cls);
    ClPt<Cls> op2 = op1;
    op1 = op2;
  }
  return 0;

Este nuevo diseño de main produce igualmente un error de ejecución. La causa es que el operador de asignación = proporcionado por el compilador (operador = por defecto) no garantiza unicidad en la propiedad del puntero. Para evitar el inconveniente sobrecargamos el referido operador para los objetos de la clase ClPt de forma adecuada ( 4.9.18a). El nuevo método operator=() presenta el aspecto siguiente:

ClPt& operator= (ClPt& obj) {   // Operador de asignacion
  if (ptr != NULL) delete ptr;  // L.1:
  if (obj.ptr == NULL) {
    ptr = NULL;
  }
  else {
    ptr = obj.ptr;
    obj.ptr = NULL;
  }
  return *this;
}

Como primera medida (L.1) si el objeto actual es propietario de un objeto, se procede a su destrucción. A continuación vienen las precauciones habituales: si el Rvalue es un puntero nulo, el Lvalue también lo será. En caso contrario, el Lvalue se hace con la propiedad y el Rvalue queda reducido a un puntero nulo.

Observe que esta definición de operator=() se aparta de la convencional, en la que se pasa una referencia constante:

ClaseX& operator= (const ClaseX& obj);

La razón es que en este caso, una asignación del tipo obj1 = obj2; además de una modificación del Lvalue, puede producir también una alteración del Rvalue.

§5 Corolario

Por razón de su comportamiento, los objetos-puntero así definidos, son denominados también punteros inteligentes. La Librería Estándar C++ presenta una versión directamente utilizable bajo la forma de la clase genérica auto_ptr ( 4.12.2b1), que incluye algunos refinamientos adicionales sobre los señalados.

  Inicio.


[1] También es invocado el destructor si el objeto es desalojado de la pila porque se ha producido una excepción ("Stack unwinding" 1.6).

[2] Otra forma de conseguirlo se denomina contador de referencias ("Reference counting"). En esta técnica se ha eliminado el concepto de propiedad, de forma que varios objetos-puntero pueden señalar al mismo objeto persistente. Para evitar que cuando uno de estos objetos salga de ámbito destruya el objeto, si existen otros que todavía lo señalan (lo que produciría punteros descolgados), el objeto referenciado mantiene un contador de los punteros que lo referencian. Cuando un objeto-puntero sale de ámbito, su destructor consulta el contador y lo disminuye en una unidad. Solo si el contador llega a cero, el objeto referenciado es destruido. El sistema es aplicado en muchos otros ámbitos. Por ejemplo, los SO Windows mantiene un mecanismo análogo para saber si una .DLL puede ser eliminada al eliminar una aplicación que la utiliza.