33 Modificadores para miembros

Existen varias alternativas a la hora de definir algunos de los miembros de las clases. Esto es lo que veremos en este capítulo. Estos modificadores afectan al modo en que se genera el código de ciertas funciones y datos, o al modo en que se tratan los valores de retorno.

Funciones en línea (inline)

A menudo nos encontraremos con funciones miembro cuyas definiciones son muy pequeñas. En estos casos suele ser interesante declararlas como inline. Cuando hacemos eso, el código generado para la función cuando el programa se compila, se inserta en el punto donde se invoca a la función, en lugar de hacerlo en otro lugar y hacer una llamada.

Esto nos proporciona una ventaja, el código de estas funciones se ejecuta más rápidamente, ya que se evita usar la pila para pasar parámetros y se evitan las instrucciones de salto y retorno. También tiene un inconveniente: se generará el código de la función tantas veces como ésta se use, con lo que el programa ejecutable final puede ser mucho más grande.

Es por esos dos motivos por los que sólo se usan funciones inline cuando las funciones son pequeñas. Hay que elegir con cuidado qué funciones declararemos inline y cuales no, ya que el resultado puede ser muy diferente dependiendo de nuestras decisiones.

Hay dos maneras de declarar una función como inline.

La primera ya la hemos visto. Las funciones que se definen dentro de la declaración de la clase son inline implícitamente. Por ejemplo:

class Ejemplo {
  public:
   Ejemplo(int a = 0) : A(a) {}

  private:
   int A;
};

En este ejemplo hemos definido el constructor de la clase Ejemplo dentro de la propia declaración, esto hace que se considere como inline. Cada vez que declaremos un objeto de la clase Ejemplo se insertará el código correspondiente a su constructor.

Si queremos que la clase Ejemplo no tenga un constructor inline deberemos declararla y definirla así:

class Ejemplo {
  public:
   Ejemplo(int a = 0);

  private:
   int A;
};

Ejemplo::Ejemplo(int a) : A(a) {}

En este caso, cada vez que declaremos un objeto de la clase Ejemplo se hará una llamada al constructor y sólo existirá una copia del código del constructor en nuestro programa.

La otra forma de declarar funciones inline es hacerlo explícitamente, usando la palabra reservada inline. En el ejemplo anterior sería así:

class Ejemplo {
  public:
   Ejemplo(int a = 0);

  private:
   int A;
};

inline Ejemplo::Ejemplo(int a) : A(a) {}

Funciones miembro constantes

Esta es una propiedad que nos será muy útil en la depuración de nuestras clases. Además proporciona ciertos mecanismos necesarios para mantener la protección de los datos.

Cuando una función miembro no deba modificar el valor de ningún dato de la clase, podemos y debemos declararla como constante. Esto evitará que la función intente modificar los datos del objeto; pero como, a fin de cuentas, el código de la función lo escribimos nosotros; el compilador nos ayudará generando un error durante la compilación si la función intenta modificar alguno de los datos miembro del objeto, ya sea directamente o de forma indirecta.

Por ejemplo:

#include <iostream>
using namespace std;

class Ejemplo2 {
  public:
   Ejemplo2(int a = 0) : A(a) {}
   void Modifica(int a) { A = a; }
   int Lee() const { return A; }

  private:
   int A;
};

int main() {
   Ejemplo2 X(6);

   cout << X.Lee() << endl;
   X.Modifica(2);
   cout << X.Lee() << endl;

   return 0;
}

Para experimentar, comprueba lo que pasa si cambias la definición de la función "Lee()" por estas otras:

int Lee() const { A++; return A; }
int Lee() const { Modifica(A+1); return A; }
int Lee() const { Modifica(3); return A; }

Verás que el compilador no lo permite.

Evidentemente, si somos nosotros los que escribimos el código de la función, sabemos si la función modifica o no los datos, de modo que en rigor no necesitamos saber si es o no constante, pero frecuentemente otros programadores pueden usar clases definidas por nosotros, o nosotros las definidas por otros. En ese caso es frecuente que sólo se disponga de la declaración de la clase, y el modificador "const" nos dice si cierto método modifica o no los datos del objeto.

Consideremos también que la función puede invocar a otras funciones, y usar este modificador nos asegura que ni siquiera esas funciones modifican datos del objeto.

Valores de retorno constantes

Otra técnica muy útil y aconsejable en muchos casos es usar valores de retorno de las funciones constantes, en particular cuando se usen para devolver punteros miembro de la clase.

Por ejemplo, supongamos que tenemos una clase para cadenas de caracteres:

class cadena {
   public:
      cadena();        // Constructor por defecto
      cadena(const char *c); // Constructor desde cadena c
      cadena(int n); // Constructor para cadena de n caracteres
      cadena(const cadena &);   // Constructor copia
      ~cadena();       // Destructor

      void Asignar(const char *dest);
      char *Leer(char *c) {
         strcpy(c, cad);
         return c;
      }
   private:
      char *cad;       // Puntero a char: cadena de caracteres
};

Si te fijas en la función "Leer", verás que devuelve un puntero a la cadena que pasamos como parámetro, después de copiar el valor de cad en esa cadena. Esto es necesario para mantener la protección de cad, si nos limitáramos a devolver ese parámetro, el programa podría modificar la cadena almacenada a pesar de se cad un miembro privado:

      char *Leer() { return cad; }

Para evitar eso podemos declarar el valor de retorno de la función "Leer" como constante:

      const char *Leer() { return cad; }

De este modo, el programa que lea la cadena mediante esta función no podrá modificar ni el valor del puntero ni su contenido. Por ejemplo:

class cadena {
...
};
...
int main() {
   cadena Cadena1("hola");

   cout << Cadena1.Leer() << endl; // Legal
   Cadena1.Leer() = cadena2;       // Ilegal
   Cadena1.Leer()[1] = 'O';        // Ilegal
}

Miembros estáticos de una clase (Static)

Ciertos miembros de una clase pueden ser declarados como static. Los miembros static tienen algunas propiedades especiales.

En el caso de los datos miembro static sólo existirá una copia que compartirán todos los objetos de la misma clase. Si consultamos el valor de ese dato desde cualquier objeto de esa clase obtendremos siempre el mismo resultado, y si lo modificamos, lo modificaremos para todos los objetos.

Por ejemplo:

#include <iostream>
using namespace std;

class Numero {
  public:
   Numero(int v = 0);
   ~Numero();

   void Modifica(int v);
   int LeeValor() const { return Valor; }
   int LeeCuenta() const { return Cuenta; }
   int LeeMedia() const { return Media; }

  private:
   int Valor;
   static int Cuenta;
   static int Suma;
   static int Media;

   void CalculaMedia();
};

Numero::Numero(int v) : Valor(v) {
   Cuenta++;
   Suma += Valor;
   CalculaMedia();
}

Numero::~Numero() {
   Cuenta--;
   Suma -= Valor;
   CalculaMedia();
}

void Numero::Modifica(int v) {
   Suma -= Valor;
   Valor = v;
   Suma += Valor;
   CalculaMedia();
}

// Definición e inicialización de miembros estáticos
int Numero::Cuenta = 0;
int Numero::Suma = 0;
int Numero::Media = 0;

void Numero::CalculaMedia() {
   if(Cuenta > 0) Media = Suma/Cuenta;
   else Media = 0;
}

int main() {
   Numero A(6), B(3), C(9), D(18), E(3);
   Numero *X;

   cout << "INICIAL" << endl;
   cout << "Cuenta: " << A.LeeCuenta() << endl;
   cout << "Media:  " << A.LeeMedia() << endl;

   B.Modifica(11);
   cout << "Modificamos B=11" << endl;
   cout << "Cuenta: " << B.LeeCuenta() << endl;
   cout << "Media:  " << B.LeeMedia() << endl;

   X = new Numero(548);
   cout << "Nuevo elemento dinámico de valor 548" << endl;
   cout << "Cuenta: " << X->LeeCuenta() << endl;
   cout << "Media:  " << X->LeeMedia() << endl;

   delete X;
   cout << "Borramos el elemento dinámico" << endl;
   cout << "Cuenta: " << D.LeeCuenta() << endl;
   cout << "Media:  " << D.LeeMedia() << endl;

   return 0;
}

Ejecutar este código en OnlineGDB.

Observa que es necesario declarar e inicializar los miembros static de la clase, esto es por dos motivos. El primero es que los miembros static deben existir aunque no exista ningún objeto de la clase, declarar la clase no crea los datos miembro estáticos, en necesario hacerlo explícitamente. El segundo es porque no lo hiciéramos, al declarar objetos de esa clase los valores de los miembros estáticos estarían indefinidos, y los resultados no serían los esperados.

En el caso de la funciones miembro static la utilidad es menos evidente. Estas funciones no pueden acceder a los miembros de los objetos, sólo pueden acceder a los datos miembro de la clase que sean static. Esto significa que no tienen acceso al puntero this, y además suelen ser usadas con su nombre completo, incluyendo el nombre de la clase y el operador de ámbito (::).

Por ejemplo:

#include <iostream>
using namespace std;

class Numero {
  public:
   Numero(int v = 0);

   void Modifica(int v) { Valor = v; }
   int LeeValor() const { return Valor; }
   int LeeDeclaraciones() const { return ObjetosDeclarados; }
   static void Reset() { ObjetosDeclarados = 0; }

  private:
   int Valor;
   static int ObjetosDeclarados;
};

Numero::Numero(int v) : Valor(v) {
   ObjetosDeclarados++;
}

int Numero::ObjetosDeclarados = 0;

int main() {
   Numero A(6), B(3), C(9), D(18), E(3);
   Numero *X;

   cout << "INICIAL" << endl;
   cout << "Objetos de la clase Numeros: "
        << A.LeeDeclaraciones() << endl;

   Numero::Reset();
   cout << "RESET" << endl;
   cout << "Objetos de la clase Numeros: "
        << A.LeeDeclaraciones() << endl;

   X = new Numero(548);
   cout << "Cuenta de objetos dinámicos declarados" << endl;
   cout << "Objetos de la clase Numeros: "
        << A.LeeDeclaraciones() << endl;

   delete X;
   X = new Numero(8);
   cout << "Cuenta de objetos dinámicos declarados" << endl;
   cout << "Objetos de la clase Numeros: "
        << A.LeeDeclaraciones() << endl;

   delete X;
   return 0;
}

Ejecutar este código en OnlineGDB.

Observa cómo hemos llamado a la función Reset con su nombre completo. Aunque podríamos haber usado "A.Reset()", es más lógico usar el nombre completo, ya que la función puede ser invocada aunque no exista ningún objeto de la clase.

Palabras reservadas usadas en este capítulo

const, inline y static.