AULA 14 - Programação II - Graduação

De IFSC
Revisão de 11h55min de 16 de maio de 2018 por imported>Fargoud
Ir para navegação Ir para pesquisar

POLIMORFISMO

Polimorfismo em linguagens orientadas a objeto, é a capacidade de objetos se comportarem de forma diferenciada, em face de suas características ou do ambiente ao qual estejam submetidos, mesmo quando executando ação que detenha, semanticamente, a mesma designação.

PRGpolim.png


Já vimos isto acontecer com os construtores sobrecarregados das classes, que executam diferentes códigos, para diferentes listas de parâmetros.

O polimorfismo em C++ se apresenta sob diversas formas diferentes, desde as mais simples, como operadores que têm múltiplos usos, funções com mesmo nome e lista de parâmetros diferentes, até as mais complexas como é o caso das funções virtuais, cujas formas de execução são dependentes da classe a qual o objeto pertence e são identificadas em tempo de execução.

Polimorfismo descreve a capacidade de um código de programação comportar-se de diversas formas dependendo do contexto;

  • É um dos recursos mais poderosos de linguagens orientadas a objetos:
  • Permite trabalhar em um nível alto de abstração;
  • Facilita a incorporação de novos códigos em um sistema existente.

A herança nos permite reutilizar numa classe derivada um código escrito para uma classe base. Nesse processo, alguns métodos podem ser redefinidos e, embora tenham nomes idênticos, ter implementações completamente distintas.

FUNÇÕES VIRTUAIS

Em muitas situações, projeta-se uma classe base que vai gerar muitas classes derivadas, porém estas são muito diferentes entre si...

Se uma classe base A tem uma função virtual f() e sua classe derivada B também tem uma função f(), de mesmo nome e tipo, então uma chamada a f() a partir de um objeto da classe derivada deveria invocar B::f().

Nem sempre isto vai acontecer:

Exemplo 1

class Componente {                      
   public:                          
      void exibe() const { cout << "Componente::exibe()" << endl; } 
};
/**************/
class Botao: public Componente {       
   public:            
      void exibe() const { cout << "Botao::exibe()" << endl; }   
};
/**************/ 
class Janela: public Componente {
   public:         
      void exibe() const { cout << "Janela::exibe()" << endl; } 
}; 
/**************/ 
void manipula(const Componente &c)  
{    c.exibe(); } 
/**************/ 
int main()  
{   Botao ok;    
    Janela ajuda;   
    manipula(ok);   //objeto da classe derivada Botao
    manipula(ajuda); //objeto da classe derivada Janela
    return 0;
} 

Executando o programa acima, veremos que a instrução c.exibe(), na função de manipulação, chamará o método exibe() da super=classe Componente!

A saída será:

PRGfv2.png

Mas isso, na prática, seria um erro; pois a exibição de um botão é uma tarefa diferente da exibição de uma janela.

Como desenvolver, então, os métodos na super-classe, de forma bastante genérica, para que cada classe possa particularizá-los na medida do que seja necessário?

FUNÇÕES VIRTUAIS

Se na função de manipulação queremos chamar o método exibe() correspondente à classe a que pertence o objeto passado à função, então temos que definir, dentro da classe base, o método exibe() como virtual!!!

Para declarar uma função como sendo virtual, é preciso preceder sua declaração com a palavra chave virtual.

Uma função virtual é uma função que é declarada como virtual em uma classe base e redefinida pela classe derivada.

A redefinição da função na classe derivada sobrepõe a definição da função virtual na classe base!!!

Em outras palavras, a declaração da função virtual na classe base age como uma espécie de indicador que especifica uma linha geral de ação e estabelece uma interface de acesso.

A redefinição da função virtual pela classe derivada especifica as operações realmente executadas pelo método.

O problema acima então ficaria:

class Componente {                      
    public:                          
      virtual void exibe() const { cout << "Componente::exibe()" << endl; } 
};   

Para maior clareza, a palavra chave virtual pode ser repetida também na declaração do método exibe() nas classes derivadas. Isto, porém, não é obrigatório neste caso.

class Botao: public Componente {       
   public: virtual void exibe() const { cout << "Botao::exibe()" << endl; }   
}; 
/*********/
class Janela: public Componente {    
   public: virtual void exibe() const { cout << "Janela::exibe()" << endl;}
};

Agora, a saída será:

PRGfv5.png

Ao encontrar uma chamada a um método virtual, o compilador terá que aguardar até o momento da execução para decidir qual o método correto a ser chamado.


Exemplo 2

Temos que definir a superclasse FiguraGeometrica, a partir da qual serão derivadas as classes Circulo, Quadrado e Triangulo.

PRGpolim2.png

O método Desenha(), porém, certamente será bastante diferente para cada uma das subclasses.

Como proceder.....???


Definir o método como virtual na superclasse e redefinir o método em cada uma das subclasses!


Exemplo 3

Deseja-se criar um vetor de ponteiros para acessar objetos....

#include <iostream>
class base  // Classe base
{  public: void print( ) { cout << "\nBASE";}
};
class deriv0: public base  //Classe derivada de base
{  public: void print( ) { cout << "\nDERIV0";}
};
class deriv1: public base  //Classe derivada de base
{  public: void print( ) { cout << "\nDERIV1";}
};
class deriv2: public base  //Classe derivada de base
{  public: void print( ) { cout << "\nDERIV2";}
};
/*************/
int main()
{   base  * ptr[3];  // cria vetor de ponteiros para classe base
    deriv0 dv0;  // cria objeto da subclasse deriv0
    deriv1 dv1; // cria objeto da subclasse deriv1
    deriv2 dv2; // cria objeto da subclasse deriv2
    // preenche vetor de ponteiros: 
    ptr[0] = &dv0; 
    ptr[1] = &dv1;
    ptr[2] = &dv2;
    for(int i = 0; i < 3; i++)
       ptr[i]->print( );  // chama método
    return 0;
}

A saída será:

PRGfv3.png

Para resolver o problema:....

#include <iostream>
class base  // Classe base
{public: virtual void print( ) { cout << "\nBASE";} //MÉTODO VIRTUAL
}; //RESTANTE DO CÓDIGO INALTERADO:
class deriv0: public base  //Classe derivada de base
{  public: void print( ) { cout << "\nDERIV0";}
};
class deriv1: public base  //Classe derivada de base
{  public: void print( ) { cout << "\nDERIV1";}
};
class deriv2: public base  //Classe derivada de base
{  public: void print( ) { cout << "\nDERIV2";}
};
/*************/
int main()
{   base  * ptr[3];  // cria vetor de ponteiros para classe base
    deriv0 dv0;  // cria objeto da subclasse deriv0
    deriv1 dv1; // cria objeto da subclasse deriv1
    deriv2 dv2; // cria objeto da subclasse deriv2
    // preenche vetor de ponteiros: 
    ptr[0] = &dv0; 
    ptr[1] = &dv1;
    ptr[2] = &dv2;
    for(int i = 0; i < 3; i++)
       ptr[i]->print( );  // chama método
    return 0;
}

O restante do código permanece exatamente igual e a saída agora fica:

PRGfv4.png


Quando trabalhamos com ponteiros para objetos de classes derivadas, é importante que os destrutores também sejam declarados virtuais.

Se um objeto da classe derivada está sendo apontado por ponteiro da classe base, e os destrutores são virtuais, então, quando esse objeto é liberado, primeiro será executado o destrutor da classe derivada e só depois o da classe base.

class Componente {                   
   public: virtual ~Componente() 
  { cout << "Destrói componente" << endl; } 
}; 
/*************/
class Botao: public Componente {                      
    public:                         
      ~Botao() { cout << "Destrói botao" << endl; } 
}; 
/*************/
void main(void) {    
  Botao *ok = new Botao;    
  Componente *c = ok;    
  delete c; 
 } 

Ao ser executado o comando delete, no programa acima, serão exibidas as mensagens "Destrói botão" e "Destrói componente".

Porém, se o destrutor não tivesse sido declarado virtual, apenas o destrutor da classe Component e, aquela a que pertence o ponteiro, seria chamado.

Nem construtores nem métodos estáticos podem ser declarados virtuais.

Além disso, a acessibilidade de um método virtual é conservada dentro de todas as classes derivadas, mesmo que ele seja redefinido com um status diferente.


Classes abstratas

Às vezes, um método virtual definido em uma classe base, serve como uma interface genérica, que deverá ser implementada nas classes derivadas.

Se não há implementação desse método na classe base, isto é, quando a função virtual não possui código próprio é chamada de função virtual pura, ou método virtual puro, ou seja, foi criado apenas para ser redefinido nas classes derivadas - INTERFACE POLIMÓRFICA PARA SUBCLASSES.

Para indicar que um método é virtual puro, adicionamos o sufixo = 0 ao seu protótipo.


Exemplo 4

class base  // Classe base
{  public: virtual void print( ) = 0; // função virtual pura
};
...


A classe que possui uma ou mais funções virtuais puras não pode gerar objetos diretamente e é chamada de classe ABSTRATA - só pode ser utilizada como superclasse!

Uma classe é abstrata se contém pelo menos um método virtual puro.

Não é possível criar uma instância de uma classe abstrata.

Exemplo 5

class Componente {
  public:
    virtual void exibe() const = 0; // método virtual puro
 };
 void main()
 {
   Componente c; // erro, não se pode instanciar uma classe abstrata!
   ...
 }


Uma classe abstrata não pode ser usada como tipo de argumento, nem como tipo de retorno de uma função.

Um método virtual puro não precisa ter uma implementação.

Além disso, uma classe abstrata somente pode ser usada a partir de um ponteiro ou de uma referência.

Uma classe derivada que não redefine um método virtual é também considerada abstrata.

Funções virtuais puras e classes abstratas Há situações em que a é desejável garantir que todas as classes derivadas de uma determinada classe base suportassem um dado método. Entretanto, algumas vezes a classe base não tem informação suficiente para permitir alguma definição para uma função virtual. Para estes casos, C ++ suporta o conceito de funções virtuais puras.

Uma função virtual pura não tem nenhuma definição dentro da classe base. Para declarar uma função virtual pura, usa-se a forma

virtual tipo nome_função (lista-args) = 0; Quando uma função virtual é feita pura, qualquer classe derivada deve fornecer sua própria definição. Caso contrário, um erro de compilação será acusado.

Classes que contenham pelo menos uma função virtual pura são chamadas de classes abstratas. Como uma classe abstrata contém um ou mais métodos para os quais não há definição (as funções virtuais puras), objetos não podem ser criados a partir de classes abstratas. Pode-se dizer que uma classe abstrata é a definição de um tipo incompleto, que serve como fundação para classes derivadas.

Objetos não podem ser criados para classes abstratas, mas apontadores para classes abstratas são válidos. Isto permite que classes abstratas suportem polimorfismo em tempo de execução.



Funções virtuais


Quando acessadas normalmente, funções virtuais se comportam como qualquer outro tipo de função membro da classe. Entretanto, o que torna funções virtuais importantes e capazes de suportar polimorfismo em tempode execução é o seu modo de comportamento quando acessado através de um apontador. Lembre-se que um apontador para a classe base pode ser usado para apontar para qualquer classe derivada daquela classe base. Quando um apontador base aponta para um objeto derivado que contém uma função virtual, C ++ determina qual versão daquela função chamar baseada no tipo do objeto apontado pelo apontador. Assim, quando objetos diferentes são apontados, versões diferentes da função virtual são executadas.

Considere o seguinte exemplo:

indentation

  1. include $<$iostream.h$>$

class base { public: virtual void vfunc() { cout $\ll$ "Esta e vfunc() de base$\backslash$n"; } };

class derived1 : public base { public: void vfunc() { cout $\ll$ "Esta e vfunc() de derived1$\backslash$n"; } };

class derived2 : public base { public: void vfunc() { cout $\ll$ "Esta e vfunc() de derived2$\backslash$n"; } };

main() { base $\ast$p, b; derived1 d1; derived2 d2;

// p aponta para base p = &b; p$\rightarrow$vfunc(); // p aponta para derived1 p = &d1; p$\rightarrow$vfunc(); // p aponta para derived2 p = &d2; p$\rightarrow$vfunc();

return(0); } O resultado da execução deste programa é:

   Esta e vfunc() de base
   Esta e vfunc() de derived1
   Esta e vfunc() de derived2

Observe que:

a declaração de vfunc() deve ser precedida pela palavra chave virtual; a palavra chave virtual não é necessária na declaração das funções vfunc() das classes derivadas; a definição de qual versão da função vfunc() é invocada só é obtida em tempo de execução: ela não é baseada no tipo declarado de p, mas sim para quem p aponta; polimorfismo em tempo de execução só é atingido através do uso de apontadores para classe base. Se vfunc() for chamada da maneira ``usual, isto é, usando o operador ponto, como em d2.vfunc(), então a função, mesmo sendo declarada como virtual, tem o mesmo comportamento de outras funções membro. A redefinição de uma função virtual por uma classe derivada é similar a sobrecarga de funções. Entretanto, este termo não é aplicado para a redefinição de funções virtuais porque há diversas diferenças. Talvez a mais importante é que o protótipo para uma função virtual deve combinar exatamente o protótipo especificado para a classe base. Caso o protótipo seja diferente, o comportamento será exatamente o mesmo de uma função sobrecarregada -- o comportamento virtual da função será perdido. Outra restrição importante é que funções virtuais devem ser membros da classe das quais elas fazem parte -- não podem simplesmente ser funções amigas. Por estes motivos, a redefinição de funções virtuais por classes derivadas é usualmente chamada de sobreposição, e não de sobrecarga.

Deve-se observar que a ``virtualidade é hereditária. Isto é, quando uma função virtual é herdada, sua natureza virtual também é herdada -- mesmo quando a palavra chave virtual não é explicitamente declarada no método da classe derivada. Assim, o caráter virtual da função é mantido, não importa qual o número de níveis na hierarquia de classes.

O uso da palavra chave virtual indica que a função pode ser sobreposta, mas não obriga a sobreposição. Caso a classe derivada não sobreponha uma função virtual, então o método correspondente da classe base será utilizado.



6.8. Herança virtual

Considere a derivação da classe D ilustrada a seguir:

Se a classe A possui um dado membro a, um objeto da classe D herdará duas cópias desse dado, uma vez da classe B e outra da classe C. Para acessar esses dados duplicados, será preciso empregar o operador de resolução de escopo.


Exemplo 6.15: void main(void) { D d; d.a = 0; // erro: ambíguo d.B::a = 1; // Ok, acessa cópia herdada de B d.C::a = 2; // Ok, acessa cópia herdade de C } A B D C Herança 41

Através da herança virtual, entretanto, é possível especificar que haja apenas uma ocorrência dos dados herdados da classe base. Para que a classe D herde apenas uma cópia dos dados da classe A, é preciso que as classes B e C herdem virtualmente de A. Exemplo 6.16: class B : virtual public A { ... }; class C : virtual public A { ... }; class D : public B, public C { ... }; void main() { D d; d.a = 0; // Ok, não há mais ambigüidade } É preciso não confundir esse status "virtual" da declaração de herança de classes com aquele dos membros virtuais que veremos mais adiante. Aqui, a palavra chave virtual apenas indica ao compilador que os dados herdados não deverão ser duplicados.





Funções Amigas << AULA 14 - Funções Virtuais e Polimorfismo Streams >>

<<= Página do curso