4.9.18d Sobrecarga del operador [ ] (Continuación)
§7 Sinopsis
En la página anterior hemos visto algunos ejemplos de sobrecarga de operadores, aunque en realidad, solo el selector de miembro [] pertenece a la clase mVector. Los demás pertenecen a la clase auxiliar Vector ya comentada en repetidas ocasiones.
Puesto que ya tenemos las herramientas necesarias, continuaremos desarrollando la clase mVector, proporcionándole los operadores necesarios para efectuar las operaciones siguientes:
mVector mv1, mv2;
mVector mv3 = mv1 + mv2;
Evidentemente necesitamos los operadores suma +
( 4.9.18b) y asignación =
(
4.9.18a) para objetos mVector,
así como un constructor-copia (
4.11.2d4) adecuado. Empezaremos por este último.
§8 Constructor-copia
Recordemos lo dicho en la página anterior: "el constructor crea una matriz de vectores
del tamaño indicado en
el argumento y la señala con el puntero mVptr. Esta matriz está alojada en memoria persistente y en cierta forma
podríamos pensar que es "externa" a la propia estructura; que esta realmente solo contiene un puntero...". La
situación se esquematiza en la figura 1.
Cuando los objetos son de estas características, la copia y asignación [1] pueden
realizarse siguiendo dos criterios que se han esquematizado en la figura 2.
La opción A, supone considerar el objeto como puntero. En una asignación obj2 = obj1, el receptor de la asignación señala en realidad a los mismos datos que el objeto del que recibe el valor.
En determinados contextos esta política puede ser válida pero no exenta de inconvenientes. Por ejemplo, la destrucción de obj-1 supone la liberación del espacio ocupado por Matriz-1. ¿Que pasa con el puntero de Objeto-2 que señala a un sitio erróneo?.
La opción B supone considerar el objeto como valor. En este tipo de asignaciones existe independencia real entre los
objetos resultantes, de modo que las posteriores modificaciones en el primero no afectan al segundo. En estas circunstancias, si
se trata de una copia no hay que preocuparse (obj-2 es creado de nuevo), pero si se trata de una asignación hay que cuidarse
de destruir la matriz previamente señalada por obj-2, reservar un nuevo espacio y asignarle los valores de Matriz-1.
§8.1 A continuación se incluye la solución propuesta para el constructor-copia, que considera el objeto
"como valor".
mVector(const mVector& mv) { // constructor-copia
dimension = mv.dimension; // dimensiones iguales
mVptr = new Vector[dimension]; // Reserva nuevo espacio
for(int i = 0; i<dimension; i++) { // copia miembro a miembro
mVptr[i]= mv.mVptr[i]; // L.5
}
}
Recuerde que los objetos mVector son matrices de objetos tipoX y que L.5 supone la asignación entre los miembros de estas matrices [2], lo que implica a su vez que debe estar definido el operador de asignación entre objetos tipoX (en el caso que nos ocupa se trata de objetos tipo Vector).
§9 Operador de asignación
A continuación se incluye una versión del operador de asignación para la clase mVector considerando los objetos "como valor".
mVector& operator=(const mVector& mv) { // Operador =
delete [] mVptr;
// rehusar espacio previo
dimension = mv.dimension;
// dimensiones iguales
mVptr = new Vector[dimension]; // Reserva nuevo espacio
for(int i = 0; i<dimension; i++) { // copia miembro a miembro
mVptr[i]= mv.mVptr[i];
}
return *this;
}
Observe que es muy parecido al constructor-copia, aunque este no crea ningún objeto nuevo (no es un constructor), e incluye una
primera sentencia encargada de rehusar el espacio de la matriz previamente señalada por el objeto que recibe la asignación, y que
se devuelve una referencia, con objeto de que el valor resultante pueda ser utilizado Rvalue y Lvalue en operaciones de asignación
encadenadas ( 4.9.18a).
§10 A este respecto debemos recordar que en el diseño actual de mVector (página anterior), el
constructor por defecto es:
mVector(int n = 1) { // constructor por defecto
dimension = n;
mVptr = new Vector[dimension];
}
En estas condiciones, cualquier objeto mVector tiene tamaño 1 si se ha utilizado el argumento por defecto del constructor.
Por ejemplo, en una sentencia del tipo mVector mv;. En cambio, si se utiliza la asignación: mVector mv = mVector(0);,
aunque la petición es de espacio cero, new devuelve un puntero no nulo
( 4.9.20), y es posible todavía
utilizar la versión anterior para el operador de asignación.
Supongamos sin embargo, que el diseño del constructor es el siguiente:
mVector(int n = 0) { // constructor por defecto
dimension = n;
if (n == 0) mVptr = 0;
else mVptr = new Vector[dimension];
}
En estas condiciones pueden existir objetos mVector a los que no corresponda un espacio en memoria persistente. En consecuencia es necesario modificar nuestra versión del operador de asignación:
mVector& operator=(const mVector& mv) { // Operador =
if ( mVptr != 0 ) { delete [] mVptr; } // rehusar espacio previo
dimension = mv.dimension;
// dimensiones iguales
if (dimension != 0 ) {
mVptr = new Vector[dimension]; // Reserva nuevo espacio
for(int i = 0; i<dimension; i++) { // copia miembro a miembro
mVptr[i]= mv.mVptr[i];
}
}
return *this;
}
También sería necesario modificar el constructor-copia:
mVector(const mVector& mv) { // constructor-copia
dimension = mv.dimension;
// dimensiones iguales
if ( dimension == 0 ) { mVptr == 0; }
else {
mVptr = new Vector[dimension]; // Reserva nuevo espacio
for(int i = 0; i<dimension; i++) { // copia miembro a miembro
mVptr[i]= mv.mVptr[i];
}
}
}
§11 Versión operativa
Después de incluir los nuevos operadores, la versión definitiva de mVector es como sigue:
#include <iostream>
using namespace std;
class Vector { // Clase auxiliar Vector
public: int x, y;
Vector& operator= (const Vector& v) { // asignación V = V
x = v.x; y = v.y;
return *this;
}
void showV() { cout << "X = " << x << "; Y = " << y << endl; }
};
class mVector { // clase mVector
int dimension;
public:
Vector* mVptr;
mVector& operator=(const mVector& mv) { // Operador =
delete [] mVptr;
dimension = mv.dimension;
mVptr = new Vector[dimension];
for(int i = 0; i<dimension; i++) {
mVptr[i]= mv.mVptr[i];
}
return *this;
}
mVector(int n = 1) { // constructor por defecto
dimension = n;
mVptr = new Vector[dimension];
}
~mVector() { // destructor
delete [] mVptr;
}
mVector(const mVector& mv)
{ // constructor-copia
dimension = mv.dimension;
mVptr = new Vector[dimension];
for(int i = 0; i<dimension; i++) {
mVptr[i]= mv.mVptr[i]; // L.5
}
}
Vector& operator[](int i) { return mVptr[i]; }
void showmem (int); // L.31: función auxilar
void show ();
// función auxilar
};
void mVector::showmem (int i) { // Ver miembro
if((i >= 0) && (i <= dimension)) mVptr[i].showV();
else cout << "Argumento incorrecto! pruebe otra vez" << endl;
}
void mVector::show () { // Ver objeto completo
cout << "Matriz de: " << dimension << " elementos." << endl;
for (int i = 0; i<dimension; i++) {
cout << i << "- ";
mVptr[i].showV();
}
}
void main() { // =====================
mVector mV1(5);
// instanciar una matriz de 5 elementos
mV1[0].x = 0; mV1[0].y = 1; // iniciar con ciertos valores
mV1[1].x = 2; mV1[1].y = 3;
mV1[2].x = 4; mV1[2].y = 5;
mV1[3].x = 6; mV1[3].y = 7;
mV1[4].x = 8; mV1[4].y = 9;
mV1.show();
// S.1: mostrar objeto
mVector mV2 = mV1; // usar el constructor-copia
mV2.show();
// S.2: verificar el resultado
mV1[0].x = 9; mV1[0].y = 0; // modificar miembro
mV2.showmem(0); // S.3: mostrar
miembro (no modificado)
mV1.showmem(0); // S.4: mostrar
miembro (modificado)
mVector mV3(0); // instanciar matriz
de 0 elementos
mV3.show(); //
S.5: mostrar objeto
mV3 = mV1;
// asignación
mV3.show(); //
S.6: mostrar objeto
}
Salida:
Matriz de: 5
elementos. S.1
0- X = 0; Y = 1
1- X = 2; Y = 3
2- X = 4; Y = 5
3- X = 6; Y = 7
4- X = 8; Y = 9
Matriz de: 5 elementos. S.2
0- X = 0; Y = 1
1- X = 2; Y = 3
2- X = 4; Y = 5
3- X = 6; Y = 7
4- X = 8; Y = 9
X = 0; Y = 1 S.3
X = 9; Y = 0 S.4
Matriz de: 0 elementos. S.5
Matriz de: 5 elementos. S.6
0- X = 9; Y = 0
1- X = 2; Y = 3
2- X = 4; Y = 5
3- X = 6; Y = 7
4- X = 8; Y = 9
Comentario
Comprobamos que en la función main hemos efectuado todas las operaciones que pretendíamos al principio con objetos de tipo mVector, y que los resultados son los esperados.
Merecen especial atención las salidas S.5 y S.6 como comprobación de que efectivamente la asignación se hace "por valor" ya que después de realizada, la modificación del primer miembro no modifica al segundo.
En la página adjunta se expone un ejemplo que utiliza la indirección
( 4.9.16)
y la sobrecarga del operador de subíndice [ ] para controlar el acceso a miembros dentro de los límites de una matriz.
Ejemplo.
[1] Ambas son "asignaciones". La diferencia estriba en que la copia (realizada por el constructor-copia) crea un objeto y luego le asigna los valores de otro, mientras que la asignación asigna los valores de un objeto a otro ya existente. Podría considerarse que la copia consiste en una creación + asignación.
[2] Aplicamos a los punteros el álgebra de subíndices (como si fuesen matrices
4.3.2).