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.2.1g1 Punteros a miembros normales ( no-estáticos)

Nota: a lo largo del presente capítulo trataremos los aspectos de notación con el mayor detalle, aunque podemos adelantar que, al constituir las clases un subespacio o ámbito de nombres con ciertas peculiaridades, el manejo de punteros a sus miembros necesariamente debe tener en cuenta esta circunstancia, por lo que su notación es muy parecida a la utilizada para acceder a miembros de subespacios ( 4.1.11c). También es importante señalar que los punteros-a-miembros no estáticos no pueden ser accedidos fuera de su espacio de direcciones.

§1 Introducción

En principio, si consideramos que un miembro de clase (variable o función) es como otro objeto cualquiera del universo C++, la idea de un puntero que señale a una de estas entidades, no debería ser especialmente difícil. Sin embargo, veremos que el concepto implica algunas singularidades que intentaremos mostrar en el siguiente ejemplo:

class C {
  int i;             // miembro entero
  int* mpi;          // miembro puntero-a-entero
  C(int n) {         // constructor
    i = n;
    mpi = &i;        // el puntero señala a un miembro
  } 
};
...
int C::* pmi = &C::i;   // puntero-a-miembro entero-de-C.


Aquí existen sendos objetos que son punteros-a-miembro: el primero, mpi, es un puntero-a-miembro; a su vez él mismo es miembro de la clase. A este tipo lo denominamos puntero interno, para distinguirlo de los que como pmi, son también punteros-a-miembro pero no pertenecen a la clase, y que denominamos externos. Si observamos la notación empleada en este último, puede comprobarse que pmi es un puntero-a-entero de un tipo especial; destinado a señalar no a un entero cualquiera, sino a un int del "subespacio" C. Observe que pmi no se inicia con ninguna propiedad de instancia concreta (del tipo &cj.i), sino de forma genérica, con la dirección de una propiedad de clase (&C::i).

Llegados a este punto cabría preguntarse: puesto que pmi es un objeto único, no es una matriz de punteros, ni tampoco un miembro de clase como mpi ¿Como puede señalar en un momento dado a la propiedad c1.i y al mismo tiempo a la propiedad c2.i? Desde luego parece una situación paradójica que podríamos sintetizar en el siguiente código:

C c1 = C(1);
C c2 = C(2);
// Cual es ahora el valor *pmi 1 o 2?

Fig. 1


La solución
al interrogante, y base para comprender el mecanismo de estos punteros, estriba en la propia estructura interna de los objetos C++. En realidad las instancias de clase son una especie de estructuras (en el sentido C del término) en las que no están presentes las funciones miembro ni las variables o constantes estáticas (que tienen su existencia en el objeto-clase 4.11.5). En el caso más general, en que se heredan propiedades de otros ancestros ( 4.11.2c), un objeto está constituido por la yuxtaposición de subobjetos que contienen las propiedades heredadas de sus bases directas mas sus propios miembros privativos si los hubiere. La figura 1muestra la disposición en memoria de varios objetos cuando una clase D deriva de otra C que deriva a su vez de una superclase B:

class B { /* ... */ };
class C : public B { /* ... */ };
class D : public C { /* ... */ };
...
B b;         // Objeto B
C c;         // Objeto C
D d;         // Objeto D


El objeto D es una zona contigua de memoria que comienza en un punto señalado por el puntero this ( 4.11.6). Cada subobjeto empieza en un punto, cuyo desplazamiento delta, es conocido por el compilador. A su vez, cada miembro de un subobjeto, incluyendo el subobjeto dominante (parte privativa de D), ocupa una posición señalada por un desplazamiento offset respecto al anterior, que también es conocido por el compilador.

Esto hace que los punteros a propiedades no-estáticas no almacenen una dirección determinada de memoria, como es el caso en los punteros a variables "normales".  En realidad, un puntero como pmi a una propiedad i de una clase C, contiene un valor que es el desplazamiento delta + offset del miembro i respecto de la estructura de elementos que comienza en el punto señalado por this. En consecuencia, no tiene sentido hablar del objeto señalado ( *pmi ) o del valor ( pmi ) aislado, y los punteros a miembros no estáticos no son considerados como auténticos punteros a objetos [1], ya que solo tienen significado cuando se refieren al miembro de un objeto c específico *(c.p), en cuyo caso el compilador obtiene la dirección concreta, sumando el desplazamiento p = delta + offset del miembro a la dirección this de inicio del objeto.

Nota: en la página adjunta se añaden algunos comentarios adicionales sobre el particular ( Nota)

Este artificio es ejecutado automáticamente por el compilador, de forma que parece "como si" existiera una versión de pmi para cada instancia de la clase, aunque a costa de una cierta complejidad en el mecanismo subyacente. Complejidad que se incrementa extraordinariamente en los casos de herencia simple o múltiple, que se traduce en una menor eficiencia del código resultante.

Puede afirmarse que, exceptuando cuestiones de herencia, el comportamiento de los punteros-a-miembro internos y externos es muy parecido, por no decir idéntico. Con la diferencia de que el puntero interno-a-miembro es una propiedad de la clase con todos los derechos, mientras que el externo se comporta como tal, pero no pertenece a la clase.


En el ejemplo anterior serían posibles expresiones como c1.mpi y *(c1.mpi), mientras que un intento similar con punteros externos (del tipo c1.pmi) conducirían a un error de compilación: 'pmi' is not a member of 'C' in.... En este caso, la sintaxis correcta sería c1.*pmi.

Estas expresiones pueden interpretarse como sigue:

c1.mpi Valor del miembro mpi del objeto c1. Puesto que mpi es un puntero, contiene una dirección de memoria.
*(c1.mpi) Contenido de la dirección de memoria señalada por el puntero-miembro. Puesto que es un puntero-a-miembro, contiene el valor de otro miembro de la instancia c1.
c1.pmi Error. Expresión no permitida: pmi no es miembro de C. Recuerde que el selector directo de miembro . es un operador binario que exige que el operando situado a la derecha sea miembro de la clase, estructura, o unión señalada por el operando de la izquierda.
c1.*pmi Contenido de la dirección de memoria obtenida sumando el desplazamiento mpi a la dirección del objeto c1. Es el valor de un miembro del objeto c1.


Como colofón de lo anterior, podemos resumir que existen dos tipos de punteros-a-miembros no estáticos: internos ( 4.2.1g1-2) y externos ( 4.2.1g1-3). En ambos casos la funcionalidad es idéntica, aunque la mecánica de funcionamiento es totalmente distinta (distinta también según que las entidades señaladas sean propiedades o métodos). Aunque el funcionamiento interno es controlado por el compilador de forma transparente, veremos a continuación que el programador debe tener en cuenta algunas diferencias sintácticas en la utilización de unos y otros.

§2 Declaración

La declaración de punteros-a-miembros no estáticos sigue las reglas que cabría esperar, considerando que referencian objetos del subespacio determinado por la clase. La sintaxis de declaración es la siguiente:

<tipo_objeto> <nombre_clase>::* <etiqueta_puntero>;

  <tipo_objeto> es el tipo de miembro al que señalará el puntero.

  <nombre_clase> es la clase a que pertenece el miembro señalado

  <etiqueta_puntero> es el identificador (nombre) del puntero.

Ejemplo:

class C {
  int x;
  int C::* iptr;      // declara puntero-a-miembro
  C (int p=0) {       // constructor
    x = p;
    iptr = &C::x;     // inicia puntero
  }
}
int C::* iptr = &C::x; // declara e inicia puntero


En el ejemplo se han declarado dos punteros-a-miembro; uno es interno y el otro externo. Observe que no existe colisión de nombres porque ambos pertenecen a espacios distintos. Observe también que el miembro C::iptr no puede ser iniciado en el mismo punto de su declaración, debe hacerse en el constructor.

Conviene recordar aquí lo indicado al tratar de la declaración de punteros ( 4.2.1a) donde señalamos que existen tantos tipos de punteros como tipos de objetos pueden ser señalados. Por ejemplo:

int* iptr;
char* cptr;

los tipos de iptr y cptr son distintos (int* y char* respectivamente).

Con los punteros-a-miembro ocurre lo mismo, con la salvedad de que dos punteros señalando al mismo tipo de miembro de dos clases distintas, tienen tipos distintos. Por ejemplo:

int A::* ipt1 = &A::x;
int B::* ipt2 = &B::x;

los tipos de ipt1 y cpt2 son distintos (son respectivamente int A::* y int B::*). Por supuesto, el valor devuelto al deferenciar ambos punteros sería un int en los dos casos.

  Inicio.


[1]   Stroustrup & Ellis: ACRM §3.6.2