[C con Clase] Funcion virtual no hace falta con objetos locales ?

Steven Davidson srd4121 en njit.edu
Lun Dic 12 00:34:13 CET 2011


Hola Stack,

2011/12/11 Stack Overflow <stackoverflow32 en gmail.com>:
> Hola lista
>
> Estoy aprendiendo C++, y copio este ejemplo que encontre para ver como
> funcionan las funciones virtuales.
> El siguiente es el codigo que estoy probando:
>
> #include <iostream>
> using namespace std;
>
> class Vehicle   //This denotes the base class of C++ virtual function
> {
> public:
>         void Make()   //This denotes the C++ virtual function
>         {
>                 virtual cout << "Member function of Base Class Vehicle Accessed" << endl;

Esto es incorrecto; debería ser,

virtual void Make()
{
  cout << "Member function of Base Class Vehicle Accessed" << endl;
}

>         }
> };
>
> class FourWheeler : public Vehicle
> {
> public:
>         void Make()
>         {
>                 cout << "Virtual Member function of Derived class FourWheeler Accessed" << endl;
>         }
> };
>
> int main()
> {
>         Vehicle *a, *b;
>         a = new Vehicle();
>         a->Make();
>         b = new FourWheeler();
>         b->Make();
>

Deberías liberar la memoria previamente adjudicada. Esto es,

delete b;
delete a;

>         return(0);
> }
>
> Lo que noto es que si en lugar de definir punteros a y b a Vehicle, usara un
> puntero a Vehicle y otro a FourWheeler, no haria falta que declare el metodo
> Make como virtual.
> El objeto puntero a FourWheeler accederia a su metodo Make correctamente.
> No termino de entender porque usaria un puntero a Vehicle si voy a crear un
> objeto FourWheeler.
> Alguien puede ilustrarme con algun caso donde tenga sentido tener metodos
> virtuales ?
>

Lo que comentas es correcto, si no se quebranta tu idea fundamental de
que sabes qué clases vas a usar a la hora de instanciar objetos. Es
decir, si sabes que necesitas un objeto de la clase 'FourWheeler',
entonces instancia tal objeto. Ahora bien, si no sabes qué clases son
los objetos a instanciar, entonces no puedes indicar una clase
derivada y por tanto no sabes el tipo exacto del objeto que vas a
instanciar. Las funciones virtuales forman parte del mecanismo del
polimorfismo y éste sirve para cambiar el tipo de un objeto de su
clase a otras superiores dentro de una jerarquía de clases. Esto
significa que necesitamos una clase común para todas las demás que
vayan a heredar de ésta.

Siguiendo el ejemplo que mencionas, la razón de usar polimorfismo en
este caso sería a la hora de tratar todos los vehículos a través de
las mismas funciones miembro ya que son comunes para todas las clases
derivadas de 'Vehiculo'. Por ejemplo,

void Make( Vehicle *pVehiculo )
{
  pVehiculo->Make();
}

Esto significa que la función global 'Make()' acepta cualquier objeto
polimórfico que sea un 'Vehicle', tanto directamente como
indirectamente a través de herencia de una clase que pertenezca a la
jerarquía de clases comenzada por 'Vehicle', como "pariente". Luego,
en 'main()', podríamos hacer esto:

Make( a );  // objeto de la clase 'Vehicle'
Make( b );  // objeto de la clase 'FourWheeler'

Como puedes ver, 'Make()' acepta cualquier objeto con tal de que su
clase sea 'Vehicle' o pertenezca a su jerarquía. La otra ventaja es
que esto permite una mayor flexibilidad, porque cualquiera puede
agregar su propio "vehículo" con tal de heredar de 'Vehicle' directa o
indirectamente y si es necesario, definir esas funciones miembro
virtuales para realizar otra tarea específica de tal clase. Por
ejemplo,

class Car : public FourWheeler
{
  virtual void Make()
  {
    cout << "Virtual Member function of Derived class Car Accessed" << endl;
  }
};

Y luego en 'main()', podemos hacer esto:

Car c;

Make( &c );

Como puedes ver, NO necesitamos modificar la función global 'Make()',
porque ésta trata todos los objetos como si trataren de la misma clase
'Vehicle'. Gracias al polimorfismo, al invocar la función virtual,
'make()', de la clase 'Vehicle', se invocará la versión correcta según
la clase correcta del objeto polimórfico. De esta manera, no tenemos
que modificar el código fuente ni las funciones que se basen en
objetos polimórficos, ya que internamente se guarda la información
necesaria para decidir correctamente cuáles funciones se deben escoger
según cada objeto polimórfico. Esto es un componente muy importante
para la cuestión de "extensibilidad".

Otra idea que también podemos usar y así también extender este ejemplo
se basa en trabajar con objetos polimórficos de 'Vehicle' en una
lista. Por ejemplo,

Vehicle *aVehiculos[100];

Ahora podemos crear varios objetos de diferentes clases, obviamente si
pertenecen a la misma jerarquía de 'Vehicle'. Por ejemplo,

aVehiculos[0] = new Vehicle;
aVehiculos[1] = new FourWheeler;
aVehiculos[2] = new Car;
aVehiculos[3] = new Bicycle;
...

Y podríamos pasar cada uno de estos objetos polimórficos a nuestra
función global 'Make()', sin siquiera alterar su implementación. Por
ejemplo,

for( int i=0; i<100; i++ )
  Make( aVehiculos[i] );

La idea principal es tratar cualquier 'Vehicle' de forma general, pero
obviamente cada uno es diferente entre sí por lo que sus detalles
yacen en sus funciones miembro correspondientes. Ampliando este
ejemplo, digamos que queremos mantener una carrera de vehículos,
podríamos implementar la siguiente función:

Vehicle ** Carrera( Vehicle *aParticipantes[], int nCant, Vehicle
*aResultados[] )
{
  // Preparar motores, calzado, ruedas, lo que sea
  for( int i=0; i<nCant; i++ )
    aParticipantes[i]->Preparar();

  // Damos la salida
  for( int i=0; i<nCant; i++ )
    aParticipantes[i]->Iniciar();

  // Mientras que haya participantes, seguimos la carrera
  while( nCant > 0 )
    for( int i=0; i<nCant; i++ )
      if( aParticipantes[i]->Distancia() <= 0 )
      {
        Agregar_Resultado( aResultados, aParticipantes, i );
        Eliminar_Participante( aParticipantes, i );
        --nCant;
      }
      else
      {
        aParticipantes[i]->Avanzar();
      }

  return aResultados;
}

Esta función servirá para cualquier tipo de vehículo, con tal de que
implemente las funciones miembro comunes de 'Vehicle()', que por el
ejemplo, podemos ver que son como mínimo:
- Preparar()
- Iniciar()
- Distancia()
- Avanzar()

Por esta razón, se suele crear una clase a modo de interfaz que
contenga solamente las funciones virtuales y puras de las operaciones
básicas y comunes que se vayan a usar por todas las clases derivadas.
Por ejemplo,

class IVehicle
{
  virtual bool Preparar() = 0;
  virtual bool Iniciar() = 0;
  virtual double Distancia() = 0;
  virtual double Avanzar() = 0;
};

Y luego, agregaríamos esta interfaz común a todas las clases
derivadas; por ejemplo,

class FourWheeler : public Vehicle, public IVehicle
{
  ...
};

class Car : public FourWheeler, public IVehicle
{
  ...
};

class Truck : public FourWheeler, public IVehicle
{
  ...
};

class TwoWheeler : public Vehicle, public IVehicle
{
  ...
};

class Motorcycle : public TwoWheeler, public IVehicle
{
  ...
};

Y así sucesivamente. He aquí la ventaja del polimorfismo. Podemos
agregar cuantas clases a la misma jerarquía como quisiéramos y además
como quisieren otros programadores, para "acoplar" sus necesidades y
posiblemente programas a nuestro programa o biblioteca.

Otro ejemplo que todos hemos venido usando es 'ostream' e 'istream',
como los objetos predefinidos de 'cout' y 'cin'. De estas clases,
aparecen las clases 'fstream', 'ofstream', e 'ifstream' para manejar
ficheros; y 'stringstream', 'ostringstream', e 'istringstream' para
manejar cadenas de caracteres. Podemos agregar otras clases a estas
jerarquías si quisiéramos, como por ejemplo, 'network', 'onetwork', e
'inetwork', para redes tanto locales como externas (internet, etc.).
Con una interfaz pública común, cada clase derivada puede implementar
los detalles para cada operación.


Espero haber aclarado las dudas.

Steven




Más información sobre la lista de distribución Cconclase