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-2  Punteros internos a miembros normales ( no-estáticos)

I think all C++ tutorials should be required by law to include a special page: At the top of this page would be a large icon of a chain saw.  Underneath, in 24-point block type it should say, "Warning: The Software General of the United States has determined that putting pointers in C++ classes can be hazardous to your health unless you fully understand Constructors, Destructors, Copy Constructors, and Assignment Operators".  David Weber  "Two C++ tutorials" C/C++ Users Journal.  Marzo 1996.

§3  Punteros internos

Ilustraremos este apartado con sendos ejemplos, en los que se aprecia la sintaxis utilizada en la declaración; asignación, y posterior referencia para su uso de punteros internos (uso que generalmente se concreta en acceder al miembro -propiedad o método- señalado). Observe con especial atención las formas de declaración y asignación de los estos punteros, así como la invocación de los referentes (miembros correspondientes) a través de ellos.

§3.1  Ejemplo-1

El programa solo pretende mostrar las posibilidades sintácticas y notacionales de este tipo de punteros, por lo que es extremadamente simple.  La clase incluye dos funciones: fun y fan; dos enteros y una matriz de caracteres. El resto de miembros son punteros a objetos de distinto tipo. Algunos de estos punteros señalan a miembros de la propia clase, aunque para mostrar las diferencias en la notación, se incluyen también miembros que son punteros a objetos externos a la clase.

Con objeto de mostrar que no hay posibilidad de confusión si se utilizan los descriptores adecuados, los nombres de los enteros y de las funciones miembro se han hecho deliberadamente iguales a las funciones y enteros del espacio global del fichero (que en este caso coincide con el del programa, dado que este ocupa un solo fuente). Se ha incluido también un constructor explícito que proporciona una inicialización adecuada a los objetos.

El cuerpo de main se reduce casi exclusivamente a una serie de salidas para ilustrar la forma de acceder a los punteros y a los objetos señalados por estos. Esta salidas utilizan todos los punteros y en algunos casos se realizan por duplicado, utilizando los dos objetos  c1 y c2, que se instancian en M.1. Su objeto es mostrar las diferencias, en su caso, entre los valores correspondientes a ambos objetos.


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

void fun () { cout << "Funcion externa-1" << endl; }
void fan () { cout << "Funcion externa-2" << endl; }
int x = 103, y = 301;
char achar[5];

class C {
  public:
  int x, y;               // miembro int
  char achar[5];          // miembro array de char
  void fun ();            // miembro funcion (metodo)
  void fan ();            // miembro funcion (metodo)
  int* pint1;             // L.16 miembro puntero-a-int
  int C::* pint2;         // L.17 miembro puntero-a-miembro-int
  char (*pachar1)[5];     // L.18 miembro puntero-a-matriz de char
  char (C::* pachar2)[5]; // L.19 miembro puntero-a-miembro-matriz de char
  void (*pfe)();          // L.20 miembro puntero-a-funcion externa
  void (C::* pfm)();      // L.21 miembro puntero-a-funcion miembro
  C (int n = 0) {         // constructor por defecto
    x = n; y = 2*n;
    achar[0] = 'A'; achar[1] = 'E'; achar[2] = 'I'; achar[3] = 'O'; achar[4] = 'U';
    pint1 = &::x;         // L.25
    pint2 = &C::x;        // L.26
    pachar1 = &::achar;   // L.27
    pachar2 = &C::achar;  // L.28
    pfe = &::fun;         // L.29
    pfm = &C::fun;        // L.30
  }
};
void C::fun() { cout << "Funcion interna-1. x ==" << x << endl; }
void C::fan() { cout << "Funcion interna-2. x ==" << x << endl; }

int main (void) {         // ========================
  C c1(10), c2(13);       // M.1: Instancias de C
  achar[0]='a'; achar[1]='e'; achar[2]='i'; achar[3]='o'; achar[4]='u';
  cout << "1.1- c1.pint1       == " << c1.pint1       << endl;
//cout << "1.2- c1.pint2       == " << c1.pint2     M.4  ERROR
  cout << "2.1- *(c1.pint1)    == " << *(c1.pint1)    << endl;
  cout << "2.2- c1.*c1.pint2   == " << c1.*c1.pint2   << endl;
  cout << "3.1- c1.pachar1     == " << c1.pachar1     << endl;
//cout << "3.2- c1.pachar2     == " << c1.pachar2   M.8  ERROR
  cout << "4.1- *(c1.pachar1)  == " << *(c1.pachar1)  << endl;
  cout << "4.2- c1.*c1.pachar2 == " << c1.*c1.pachar2 << endl;
  cout << "5.1- Invocar c1.pfe == "; c1.pfe();
  cout << "5.2- Invocar c1.pfm == "; (c1.*c1.pfm)();
  cout << "Algunas modificaciones runtime -----------------\n";
  c2.pint1 = &y;          // M.14
  cout << "6.1- *(c1.pint1) == " << *(c1.pint1) << endl;
  cout << "6.2- *(c2.pint1) == " << *(c2.pint1) << endl;
  c2.pint2 = &C::y;       // M.17
  cout << "7.1- c1.*c1.pint2 == " << c1.*c1.pint2 << endl;
  cout << "7.2- c2.*c2.pint2 == " << c2.*c2.pint2 << endl;
  c1.pfe = &fan;          // M.20
  cout << "8.1- Invocar c1.pfe == "; c1.pfe();
  cout << "8.2- Invocar c2.pfe == "; c2.pfe();
  c1.pfm = &C::fan;       // M.23
  cout << "9.1- Invocar c1.pfm == "; (c1.*c1.pfm)();
  cout << "9.2- Invocar c2.pfm == "; (c2.*c2.pfm)();
  return 0;
}

Salida:

1.1- c1.pint1      == 0041A178
2.1- *(c1.pint1)   == 103
2.2- c1.*c1.pint2  == 10
3.1- c1.pachar1    == 00420670
4.1- *(c1.pachar1) == aeiou
4.2- c1.*c1.pachar2 == AEIOU
5.1- Invocar c1.pfe == Funcion externa-1
5.2- Invocar c1.pfm == Funcion interna-1. x ==10
Algunas modificaciones runtime -----------------
6.1- *(c1.pint1) == 103
6.2- *(c2.pint1) == 301
7.1- c1.*c1.pint2 == 10
7.2- c2.*c2.pint2 == 26
8.1- Invocar c1.pfe == Funcion externa-2
8.2- Invocar c2.pfe == Funcion externa-1
9.1- Invocar c1.pfm == Funcion interna-2. x ==10
9.2- Invocar c2.pfm == Funcion interna-1. x ==13

Comentario:

Nota: respecto a la utilización de estos ejemplos con el compilador MS VC++ 6.0 ver  " Notas particulares" ( 4.2.1g1.3)

En las líneas L.16/21 del cuerpo de la clase se realiza la declaración de los miembros que son punteros. Las declaraciones L.16/18/20 no ofrecen ninguna peculiaridad, se declaran miembros que son punteros a objetos normales (no son miembros de clase) y su declaración es idéntica a la que se usaría con punteros del espacio global. En cambio, sí merece destacarse la sintaxis utilizada en L.17/19/21 donde se declaran miembros que son punteros-a-miembros.

Las asignaciones se realizan en las líneas L25/30 del constructor. Nótese que las asignaciones L.25/27/29 de los punteros que señalan a objetos "normales", utilizan el operador :: de resolución de ámbito sin ningún prefijo para referirse al espacio global de nombres ( 4.1.11c).

La asignación de L.30 es digna de mención, pues utiliza explícitamente el identificador cualificado ( 4.1.11c) del método cuya dirección se asigna al puntero. Pemos afirmar que se trata de una verdadera excepción en la sintaxis y semántica del lenguaje. No se cumple la regla general de que el identificador de la función es equivalente a su dirección ( 4.2.4a). Es decir, la expresión

pfm = &C::fun;      // L.30

no puede ser sustituida [5] por

pfm = C::fun;


Las salidas 1.1 y 3.1 muestran el valor de los miembros pint1 y pachar1 del objeto c1, utilizando el selector directo de miembro . sobre el objeto. Por ejemplo: c1.pint1. Puesto que ambos son punteros, estos valores son direcciones de memoria y como es tradición en C++, se muestra en hexadecimal ( 2.2.4b). Observe que un intento análogo para mostrar los valores de los punteros-a-miembros. Por ejemplo, c1.pint2, conduce a error [4] (líneas M.4 y M.8).

Los grupos de salidas 2 y 4 muestran el valor de los objetos señalados por los punteros. Es muy notable la diferente sintaxis empleada según se trate de un puntero a objeto externo o a miembro:

*(c1.pint1)
c1.*c1.pint2

En mi opinión, uno de esos casos que justifican quizás la "mala fama" del C++ [1]. Observe que esta diferencia se mantiene en todas las expresiones de salida que utilizan el valor del puntero, incluyendo las invocaciones a funciones-miembro (salidas 5, 8 y 9).

Las salidas 5.1, 8.1 y 8.2, corresponden a invocaciones de las funciones externas a través de sus punteros. La única singularidad a resaltar aquí es que la invocación utiliza los identificadores de los punteros en sustitución de los identificadores de las funciones ( 4.2.4b). Recuerde que las siguientes sentencias son equivalentes ( Nota):

cout << "Invocar c1.pfe == "; c1.pfe();
cout << "Invocar c1.pfe == "; (*c1.pfe)();
cout << "Invocar c1.pfe == "; (*(c1.pfe))();


Su contrapartida son las salidas 5.2,  9.1 y 9.2, que representan la invocación de las funciones-miembro mediante sus punteros. Tenga en cuanta que en este caso  (c1.*c1.pfm)(); es la simplificación de (c1.*(c1.pfm))(); una vez eliminado el paréntesis interno, y no es equivalente a  c1.*c1.pfm();:

cout << " Invocar c1.pfm == "; (c1.*c1.pfm)();   // Ok.
cout << " Invocar c1.pfm == "; c1.*c1.pfm();     // ERROR!!

Nota: la eliminación del paréntesis interior es posible porque el operador deferencia de punteros-a-miembros de clase .*, tiene una precedencia menor que la del selector directo ..

Las sentencias M.14/17/20/23 muestran la forma de modificar estos miembros de objetos (punteros). Las salidas que les siguen muestran el resultado de los cambios. Es digno de mención que un valor como c2.pint2 no pueda ser mostrado ni almacenado , sin embargo si puede ser asignado:

c2.pint2 = &C::y;       // M.17

§3.2  Ejemplo-2

Presentamos aquí una versión prácticamente idéntica al ejemplo anterior, aunque con pequeñas modificaciones. Las salidas son básicamente las mismas, pero se han trasladado desde el cuerpo de main a una función auxiliar fs, ya que se pretende mostrar la sintaxis cuando el acceso se realiza a través de un puntero a la clase (en realidad un puntero al objeto).

Observe que algunos accesos utilizan una doble indirección: primero se accede al objeto a través de un puntero; después se accede al miembro a través de otro.


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

void fun () { cout << "Funcion externa-1" << endl; }
void fan () { cout << "Funcion externa-2" << endl; }
int x = 103, y = 301;
char achar[5];

class C {
  public:
  int x, y;               // miembro int
  char achar[5];          // miembro array de char
  void fun ();            // miembro funcion (metodo)
  void fan ();            // miembro funcion (metodo)
  int* pint1;             // L.16 miembro puntero-a-int
  int C::* pint2;         // L.17 miembro puntero-a-miembro-int
  char (*pachar1)[5];     // L.18 miembro puntero-a-matriz de char
  char (C::* pachar2)[5]; // L.19 miembro puntero-a-miembro-matriz de char
  void (* pfe)();         // L.20 miembro puntero-a-funcion externa
  void (C::* pfm)();      // L.21 miembro puntero-a-funcion miembro
  C (int n = 0) {         // constructor por defecto
    x = n; y = 2*n;
    pint1 = &::x;           // L.24
    pint2 = &C::x;
    achar[0]='A'; achar[1]='E'; achar[2]='I'; achar[3]='O'; achar[4]='U';
    pachar1 = &::achar;
    pachar2 = &C::achar;
    pfe = &::fun;           // L.28
    pfm = &C::fun;          // L.29
  }
};
void C::fun() { cout << "Funcion interna-1. x ==" << x << endl; }
void C::fan() { cout << "Funcion interna-2. x ==" << x << endl; }
void fs(C*);                // funcion de salidas

int main (void) {           // ========================
  achar[0]='a'; achar[1]='e'; achar[2]='i'; achar[3]='o'; achar[4]='u';
  C* pc1 = new C(10);       // M.2
  fs(pc1);
  return 0;
}

void fs(C* cpt) {
  cout << "1.1- cpt->pint1         == " << cpt->pint1       << endl;
//cout << "1.2- cpt->pint2         == " << cpt->pint2;      ERROR
  cout << "2.1- *(cpt->pint1)      == " << *(cpt->pint1)    << endl;
  cout << "2.2- cpt->*cpt->pint2   == " << cpt->*cpt->pint2 << endl;
  cout << "3.1- cpt->pachar1       == " << cpt->pachar1     << endl;
//cout << "3.2- cpt->pachar2       == " << cpt->pachar2;    ERROR
  cout << "4.1- *(cpt->pachar1)    == " << *(cpt->pachar1)  << endl;
  cout << "4.2- cpt->*cpt->pachar2 == " << cpt->*cpt->pachar2 << endl;
  cout << "5.1- Invocar cpt->pfe == "; cpt->pfe();
  cout << "5.2- Invocar cpt->pfm == "; (cpt->*(cpt->pfm))();
  cout << "Algunas modificaciones runtime -----------------\n";
  cpt->pint1 = &y;               // FS.12
  cout << "6.1- *(cpt->pint1)    == " << *(cpt->pint1)    << endl;
  cpt->pint2 = &C::y;            // FS.14
  cout << "7.1- cpt->*cpt->pint2 == " << cpt->*cpt->pint2 << endl;
  cpt->pfe = &fan;               // FS.16
  cout << "8.1- Invocar cpt->pfe == "; cpt->pfe();
  cpt->pfm = &C::fan;            // FS.18
  cout << "9.1- Invocar cpt->pfm == "; (cpt->*cpt->pfm)();
}

Salida:

1.1- cpt->pint1         == 0041A178
2.1- *(cpt->pint1)      == 103
2.2- cpt->*cpt->pint2   == 10
3.1- cpt->pachar1       == 00420648
4.1- *(cpt->pachar1)    == aeiou
4.2- cpt->*cpt->pachar2 == AEIOU
5.1- Invocar cpt->pfe == Funcion externa-1
5.2- Invocar cpt->pfm == Funcion interna-1. x ==10
Algunas modificaciones runtime -----------------
6.1- *(cpt->pint1)    == 301
7.1- cpt->*cpt->pint2 == 20
8.1- Invocar cpt->pfe == Funcion externa-2
9.1- Invocar cpt->pfm == Funcion interna-2. x ==10

Comentario:

Las salidas se han trasladado desde main a la función fs, y son simétricas a las del ejemplo anterior. Observe que las instrucciones también guardan una simetría. Simplemente se ha sustituido el prefijo c1., objeto + selector directo de miembro ( 4.9.16) por el prefijo cpt->, puntero + selector indirecto ( 4.9.16). Incluso se han mantenido algunas sentencias que producen error, para recordar que no es posible obtener el valor de uno de estos punteros [4].

Recuerde que, en C++, ->* es un solo operador (indirección de punteros a punteros a miembros) con precedencia menor que la del selector indirecto -> ( 4.9.0a), de forma que la salida 5.2 puede ser expresada mediante:

cout << "5.2.- Invocar cpt->pfm == "; (cpt->*cpt->pfm)();


Observe también que al ser pfe un puntero-a-función, las invocación que podríamos llamar "ortodoxa" en las salidas 5.1 y 8.1 adoptaría la forma:

cout << "5.1.- Invocar cpt->pfe == "; (*(cpt->pfe))();

Sin embargo, por las razones ya comentadas, las que siguen son equivalentes:

cout << "5.1.- Invocar cpt->pfe == "; (*(cpt->pfe))();
cout << "5.1.- Invocar cpt->pfe == "; (cpt->pfe)();
cout << "5.1.- Invocar cpt->pfe == "; cpt->pfe();


Es digno de mención que el objeto instanciado en M.2 carece de identificador. En realidad solo se conoce su puntero pc1 (que es pasado como argumento a la función de salida fs). Todas las expresiones de acceso utilizadas en las salidas comienzan con una indirección de este puntero para acceder al objeto; a continuación se accede directamente al miembro. En los casos en que este es a su vez un puntero (salidas 2, 4, 6 y 7), se hace necesaria una nueva indirección para acceder al objeto final.

Las asignaciones FS12/14/16/18 se corresponden con M14/17/20/23 del ejemplo anterior, y muestran la forma de modificar los miembros de instancia a través del puntero al objeto.

§3.3  Utilización de los miembros-puntero

Los miembros de clases que son a su vez punteros, se prestan a las mismas técnicas y aplicaciones que los punteros normales, aunque con el coste de eficiencia ya señalado ( 4.2.1g1). Entre los muchos ejemplos que podrían plantearse, supongamos que una clase CTransporte representa el costo de envío de los productos de un fabricante. Deseamos obtener el costo mediante invocación de uno de sus métodos en función del peso total del envío, pero los métodos utilizados varían. Por ejemplo, para menos de 1 Kg se utiliza correo ordinario; para 1 a 10 Kg. mensajería normal; para 10 a 20 Kg agencia de transporte exterior, y para más de 20 Kg medios propios. Cada una de estas formas de envío tiene distintas formas de cálculo de costo, por lo que utiliza métodos distintos.

Una posibilidad sería diseñar distintos métodos que representaran las distintas formas de cálculo de costo, utilizando un puntero-a-método para acceder a la función correspondiente. El puntero es iniciado con la dirección del método adecuado en el momento de la construcción del objeto en función la clase de envío:

class CTransporte {
  private:
  float correo(float);
  float mensajeria(float);
  float agencia(float);
  float propio(float);
  public:
  float (CTransporte::* costo)(float); // puntero
  CTransporte(float);                  // constructor
};
...
float CTransporte::correo (float peso) { /* ... */ };
float CTransporte::mensajeria (float peso) { /* ... */ };
float CTransporte::agencia (float peso) { /* ... */ };
float CTransporte::propio (float peso) { /* ... */ };
...
CTransporte::CTransporte(float peso) {
   if (peso < 1)       costo = &CTransporte::correo;
   else if (peso < 10) costo = &CTransporte::mensajeria;
   else if (peso < 20) costo = &CTransporte::agencia;
   else                costo = &CTransporte::propio;
}
 
float costoExp(float exp[], int items) {  // calcula el costo de expedicion
  float cTot = 0;
  for (int item = 0; item < items; item++) {
    CTransporte ct(exp[item]);
    cTot += (ct.*ct.costo)(exp[item]);
  }
  return cTot;
}

Comentario:

La función costoExp recibe una matriz de float que contiene los pesos de los paquetes que componen una expedición, y un int que indica el número de paquetes que la componen. El argumento items puede calcularse mediante la expresión:

int items = sizeof exp / sizeof exp[0];

En la página adjunta se muestra una versión ejecutable del mismo ejemplo ( Ejemplo).

  Inicio.


[1]  Tal vez se puede reconocer una cierta "lógica" a la sintaxis utilizada, pero encontrar una expresión como esta procediendo "por tanteos", no parece una opción razonable para la utilización de un lenguaje. En mi caso, debo agradecer al Sr. Stroustrup la clarificación al respecto.

[4]  TC++PL  Stroustrup §15.5:  "No es posible almacenar el valor de un puntero a miembro para uso posterior".  En otra obra: Stroustrup & Ellis,  ACRM  §5.5.  El autor reconoce que desde luego, podría haberse generalizado la noción de puntero-a-miembro, de forma que pudieran ser almacenados y tratados en general como objetos de primera clase. Pero que esto supondría abrir una puerta que conduciría a técnicas de implementación alejadas del ámbito C tradicional. En consecuencia, se decidió limitar las posibilidades de este tipo de objetos relegándolos a una existencia de "segunda clase", de forma que solo puede utilizarse el valor al que señalan (su deferencia) si señalan a propiedades, o invocarlos si señalan a métodos.

[5]  Desde luego, el "identificador cualificado" no es equivalente a la dirección de la función. La razón de esta singularidad es desvelada por el creador del lenguaje en contestación a una consulta de Christopher Skelly [5a]: "Only &X::f yields a pointer to member (see the reference manual under &). The reason is that in a member function of X, X::f means this->X::f and I didn't want X::f to mean one thing in one context and something radically different in another".

[5a]  "Powerful Pointers To Member Functions" por Christopher Skelly.  C/C++ Users Journal Volume 12 Number 10 Octubre 1994.