FUNCIONES VIRTUALES(1)

PUNTEROS A TIPOS DERIVADOS

Los punteros a tipos derivados son similares a aquellos a tipos base. Si suponemos que B_clase es el tipo base y que D_clase es el tipo derivado, cualquier puntero declarado a B_clase pude ser puntero a D_clase.

 

  • B_classe *p: // puntero a un objeto tipo B_classe
    B_classe B_ob; // objeto de tipo B_classe
    D_classe D_ob; // objeto de tipo D_classe


Lo que sigue es correcto:


  • p = &B_ob; // p apunta a un objeto de tipo B_classe
    p = &D_ob; /* p apunta a un objeto de tipo D_classe, que es un objeto derivado de B_classe. */

Por medio de p todos los elementos de D_ob heredados de B_ob son accesible. No obstante no es posible hacer una referencia a los elementos específicos de D_ob a través de p si no es mediante una conversión de tipo.
Considérese por ejemplo el siguiente programa en el que se define una clase fundamental llamada B_classe y una derivada D_classe. Esta última realiza una llamada automática de teléfono.

Uso de punteros a clases derivadas

  • #include <iostream.h>
    #include <string.h>
    class B_classe
  • {

  • char nome[80];
    public:
    void richiama_nome(char *s) {strcpy(nome, s); }
    void visualizza_nome() {cout << nome <<" ";}

};

  • class D_classe:public B_classe
  • {

  • char num_telefono[80];
    public:
    void richiama_telefono(char *num)
    {

  • strcpy(num_telefono, num);
    }

  • void visualizza_telefono() {cout<< num_telefono<< "\n";}
  • };

    • main(void)
    • {

    • B_classe *p; B_classe B_ob;
      D_classe *dp;
      D_classe D_ob;
      p = &B_ob; // dirección de la base
      // Acceso a la B_classe por medio de puntero
      p->richiama_nome("Thomas Edison");
      // Acceso a la D_classe por medio de puntero
      p = &D_ob;
      p->richiama_nome("Albert Einstein");
      /*
      Dado que las funciones richiama_telefono y visualizza_telefono no son parte de la clase de base no son accesibles con el puntero a la clase base q por tanto es necesario acceder directamente a ellos, o como se muestra aqui, con un tipo derivado.
      */
      dp = &D_ob;
      dp->richiama_telefono("039 6045-234");
      p->visualizza_nome(); /* tanto p como dp pueden ser utilizados en esta linea pues los dos apuntan a D_ob. */
      dp->visualizza_telefono();
      return 0;

     }

    En este ejemplo el puntero p está definido como puntero a B_classe y por ello puede apuntar a un objeto de la clase derivada D_classe y ser utilizado para acceder a los elementos de la clase derivada definidos en la clase base. Sin embargo no puede acceder a los elementos de la clase derivada que están definidos en ella misma sin hacer un reconversión de tipo.
    Esto explica por qué a visualizza_telefono() se accede por medio de dp, puntero a la clase derivada. Si se desea acceder a los elementos definidos de un tipo derivado con un putero de tipo fundamental, hay que convertir este último en un puntero a un tipo derivado. Por ejemplo la línea que sigue hace una llamada correcta a la función visualizza_telefono() definida en D_ob:

    • ((D_classe *)p)-> visualizza_telefono();

    Los paréntesis extremos son necesarios para asociar la conversión a p y no al tipo que retorna visualizza_telefono(). Técnicamente hablando aunque esta conversión es correcta, es mejor evitarla para no generar confusión en el código del programa.
    Un puntero a la clase base o fundamental puede apuntar también a una clase derivada. En cambio un puntero a la clase derivada no puede apuntar a la fundamental.
    Hay que destacar además que las operaciones de incremento y decremento modifican el puntero respeto a su tipo fundamental. En consecuencia, cuando un puntero a una clase fundamental apunta a una clase derivada, no es cierto que cuando se incrementa o decrementa apunte al objeto sucesivo de la clase derivada. Operaciones de incremento o decremento del puntero a la clase derivada son por tanto consideradas incorrectas.
    El hecho que un puntero a un tipo fundamental pueda ser utilizado para apuntar a cualquier objeto derivado de aquel tipo es de gran importancia en el C++, sobretodo considerando el modo en que este lenguaje realiza el polimorfismo.

    FUNCIONES VIRTUALES

    El polimorfismo en fase de ejecución se consigue mediante el uso de tipos derivados y de funciones virtuales. En síntesis, una función virtual es una función declarada virtual en una clase fundamental y redefinida en una o más clases derivadas. La peculiaridad de estas funciones es que cuando se accede a ellas en clases derivadas por medio de un puntero a la clase fundamental, el C++ determina en la fase de ejecución del programa la dirección correcta de la función que debe de llamar según el objeto a donde apunte el puntero. Gracias a tal característica, apuntando a objetos diferentes se hacen llamadas a funciones virtuales diferentes (las cuales como es natural corresponden siembre a su objeto).
    Una función se declara virtual en la clase base anteponiendo a la declaración la palabra llave virtual. Cuando una función virtual es redefinida en una clase derivada, no es necesario repetir la palabra llave virtual, aunque si se repite no se origina error.
    Par ilustrar lo dicho véase el siguiente ejemplo:

     

    • // Ejemplo simple de utilización de funciones virtuales.
      #include <iostream.h>
      class Base
    • {

    • public:
      virtual void chi() // declaración virtual

    • cout << "Base\n";
     

    };

    • class prima_d : public Base
    • {

    • public:
      // definción de chi() relativa a la clase prima_d
      void chi()
    • {

    • cout << "Primera derivación\n";

     

    };

    • class seconda_d : public Base
    • {

    • public:
      // definción de chi() relativa a la clase seconda_d
      void chi()

    • cout << "Segunda derivación \n";
     

    };

    • main(void)
    • {

    • Base base_ogg;
      Base *p;
      prima_d primo_ogg;
      seconda_d secondo_ogg;
      p = &base_ogg;
      p->chi(): // acceso a chi de la clase Base
      // acceso a chi de la clase prima_d
      p = &primo_ogg;
      p->chi();
      // acceso a chi de la clase seconda_d
      p - &secondo_ogg;
      p->chi();
      return 0;

    El programa visualiza lo siguiente:

    • Base
      Prima derivazione
      Seconda derivazione

    La función chi() en la clase Base está declarada virtual. Esto significa que puede ser redefinida en una clase derivada. Tal función redefinida, tanto en el interior de prima_d como en el interior de prima_d como en el interior de seconda_d. En main() hay declaradas cuatro variables:

    • Base base_ogg;
      Base *p;
      prima_d primo_ogg;
      seconda_d secondo_ogg;

    Al inicio del programa a p se le asigna la dirección de base_ogg y llama a la función chi(). Dado que chi() ha sido declarada como virtual, el C++ determina en fase de ejecución a qué versión de chi() se refiere el tipo de objeto apuntado por p. en este caso se trata de un objeto de tipo Base por lo que se ejecuta la versión de chi() declarada en Base. A p se le asigna la dirección de primo_ogg.
    Como se ha dicho, se puede utilizar un puntero de la clase fundamental para hacer una referencia a cualquier clase derivada. Cuando se hace una nueva llamada a chi(), el C++ examina otra vez el tipo de objeto apuntado por p para determinar qué versión de chi() debe llamar.
    Más adelante en el programa se asigna a p la dirección de prima_d y después sigue una llamada chi(). De nuevo el C++, en base a dicha asignación determina la función chi() a la que debe de llamar haciéndolo con la chi() definida en la clase prima_d. Analogamente cuando a p se le asigna la dirección secondo_ogg, una llamada a la función chi(), lo hace llamando a chi() de la clase seconda_d.
    Hay que señalar que para llevar a cabo el polimorfismo en la fase de ejecución es indispensable acceder a las funciones virtuales por medio de un puntatore a la clase fundamental pues si bien es posible llamar explícitamente una función virtual utilizando el nombre del objeto, como se hace en los elementos función, el polimorfismo en fase de ejecución se consigue sólo accediendo a una función virtual por medio de un puntero a la clase fundamental.
    La redefinición de una función virtual en una clase derivada es una forma particular de sobrecarga de funciones. Se prefiere no obstante no adoptar esta forma ya que esta manera de sobrecarga presenta varios límites. En la sobrecarga de una función normal el valor de retorno así como el número de parámetros puede diferir respecto a los de la clase fundamental. En cambio en el caso de las funciones virtuales el valor de retorno así como el número y el tipo de parámetros debe de coincidir con los de la clase fundamental ya que de no ser así dicha función pierde su característica de virtual. Se considera también erróneo que la función difiera sólo en el valor de retorno (resulta ambiguo de todas formas que una función difiere solo en el valor de retorno). Además una función virtual tiene que ser un elemento de la clase para la cal fue definida y no una función friend. No obstante una función virtual puede ser friend d otra clase. Por último las funciones virtuales pueden ser destructores pero no constructores.
    Dada las diferencias y limitaciones entre la sobrecarga de funciones normales y de funciones virtuales, para describir la redefinición de una función virtual se recurre al término ovrerriding.
    Siempre que una función es declarada virtual permanece así independientemente del nivel de derivación en el que se declaró virtual. Por ejemplo si seconda_d deriva de prima_d en lugar que de Base, como se muestra a continuación, chi() es siempre virtual y el programa ejecuta siempre la versión correcta.

     

    • // Derivación de la clase prima_d y no de Base.
      class seconda_d : public prima_d
    • {

    • public:
      // definición de chi() relativa a la clase seconda_d
      void chi()

    • cout <<"Segunda derivacion\n";
     

    };

    Cuando una clase derivada no redefine la función virtual, es la versión definida en la clase fundamental la que es ejecutada. Obsérvese el ejemplo que sigue:

     

    • #include <iostream.h>
      class Base
    • {

    • public:
      virtual void chi() // declaración virtual

    • cout << "Base\n";
     

    };

    • class prima_d : public Base
    • {

    • public:
      // definición de chi() relativa alla classe prima_d
      void chi()

    • cout << "Primera derivación\n";
     

    };

    • class seconda_d : public Base
    • {

    • // chi no está definida
    • };
    • main(void)
    • {

    • Base base_ogg;
      Base *p;
      prima_d primo_ogg;
      seconda_d secondo_ogg;
      p = &base_ogg;
      p->chi(): // acceso a chi() de la clase Base
      // acceso a chi() de la clase prima_d
      p = &primo_ogg: p->chi();
      // acceso a chi() de la clase seconda_d
      p - &secondo_ogg;
      p->chi(); /* acceso a chi() de la clase Base ya que en la clase seconda_d no ha sido definida */
      return 0;

    Esta vez el programa visualiza:

    • Base
      Prima derivazione
      Base

    Hay que tener presente que las características heredadas son jerárquicas. Por tanto, si seconda_d se deriva de prima_d en vez de Base, cuando se llama chi() en relación a un objeto de tipo soconda_d la versión de chi() que es ejecutada es aquella declarada en prima_d, que es la clase más próxima a la clase seconda_d.