Guião da 7ª Aula Teórica

Sumário

Guião

Se há mecanismos que sejam fundamentais para a Programação Orientada para Objectos são os de herança, polimorfismo e ligação dinâmica.  Ao longo das próximas aulas será este o nosso assunto.

Suponha-se que se pretendia escrever um programa para gerir o pessoal de uma empresa.  Podíamos começar pelo conceito mais simples: o de empregado.  Normalmente cada conceito, que corresponde usualmente a um substantivo na linguagem corrente, corresponde a uma classe.  Vamos pois começar por definir a classe Empregado. Suponha-se, para simplificar, que a informação relevante acerca de cada empregado é o seu nome e sexo:

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;
}

Notar que se admite que Sexo está definida algures.

Isto está muito bonito e trivial, mas na prática há vários tipos de empregados.  Os chefes, por exemplo, têm um nível de chefia e um secretário atribuído, os motoristas têm um veículo a seu cargo, cada secretário pode secretariar vários chefes...

Pensemos primeiro nos chefes.  Os chefes têm tudo o que um empregado tem, mas adicionalmente têm um nível de chefia, para simplificar.  Solução: escrever uma nova classe:

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

    string const& nome() const;
    Sexo sexo() const;
    int nível() const;
    void mostra() const;

  private:

    string nome_;
    Sexo sexo_;
    int nível_;
};

inline Chefe::Chefe(string const& nome, Sexo const sexo, int const nível)
    : nome_(nome), sexo_(sexo), nível_(nível)
{

}

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

    return nome_;
}

inline Sexo Chefe:: sexo() const 
{

    return sexo_;
}

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

    return nível_;
}

inline void Chefe:: mostra() const 
{

    cout << "Nome: " << nome() << endl
         << "Sexo: " << sexo() << endl
         << "Nível: " << nível() << endl;
}

Esta solução tem vários problemas. 

  1. O primeiro e menos grave é que nos fartamos de repetir código...  É quase tudo igual entre um empregado e um chefe... 
  2. O segundo e muito mais grave é que não há nenhuma relação entre as duas classes para além da que existe na nossa cabeça.  As duas classes são totalmente independentes, embora nós saibamos que um chefe é um empregado!

Repito: um chefe é um empregado.  Esta é uma relação muito importante e que se presta a confusões.  O que queremos dizer é que qualquer chefe é também um empregado. 

É muito diferente dizer "eu sou um humano" ou dizer "um humano é um mamífero".

Escrever no quadro.

A primeira relação é entre uma instância ou objecto e a sua classe: eu sou um objecto da classe dos humanos.

A segunda é uma relação entre duas classes: os humanos são mamíferos.

A primeira é uma relação de instanciação: eu sou um exemplo ou instância ou exemplar dos humanos.

A segunda é uma relação de especialização ou generalização, consoante a direcção em que olharmos.  O conceito de humano é uma especialização de mamífero e o conceito de mamífero é uma generalização de humano (e não só...).

É interessante fazer um paralelo entre a linguagem natural e o C++.  O quadro que se segue tem de se ir construindo ao longo da aula:

Linguagem natural C++
Nome comum:

"humano"

Classe:

Humano

Definida como:

class Humano {

    ...

};

Nome próprio:

"Zé"

Variável:

 

"O Zé é um humano." Definição de variável:

Humano zé;

"Um humano é um mamífero", ou
"Qualquer humano é um mamífero".
Derivação de classe:

class Humano : public Mamífero {

    ...

}

"Os empregados têm (estão associados a) um chefe." Associação simples:

class Empregado {

    ...

  private:
    Chefe* chefe_;
};

"Uma turma tem (agrega) alunos." Agregação:

class Turma {

    ...

  private:
    list<Aluno*> alunos;
};

"Um humano tem (é composto por) cabeça." Composição:

class Humano {

    ...

  private:
    Cabeça cabeça;
};

Queremos pois representar a relação de generalização que existe entre chefe e empregado.  Essa relação pode ser representada como se segue:

Que impacto deveria ter no nosso código que o C++ soubesse que um Chefe é um  Empregado?  Várias.  A primeira e menos útil seria que nos permitiria escrever algo como:

Chefe ana_maria("Ana Maria", feminino, 4);
Empregado sósia_de_ana_maria_como_empregado = ana_maria;

A esta operação, se fosse possível, chamar-se-ia corte (slicing), pois faz uma cópia da Ana Maria cortando tudo aquilo que a torna chefe (o que será?).  É uma operação perigosa (dolorosa?) e de evitar, como se verá.

Mais importante era que nos fosse possível colocar ponteiros para todos os empregados numa lista, independentemente do seu tipo específico, e percorrer essa lista para, por exemplo, visualizar todos os empregados:

list<Empregado*> empregados;

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

...

for(list<Empregado*>::const_iterator i = empregados.begin();
    i != empregados.end();
    ++i) {
    (*i)->mostra();
    cout << endl;
}

E que surgisse:

Nome: João Maria
Sexo: masculino

Nome: Ana Maria
Sexo: feminino
Nível: 4

Neste ponto fazer representação UML da lista e explicar diferença entre tipo do ponteiro e tipo do objecto apontado.

Discutir com os alunos possível soluções para o problema.  Tentar chegar à solução "pedestre" de uma só classe para depois a discutir.  Se se chegar à solução de um empregado dentro de um chefe dizer que já se discute.

Uma possível tentativa de resolver o problema passa por definir apenas uma classe e distinguir internamente o tipo específico de empregado através, por exemplo, de um tipo enumerado:

class Empregado {
  public:

    enum Tipo {empregado, chefe};

    Empregado(Tipo tipo,
string const& nome, Sexo const sexo, int const nível = 0);

    string const& nome() const;
    Sexo sexo() const;
    int nível() const;
    void mostra() const;

  private:
    string nome_;
    Sexo sexo_;
    int nível_;
    Tipo tipo;
};

inline Empregado::Empregado(Tipo tipo, string const& nome, Sexo const sexo,
                            int const nível = 0)
    : tipo(tipo), nome_(nome), sexo_(sexo), nível_(nivel)
{
    assert(tipo != chefe or nível == 0);
}

inline string const& Empregado:: nome() const
{
    return nome_;
}

inline Sexo Empregado:: sexo() const
{
    return sexo_;
}

inline int Empregado:: nível() const 
{
    assert(tipo == chefe);

    return nível_;
}

inline void Empregado:: mostra() const
{
    cout << "Nome: " << nome() << endl
         << "Sexo: " << sexo() << endl;
    if(tipo == chefe)
        cout << "Nível: " << nível() << endl;
}

O primeiro problema que surge é que nível atribuir aos empregados.  Aqui arbitrou-se ser nível zero, mas nem sempre poderá ser possível uma solução semelhante.

Imagine-se agora que se pretendia representar também secretários, motoristas, chefes dos motoristas, carpinteiros, etc...  Em breve o caos estava instalado.  O construtor teria de prever todos os casos e verificar todas as combinações impossíveis (e.g., atribuição de automóveis a carpinteiros, por exemplo).  Todos os métodos com especializações para cada tipo de empregado seriam monstruosos, provavelmente dominados por inúmeros e gigantescos switch...  Para além da questão estética, não despicienda, este código era:

  1. difícil de compreender.
  2. difícil de estender.
  3. difícil de alterar.
  4. difícil de depurar.

Há uma outra possível solução.  Usam-se duas classes separadas mas põe-se um empregado dentro de um chefe:

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

    string const& nome() const;
    Sexo sexo() const;
    int nível() const;
    void mostra() const;

  private:

    Empregado empregado;
    int nível_;
};

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

}

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

    return empregado.nome();
}

inline Sexo Chefe::sexo() const
{
    return empregado.sexo();
}

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

    return nível_;
}

inline void Chefe::mostra() const
{

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

Esta solução tem pelo menos a vantagem de poupar código.  Reparem como o Chefe delega no Empregado a maior parte das suas tarefas...  O problema é que ainda não é possível tratar um chefe como se de um empregado se tratasse e, pior, dá a sensação que os chefes possuem fisicamente um pequenino homúnculo dentro deles encarregue de trabalhar por eles...  O nosso código não corresponde ao que queremos modelar, o que é sempre má ideia.

Podíamos resolver parcialmente o primeiro problema acrescentando na classe Empregado um construtor que recebesse um chefe como argumento e dele utilizasse apenas o que é relevante para construir um empregado (cortando a informação especializada) e um operador de atribuição semelhante.  Mas isso implicaria que, por cada conceito especializado de empregado seria necessário um construtor e um operador de atribuição adicionais na classe Empregado.  E, além disso, nada disto nos permitiria escrever o nosso código objectivo:

list<Empregado*> empregados;

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

...

for(list<Empregado*>const_iterator i = empregados.begin();
    i != empregados.end();
    ++i) {
    (*i)->mostra();
    cout << endl;
}

É importante perceber-se bem o interesse das instruções:

Chefe o_zé("Zé Maria", masculino, 4);
Empregado& de_novo_o_zé = o_zé;
Empregado* ponteiro_para_o_zé = &zé;

Desenhar diagrama UML!

É que nos permite, por exemplo, colocar endereços de qualquer tipo específico de empregado numa lista e percorrê-la duma forma uniforme, como vimos.  Ou permite-nos escrever um procedimento:

void pagaSalário()
{
   
...
}

capaz de lidar com qualquer tipo específico de empregado.

A solução para este dilema é-nos fornecida pelo chamado mecanismo de herança do C++.

A sintaxe é a seguinte:

class Chefe : public Empregado {
   
...
};

Dir-se-á que a classe Chefe herda de, deriva de ou especializa a classe Empregado.  Logo, um Chefe é um EmpregadoEmpregado é a classe base nesta relação e Chefe a classe derivada.

Dizer que podia haver mais do que uma base!

Em bom rigor herdar e especializar não são necessariamente a mesma coisa, mas o C++ confunde os dois conceitos.  Quem quiser discutir em privado está à vontade....

O que acontece é que a classe Chefe herda da classe Empregado todos os seus atributos e operações.  Assim, só é necessário definir o que é novo ou especializado.  Ou seja:

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 int Chefe::nível() const 
{

    return nível_;
}

Discutir: 

  1. Qual é a base e qual é a derivada?
  2. Qual o sentido (direcção) da relação é um?
  3. Categorias de acesso.  O que é privado na classe base não pode ser mexido na classe derivada!  O que é público na classe base pode ser mexido por toda a gente, mesmo a classe derivada.
  4. A classe derivada tem acesso directo ao nome, por exemplo?
  5. Ambas têm um método mostra().  Que significa?  Discutir ocultação!

Perfeito.  A classe derivada herdou tudo o que queria da classe base, acrescentou o inspector para o nível e tem de especializar o método mostra().  Para isso fornece uma versão ele próprio, ocultando e especializando a versão da classe base: 

inline void Chefe::mostra() const
{

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

Explicar necessidade do operador de resolução de âmbito.

E o construtor?

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

Acontece que é necessário inicializar atributos privados da classe base.  Não podemos escrever:

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

Nem seria boa ideia!  Discutir brevemente invariantes de classe.

O C++ diz-nos que devemos inicializar a parte do novo objecto que é da classe base invocando o seu construtor na lista de inicializadores:

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

Explicar ordem de construção:

  1. Primeiro as classes base, por ordem de declaração no cabeçalho da classe derivada.
  2. Depois os atributos, por ordem de declaração no corpo da classe.
  3. Finalmente é executado o corpo do construtor.

A destruição ocorre pela ordem inversa!

É possível definir hierarquias de classes.  Por exemplo:

Mas ainda não resolvemos o problema!  Se corrermos o nosso querido pedaço de programa:

list<Empregado*> empregados;

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

for(list<Empregado*>const_iterator i = empregados.begin();
    i != empregados.end();
    ++i) {
    (*i)->mostra();
    cout << endl;
}

O que aparece?

Discutir com base no diagrama UML:

Concluir que é:

Nome: João Maria
Sexo: masculino

Nome: Ana Maria
Sexo: feminino

Ou seja, o método mostra()  executado aquando da invocação de mostra() depende do ponteiro através do qual se faz a invocação e não do verdadeiro tipo do objecto apontado!  

De facto, através da herança estabelecida, o C++ já considera que um Chefe é um Empregado.  Mas ainda falta algo.

A ligação entre a operação invocada (neste caso mostrar()) e o método efectivamente executado é neste caso estática (decidida pelo compilador).  Falta-nos polimorfismo, que permite a operações realizadas sobre objectos apontados por ponteiros de um único tipo terem comportamentos diversos consoante o objecto apontado: falta-nos ligação dinâmica entre a operação invocada e o método executado (decidida durante a execução do programa).  Assunto para a próxima aula...

Finalmente, para terminar a aula, uma pequena explicação.  Que acontece se, ao especificarmos uma herança, usarmos a palavra chave privateApontar exemplo no quadro.  Duas coisas:

  1. Os métodos públicos da classe base tornam-se privados da classe derivada!
  2. Não é estabelecida relação é um entre a classe derivada e a classe base.

Para que serve então?  Para nada, praticamente.  Bom, tem algumas aplicações restritas.  Sempre que um conceito for diferente de outro, mas puder ser implementado à sua custa com pequenas variações, que correspondem normalmente a uma restrição da interface, então pode-se usar.  No fundo, uma herança privada estabelece um tipo especial de relação:

funciona como um ... mas

Pense-se no caso das pilhas e das listas.  Qual pode ser implementada à custa de qual?

Discutir!

É claro que:

a classe PinhaDeInt funciona como uma ListaDeInt, mas apenas permite algumas operações, com nomes diferentes!

Claro está que se podia implementar a classe PilhaDeInt à custa de um atributo da classe ListaDeInt:

class PilhaDeInt {
  public:
    typedef ListaDeInt::Item Item;

    int altura() const;
    bool estáVazia() const;

    Item const& topo() const;

    Item& topo();

    void põe(Item const& novo_item);
    void tiraItem();

  private:

    ListaDeInt lista;
};

inline int PilhaDeInt:: altura() const
{

    return lista.comprimento();
}

inline bool PilhaDeInt::estáVazia() const
{

    return lista.estáVazia();
}

inline Item const& PilhaDeInt::topo() const
{

    return lista.trás();
}

inline Item& PilhaDeInt::topo(
{

    return lista.trás();
}

inline void PilhaDeInt::põe(Item const& novo_item) 
{

    lista.põeAtrás(novo_item);
}

inline void PilhaDeInt::tiraItem(
{

    lista.tiraDeTrás();
}

Mas é mais simples fazer uma herança privada e alterar só o que é necessário:

class PilhaDeInt : private ListaDeInt {
  public:
    typedef ListaDeInt::Item Item;

    int altura() const;
    bool estáVazia() const;

    Item const& topo() const;

    Item& topo();

    void põe(Item const& novo_item);
    void tiraItem();
};

inline int PilhaDeInt::altura() const
{

    return comprimento();
}

inline bool PilhaDeInt::estáVazia() const
{

    return ListaInt::estáVazia();
}

inline Item const& PilhaDeInt::topo() const
{

    return trás();
}

inline Item& PilhaDeInt::topo()
{

    return trás();
}

inline void PilhaDeInt::põe(Item const& novo_item)
{

    põeAtrás(novo_item);
}

inline void PilhaDeInt::tiraItem()
{

    tiraDeTrás();
}

Ou seja, a herança privada é apenas uma forma estranha de composição.  Mas como a composição original também estava a ser usada de uma forma estranha, pois não se pode dizer que uma pilha seja composta por uma lista....

Simplificam-se as coisas se se disser que determinados membros da classe base devem ser públicos da classe derivada apesar da herança ser privada:

class PilhaDeInt : private ListaDeInt {
  public:
    using ListaDeInt::Item;

    int altura() const;


    using ListaInt::estáVazia;


    Item const& topo() const;

    Item& topo();

     void põe(Item item);
    void tiraItem();

};

inline int PilhaDeInt::altura() const
{

    return comprimento();
}

inline Item const& PilhaDeInt::topo() const
{

    return trás();
}

inline Item& PilhaDeInt::topo()
{

    return trás();
}

inline void PilhaDeInt::põe(Item const& novo_item)
{

    põeAtrás(novo_item);
}

inline void PilhaDeInt::tiraItem()
{

    tiraDeTrás();
}

Se estivermos dispostos a usar os mesmos nomes já existentes...

class PilhaDeInt : private ListaDeInt {
  public:
    using ListaDeInt::Item;

    using ListaDeInt::comprimento;

    using ListaDeInt::estáVazia;

    using ListaDeInt::trás;


    using ListaDeInt::põeAtrás;

    using ListaDeInt::tiraDeTrás;
};