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]


Sig.

4.9.18d  Sobrecarga del operador [ ]

§1  Sinopsis

Recordemos ( 4.9.16) que este operador sirve para señalar subíndices de matrices simples y multidimensionales; de ahí su nombre, operador subíndice o de selección de miembro de matriz.

La expresión:

<exp1>[exp2]

se define como: *((exp1) + (exp2)) donde exp1 es un puntero y exp2 es un entero o viceversa.

Por ejemplo,  arrX[3] se define como: *(arrX + 3) o *(3 + arrX), donde arrX es un puntero al primer elemento de la matriz. (arrX + 3) es un puntero al cuarto elemento, y *(arrX + 3) es el valor del cuarto elemento de la matriz.

Lo anterior puede sintetizarse en la siguiente relación:

arrX[3] == *(arrX + 3)      //  §1a


§2  Cuando se utiliza con tipos definidos por el usuario, este operador puede ser sobrecargado mediante la función operator[ ]( ) ( 4.9.18). Para ilustrarlo con un ejemplo, utilizaremos la clase mVector que contiene una matriz (es una matriz de vectores), y suponemos que los elementos de la matriz son vectores deslizantes de un espacio bidimensional. El diseño básico es el que se indica:

class Vector {    // definición de la clase Vector
  public: int x, y;
};

class mVector {   // definición de la clase mVector
  public:
  Vector* mVptr;            // L.6:
  mVector(int n = 1) {      // constructor por defecto
    mVptr = new Vector[n];  // L.8:
  }
  ~mVector() {              // destructor
    delete [] mVptr;
  }
};

Comentario

La clase Vector tiene solo dos miembros, que suponemos las componentes escalares de cada vector del plano. Por simplicidad hemos supuesto que son int, pero podrían ser otros tipos de punto flotante, por ejemplo float o double. Esta clase auxiliar la hemos definido externa e independiente de la clase mVector.

También podría utilizarse otro diseño en el que Vector estuviese definida "dentro" de la clase mVector. Las diferencias entre ambos y los criterios de uso se discuten en ( 4.13.2):

class mVector {      // definición de la clase mVector
  ...
  class Vector {     // clase anidada
     ...
  };
  ...
};


La clase mVector tiene un solo miembro; un puntero-a-Vector mVptr (L.6). También definimos un constructor por defecto y un destructor.

Observe (L.8) que el constructor del objeto tipo mVector, crea una matriz de objetos tipo Vector del tamaño indicado en el argumento (1 por defecto) y la señala con el puntero mVptr. Esta matriz está alojada en memoria persistente ( 4.9.20c) y en cierta forma podríamos pensar que es "externa" al objeto, ya que este realmente solo contiene un puntero [1]. Precisamente en razón de esta persistencia, el destructor debe rehusar la memoria asignada a la matriz, pues de otro modo este espacio se perdería al ser destruido el objeto ( 4.9.21).

Siguiendo el paradigma de la POO, esta clase deberá contener los datos (la matriz) y los algoritmos (métodos) para manejarla. Deseamos utilizar los objetos de tipo mVector como auténticas matrices, por lo que deberíamos poder utilizarlos con álgebra de matrices C++. Utilizando una analogía, si por ejemplo m es una matriz de enteros, sabemos que el lenguaje nos permite utilizar las expresiones siguientes:

m[i];                // L.1: acceso a elemento con el operador subíndice
int x = m[i];        // L.2: asignación a un miembro de la clase int
m[i] = m[j];         // L.3: asignación a miembro
m[i] = 3 * m[j];     // L.4: producto por un escalar
m[i] = m[j] * m[k];  // L.5: producto entre miembros

En consecuencia, debemos preparar el diseño de la clase mVector de forma que que pueda mimetizarse el comportamiento anterior con sus objetos. Es decir, deben permitirse las siguientes expresiones:

mVector m1;
m1[i];                 // acceso a elemento con el operador subíndice
Vector v1 = m1[i];     // asignación a un miembro de la clase Vector
m1[i] = m1[j];         // asignación a miembro
m1[i] = 3 * m1[j];     // producto por un escalar
m1[i] = m1[j] * m1[k]; // producto entre miembros

§3  Operador subíndice

Para mimetizar este comportamiento con los objetos de la nueva clase empezaremos por poder referenciarlos mediante el operador subíndice [ ] (L.1 ). Este operador debe recibir un int y devolver el miembro correspondiente de la matriz (recordemos que los miembros son tipo Vector). Como sabemos que debe gozar del doble carácter de Rvalue y Lvalue (L.3 ), deducimos que debe devolver una referencia ( 4.9.18c). A "vote pronto" podría parecernos que la definición debe ser del tipo:

const size_t sV = sizeof(Vector);
Vector& operator[](int i) { return  *( mVptr + (i * sV)); }

sin embargo, reflexionando más detenidamente recordamos que mVptr está definido precisamente como puntero-a-Vector, por lo que su álgebra lleva implícito el tamaño de los objetos Vector ( 4.2.2), lo que significa que podemos prescindir del factor sV:

Vector& operator[](int i) { return  *( mVptr + i ); }

Recordando la definición de subíndice §1a , y la relación entre punteros y matrices ( 4.3.2) la expresión anterior equivale a:

Vector& operator[](int i) { return  mVptr[i]; }

esta es justamente la definición que utilizamos para la función-operador operator[ ] ( L.30) de nuestra clase. Como resultado, podemos utilizar expresiones del tipo:

mVector mV1(5);      // objeto tipo mVector (matriz de 5 Vectores)
mV1[2];              // tercer elemento de la matriz


§4  Operador de asignación

Para utilizar el operador de asignación = con los objetos devueltos por el selector de miembro [ ], debemos sobrecargarlo para los objetos tipo Vector. Esto se ha visto en el epígrafe correspondiente, por lo que nos limitamos a copiar dicha definición ( 4.9.18a):

Vector& operator= (const Vector& v) {    // función operator=
  x = v.x;   y = v.y;
  return *this;
}

Su implementación en la versión definitiva ( L.6), nos permite utilizar expresiones del tipo:

  Vector v1;
  v1 = mV1[0];    // M.6:


§5  Producto por un escalar

Para mimetizar el comportamiento expresado en L.4 , sobrecargamos el operador producto para la clase Vector de forma que acepte un int. La definición la hacemos de forma que corresponda a la definición tradicional. Es decir, la resultante es un vector cuyos componentes son el producto de los componentes x y del vector operando por el escalar.

Es importante observar aquí que, en el caso de la matriz de enteros m, las dos sentencias siguientes son equivalentes:

m[i] = m[j] * 3;    // producto por un escalar (por la derecha)
m[i] = 3 * m[j];    // producto por un escalar (por la izquierda)


§5.1  Esto significa que debemos definir el producto en ambos sentidos.  Para el primero podemos definir una función miembro que acepte un argumento tipo int (además del correspondiente puntero this). Este método tiene el aspecto que se indica:

Vector operator* (int i) {  // producto por un escalar (por la derecha)
  Vector vr;
  vr.x = x * i;
  vr.y = y * i;
  return vr;
}

Después de implementado en la versión definitiva ( L.9), nos permite expresiones del tipo:

mV1[4] = mV1[0] * 5;   // M.8:


§5.2  El producto por la izquierda debemos definirlo como una función-operador externa. Se trata de una función independiente (no pertenece a una clase) que acepta dos argumentos, un int y un Vector. Como es usual, la declaramos friend de la clase Vector ( L.15) para que pueda tener acceso a sus miembros (aunque en este caso no es necesario porque todos son públicos). Su diseño es muy parecido al anterior, aunque en este caso no existe puntero implícito this y debemos referenciar el objeto Vector directamente:

Vector operator* (int i, Vector v) {
  Vector vr;
  vr.x = v.x * i;
  vr.y = v.y * i;
  return vr;
}

Su implementación ( L.38) hace posible expresiones como:

mV1[2] = 5 * mV1[0];          // M.11:


§6  Una vez introducidas todas las modificaciones anteriores en la versión básica , el diseño resultante es el siguiente:

#include <iostream>
using namespace std;

class Vector {     // definición de clase Vector
  public: int x, y;
  Vector& operator= (const Vector& v) { // L.6: asignación V = V
    x = v.x;   y = v.y;   return *this;
  }
  Vector operator* (int i) {       // L.9: Producto V * int
    Vector vr;
    vr.x = x * i;   vr.y = y * i;
    return vr;
  }
  void showV();
  friend Vector operator* (int, Vector); // L.15: Producto int * V
};
void Vector::showV() { cout << "X = " << x << "; Y = " << y << endl; }

class mVector {    // definición de clase mVector
  int dimension;                    // L.20:
  public:
  Vector* mVptr;
  mVector(int n = 1) {             // constructor por defecto
    dimension = n;
    mVptr = new Vector[dimension]; // L.25:
  }
  ~mVector() {                      // destructor
    delete [] mVptr;
  }
  Vector& operator[](int i) { return mVptr[i]; }
  void showmem (int);              // L.31:
};

void mVector::showmem (int i) {    // L.34:
  if((i >= 0) && (i <= dimension)) mVptr[i].showV();
  else cout << "Argumento incorrecto! pruebe otra vez" << endl;
}
Vector operator* (int i, Vector v) { // L.38:
  Vector vr;
  vr.x = v.x * i;    vr.y = v.y * i;
  return vr;
}

void main() {      // =====================
  mVector mV1(5);          // M.1:
  mV1[0].x = 2;  mV1[0].y = 3;
  mV1.showmem(0);

  Vector v1;
  v1 = mV1[0];             // M.6:
  v1.showV();
  mV1[4] = mV1[0] * 5;     // M.8:
  mV1.showmem(4);

  mV1[2] = 5 * mV1[0];     // M.11:
  mV1.showmem(2);
}

Salida:

X = 2; Y = 3
X = 2; Y = 3
X = 10; Y = 15
X = 10; Y = 15

Comentario

En un programa real, se debería implementar un mecanismo de control de excepciones que pudiera controlar la posibilidad de que el operador new del constructor (L.25) fuese incapaz de crear el objeto ( 4.9.20). Es decir, controlar que operaciones como la de M.1 concluyen con éxito.

Para manejar convenientemente los límites incluimos en mVector el miembro dimension (L.20); su valor es iniciado por el constructor (L.24 ) y acompaña a cada instancia. El efecto es que es posible implementar la interfaz de la clase de forma que el usuario no pueda acceder un elemento fuera del espacio de la matriz.

Para facilitar la lectura incluimos en la mVector el método showmem (L.31) que muestra los componentes de un elemento de la matriz. Este método utiliza el miembro dimension para verificar que no pretendemos acceder a un elemento fuera de los límites del objeto previamente creado.

  Inicio.


[1]  Naturalmente esto depende del nivel de abstracción que se maneje. A ciertos efectos puede pensarse en la estructura como un único objeto (esta es precisamente una de las ventajas de la POO 1.1), mientras que en otros casos hay que tener muy en cuenta esta circunstancia. Por ejemplo si necesitamos una rutina que escriba y lea estos objetos a ficheros en disco.

Sig.