Un puntero es una variable que contiene la dirección de alguna ubicación. Se puede cambiar a lo que el puntero apunta en tiempo de ejecución. La ubicación a la que apunta puede ser un dato o función. El puntero a miembro de C++ sigue el mismo concepto, excepto que a lo que apunta es una ubicación dentro de una clase. Pero surge el dilema de que un puntero necesita una dirección, pero no hay «dirección» alguna dentro de una clase; La selección de un miembro de una clase se realiza mediante un desplazamiento dentro de la clase. Pero primero hay que conocer la dirección donde comienza un objeto en particular para luego sumarle el desplazamiento y así localizar el miembro de la clase. La sintaxis de los punteros a miembros requiere que usted seleccione un objeto al mismo tiempo que está accediendo al contenido del puntero al miembro.
Para entender esta sintaxis, considere una estructura simple,
con un puntero sp
y un objeto
so
. Puede seleccionar sus miembros de la
misma manera que en el siguiente ejemplo:
//: C11:SimpleStructure.cpp struct Simple { int a; }; int main() { Simple so, *sp = &so; sp->a; so.a; } ///:~
Listado 11.11. C11/SimpleStructure.cpp
Ahora suponga que tiene un puntero normal que se llama
ip
y que apunta a un entero. Para acceder a
lo que apunta ip
, ha de estar precedido por
un '*':
*ip=4;
Finalmente, se preguntará qué pasa si tiene un puntero que está apuntando a algo que está dentro de un objeto, incluso si lo que realmente representa es un desplazamiento dentro del objeto. Para acceder a lo que está apuntando, debe preceder el puntero con '*'. Pero como es un desplazamiento dentro de un objeto, también ha de referirse al objeto con el que estamos tratando. Así, el * se combina con el objeto. Por tanto, la nueva sintaxis se escribe ->* para un puntero que apunta a un objeto, y .* para un objeto o referencia, tal como esto:
objectPointer->*pointerToMember = 47; object.*pointerToMember = 47;
Pero, ¿cuál es la sintaxis para definir el
pointerToMember
? Pues como cualquier puntero,
tiene que decir el tipo al que apuntará, por lo que se
utilizaría el '*' en la definición. La única diferencia es que
debe decir a qué clase de objetos apuntará ese atributo
puntero. Obviamente, esto se consigue con el nombre de la clase
y el operador de resolución de ámbito. Así,
int ObjectClass::*pointerToMember;
define un atributo puntero llamado
pointerToMember
que apunta a cualquier entero
dentro de ObjectClass
. También puede
inicializar el puntero cuando lo defina (o en cualquier otro
momento):
int ObjectClass::*pointerToMember = &ObjectClass::a;
Realmente no existe una «dirección» de
ObjectClass::a
porque se está refiriendo a la clase
y no a un objeto de esa clase. Así,
&ObjectClass::a
se puede utilizar sólo con la
sintaxis de un puntero a miembro.
He aquí un ejemplo que muestra cómo crear y utilizar punteros a atributos:
//: C11:PointerToMemberData.cpp #include <iostream> using namespace std; class Data { public: int a, b, c; void print() const { cout << "a = " << a << ", b = " << b << ", c = " << c << endl; } }; int main() { Data d, *dp = &d; int Data::*pmInt = &Data::a; dp->*pmInt = 47; pmInt = &Data::b; d.*pmInt = 48; pmInt = &Data::c; dp->*pmInt = 49; dp->print(); } ///:~
Listado 11.12. C11/PointerToMemberData.cpp
Obviamente, son muy desagradables de utilizar en cualquier lugar excepto para caso especiales (que es exactamente para lo que se crearon).
Además, los punteros a miembro son bastante limitados: pueden asignarse solamente a una ubicación específica dentro de una clase. No podría, por ejemplo, incrementarlos o compararlos tal como puede hacer con punteros normales.
Un ejercicio similar se produce con la sintaxis de puntero a miembro para métodos. Un puntero a una función (presentado al final del Capítulo 3) se define como:
int (*fp)(float);
Los paréntesis que engloban a (*fb)
son
necesarios para que fuercen la evaluación de la definición
apropiadamente. Sin ellos sería una función que devuelve un
int*
.
Los paréntesis también desempeñan un papel importante cuando se definen y utilizan punteros a métodos. Si tiene una función dentro de una clase, puede definir un puntero a ese método insertando el nombre de la clase y el operador de resolución de ámbito en una definición habitual de puntero a función:
//: C11:PmemFunDefinition.cpp class Simple2 { public: int f(float) const { return 1; } }; int (Simple2::*fp)(float) const; int (Simple2::*fp2)(float) const = &Simple2::f; int main() { fp = &Simple2::f; } ///:~
Listado 11.13. C11/PmemFunDefinition.cpp
En la definición de fp2
puede verse que el
puntero a un método puede inicializarse cuando se crea, o en
cualquier otro momento. A diferencia de las funciones no son
miembros, el &
no
es opcional para obtener la dirección de un método. Sin
embargo, se puede dar el identificador de la función sin la
lista de argumentos, porque la sobrecarga se resuelve por el
tipo de puntero a miembro.
Lo interesante de un puntero es que se puede cambiar el valor del mismo para apuntar a otro lugar en tiempo de ejecución, lo cual proporciona mucha flexibilidad en la programación porque a través de un puntero se puede cambiar el comportamiento del programa en tiempo de ejecución. Un puntero a miembro no es distinto; le permite elegir un miembro en tiempo de ejecución. Típicamente, sus clases sólo tendrán métodos visibles públicamente (los atributos normalmente se consideran parte de la implementación que va oculta), de modo que el siguiente ejemplo elige métodos en tiempo de ejecución.
//: C11:PointerToMemberFunction.cpp #include <iostream> using namespace std; class Widget { public: void f(int) const { cout << "Widget::f()\n"; } void g(int) const { cout << "Widget::g()\n"; } void h(int) const { cout << "Widget::h()\n"; } void i(int) const { cout << "Widget::i()\n"; } }; int main() { Widget w; Widget* wp = &w; void (Widget::*pmem)(int) const = &Widget::h; (w.*pmem)(1); (wp->*pmem)(2); } ///:~
Listado 11.14. C11/PointerToMemberFunction.cpp
Por supuesto, no es razonable esperar que el usuario casual
cree expresiones tan complejas. Si el usuario necesita
manipular directamente un puntero a miembro, los
typedef
vienen al rescate. Para dejar aún mejor las
cosas, puede utilizar un puntero a función como parte del
mecanismo interno de la implementación. He aquí un ejemplo
que utiliza un puntero a miembro dentro
de la clase. Todo lo que el usuario necesita es pasar un
número para elegir una función.
[69]
//: C11:PointerToMemberFunction2.cpp #include <iostream> using namespace std; class Widget { void f(int) const { cout << "Widget::f()\n"; } void g(int) const { cout << "Widget::g()\n"; } void h(int) const { cout << "Widget::h()\n"; } void i(int) const { cout << "Widget::i()\n"; } enum { cnt = 4 }; void (Widget::*fptr[cnt])(int) const; public: Widget() { fptr[0] = &Widget::f; // Full spec required fptr[1] = &Widget::g; fptr[2] = &Widget::h; fptr[3] = &Widget::i; } void select(int i, int j) { if(i < 0 || i >= cnt) return; (this->*fptr[i])(j); } int count() { return cnt; } }; int main() { Widget w; for(int i = 0; i < w.count(); i++) w.select(i, 47); } ///:~
Listado 11.15. C11/PointerToMemberFunction2.cpp
En la interfaz de la clase y en main()
,
puede observar que toda la implementación, funciones
incluidas, es privada. El código ha de pedir el
count()
de las funciones. De esta
manera, el que implementa la clase puede cambiar la cantidad
de funciones en la implementación por debajo sin que afecte
al código que utilice la clase.
La inicialización de los punteros a miembro en el constructor puede parecer redundante. ¿No debería ser capaz de poner
fptr[1] = &g;
porque el nombre g
es un método, el cual
está en el ámbito de la clase? El problema aquí es que no
sería conforme a la sintaxis de puntero a miembro. Así todo
el mundo, incluido el compilador, puede imaginarse qué está
pasando. De igual forma, cuando se accede al contenido del
puntero a miembro, parece que
(this->*fptr[i])(j);
también es redundante; this
parece redundante. La
sintaxis necesita que un puntero a miembro siempre esté
ligado a un objeto cuando se accede al contenido al que
apunta.