Guião da 8ª Aula Teórica

Sumário

Guião

Atenção!  Deixar claro que é conveniente seguir sempre as seguintes convenções:

  1. Uma classe polimórfica tem sempre destrutor polimórfico.  Esta não é bem uma convenção.  Não a seguir é um erro.
  2. Uma classe abstracta deve ter sempre um destrutor abstracto (que apesar de o ser tem de ser implementado).  Esta é uma convenção.  Serve para alertar o leitor do código.
Fazer pequena introdução sobre protected:.

Começar por escrever no quadro as classes Empregado e Chefe da Aula 7:

class Empregado {
  public:
    Empregado(string const& nome, Sexo const sexo);

    string const& nome() const;
    Sexo sexo() const;
    void mostra() const;

  private:
    string nome_;
    Sexo sexo_;
};

inline Empregado::Empregado(string const& nome, Sexo const sexo)
    : nome_(nome), sexo_(sexo)
{

}

inline string const& Empregado:: nome() const
{

    return nome_;
}

inline Sexo Empregado:: sexo() const
{

    return sexo_;
}

inline void Empregado:: mostra() const
{

    cout << "Nome: " << nome() << endl
         << "Sexo: " << sexo() << endl;
}

class Chefe : public Empregado {
  public:
    Chefe(string const& nome, string const& morada, Sexo const sexo, 
          int const nível);

    int nível() const;
    void mostra() const;

  private:

    int nível_;
};

inline Chefe::Chefe(string const& nome, string const& morada, Sexo const sexo,
                    int const nível)
    : Empregado(nome, sexo), nível_(nível) 
{
}

inline int Chefe::nível() const 
{

    return nível_;
}

inline void Chefe::mostra() const
{

    Empregado::mostra();
    cout << "Nível: " << nível() << endl;
}

Explicar de novo conceito de herança.  Introduzir:
  1. Herança: classe base e classe derivada.
  2. Classe derivada herda todos os membros da classe base.
  3. Membros herdados mantêm categoria de acesso se herança for pública.
  4. Membros herdados são privados se herança for privada.
  5. Relação é um e relação tem umÉ um: herança pública, princípio da substituibilidade . Tem um: composição ou agregação (variável membro)!
  6. Explicar brevemente diferença entre composição e agregação: braço e relógio de pulso relativo a humano.
  7. Consequências da relação é um: corte (slicing), mas sobretudo ponteiros e referência podem-se referir a objectos de classes derivadas!
  8. Ocultação: se a classe derivada declarar operação com mesmo nome de uma existente na classe base dá-se uma ocultação.
O nosso objectivo era poder tratar empregados e chefes uniformemente.  Queríamos pôr chefes, empregados, secretárias, motoristas, todos numa lista:

list<Empregado*> pessoal;

pessoal.push_back(new Empregado("João Maria", masculino));
pessoal.push_back(new Chefe("Ana Maria", feminino, 4));

...

for(list<Empregado*>::iterator i = pessoal.begin(); 
    i != pessoal.end();
++i)
    (*i)->mostra();

...

for(list<Empregado*>::iterator i = pessoal.begin(); 
    i != pessoal.end(); ++i)

    delete *i;

O problema é que aparece tudo como empregados!

Mostrar resultado:

Nome: João Maria
Sexo: masculino

Nome: Ana Maria
Sexo: feminino

É que o método invocado depende do tipo do ponteiro e não do tipo do objecto apontado...

Fazer boneco mostrando a lista e os tipos envolvidos.

O que nós gostávamos era que fosse o tipo dos objectos apontados a determinar o método a executar...  Precisamos de polimorfismo: que os empregados possam assumir diversas formas e, portanto, diferentes comportamentos.  Ou seja, precisamos que invocações de operações através de ponteiros para Empregado levem a executar o respectivo método da classe do objecto apontado!

Para isso temos de dizer ao C++ que a operação mostra() é polimórfica ou virtual!

class Empregado {
  public:
    Empregado(string const& nome, Sexo const sexo);

    string const& nome() const;
    Sexo sexo() const;
    virtual void mostra() const;

  private:
    string nome_;
    Sexo sexo_;
};

Se uma operação numa classe base for polimórfica, então as classes derivadas podem-na redefinir, como já acontecia.  Mas o que ocorre deixa de ser uma simples ocultação: passa a ser uma sobreposição!  Para que seja uma sobreposição a assinatura da operação tem de ser rigorosamente igual!

No fundo, uma sobreposição corresponde ao fornecimento de um método especializado para a classe derivada que implementa a operação da classe base.

Chama-se sobreposição porque o método a executar passa a ser seleccionado dependendo do tipo do objecto e não do tipo do ponteiro ou referência.  Ou seja, o método da classe derivada sobrepõe-se ao da classe base mesmo quando invocado através de um ponteiro para a classe base: quem manda é o tipo do objecto apontado.

No caso acima o método mostra() da classe Chefe sobrepõe-se ao método mostra() da classe Empregado se o objecto apontado for um Chefe.  Por isso o nosso código passou a funcionar!

list<Empregado*> pessoal;

pessoal.push_back(new Empregado("João Maria", masculino));
pessoal.push_back(new Chefe("Ana Maria", feminino, 4));

...

for(list<Empregado*>::iterator i = pessoal.begin(); 
    i != pessoal.end(); ++i)

    (*i)->mostra();

...

for(list<Empregado*>::iterator i = pessoal.begin(); 
    i != pessoal.end(); ++i)

    delete *i;

Fazer traçado!

Se numa classe existir pelo menos uma operação polimórfica ou virtual, então a classe diz-se polimórfica.  Se uma classe derivada declarar uma operação com a mesma assinatura que uma operação polimórfica da classe base, porque tenciona sobrepor o respectivo método, então essa operação será também polimórfica .  Mas é boa ideia deixar esse facto bem explícito:

class Chefe : public Empregado {
  public:
    Chefe(string const& nome, string const& morada, Sexo const sexo, 
          int const nível);

    int nível() const;
    virtual void mostra() const;

  private:

    int nível_;
}

De outra forma o leitor do código só sabe se uma operação é polimórfica olhando para as classes base, tendo eventualmente que subir uns quantos níveis na hierarquia de classes para o esclarecer...

A distinção que temos vindo a fazer entre operação e método só é verdadeiramente útil no contexto do polimorfismo.

Uma operação é algo que se pode invocar para uma instância de uma classe, eventualmente através de um ponteiro ou de uma referência, para atingir determinado objectivo.  Assim, na classe Empregado existe uma operação mostra() que é suposto mostrar informação completa sobre um empregado no ecrã.

Um método é a implementação de um método para uma classe concreta.  Assim, quando se invoca uma operação, é um método que é executado.

A distinção, claro está, é particularmente relevante quando há polimorfismo.  Quando não há polimorfismo, a cada operação corresponde um método, que é a sua única implementação.

Rever exemplo das listas e dizer que no ciclo se invoca a operação, que leva à execução de diferentes métodos.

A questão portanto está em saber como é feita a ligação entre a operação invocada e o método de facto executado.

Quando uma operação não é polimórfica, é o compilador que decide qual o método que é executado quando se invoca a operação: nesse caso diz-se que houve ligação estática.  O compilador decide o método que é executado com base no tipo da variável através do qual essa operação é invocada, mesmo que seja um ponteiro ou uma referência.  Ou seja, a distinção entre operação e método é pouco relevante.

Quando uma operação é polimórfica, só durante a execução do programa se sabe ao certo que método é executado quando se invoca a operação, pois depende da classe do objecto e não da classe do ponteiro ou referência para ele: chama-se ligação dinâmica!  O compilador não pode adivinhar o tipo do objecto apontado.  Só durante a execução do programa essa questão poderá ser esclarecida.  (Na prática a ligação continua a ser estática se a invocação se fizer através de uma variável simples e não de um ponteiro ou referência.)  Neste caso a distinção entre operação e método é fundamental, pois podem existir vários métodos que implementam a mesma operação.

Uma classe derivada não é obrigada a sobrepor versões especializadas dos métodos da classe base!  I.e., uma classe não é obrigada a fornecer implementações especializadas de uma operação polimórfica de uma classe base.  Só se achar que tal é necessário.  

Há duas razões para que algumas operações de classes polimórficas não sejam polimórficas:

  1. Se o programador que desenhou a classe base entende que as classes derivadas não devem fornecer métodos especializados como implementação dessa operação, não a tornar polimórfica é um bom aviso desse facto.
  2. A invocação de operações polimórficas usa ligação dinâmica e portanto é um pouco menos eficiente que a invocação de operações não polimórficas.  Logo, se o programador que desenhou a classe base entendeu que uma determinada operação não deveria ser implementada nas classe derivadas, torná-la polimórfica seria dar um sinal errado a quem desenvolvesse classes derivadas, como vimos, e tornaria a sua execução menos eficiente sem qualquer vantagem...
Vimos então que para tratar uniformemente os vários tipos de empregado duma empresa sem com isso perder o que é específico de cada um deles temos de recorrer ao polimorfismo e às operações polimórficas ou virtuais.

O polimorfismo tem um pequeno problema ainda.  Suponham que a classe Chefe tem dentro uma lista de subordinados:

class Chefe : public Empregado {
  public:
    Chefe(string const& nome, string const& morada, Sexo const sexo, 
          int const nível);

    int nível() const;
    virtual void mostra() const;

    void subordina(Empregado* const subordinado);

  private:
    int nível_;
    list<Empregado*> subordinados;
}

inline void Chefe:: subordina(Empregado* const subordinado) 
{

    subordinados.push_back(subordinado);
}

Discutir um pouco que a hierarquia de classes é conceptual e nada tem a ver com relações de poder, que estão representadas através da lista de subordinados.

Que acontece no seguinte código?

list<Empregado*> pessoal;
Empregado* pe = new Empregado("João Maria", masculino);
pessoal.push_back(pe);

Chefe* pc = new Chefe("Ana Maria", feminino, 4);
pc->subordina(pe);
pessoal.push_back(pc);

for(list<Empregado*>::iterator i = pessoal.begin(); i != pessoal.end(); ++i)
    (*i)->mostra();

...

for(list<Empregado*>::iterator i = pessoal.begin(); 
    i != pessoal.end(); ++i)

    delete *i;

Acontece que ao se destruir o chefe, o C++ chama o destrutor do Empregado!  Por isso a lista de subordinados nunca é destruída!  Os elos da cadeia duplamente ligada ficam a ocupar memória!  

Com resolver o problema?  Temos de tornar o destrutor polimórfico ou virtual na classe base mesmo que não faça nada!

class Empregado {
  public:
    Empregado(string const& nome, Sexo const sexo);
    virtual ~Empregado() {}

    string const& nome() const;
    Sexo sexo() const;
    virtual void mostra() const;

  private:
    string nome_;
    Sexo sexo_;
};

Regra:

Se uma classe é base de uma hierarquia de classes polimórficas, tem de definir um destrutor virtual!

Ok.  Até agora vimos operações polimórficas ou virtuais e vimos que conduziam ao polimorfismo e à ligação dinâmica.  Falta-nos outro assunto importante.

Suponham os seguintes conceitos relacionados:

class Veículo {
    ...
};

class Automóvel : public Veículo {
    ...
};

class Motociclo: public Veículo {
    ...
};

class HondaNX650 : public Motociclo {
    ...
};

class AudiTT : public Automóvel {
    ...
};

Quais destes conceitos são concretos e quais são abstractos?  

Existem instâncias de HondaNX650 por aí?  E de AudiTT?  Sim!  São classe concretas!

Existem instâncias de Motociclo por aí?  Não!  Existem instâncias de HondaNX650, de YamahaDiversion, etc., mas não simplesmente de Motociclo, Automóvel ou Veículo!  São conceitos abstractos!

Suponham que pretendíamos fazer um editor de figuras bidimensionais.

Começamos por definir algumas classes úteis:  

Discutir.  O melhor é deixar as classes por definir mesmo e puxar pelo poder de abstracção dos alunos.

class Posição {
  public:
    Posição(double const x, double const y);

    double x() const;
    double y() const;

  private:
    double x_;
    double y_;
};

class Dimensão {
  public:
    Dimensão(double const largura, double const altura);

    double largura() const;
    double altura() const;

  private:
    double largura_;
    double altura_;
};

class Caixa {
  public:
    Caixa(Posição const& posição, Dimensão const& dimensão);

    Posição posição() const;
    Dimensão dimensão() const;

  private:
    Posição posição_;
    Dimensão dimensão_;
};

Uma figura consiste numa colecção de formas:

list<Forma*> figura;

Suponhamos apenas as formas específicas Quadrado e Círculo.  O que é uma forma?  Como representá-la?

O conceito de Forma é abstracto!  Já os conceitos de Quadrado ou Círculo são bem concretos!

Podemos ver o conceito de Forma como uma generalização dos conceitos de Quadrado, Circulo, etc.  O que é que é possível fazer com qualquer forma?

Discutir.  Concluir:

  1. Saber área.
  2. Saber perímetro.
  3. Saber posição.
  4. Alterar posição.
  5. Saber caixa envolvente.
  6. Etc.
Então em que consiste a classe Forma?

class Forma {
  public:
    Forma(Posição const& posição);

    double área() const;
    double perímetro() const;
    Posição const& posição() const;
    Caixa const caixaEnvolvente() const;

    void movePara(Posição const& nova_posição);

  private:
    Posição posição_;
};

Forma:: Forma(Posição const& posição)
    : posição_(posição)
{

}

Posição const& Forma::posição() const
{

    return posição_;
}

void Forma::movePara(Posição const& nova_posição)
{

    posição_ = nova_posição;
}

Discutir membros!

É muito importante perceber que, devido a Forma ser um conceito abstracto, não é possível implementar as operações para cálculo da área, do perímetro e da caixa envolvente.  Claro está que para uma forma concreta, como círculo, isso já é possível.  Para já, no entanto, vamos discutir o caso da posição.

Todas as formas têm uma posição.  A posição é dada tipicamente pela posição de um ponto importante da forma, que pode ser o centro de um círculo ou um foco de uma elipse, um dos cantos de um quadrado ou rectângulo, o centro geométrico de um triângulo, ou um dos seus vértices, etc.

Por isso faz sentido definir um atributo para guardar a posição e implementar operações para inspeccionar e alterar o seu valor (mover para) na classe Forma!

Outra questão importante é que operações devem ser polimórficas.  Claramente deverão ser:

  1. área()
  2. perímetro()
  3. caixaEnvolvente()
Porquê?  Porque só mesmo as classes derivadas é que as saberão implementar duma forma aceitável!  Ou seja, só as classes derivadas saberão fornecer métodos para essas operações.

E as operações relativas à posição?  É discutível.  Quando se desenha uma classe base de uma hierarquia de classes há que ter o espírito aberto!  Será que alguém alguma vez pode querer definir uma classe derivada de Forma que tenha uma maneira diferente de se mover?  É discutível.  Mas em geral em geral é má ideia tornar polimórficas as operações inspectoras que se limitam a devolver o valor de um atributo.  E quanto à operação modificadora?  Vamos admitir que queremos deixar em aberto a possibilidade de alguém especializar a forma como uma forma se desloca.

Ou seja:

class Forma {
  public:
    Forma(Posição const& posição);
    virtual ~Forma() {}

    virtual double área() const;
    virtual double perímetro() const;
    Posição const& posição() const;
    virtual Caixa const caixaEnvolvente() const;

    virtual void movePara(Posição const& nova_posição);

  private:
    Posição posição_;
};

Notar destrutor virtual!

E a classe Círculo?  Um Círculo é uma Forma que, para além da posição que todas as formas têm (que corresponde ao centro do círculo), tem um raio.  Logo: 

Discutir operações.

class Círculo : public Forma {
  public:
    Circulo(Posição const& posição, double raio);

    virtual double área() const;
    virtual double perímetro() const;
    virtual Caixa const caixaEnvolvente() const;
    double raio() const;

  private:
    double raio_;
};

Círculo:: Circulo(Posição const& posição, double raio)
    : Forma(posição), raio_(raio) 
{

}

double Círculo::área() const
{

    return 2.0 * pi * raio();
}

double Círculo::perímetro() const
{

    return pi * raio() * raio();
}

Caixa const Círculo::caixaEnvolvente() const
{

    return Caixa(Posição(posição().x() - raio(),
                         posição().y() - raio()),
                 Dimensão(2.0 * raio(), 2.0 * raio());
}

double Círculo:: raio() const
{

    return raio_;
}

Note-se que o inspector do raio não é polimórfico.  Com isso deixa-se uma mensagem clara a quem queira especializar círculos: não deve especializar esse inspector.  O método fornecido é definitivo e não pode ser especializado.

Mas há aqui ainda um problema.  Para que uma operação como caixaEnvolvente() possa ser usada para qualquer Forma da Figura tem de estar declarada na classe base, claro.  Mas não pode ser definida, pois só sabemos calcular a envolvente de formas concretas!  Ou seja, a classe Forma fornece uma operação caixaEnvolvente(), mas não fornece nenhum método que a implemente!

Tal como as coisas estão obteremos um erro de fusão: o fusor vai-se queixar que não encontra a definição dos métodos Forma::caixaEnvolvente(), Forma::área() e Forma::perímetro() da classe Forma!

Em C++ esta "bota" descalça-se indicando claramente que não é suposto estas operações serem implementados pela classe Forma: são abstractas ou puramente virtuais, i.e., não têm método definido na classe, o que é o mesmo que dizer que a classe se pode limitar a declarar essas operações, não fornecendo a respectiva implementação:

class Forma {
  public:
    Forma(Posição const& posição);
    virtual ~Forma() = 0;

    virtual double área() const = 0;
    virtual double perímetro() const = 0;
    Posição const& posição() const;
    virtual Caixa const caixaEnvolvente() const = 0;

    virtual void movePara(Posição const& nova_posição);

  private:
    Posição posição_;
};

Forma::~Forma() 
{
}

Desenhar diagrama no quadro:

Regras:

  1. Uma declaração de uma rotina membro indica a presença de uma operação da classe.
  2. Uma definição de uma rotina membro indica a presença de um método que implementa uma operação da classe.
  3. Uma operação abstracta não precisa de ser definida, não precisa de ter o correspondente método.
  4. Uma classe que declare uma operação abstracta (pelo menos) diz-se também uma classe abstracta!
  5. Uma classe sem quaisquer operações abstractas (i.e., em que todas as operações têm métodos que as implementam) é uma classe concreta.
  6. Uma classe derivada que herde das classes base operações abstractas só é concreta se fornecer métodos que implementem cada uma dessas operações!
  7. Uma classe abstracta não pode ser instanciada!
Tudo isto está de acordo com a nossa noção intuitiva de abstracto/concreto.

Notar que é comum uma classe abstracta ter o destrutor não apenas polimórfico mas também abstracto!  Isso garante que a classe seja abstracta mesmo que se decida que todas as restantes operações sejam simplesmente polimórficas, e não abstractas.