31 El puntero this
Para cada objeto declarado de una clase se mantiene una copia de sus datos, pero todos comparten la misma copia de las funciones de esa clase.
Esto ahorra memoria y hace que los programas ejecutables sean más compactos, pero plantea un problema.
Cada función de una clase puede hacer referencia a los datos de un objeto, modificarlos o leerlos, pero si sólo hay una copia de la función y varios objetos de esa clase, ¿cómo hace la función para referirse a un dato de un objeto en concreto?
La respuesta es: usando el puntero especial llamado this. Se trata de un puntero que tiene asociado cada objeto y que apunta a si mismo. Ese puntero se puede usar, y de hecho se usa, para acceder a sus miembros.
Volvamos al ejemplo de la clase pareja:
#include <iostream> using namespace std; class pareja { public: // Constructor pareja(int a2, int b2); // Funciones miembro de la clase "pareja" void Lee(int &a2, int &b2); void Guarda(int a2, int b2); private: // Datos miembro de la clase "pareja" int a, b; };
Para cada dato podemos referirnos de dos modos distintos, lo veremos con la función Guarda. Esta es la implementación que usamos en el capítulo 29, que es como normalmente nos referiremos a los miembros de las clases:
void pareja::Guarda(int a2, int b2) { a = a2; b = b2; }
Veamos ahora la manera equivalente usando el puntero this:
void pareja::Guarda(int a2, int b2) { this->a = a2; this->b = b2; }
Veamos otro ejemplo donde podemos aplicar el operador this. Se trata de la aplicación más frecuente, como veremos al implementar el constructor copia, o al sobrecargar ciertos operadores.
A veces necesitamos invocar a una función de una clase con una referencia a un objeto de la misma clase, pero las acciones a tomar serán diferentes dependiendo de si la referencia que pasamos se refiere al mismo objeto o a otro diferente, veamos cómo podemos usar el puntero this para determinar esto:
#include <iostream> using namespace std; class clase { public: clase() {} void EresTu(clase& c) { if(&c == this) cout << "Sí, soy yo." << endl; else cout << "No, no soy yo." << endl; } }; int main() { clase c1, c2; c1.EresTu(c2); c1.EresTu(c1); return 0; }
Ejecutar este código en OnlineGDB.
La función "EresTu" recibe una referencia a un objeto de la clase "clase". Para saber si se trata del mismo objeto, comparamos la dirección del objeto recibido con el valor de this, si son la misma, es que se trata del mismo objeto.
No, no soy yo. Sí, soy yo.
Este puntero nos resultará muy útil en próximos capítulos, en los que nos encontraremos situaciones en las que es imprescindible su uso.
Cómo funciona
Para intentar comprender algo mejor cómo funciona este puntero, veremos una forma de simularlo usando un ejemplo con estructuras.
Intentaremos crear una estructura sencilla que tenga un miembro con un puntero a un objeto de su mismo tipo. Además, haremos que ese puntero contenga la dirección del propio objeto. De modo que si creamos varios objetos de esa clase, cada uno de ellos apunte a si mismo.
Después de intentarlo un rato, comprobaremos que no es posible hacer esto dentro del constructor, ya que
en ese momento no tenemos referencias al objeto. Tendremos que hacerlo, pues, añadiendo un parámetro más al
constructor o después de crear el objeto, añadiendo una línea obj.esto = &obj;
.
#include <iostream> using namespace std; struct ejemplo { ejemplo(int v, ejemplo* e); int valor; ejemplo *esto; }; ejemplo::ejemplo(int v, ejemplo *e) : valor(v), esto(e) {} int main() { ejemplo e1(19, &e1); cout << &e1 << " " << (void*)e1.esto << endl; return 0; }
Ejecutar este código en OnlineGDB.
Bien, esto es lo que hace el compilador en nuestro lugar cuando crea el puntero this. Y lo pongo en cursiva porque en realidad ese puntero no se almacena junto con el objeto. No ocupa memoria, y por lo tanto no se tiene en cuenta al calcular el tamaño de la estructura con el operador sizeof, no se crea, ni se iniciliza. El compilador sabe durante la fase de compilación dónde almacena cada objeto, y por lo tanto, puede sustituir las referencias al puntero this por sus valores en cada caso.
El puntero this nos permite hacer cosas como obtener una referencia al propio objeto como valor de retorno en determinadas funciones u operadores. En ocasiones puede resolver ambigüedades o aclarar algún código.
Palabras reservadas usadas en este capítulo
this.
Ejemplos capítulos 27 a 31
Ejemplo 31.1
Ahora estamos en disposición de empezar a usar clases para modelar algunos problemas cotidianos.
Empezaremos por las fracciones. Ya hemos hecho algunas aproximaciones usando estructuras, ahora usaremos una clase, y en sucesivos capítulos, iremos añadiendo más refinamientos.
// Clase fracción // (C) 2009 Con Clase // Salvador Pozo #include <iostream> using namespace std; class fraccion { public: fraccion(int n=0, int d=0) : numerador(n), denominador(d) { Simplificar(); } void Simplificar(); void Mostrar(); private: int numerador; int denominador; int MCD(int, int); }; void fraccion::Simplificar() { int mcd = MCD(numerador, denominador); numerador /= mcd; denominador /= mcd; } void fraccion::Mostrar() { cout << numerador << "/" << denominador << endl; } int fraccion::MCD(int a, int b) { if(a < b) return MCD(b,a); if(b == 0) return a; return MCD(b, a % b); } int main() { fraccion f1(234, 2238); fraccion f2(64, 1024); f1.Mostrar(); f2.Mostrar(); return 0; }
Ejecutar este código en OnlineGDB.
Hemos invocado al método Simplificar dentro del constructor, de ese modo, la fracción siempre se almacenará simplificada cuando se cree. Probablemente esto haga que sea innecesario que Simplificar sea pública, ya que cualquier llamada desde el exterior no afectará a la fracción. Podríamos haberla declarado como privada.
Ejemplo 31.2
Añadamos una función Sumar a esta clase, que nos sirva para sumar dos fracciones:
// Clase fracción // (C) 2009 Con Clase // Salvador Pozo #include <iostream> using namespace std; class fraccion { public: fraccion(int n=0, int d=0) : numerador(n), denominador(d) { Simplificar(); } void Simplificar(); void Sumar(fraccion); void Mostrar(); private: int numerador; int denominador; int MCD(int, int); }; void fraccion::Simplificar() { int mcd = MCD(numerador, denominador); numerador /= mcd; denominador /= mcd; } void fraccion::Sumar(fraccion f2) { numerador = numerador*f2.denominador+denominador*f2.numerador; denominador = denominador*f2.denominador; Simplificar(); } void fraccion::Mostrar() { cout << numerador << "/" << denominador << endl; } int fraccion::MCD(int a, int b) { if(a < b) return MCD(b,a); if(b == 0) return a; return MCD(b, a % b); } int main() { fraccion f1(234, 2238); fraccion f2(64, 1024); f1.Mostrar(); f2.Mostrar(); f1.Sumar(f2); f1.Mostrar(); return 0; }
Ejecutar este código en OnlineGDB.
Más adelante veremos como sobrecargar el operador suma para implementar esta función.
Ejemplo 31.3
En este ejemplo crearemos una clase "tonta", que nos servirá para seguir la pista a constructores y destructores a medida que son invocados de forma implícita o explícita. En próximos capítulos añadiremos más cosas para monitorizar otras características.
// Clase tonta // (C) 2009 Con Clase // Salvador Pozo #include <iostream> using namespace std; class tonta { public: tonta(); tonta(int); tonta(const tonta&); ~tonta(); int Modifica(int); int Lee(); private: int valor; }; tonta::tonta() : valor(0) { cout << "Constructor sin parámetros (0)" << endl; } tonta::tonta(int v) : valor(v) { cout << "Constructor con un parámetro (" << v << ")" << endl; } tonta::tonta(const tonta &t) : valor(t.valor) { cout << "Constructor copia (" << t.valor << ")" << endl; } tonta::~tonta() { cout << "Destructor (" << valor << ")" << endl; } int tonta::Modifica(int v) { int retval = valor; cout << "Modificar valor (" << valor << ") -> (" << v << ")" << endl; valor = v; return retval; } int tonta::Lee() { return valor; } int main() { tonta obj1; tonta *obj2; tonta obj3 = obj1; obj2 = new tonta(2); obj1.Modifica(3); cout << "Objeto1: " << obj1.Lee() << endl; cout << "Objeto2: " << obj2->Lee() << endl; cout << "Objeto3: " << obj3.Lee() << endl; delete obj2; return 0; }
Ejecutar este código en OnlineGDB.
Viendo la salida de este programa:
Constructor sin parámetros (0) Constructor copia (0) Constructor con un parámetro (2) Modificar valor (0) -> (3) Objeto1: 3 Objeto2: 2 Objeto3: 0 Destructor (2) Destructor (0) Destructor (3)
vemos que para obj1 se invoca al constructor sin parámetros cuando es declarado, y que para obj2 se invoca al constructor con un parámetro cuando es invocado explícitamente junto al operador new.
Con los destructores pasa lo mismo, para obj2 se incova al destruir el objeto mediante el operador delete, y para obj1 se invoca de forma implícita al terminar su ámbito temporal.