Aula 8


1  Índice



2  Resumo

2.1  Categorias de acesso: acesso protegido

Existe uma categoria de acesso aos membros de uma classe que impõe mais limitações que a categoria de acesso público e menos que a categoria de privado: a categoria de acesso protegido, introduzida por intermédio de protected:.  Membros com esta categoria de acesso funcionam como privados para o público em geral, mas como públicos para as classes derivadas.  Assim:
 
Categoria de acesso dos membros Acesso
privado (private:) Só membros e amigos (mas a amizade não é hereditária) da classe.
protegido (protected:) Membros e amigos (mas a amizade não é hereditária) da classe e de classes derivadas.
público (public:) Sem restrições.

Quando se deriva uma classe a partir de outra a derivação pode também ser pública, protegida ou privada.  Sendo B uma classe derivada de A:

class A {
  public:
    ...
  protected:
    ...
  private:
    ...
};

class B : categoria A {
  public:
    ...
  protected:
    ...
  private:
    ...
};

class C : categoria B {
    ...
};

onde categoria pode ser public, protected ou private, tem-se:
 
Categoria de acesso da herança Acesso aos membros de A (classe base) Relação entre B (classe derivada) e A (classe base)
Categoria de acesso dos membros de A (classe base) Acesso através de B (classe derivada) Do ponto de vista do público em geral Do ponto de vista das classes derivadas de B (e.g. C) Do ponto de vista de B (classe derivada)
privado (private) privado (private:) Ninguém. Não há relação.

(Ou melhor, pode ser considerada uma relação de composição, ou seja, a classe derivada tem uma classe base.)

Não há relação. B é um A.
protegido (protected:) Só membros e amigos de B.
público (public:) Só membros e amigos de B.
protegido (protegido) privado (private:) Ninguém. Não há relação. Não há relação.

Mas, do ponto de vista de C, há relação entre C e A: C é um A.

B é um A.
protegido (protected:) Membros e amigos de B e de classes derivadas desta (e.g., C).
público (public:) Membros e amigos de B e de classes derivadas desta (e.g., C).
público (public) privado (private:) Ninguém. B é um A. B é um A. B é um A.
protegido (protected:) Membros e amigos de B e de classes derivadas desta (e.g., C).
público (public:) Sem restrições.

Não tente decorar nem perceber à primeira esta tabela.  Recorra a ela quando sentir dificuldades.

2.2  Polimorfismo e funções ou procedimentos virtuais

Na aula anterior viu-se que o seguinte código
ListaPonteiroEmpregado pessoal;
// Inserção dos empregados:
pessoal.põeFim(new Empregado("Manela", "Rua B", feminino));
pessoal.põeFim(new Chefe("Zé", "Rua A", masculino, 2));
...
// Visualização dos empregados:
for(ListaPonteiroEmpregado::Iterador i = pessoal.primeiro();
    i != pessoal.fim();
    ++i)
    (*i)->mostra();
embora compilasse correctamente, tinha um comportamento pouco desejado: todos os objectos na lista, incluindo chefes e empregados, eram mostrados como simples empregados.  Este comportamento não é surpreendente, visto que a todos os objectos se faz referência através de ponteiros para a classe Empregado.

Ponteiros de uma classe base pública podem conter endereços de objectos de classes derivadas.  Dois tipos de comportamento são portanto plausíveis ao se invocar através de um ponteiro para a classe base uma função ou procedimento membro dessa classe que a classe derivada ocultou fornecendo a sua própria versão:

  1. Executar de acordo com a classe do ponteiro.
  2. Executar de acordo com a classe do objecto apontado.
O mesmo se passa para as referências.  Referências de uma classe base pública podem referenciar objectos de classes derivadas.  Dois tipos de comportamento são portanto plausíveis ao se invocar através de uma referência para a classe base uma função ou procedimento membro dessa classe que a classe derivada ocultou fornecendo a sua própria versão:
  1. Executar de acordo com a classe da referência.
  2. Executar de acordo com a classe do objecto referenciado.
O primeiro comportamento é o que ocorre, por omissão, no C++.  Ao segundo dá-se o nome de comportamento polimórfico.  As funções ou procedimentos membro que proporcionam comportamento polimórfico à respectiva classe dizem-se virtuais.  Basta que uma classe tenha uma função ou procedimento membro virtual para se dizer polimórfica.

2.2.1  Funções ou procedimentos virtuais

Em C++ o comportamento polimórfico consegue-se declarando funções ou procedimentos membro da classe base como virtuais através do especificador virtual.  Basta que exista uma função ou procedimento virtual, declado na classe ou herdado de uma classe base, para a classe se dizer polimórfica.

Ao se desenhar a classe Empregado, e reconhecendo-se a vantagem de permitir derivações futuras, dever-se-ia ter declarado o procedimento mostra() como virtual:

class Empregado {
  public:
    Empregado(string const& nome, string const& morada, Sexo sexo)
        : nome_(nome), morada_(morada), sexo_(sexo) {
    }
    string nome() const {
        return nome_;
    }
    string morada() const {
        return morada_;
    }
    Sexo sexo() const {
        return sexo_;
    }
    virtual void mostra() const {
        cout << "Nome: " << nome() << endl
             << "Morada: " << morada() << endl
             << "Sexo: " << sexo() << endl;
    }

  private:
    string nome_;
    string morada_;
    Sexo sexo_;
};

Note-se que a classe Empregado é polimórfica e no entanto as funções membro nome(), morada() e sexo() não são virtuais.  Aliás não o são por boas razões: não é suposto que estas funções sejam redefinidas em classes derivadas!

Se uma classe derivada de uma classe polimórfica redefinir uma função ou procedimento membro virtual de uma classe base, diz-se que sobrepôs a sua própria especialização da função ou procedimento.  Note-se que só é uma sobreposição se a a função ou procedimento original for virtual!  Caso contrário trata-se de uma simples ocultação.  Por exemplo, o procedimento mostra() da classe Chefe sobrepõe-se ao procedimento mostra() da classe Empregado:

class Chefe : public Empregado {
  public:
    Chefe(string const& nome, string const& morada, Sexo sexo, int nível)
        : Empregado(nome, morada, sexo), nível_(nível) {
    }
    int nível() const {
        return nível_;
    }
    virtual void mostra() const {
        Empregado::mostra();
        cout << "Nível: " << nível() << endl;
    }
  private:
    int nível_;
};
Uma função ou procedimento membro sobreposto a uma função ou procedimento membro virtual de uma clase base é sempre também virtual.  Note-se, no entanto, que a sobreposição não é obrigatória.  Uma classe derivada pode-se limitar a herdar uma função ou procedimento membro virtual sem lhe sobrepor uma versão própria.  Por exemplo:
class A {
  public:
    virtual void f() {
        ...
    }
    ...
};

class B : public A {
    ...
};

class C : public B {
  public:
    void f() {
        ...
    }
    ...
};

Neste exemplo existem três classes, todas elas polimórficas.  Todas possuem um procedimento virtual void f().  A classe base A define o procedimento virtual f(), a classe B herda-o, e a classe C herda-o por intermédio de B e sobrepõe-lhe uma versão especializada, também virtual por se sobrepor a um procedimento virtual.  Neste caso a invocação de f() através de um ponteiro para A (tipo A*) resulta na invocação de: Note-se que, na classe C, teria sido possível explicitar o facto de f() ser virtual por se sobrepor a um procedimento virtual da classe base:
class C : public B {
    ...
  public:
    virtual void f() {
        ...
    }
    ...
};
Aliás, mesmo sendo redundante, é boa prática deixar claro que uma função ou procedimento membro é virtual, pois isso facilita a vida de um programador que pretenda definir uma nova classe por derivação da classe em causa: é que se o especificador virtual for omitido, o programador terá de inspeccionar as sucessivas classes base até esclarecer se a função ou procedimento membro é ou não virtual.

Note-se que quando uma classe derivada define uma função ou procedimento com o mesmo nome que uma função ou procedimento de uma classe base (directa ou indirecta), a função ou procedimento da classe base fica oculta.  Isto passa-se mesmo que a função ou procedimento da classe derivada tenha uma assinatura diferente da da classe base.  Recorda-se que uma assinatura consiste no número e tipo de parâmetros de uma função ou procedimento.

Por exemplo:

class A {
public:
    int f(int i) {
        return i;
    }
};

class B : public A {
public:
    void f() {            // ocultação de A::f()
        int j = f(10);    // erro: compilador não encontra A::f() porque está oculta.
        int k = A::f(10); // ok: o prefixo A:: permite ao compilador encontrar a função
                          // pretendida.
    }
};

Uma ocultação só é uma sobreposição quando:
  1. A função ou procedimento membro ocultada for virtual.
  2. A função ou procedimento membro da classe derivada tiver a mesma assinatura da função ou procedimento ocultada.
Além disso as funções ou procedimentos têm de ter o mesmo tipo de devolução*.  É um erro tentar sobrepor uma função ou procedimento com um tipo de devolução incompatível com o da função ou procedimento herdado.  Por exemplo:
class A {
public:
    virtual int g(int);
};

class B : public A {
public:
    double g(int); // erro: mesma assinatura que A::g() virtual (sobreposição), mas com tipo
                   // de devolução incompatível.
};

* Na realidade o tipo de devolução pode ser diferente, desde que seja covariante (ver [2, pág. 425]).

2.2.2  Destrutores virtuais

Os destrutores, tal como os construtores, não são herdados.  Os construtores da classe derivada invocam sempre, explícita ou implicitamente, os construtores das classes base, o mesmo se passando com os destrutores (embora neste caso a invocação seja sempre implícita).  Esta facto coloca alguns problemas quando se destroem variáveis dinâmicas através de ponteiros para uma classe base.  Por exemplo:
class A {
  public:
    virtual void f();
    ...
    // Sem destrutor explícito (compilador fornece um automaticamente).
};

class B : public A {
  public:
    B() {
        pf = new float(0.0f);
    }
    ~B() {
        delete pf;
    }
    ...
  private:
    float* pf;
    ...
};

...

int main() {
    B* pb = new B;
    A* pa = pb;
    delete pa;
}

O que sucede quando, na função main(), se destrói a variável dinâmica da classe B cujo endereço está guardado na variável pa do tipo A*?  Como sempre, o destrutor é invocado.  Mas, como o destrutor de A (fornecido automaticamente pela linguagem) não é virtual, é o destrutor de A que é invocado, e não o de B, não se chegando portanto a destruir a variável dinâmica do tipo float construída no construtor de B!  Isto significa que classes polimórficas devem ter sempre destrutores virtuais!  Ou seja, a classe A deveria ter sido definida como:
class A {
    ...
  public:
    ...
    // Com destrutor explícito virtual, mesmo que vazio:
    virtual ~A() {}
};
Logo a base da hierarquia de classes representando tipos de empregados na empresa deve ter também um destrutor virtual:
class Empregado {
  public:
    Empregado(string const& nome, string const& morada, Sexo sexo)
        : nome_(nome), morada_(morada), sexo_(sexo) {
    }
    virtual ~Empregado() {}
    string nome() const {
        return nome_;
    }
    string morada() const {
        return morada_;
    }
    Sexo sexo() const {
        return sexo_;
    }
    virtual void mostra() const {
        cout << "Nome: " << nome() << endl
             << "Morada: " << morada() << endl
             << "Sexo: " << sexo() << endl;
    }

  private:
    string nome_;
    string morada_;
    Sexo sexo_;
};

2.2.3  Ligação estática vs. dinâmica

Na realidade aquilo que se consegue ao usar polimorfismo é que as funções ou procedimentos a executar sejam determinados durante a execução do programa, e não durante a compilação, como é habitual.  É que a função ou procedimento virtual a executar depende do tipo do objecto apontado e não do tipo do ponteiro, e portanto só pode ser conhecido durante a execução do programa.

A este comportamento, em que a função ou procedimento a executar só é determinado durante a execução do programa, chama-se ligação dinâmica (dinamic binding), por oposição a ligação estática (static binding).

Recorde-se de novo o exemplo da lista de empregados:

// Visualização dos empregados:
for(ListaPonteiroEmpregado::Iterador i = pessoal.primeiro();
    i != pessoal.fim();
    ++i)
    (*i)->mostra();
O compilador, ao observar o código acima, não tem qualquer possibilidade de saber se os ponteiros na lista apontam para objectos que são empregados, chefes, ou instâncias de qualquer outra classe derivada directa ou indirectamente da classe Empregado.  Isso significa que, sendo o procedimento mostra() virtual na classe base Empregado, a decisão de qual o procedimento a invocar em cada caso tem de ser tomada durante a execução do programa.

Aliás, a função ou procedimento realmente invocada pode pertencer a uma classe que nem sequer existia quando a classe base foi criada.  Esta é uma das enormes vantagens das funções e procedimentos membro virtuais e da ligação dinâmica: para acrescentar mais um tipo de empregado da empresa basta definir uma nova classe derivada de empregado.  A classe empregado e todo o código já existente para lidar com os empregados não precisa de ser alterado (salvo, bem entendido, no local onde se criam os empregados, por exemplo para os inserir na lista).

A ligação dinâmica, que está na base de toda a verdadeira programação orientada para objectos, leva a que a invocação de funções ou procedimentos membro virtuais seja um pouco menos eficiente do que a invocação de funções ou procedimentos normais.  Mas isso não deve servir de justificação para a não utilizar o polimorfismo onde ele for apropriado!

Invocação estática explícita

Se se desejar, por alguma razão, invocar uma função ou procedimento virtual de uma forma estática (e não dinâmica), tem de se qualificar o nome da função ou procedimento com o nome da classe respectiva.  Por exemplo, se se pretendesse listar os empregados da empresa como simples empregados, poder-se-ia usar:
// Visualização dos empregados:
for(ListaPonteiroEmpregado::Iterador i = pessoal.primeiro();
    i != pessoal.fim();
    ++i)
    (*i)->Empregado::mostra();
A qualificação explícita dá sempre origem à ligação estática: o compilador fica a saber imediatamente a versão da função ou procedimento a invocar.  De resto, é isto que permite, durante a especialização do procedimento mostra() feito pela classe Chefe, invocar o procedimento mostra() da classe Empregado usando ligação estática (se a ligação fosse dinâmica também neste caso acabar-se-ia com uma chamada recursiva ao procedimento mostra() da classe Chefe):
void Chefe::mostra() const {
    Empregado::mostra(); // invocação com ligação estática.
    cout << "Nível: " << nível() << endl;
}

2.3  Classes abstractas e funções ou procedimentos puramente virtuais

Suponha-se que se pretendia construir um sistema de edição de figuras em que formas bidimensionais pudessem ser manipuladas pelo utilizador  (isto parece dejá vu, não parece?).  Como o construir?  Que classes deveriam ser criadas?  Claramente as formas bidimensionais existem num mundo (virtual) e são representadas num determinado dispositivo.  Para simplificar, considera-se que as formas existem num quadro de coordenadas inteiras e que a representação se faz num ecrã rectangular correspondendo a uma matriz de píxeis (do inglês picture element) rectangulares que ou são pretos ou brancos.

2.3.1  Uma classe simples para desenhar no ecrã

A classe Quadro que se segue representa um quadro (virtual) no qual se pode desenhar e que pode ser visualizado num ecrã verdadeiro quando necessário.  Observe-se atentamente a sua definição, lendo os respectivos comentários.  As versões completas das classes que se forem apresentando daqui em diante encontram-se na Secção 2.4.
/**
   Classe Quadro.  Representa um quadro onde se pode desenhar.  O quadro está
   dividido em rectângulos organizados em linhas e colunas.  Cada rectângulo
   chama-se um píxel.  A linha do topo é a linha 0 (zero).  A coluna da
   esquerda é a coluna 0 (zero).
   Os píxeis podem ser pintados de branco ou de preto.  A razão entre a
   largura e altura dos píxeis chama-se aspecto.  É possível escrever um
   quadro num canal, usualmente ligado ao ecrã.  Nesse caso cada píxel é
   representado por um caracter, sendo ' ' usado para a cor preta e '×' usado
   para a cor branca.
 */
class Quadro {
  public:
    // Usa-se preto e branco:
    typedef PB Cor;

    /**
       Constrói um quadro com tamanho dado (24x80 por omissão) e com fundo
       da cor fundo (preto por omissão).  Os píxeis válidos têm posições entre 0 e
       tamanho.linhas() - 1 (para a linha) e entre 0 e tamanho.colunas() - 1
        (para a coluna).  A razão entre a largura e a altura dos píxeis é dada por aspecto:
     */
    Quadro(Tamanho const& tamanho = Tamanho(24, 80),
           Cor fundo = preto,
           double aspecto = 1.0);

    // Pintura de um pixel numa dada posição com uma dada cor:
    void pinta(Posicao const& posicao, Cor const& cor);

    // Pintura de um pixel numa dada posição com um cor contrastante com o
    // fundo do quadro:
    void pinta(Posicao const& posicao);

    // "Despinta" um pixel numa dada posição pintando-o com a cor do fundo:
    void limpa(Posicao const& posicao);

    // Pinta todo o quadro com a cor do fundo:
    void apaga();

    // Escreve o quadro no canal saida (que se presume ligado a um ecrã em modo
    // texto), com conversão para os caracteres ' ' e '×' (preto e branco
    // respectivamente):
    void mostra(std::ostream& saida) const;

    // Devolve a cor do pixel na posição dada.  Se a posição estiver fora do
    // quadro devolve a cor do fundo:
    Cor cor(Posicao const& posicao) const;

    // Indica se uma dada posição está dentro do quadro:
    bool dentro(Posicao const& posicao) const;

    // Devolve a cor do fundo:
    Cor fundo() const;

    // Devolve a relação largura/altura dos píxeis do quadro:
    double aspecto() const;

    // Devolve o tamanho do quadro:
    Tamanho tamanho() const;

  private:
    ...
};

2.3.2  Figuras: listas de formas

Definida classe Quadro para representar o conceito de quadro, é preciso concretizar o conceito de figura.  Numa abordagem simples pode-se dizer que uma figura consiste numa lista de formas concretas de vários tipos, como quadrados, círculos, etc.  Para que se possa realizar em C++ uma lista capaz de conter os vários tipos de formas, é necessário que estes tipos estejam organizados numa hierarquia, da mesma forma que se fez para o pessoal da empresa.  Seja Forma a classe base desta hierarquia.  Sendo todos os tipos concretos de formas representados por classes (tais como Quadrado e Circulo) derivadas da classe base Forma, é possível pensar numa figura como uma lista de ponteiros para Forma em que os objectos apontados são na realidade dos vários tipos concretos derivados de Forma.  Esta solução é possível porque a utilização de derivação por herança pública estabelece uma relação é um entre as classes derivadas e a respectiva classe base.  Ou seja, um Quadrado é uma Forma, um Circulo é uma Forma, etc.

Note-se que a classe base Forma não foi introduzida apenas para que se pudessem criar listas de ponteiros para objectos de vários tipos de forma.  De facto, é possível falar de formas em abstracto sem que se refira nenhum tipo de forma em concreto.  Sob este ponto de vista, a classe Forma representa um conceito abstrato.  Por outro lado, a classe Quadrado representa o conceito concreto de quadrado.  Note-se que a diferença que se estabeleceu entre conceito concreto ou abstracto tem a ver com a possibilidade de existência física de exemplos (ou instâncias) desse conceito.  Quadrados já todos vimos, mas nunca vimos uma forma que fosse simplesmente uma forma...  As formas que se vêem são sempre concretizações vêem são exemplos de conceitos mais concretos: triângulos, polígonos, mas não simplesmente formas.

A mesma observação se poderia ter feito acerca da hierarquia de classes representando os seres vivos (de acordo com a taxonomia usada na biologia) apresentada na Aula 7.  Dessa hierarquia fazem parte, por exemplo, gorilas e chimpanzés:

class Pongídeo /* família */ : public Primata {
    ...
};
class Gorilla /* género */ : public Pongídeo {
    ...
};
class Gorilla_gorilla /* espécie */ : public Gorilla {
    ...
};
class Pan /* género */ : public Pongídeo {
    ...
};
class Pan_troglodytes /* espécie */ : public Pan {
    ...
};
Uma ida ao Jardim Zoológico permitirá observar exemplares (instâncias) das espécies (classes) Gorilla gorilla e Pan troglodytes (o chimpanzé), bem como, do outro lado das grades, a sub-espécie Homo sapiens sapiens.  Mas não se encontrará em qualquer jaula um exemplar da família dos primatas: encontram-se exemplares de espécies e sub-espécies que são primatas, mas não exemplares que sejam simplesmente primatas, não pertencendo a qualquer espécie.  A classe Primata, na hierarquia apresentada, representa pois um conceito abstrato que sumariza as características comuns às várias espécies dessa família, essas sim representadas por classes concretas como Homo_sapiens_sapiens ou Pan_troglodytes.

2.3.3  Classes abstractas

Afinal, o que é uma forma, pelo menos no contexto dos desenhos bidimensionais que se pretende abordar?  Numa primeira aproximação, pode-se dizer que uma forma é algo que tem uma posição, que pode ser desenhado, e que se pode mover, i.e., cuja posição pode variar.  Assim sendo, uma primeira versão da classe forma poderia ser como se segue:
class Forma {
  public:
    // Construtor (dada uma posição):
    Forma(Posicao const& posicao)
        : posicao_(posicao) {
    }

    // Destrutor virtual.  Garante que os destrutores das classes derivadas
    // são invocados mesmo que através dum ponteiro para a classe Forma:
    virtual ~Forma() {}

    // Devolve a posição da forma:
    Posicao posicao() const {
        return posicao_;
    }

    // Desloca a forma para nova posição:
    virtual void move(Posicao const& posicao) {
        posicao_ = posicao;
    }

    // Desenha a forma num quadro.
    virtual void desenha(Quadro& quadro) const;

  private:
    // A posição da forma (um ponto particular, como um centro ou canto):
    Posicao posicao_;
};

A classe possui uma variável membro posicao_ que guarda a posição corrente da forma e que pode ser inicializada através do construtor.  A função posicao() devolve essa posição e é uma função membro normal, não-virtual, por parecer pouco plausível que alguma classe derivada necessite de sobrepor uma versão especializada a esta função.  O mesmo não se passa com o procedimento move(), pois pode haver necessidade de fornecer especializações em classes derivadas, quer porque se podem conceber formas com restrições nos modos de movimentação, quer porque para determinadas formas pode haver movimentos que não impliquem uma alteração da posição de origem.  Assim, o procedimento move() foi declarado virtual, para que a classe forma apresente comportamento polimórfico durante invocações deste procedimento por intermédio de ponteiros ou referências e portanto formas diferentes se possam mover de forma diferente.

O procedimento desenha() é um pouco mais delicado.  É óbvio que desenha() deve ser declarado como procedimento virtual, pois cada classe derivada concreta saberá como desenhar-se e portanto sobreporá uma especialização a este procedimento.  Mas como definir desenha() na classe Forma?  Sendo Forma uma representação duma abstracção, conclui-se que não tem qualquer sentido definir esse procedimento: é possível desenhar um quadrado ou um círculo, mas não uma forma que não seja nenhuma forma concreta.  Não se pode desenhar uma pura abstracção.

Mas a linguagem C++ obriga a definir todas as funções ou procedimentos declarados.  Que fazer?  O C++ permite, nestes casos, declarar a função ou procedimento membro não apenas virtual mas puramente virtual.  Para isso coloca-se a estranha construção = 0 após o cabeçalho da função ou procedimento.  É que as funções ou procedimentos membro puramente virtuais não precisam de ser definidos!

Claro está que um função ou procedimento membro puramente virtual que não tenha sido definido também não podem ser invocados.  Ou melhor, pode, desde que o seja uma forma polimórfica, isto é, invocados através dum ponteiro que endereça na realidade um objecto de uma classe derivada que sobreponha uma versão própria dessa função ou procedimento.

A linguagem C++ resolve este problema dizendo que as classes com funções ou procedimentos membro puramente virtuais são abstractas, i.e., são classes das quais não se podem criar instâncias.

A solução no caso das figuras passa, então, por tornar a classe Forma abstracta, o que de resto está de acordo com a discussão anterior, onde se viu que forma era um conceito abstracto:

class Forma {
    ...
    /**
       Desenha a forma num quadro.  Sendo uma forma um conceito abstracto, é
       claro não se pode dizer COMO desenhá-la.  Formas concretas (que serão
       derivadas desta classe) saberão como fazê-lo.  Por exemplo, um quadrado
       é uma forma e sabe-se desenhar.  Este procedimento é, portanto,
       puramente virtual, sendo a sua existência que torna a classe Forma
       abstracta.
    */
    virtual void desenha(Quadro& quadro) const = 0;
    ...
};
Classes derivadas são abstractas se declararem alguma função membro como puramente virtual ou se não sobrepuserem definições a todas as funções ou procedimentos puramente virtuais herdados das classes base.  Assim, as formas concretas, como círculos ou quadrados, derivadas por herança pública da classe base Forma, deverão sempre sobrepor uma especialização do procedimento desenha(), pois caso contrário seriam também classes abstractas.

Como não se podem criar instâncias de classes abstractas, o código que se segue produz um erro de compilação:

Forma trambolho(Posicao(10, 30));

2.3.4  Classes concretas

Um quadrado é um tipo concreto de forma.  Assim, a sua definição faz-se por herança pública de Forma com acrescento de variáveis e funções ou procedimentos membro específicos e sobreposição de uma especialização do procedimento desenha() herdado como puramente virtual da classe base:
// Um tipo específico de forma: quadrado aberto com lados paralelos aos eixos
// coordenados.  Posição é o canto superior direito:
class Quadrado : public Forma {
  public:
    // Construtor, dadas a posição e a dimensão do lado (em colunas do quadro):
    Quadrado(Posicao const& posicao, int lado);
        : Forma(posicao), lado_(lado) {
    }

    // Devolve dimensão do lado em colunas do quadro:
    int lado() const {
        return lado_;
    }

    // Desenha o quadrado no quadro, compensando o facto dos píxeis serem
    // rectangulares:
    virtual void desenha(Quadro& quadro) const;

  private:
    // Dimensão do lado (em colunas):
    int lado_;
};

// Desenha o quadrado no quadro q, compensando píxeis com relação largura/altura
// quadro.aspecto():
void Quadrado::desenha(Quadro& quadro) const {
    ... // Ver implementação mais abaixo...
}

A sobreposição da versão especializada do procedimento desenha(), que é o único puramente virtual herdado,  tem como resultado que Quadrado seja uma classe concreta, i.e., uma classe da qual se podem criar instâncias.  Por exemplo, pode-se criar um quadrado como se segue:
Quadrado quadrado(Posicao(10, 20), 7); // quadrado em (10, 20) com lado 7.
Da mesma forma se poderiam definir outras formas concretas, tais como círculos e triângulos.  Todas elas derivariam directa ou indirectamente, e sempre de forma pública, da classe Forma.  Assim, devido ao facto de este tipo de herança representar relações é um, seria possível guardar endereços de objectos (instâncias) de qualquer forma concreta numa lista de ponteiros do tipo Forma*.

Note-se que essa lista só poderia conter endereços de instâncias de classes concretas, pois o C++ proíbe a criação de instâncias de classes abstractas, independentemente de serem variáveis estáticas ou dinâmicas.  É isso que permite garantir que a invocação do procedimento desenha() através dos ponteiros na lista está sempre bem definida (ver programa de teste na Secção 2.4.9).

Note-se que a construção da classe abstracta Forma e das classes concretas que dela derivam permite resolver vários problemas.

Em primeiro lugar, permite quardar num único contentor (e.g., uma lista), ponteiros do tipo Forma* (ou referências do tipo Forma&) para objectos das várias classes concretas derivadas garantindo, devido ao polimorfismo introduzido pelas funções e procedimentos virtuais, que a invocação de desenha() através desses ponteiros (ou referências) invoca a especialização do procedimento para a classe efectiva do objecto.

Em segundo lugar, consegue-se garantir que não podem ser criadas intâncias de uma classe que representa um conceito abstracto como o de forma, uma vez que existe um procedimento puramente virtual que torna a classe abstracta.

Finalmente, reaproveita-se código, pois aquilo que é realmente comum a todas as formas, a sua posição, fica representado directamente na classe base, sem necessidade de ser reproduzido em cada classe derivada.  As classes derivadas dedicam-se apenas ao que é essencial: representar aquilo que lhes é específico.

2.4  Módulos físicos completos

As classes apresentadas no texto anterior são aqui apresentadas na sua versão completa.  Inclui-se também um pequeno programa de teste e o respectivo ficheiro de construção.

2.4.1  Utilitários

util.H
#ifndef UTIL_H
#define UTIL_H

template <class R, class T>
R arredonda(T v);

template <class T>
T quadrado(T v);

#include "util_impl.H"

#endif // UTIL_H

util_impl.H
template <class R, class T>
inline R arredonda(T v) {
    return v < 0 ? R(v - 0.5) : R(v + 0.5);
}

template <class T>
inline T quadrado(T v) {
    return v * v;
}

Os modelos (templates) serão vistos na Aula 11.

2.4.2  Cores dos píxeis

cor.H
#ifndef COR_H
#define COR_H

// Um tipo para preto e branco:
enum PB {preto, branco};

// Devolve a cor que contrasta com cor:
PB contrasta(const PB& cor);

// Devolve um caracter que represente aproximadamente a cor cor num ecrã
// em modo texto:
char caractere(const PB& c);

#include "cor_impl.H"

#endif // COR_H

cor_impl.H
inline PB contrasta(const PB& cor) {
    return cor == preto ? branco : preto;
}

inline char caractere(const PB& cor) {
    return cor == preto ? ' ' : '×';
}

2.4.3  Posições no quadro

posicao.H
#ifndef POSICAO_H
#define POSICAO_H

// Classe para representar posições:
class Posicao {
  public:
    Posicao(int linha = 0, int ccluna = 0);

    void move(int linha = 0, int coluna = 0);

    int linha() const;
    int coluna() const;

  private:
    int linha_;
    int coluna_;
};

#include "posicao_impl.H"

#endif // POSICAO_H

posicao_impl.H
inline Posicao::Posicao(int linha, int coluna)
    : linha_(linha), coluna_(coluna) {
}

inline void Posicao::move(int linha, int coluna) {
    linha_ = linha;
    coluna_ = coluna;
}

inline int Posicao::linha() const {
    return linha_;
}

inline int Posicao::coluna() const {
    return coluna_;
}

2.4.4  Tamanhos no quadro

tamanho.H
#ifndef TAMANHO_H
#define TAMANHO_H

// Classe para representar tamanhos:
class Tamanho {
 public:
    Tamanho(int linhas = 0, int colunas = 0);

    int linhas() const;
    int colunas() const;

 private:
    int linhas_;
    int colunas_;
};

#include "tamanho_impl.H"

#endif // TAMANHO_H

tamanho_impl.H
#include <cassert>

inline Tamanho::Tamanho(int linhas, int colunas)
    : linhas_(linhas), colunas_(colunas) {
    assert(linhas_ >= 0 && colunas_ >= 0);
}

inline int Tamanho::linhas() const {
    return linhas_;
}

inline int Tamanho::colunas() const {
    return colunas_;
}

2.4.5  Quadro de desenho

quadro.H
#ifndef QUADRO_H
#define QUADRO_H

#include <iostream>
#include <vector>

#include "cor.H"
#include "posicao.H"
#include "tamanho.H"

/**
   Classe Quadro.  Representa um quadro onde se pode desenhar.  O quadro está
   dividido em rectângulos organizados em linhas e colunas.  Cada rectângulo
   chama-se um píxel.  A linha do topo é a linha 0 (zero).  A coluna da
   esquerda é a coluna 0 (zero).
   Os píxeis podem ser pintados de branco ou de preto.  A razão entre a
   largura e altura dos píxeis chama-se aspecto.  É possível escrever um
   quadro num canal, usualmente ligado ao ecrã.  Nesse caso cada píxel é
   representado por um caracter, sendo ' ' usado para a cor preta e '×' usado
   para a cor branca.
 */
class Quadro {
  public:
    // Usa-se preto e branco:
    typedef PB Cor;

    /**
       Constrói um quadro com tamanho dado (24x80 por omissão) e com fundo
       da cor fundo (preto por omissão).  Os píxeis válidos têm posições entre 0 e
       tamanho.linhas() - 1 (para a linha) e entre 0 e tamanho.colunas() - 1
        (para a coluna).  A razão entre a largura e a altura dos píxeis é dada por aspecto:
     */
    Quadro(Tamanho const& tamanho = Tamanho(24, 80),
           Cor fundo = preto,
           double aspecto = 1.0);

    // Pintura de um pixel numa dada posição com uma dada cor:
    void pinta(Posicao const& posicao, Cor const& cor);

    // Pintura de um pixel numa dada posição com um cor contrastante com o
    // fundo do quadro:
    void pinta(Posicao const& posicao);

    // "Despinta" um pixel numa dada posição pintando-o com a cor do fundo:
    void limpa(Posicao const& posicao);

    // Pinta todo o quadro com a cor do fundo:
    void apaga();

    // Escreve o quadro no canal saida (que se presume ligado a um ecrã em modo
    // texto), com conversão para os caracteres ' ' e '×' (preto e branco
    // respectivamente):
    void mostra(std::ostream& saida) const;

    // Devolve a cor do pixel na posição dada.  Se a posição estiver fora do
    // quadro devolve a cor do fundo:
    Cor cor(Posicao const& posicao) const;

    // Indica se uma dada posição está dentro do quadro:
    bool dentro(Posicao const& posicao) const;

    // Devolve a cor do fundo:
    Cor fundo() const;

    // Devolve a relação largura/altura dos píxeis do quadro:
    double aspecto() const;

    // Devolve o tamanho do quadro:
    Tamanho tamanho() const;

  private:
    Tamanho tamanho_;           // tamanho do quadro.
    Cor fundo_;                 // cor de fundo do quadro.
    double aspecto_;

    int numero_de_pixeis;       // número de píxeis do quadro.
    std::vector<Cor> pixeis;    // vector com os píxeis do quadro.

    // Devolve o índice no vector de píxeis correspondente a uma dada posição, que
    // se admite estar dentro do quadro:
    int indice(Posicao const& posicao) const;
};

#include "quadro_impl.H"

#endif // QUADRO_H

quadro_impl.H
inline Quadro::Quadro(Tamanho const& tamanho, Cor fundo, double aspecto)
    : tamanho_(tamanho), fundo_(fundo), aspecto_(aspecto),
      numero_de_pixeis(tamanho.linhas() * tamanho.colunas()),
      pixeis(numero_de_pixeis) {
    apaga();
}

inline void Quadro::pinta(Posicao const& posicao, Cor const& cor) {
    if(dentro(posicao))
        pixeis[indice(posicao)] = cor;
}

inline void Quadro::pinta(Posicao const& posicao) {
    if(dentro(posicao))
        pixeis[indice(posicao)] = contrasta(fundo());
}

inline void Quadro::limpa(Posicao const& posicao) {
    pinta(posicao, fundo());
}

inline Quadro::Cor Quadro::cor(Posicao const& posicao) const {
    return dentro(posicao) ? pixeis[indice(posicao)] : fundo();
}

inline bool Quadro::dentro(Posicao const& posicao) const {
    return (0 <= posicao.linha() && posicao.linha() < tamanho().linhas() &&
            0 <= posicao.coluna() && posicao.coluna() <
            tamanho().colunas());
}

inline Quadro::Cor Quadro::fundo() const {
    return fundo_;
}

inline double Quadro::aspecto() const {
    return aspecto_;
}

inline Tamanho Quadro::tamanho() const {
    return tamanho_;
}

inline int Quadro::indice(Posicao const& posicao) const {
    return posicao.linha() * tamanho().colunas() + posicao.coluna();
}

quadro.C
#include "quadro.H"

using namespace std;

void Quadro::apaga() {
    for(int i = 0; i != numero_de_pixeis; ++i)
        pixeis[i] = fundo();
}

void Quadro::mostra(ostream& saida) const {
    saida << '\n';
    // Não se mostra a última coluna da última linha.  Experimente para ver
    // porquê...
    for(int i = 0; i != numero_de_pixeis - 1; ++i)
        saida << caractere(pixeis[i]);
    saida << flush;
}

2.4.6  Conceito de forma

forma.H
#ifndef FORMA_H
#define FORMA_H

#include "quadro.H"

// Classe abstracta representando o conceito de forma com algo que tem uma
// posição, que se pode mover, e que se sabe desenhar num quadro.
class Forma {
  public:
    // Construtor (dada uma posição):
    Forma(Posicao const& posicao);

    // Destrutor virtual.  Garante que os destrutores das classes derivadas
    // são invocados mesmo que através dum ponteiro para a classe Forma:
    virtual ~Forma() {}

    // Devolve a posição da forma:
    Posicao posicao() const;

    // Desloca a forma para nova posição:
    virtual void move(Posicao const& posicao);

    /**
       Desenha a forma num quadro.  Sendo uma forma um conceito abstracto, é
       claro não se pode dizer COMO desenhá-la.  Formas concretas (que serão
       derivadas desta classe) saberão como fazê-lo.  Por exemplo, um quadrado
       é uma forma e sabe-se desenhar.  Este procedimento é, portanto,
       puramente virtual, sendo a sua existência que torna a classe Forma
       abstracta.
     */
    virtual void desenha(Quadro& quadro) const = 0;

  private:
    // A posição da forma (um ponto particular, como um centro ou canto):
    Posicao posicao_;
};

#include "forma_impl.H"

#endif // FORMA_H

forma_impl.H
inline Forma::Forma(Posicao const& posicao)
    : posicao_(posicao) {
}

inline Posicao Forma::posicao() const {
    return posicao_;
}

inline void Forma::move(Posicao const& posicao) {
    posicao_ = posicao;
}

2.4.7  Quadrado

quadrado.H
#ifndef QUADRADO_H
#define QUADRADO_H

#include "forma.H"

// Um tipo específico de forma: quadrado aberto com lados paralelos aos eixos
// coordenados.  Posição é o canto superior esquerdo:
class Quadrado : public Forma {
  public:
    // Construtor, dadas a posição e a dimensão do lado (em colunas do quadro):
    Quadrado(Posicao const& posicao, int lado);

    // Devolve dimensão do lado em colunas do quadro:
    int lado() const;

    // Desenha o quadrado no quadro, compensando o facto dos píxeis serem
    // rectangulares:
    virtual void desenha(Quadro& quadro) const;

  private:
    // Dimensão do lado (em colunas):
    int lado_;
};

#include "quadrado_impl.H"

#endif // QUADRADO_H

quadrado_impl.H
inline Quadrado::Quadrado(Posicao const& posicao, int lado)
    : Forma(posicao), lado_(lado) {
}

inline int Quadrado::lado() const {
    return lado_;
}

quadrado.C
#include "quadrado.H"

#include "util.H"

// Desenha o quadrado no quadro q, compensando píxeis com relação largura/altura
// quadro.aspecto():
void Quadrado::desenha(Quadro& quadro) const {
    // Posição do quadrado:
    int l = posicao().linha();
    int c = posicao().coluna();

    int altura = arredonda<int>(lado() * quadro.aspecto());

    // Desenho dos lados horizontais:
    for(int i = 0; i != lado() - 1; ++i) {
        quadro.pinta(Posicao(l, c + i));
        // Altura compensada pelo rácio largura/altura dos píxeis:
        quadro.pinta(Posicao(l + altura - 1, c + 1 + i));
    }
    // Desenho dos lados verticais (altura compensada pelo rácio
    // largura/altura dos píxeis):
    for(int i = 0; i != altura - 1; ++i) {
        quadro.pinta(Posicao(l + 1 + i, c));
        quadro.pinta(Posicao(l + i, c + lado() - 1));
    }
}

2.4.8  Círculo

circulo.H
#ifndef CIRCULO_H
#define CIRCULO_H

#include "forma.H"

// Um tipo específico de forma: círculo aberto.  Posição é o centro.
class Circulo : public Forma {
 public:
    // Construtor, dadas a posição e a dimensão do raio (em colunas do quadro):
    Circulo(Posicao const& posicao, int raio);

    // Devolve dimensão do raio em colunas do quadro:
    int raio() const;

    // Desenha o círculo no quadro, compensando o facto dos píxeis serem
    // rectangulares:
    virtual void desenha(Quadro& quadro) const;

private:
    // Dimensão do raio (em colunas):
    int raio_;
};

#include "circulo_impl.H"

#endif // CIRCULO_H

circulo_impl.H
inline int Circulo::raio() const {
    return raio_;
}

inline Circulo::Circulo(Posicao const& posicao, int raio)
    : Forma(posicao), raio_(raio) {
}

circulo.C
#include "circulo.H"

#include <cmath>
#include "util.H"

// Desenha o círculo compensando píxeis com relação largura/altura quadro.aspecto()
// do quadro quadro.  É uma implementação naïve.  Ver bons livros de computação
// gráfica para melhores métodos.
void Circulo::desenha(Quadro& quadro) const {
    // Posição do círculo:
    int l = posicao().linha();
    int c = posicao().coluna();
    double aspecto = quadro.aspecto();

    if(aspecto <= 1.0) {
        // Tamanho do ciclo de colunas para meio semi-círculo:
        int meio = arredonda<int>(raio() /
                                  sqrt(quadrado(aspecto) + 1));

        // Ciclo de colunas ("quartos" de círculo superior e inferior):
        for(int i = - meio; i != meio + 1; ++i) {
            Posicao ps(l - arredonda<int>(aspecto * sqrt(quadrado(raio())
                                                         - quadrado(i))),
                       c + i);
            quadro.pinta(ps);
            Posicao pi(l + arredonda<int>(aspecto * sqrt(quadrado(raio())
                                                         - quadrado(i))),
                       c + i);
            quadro.pinta(pi);
        }

        // Ciclo de linhas ("quartos" de círculo esquerdo e direito):
        for(int i = - arredonda<int>(aspecto * meio);
            i != arredonda<int>(aspecto * meio) + 1; ++i) {
            Posicao pe(l + i,
                       c - arredonda<int>(sqrt(quadrado(raio()) -
                                               quadrado(i / aspecto))));
            quadro.pinta(pe);
            Posicao pd(l + i,
                       c + arredonda<int>(sqrt(quadrado(raio()) -
                                               quadrado(i / aspecto))));
            quadro.pinta(pd);
        }
    } else {
        // Tamanho do ciclo de linhas para meio semi-círculo:
        int meio = arredonda<int>(raio() * quadrado(aspecto) /
                                  sqrt(quadrado(aspecto) + 1));

        // Ciclo de linhas ("quartos" de círculo esquerdo e direito):
        for(int i = - meio; i != meio + 1; ++i) {
            Posicao pe(l + i,
                       c - arredonda<int>(sqrt(quadrado(raio())
                                               - quadrado(i / aspecto))));
            quadro.pinta(pe);
            Posicao pd(l + i,
                       c + arredonda<int>(sqrt(quadrado(raio())
                                               - quadrado(i / aspecto))));
            quadro.pinta(pd);
        }

        // Ciclo de colunas ("quartos" de círculo superior e inferior):
        for(int i = - arredonda<int>(meio / aspecto);
            i != arredonda<int>(meio / aspecto) + 1; ++i) {
            Posicao ps(l - arredonda<int>(aspecto*sqrt(quadrado(raio())
                                                       - quadrado(i))),
                       c + i);
            quadro.pinta(ps);
            Posicao pi(l + arredonda<int>(aspecto*sqrt(quadrado(raio())
                                                       - quadrado(i))),
                       c + i);
            quadro.pinta(pi);
        }
    }
}

2.4.9  Programa de teste

teste.C
// Corra numa consola com 24 linhas por 80 colunas!
#include <cstdlib> // para rand().
#include <list>

using namespace std;

#include "quadro.H"
#include "quadrado.H"
#include "circulo.H"

int main()
{
    int const linhas = 24;
    int const colunas = 80;
    Quadro quadro(Tamanho(linhas, colunas), preto, 0.5);
    list<Forma*> lista;

    for(int i = 0; i != 10; ++i) {
        int l = rand() % linhas;
        int c = rand() % colunas;
        lista.push_back(new Quadrado(Posicao(l, c), rand() % 15 + 1));
        l = rand() % linhas;
        c = rand() % colunas;
        lista.push_back(new Circulo(Posicao(l, c), rand() % 7 + 1));
    }
    for(list<Forma*>::iterator i = lista.begin(); i != lista.end(); ++i) {
        (*i)->desenha(quadro);
        quadro.mostra(cout);
        cin.get();
    }
}

2.4.10  Ficheiro de construção

Makefile
teste: circulo.o quadrado.o quadro.o teste.o

circulo.o: circulo.H forma.H quadro.H cor.H cor_impl.H posicao.H \
           posicao_impl.H tamanho.H tamanho_impl.H quadro_impl.H \
           forma_impl.H circulo_impl.H util.H util_impl.H

quadrado.o: quadrado.H forma.H quadro.H cor.H cor_impl.H posicao.H \
            posicao_impl.H tamanho.H tamanho_impl.H quadro_impl.H \
            forma_impl.H util.H util_impl.H

quadro.o: quadro.H cor.H cor_impl.H posicao.H posicao_impl.H tamanho.H \
          tamanho_impl.H quadro_impl.H

teste.o: quadro.H cor.H cor_impl.H posicao.H posicao_impl.H tamanho.H \
         tamanho_impl.H quadro_impl.H quadrado.H forma.H forma_impl.H \
         circulo.H circulo_impl.H

2.5  Leitura recomendada

Recomenda-se a leitura do Capítulo 17 de [1].


3  Exercícios

1.a)  Altere o programa de teste que escreveu com solução do exercício 2 da Aula 7 de modo a guardar num vector de ponteiros para Filme os três filmes construídos a partir do conteúdo do ficheiro filme.txt:
Tédio fatal
Manoel Oliveira
250
Star Wars
Steven Spielberg
230
Inglês
EUA
Clockers
Spike Lee
96
Nenhumas...
O vector guarda ponteiros para variáveis dinâmicas das classes Filme, FilmeEstrangeiro e EdicaoDoRealizador.

Finalmente, o programa deve percorrer o vector pedindo a cada filme para se mostrar no ecrã (i.e., invocando o procedimento membro mostra()).

Execute o programa.  Aconteceu o que previa?  Aconteceu o desejável?

1.b)  À luz do que aprendeu sobre funções e procedimentos membro virtuais e polimorfismo, leve o programa de teste a mostrar correctamente cada uma dos filmes (i.e., um filme estrangeiro deve ser mostrada como tal, incluindo a nacionalidade e o idioma).  Ou seja, pretende-se que invocações do procedimento membro mostra() através de ponteiros para a classe Filme levem à execução do procedimento membro mostra() da classe a que pertence o objecto apontado (ligação dinâmica).

2.  Usando a biblioteca Slang++, escreva um programa que desenhe no centro do ecrã uma cruz de asteriscos amarelos sobre fundo vermelho, escreva "carregue em qualquer tecla para sair" no canto inferior direito do ecrã, e espere por uma tecla antes de terminar (Manual do Slang++ em linha ou em PDF).

3.  Escreva um programa (usando a biblioteca Slang++) que exiba um rectângulo no ecrã.  O rectângulo deve ser branco sobre fundo azul, com borda representada por asteriscos e interior representado por pontos.  Deve ter origem na linha 10 coluna 30 e deve ter 5 linhas de altura por 30 de largura.  Deve definir e usar uma classe Rectangulo com um construtor apropriado para poder representar qualquer rectângulo que se pretenda e com um procedimento desenha() para desenhar os rectângulos no ecrã.

4.  Melhore o programa anterior, introduzindo uma classe Forma da qual Rectangulo deve ser derivada (usando herança pública: repare que um Rectangulo é uma Forma).  Deste modo, podemos colocar na classe Forma as responsabilidades comuns a todas as formas.  Escreva também uma classe Ponto (e um ponto também é uma forma...) que represente um simples ponto no ecrã.  Altere o programa de modo a colocar um rectângulo e um ponto num vector de ponteiros para Forma e por fim percorrer esse vector pedindo às formas apontadas para se desenharem no ecrã.


4  Referências

[1]  Stanley B. Lippman e Josée Lajoie, "C++ Primer", 3ª edição, Addison-Wesley, Reading, Massachusetts, 1998. (Existe um exemplar na biblioteca do ISCTE.)

[2]  Bjarne Stroustrup, "The C++ Programming Language", 3ª edição, Addison-Wesley, Reading, Massachusetts, 1997. (Existem três exemplares na biblioteca do ISCTE.)