Aula 8

1  Resumo da matéria

1.1  Categoria de acesso protected

A completar.

1.1  Polimorfismo e funções ou procedimentos virtuais

Na aula anterior viu-se que o seguinte código
ListaPEmpregado 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(ListaPEmpregado::Iterador i = pessoal.primeiro();
    i != pessoal.fim();
    ++i)
    i.item()->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.

Torna-se então evidente que, dados que ponteiros de uma classe base pública podem conter endereços de objectos de classes derivada, dois tipos de comportamento são plausíveis ao se invocar uma função ou procedimentos membro da classe base a que a classe derivada sobrepôs a sua própria versão:

  1. Executar de acordo com a classe do ponteiro.
  2. Executar de acordo com a classe do objecto.
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.

1.1.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 para a classe se dizer polimórfica).  Por exemplo, ao se desenhar a classe Empregado, reconhecendo-se a vantagem de permitir derivações futuras, dever-se-ia ter declarado o procedimento mostra() como virtual:
class Empregado {
    std::string nome_;
    std::string morada_;
    Sexo sexo_;
  public:
    Empregado(std::string n, std::string m, Sexo s)
        : nome_(n), morada_(m), sexo_(s) {
    }
    std::string nome() const {
        return nome_;
    }
    std::string morada() const {
        return morada_;
    }
    Sexo sexo() const {
        return sexo_;
    }
    virtual void mostra() const {
        std::cout << "Nome: " << nome() << std::endl
                  << "Morada: " << morada() << std::endl
                  << "Sexo: " << sexo() << std::endl;
    }
};
Se uma classe derivada de uma classe polimórfica sobrepuser a uma função ou procedimento virtual duma classe base uma versão especializada, então essa especialização será também virtual e portanto a classe polimórfica.  Note-se, no entanto, que a sobreposição não é obrigatória:
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 f().  A classe base A define o procedimento virtual f(), a classe B herda-o, e a classe C 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() {
        ...
    }
    ...
};

1.1.2  Destrutores virtuais

Os destrutores, tal como os construtores, não são herdados.  Isso coloca alguns problemas quando classes polimórficas têm necessidade de destrutores explícitos.  Por exemplo:
class A {
    ...
  public:
    ...
    // Sem destrutor explícito (compilador fornece um).
};

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

...

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

O que sucede quando, na função main(), se destroi 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 (implícito) de A não é virtual, é o destrutor de A que é invocado, não se chegando portanto a libertar a memória dinâmica reservada 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() {
    }
};
Isto significa, é claro, que a base da hierarquia de classes representando tipos de empregados na empresa deve ter também um destrutor virtual:
class Empregado {
    std::string nome_;
    std::string morada_;
    Sexo sexo_;
  public:
    Empregado(std::string n, std::string m, Sexo s)
        : nome_(n), morada_(m), sexo_(s) {
    }
    virtual ~Empregado() {
    }
    std::string nome() const {
        return nome_;
    }
    std::string morada() const {
        return morada_;
    }
    Sexo sexo() const {
        return sexo_;
    }
    virtual void mostra() const {
        std::cout << "Nome: " << nome() << std::endl
                  << "Morada: " << morada() << std::endl
                  << "Sexo: " << sexo() << std::endl;
    }
};

1.1.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 e/ou fusão, como habitual.  A este comportamento chama-se ligação dinâmica (dinamic binding), por oposição a ligação estática (static binding).  Este facto é fácil de verificar recorrendo de novo ao exemplo da lista de empregados:
// Visualização dos empregados:
for(ListaPEmpregado::Iterador i = pessoal.primeiro();
    i != pessoal.fim();
    ++i)
    i.item()->mostra();
O compilador, ao observar o código acima, não tem qualquer possibilidade de saber se os ponteiros na lista se referem a empregados, chefes, ou instâncias de qualquer outra classe derivada directa ou indiractamente da classe Empregado.  Isso significa que, sendo o procedimento mostra() virtual na classe base, a decisão de qual o procedimento a invocar em cada caso tem de ser tomada durante a execução do programa.  Este facto, que está na base de toda a verdadeira programação orientada para objectos, leva a que a invocação de funções ou procedimentos virtuais seja um pouco menos eficiente do que a invocação de funções ou procedimentos normais, o que, no entanto, não deve servir de justificação para a não utilizar o polimorfismo onde este é desejável!

Invocação estática explícita

Se se desejar, por alguma razão, invocar uma função ou procedimento virtual duma 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(ListaPEmpregado::Iterador i = pessoal.primeiro();
    i != pessoal.fim();
    ++i)
    i.item()->Empregado::mostra();
A qualificação explícita dá sempre origem à ligação estática.  De resto, é isso que permite, durante a especialização do procedimento mostra() feito pela classe Chefe, invocar o procedimento mostra() da classe Empregado duma forma estática (se a ligação fosse dinâmica neste caso também 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.
    std::cout << "Nível: " << nível() << std::endl;
}

1.2  Classes base abstractas e funções ou procedimentos puramente virtuais

Suponha-se que se pretendia construir um sistema de edição de desenhos em que formas bidimensionais pudessem ser manipuladas pelo utilizador.  Como o construir?  Que classes deveriam ser criadas?  Claramente as formas bidimensionais existem num mundo 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) que ou são pretos ou brancos.

1.2.1  Uma classe simples para representar o 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 atentamente a sua definição e leia os respectivos comentários.  As versões completas das classes que se forem apresentando encontram-se na Secção 1.3.
/**
 * 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 branca e '*' usado
 * para a cor preta.
 */
class Quadro {
 public:
    // Usa-se preto e branco:
    typedef PB Cor;

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

    // Destroi um quadro:
    ~Quadro();

    // Constroi por cópia:
    Quadro(const Quadro&);

    // Atribui por cópia:
    Quadro& operator = (const Quadro&);

    // Pinta o píxel na posição p com a cor c (por omissão a cor mais
    // contrastante com o fundo):
    void pinta(const Posicao& p, const Cor& c);
    void pinta(const Posicao& p);

    // Pinta o píxel na posição p com a cor do fundo:
    void limpa(const Posicao& p);

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

    // Escreve o quadro no canal saida, com conversão para os caracteres ' ' e
    // 'X' (preto e branco respectivamente):
    void mostra(ostream& saida) const;

    // Devolve a cor corrente do píxel na posição p:
    Cor cor(const Posicao& p) const;

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

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

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

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

 private:
    ...
};

1.2.2  Desenhos: listas de formas

Definido o conceito de quadro, é preciso definir desenho.  Numa abordagem simples pode-se dizer que um desenho consiste numa lista de formas concretas elementares 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.  Seja Forma a classe base desta hierarquia.  Nesse caso, sendo todos os tipos específicos de formas (e.g., Quadrado e Circulo) derivados da classe base Forma, é possível pensar num desenho 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 da classe.  Quadrados já todos vimos, mas nunca vimos uma forma que fosse simplesmente uma forma.  É sempre uma concretização do conceito: um triângulo, um polígono, mas não simplesmente uma forma.

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.

A classe abstracta Forma

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(const Posicao& 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(const Posicao& posicao) {
        posicao_ = posicao;
    }

    // Desenha a forma num quadro.
    virtual void desenha(Quadro& q) 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 um versão especializada desta 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.

O procedimento desenha() é um pouco mais delicado.  É óbvio que desenha() deve ser declarado como procedimento virtual, pois cada classe derivada concreta é que 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.  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 como puramente virtual, para o que se coloca = 0 no final do cabeçalho.  Estas funções ou procedimentos, puramente virtuais, não precisam de ser definidos.  Claro está que, nesse caso, também não podem ser invocados, a não ser duma forma polimórfica, isto é, invocados através dum ponteiro que endereça na realidade um objecto duma classe derivada.  A linguagem C++ resolve o problema dizendo que as classes com funções ou procedimentos membro puramente virtuais são abstractas, i.e., não se podem criar instâncias dessas classes.  A solução 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& q) 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 sobrepor uma especialização do procedimento desenha(), pois caso contrário seriam também classes abstractas.

Uma classe concreta: Quadrado

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 duma 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(const Posicao& 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 ecrã, compensando o facto dos píxeis serem
    // rectangulares:
    virtual void desenha(Quadro& q) const;

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

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

    // Desenho dos lados horizontais:
    ...
    // Desenho dos lados verticais (altura compensada pelo rácio
    // largura/altura dos píxeis):
    ...
}

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.

Da mesma forma se poderiam definir outras formas concretas, tais como círculos e triângulos.  Todas elas herdariam directa ou indirectamente, 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.  É 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 1.3.7).

Note-se que a construção da classe abstacta 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 to 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 aexiste 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.

1.3  Módulos 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.

1.3.1  Utilitários

Ficheiro utils.h

#ifndef UTILS_H
#define UTILS_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;
}

#endif // UTILS_H

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

1.3.2  Cores dos píxeis

Ficheiro cor.h

#ifndef COR_H
#define COR_H

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

// Devolve a cor que contrasta com c:
inline PB contrasta(const PB& c) {
    return c == preto ? branco : preto;
}

// Devolve um caracter que represente aproximadamente a cor c num ecrã
// alfanumérico:
inline char caractere(const PB& c) {
    return c == preto ? ' ' : 'X';
}

#endif // COR_H

1.3.3  Posições no quadro

Ficheiro posicao.h

#ifndef POSICAO_H
#define POSICAO_H

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

    void move(int l = 0, int c = 0);

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

 private:
    int linha_;
    int coluna_;
};

inline Posicao::Posicao(int l, int c) : linha_(l), coluna_(c) {
}

inline void Posicao::move(int l, int c) {
    linha_ = l;
    coluna_ = c;
}

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

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

#endif // POSICAO_H

1.3.4  Tamanhos no quadro

Ficheiro tamanho.h

#ifndef TAMANHO_H
#define TAMANHO_H

#include <cassert>

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

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

 private:
    int linhas_;
    int colunas_;
};

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

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

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

#endif // TAMANHO_H

1.3.5  Quadro de desenho

Ficheiro quadro.h

#ifndef QUADRO_H
#define QUADRO_H

#include <iostream>

#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 branca e '*' usado
 * para a cor preta.
 */
class Quadro {
 public:
    // Usa-se preto e branco:
    typedef PB Cor;

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

    // Destroi um quadro:
    ~Quadro();

    // Constroi por cópia:
    Quadro(const Quadro&);

    // Atribui por cópia:
    Quadro& operator = (const Quadro&);

    // Pinta o píxel na posição p com a cor c (por omissão a cor mais
    // contrastante com o fundo):
    void pinta(const Posicao& p, const Cor& c);
    void pinta(const Posicao& p);

    // Pinta o píxel na posição p com a cor do fundo:
    void limpa(const Posicao& p);

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

    // Escreve o quadro no canal saida, com conversão para os caracteres ' ' e
    // 'X' (preto e branco respectivamente):
    void mostra(ostream& saida) const;

    // Devolve a cor corrente do píxel na posição p:
    Cor cor(const Posicao& p) const;

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

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

    // Devolve a relação largura/altura dos píxeis:
    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 n_pixeis;               // número de píxeis do quadro.
    Cor *pixeis;                // matriz com os píxeis do quadro.

    // Converte posição para índice da matriz com os píxeis:
    int indice(const Posicao& p) const;
};

// Construtor:
inline Quadro::Quadro(Tamanho t, Cor f, double a)
    : tamanho_(t), fundo_(f), aspecto_(a),
      n_pixeis(t.linhas() * t.colunas()),
      pixeis(new Cor[n_pixeis])
{
    apaga();
}

// Destrutor:
inline Quadro::~Quadro() {
    delete[] pixeis;
}

// Pintura de um pixel numa dada posição com uma dada cor:
inline void Quadro::pinta(const Posicao& p, const Cor& c) {
    if(dentro(p))
        pixeis[indice(p)] = c;
}

// Pintura de um pixel numa dada posição com um cor contrastante com o
// fundo do quadro:
inline void Quadro::pinta(const Posicao& p) {
    if(dentro(p))
        pixeis[indice(p)] = contrasta(fundo());
}

// "Despinta" um pixel numa dada posição pintando-o com a cor do fundo:
inline void Quadro::limpa(const Posicao& p) {
    pinta(p, fundo());
}

// Devolve a cor do pixel na posição dada.  Se a posição estiver fora do
// quadro devolve a cor do fundo:
inline Quadro::Cor Quadro::cor(const Posicao& p) const {
    return dentro(p) ? pixeis[indice(p)] : fundo();
}

// Indica se uma dada posição está dentro do quadro:
inline bool Quadro::dentro(const Posicao& p) const {
    return (0 <= p.linha() && p.linha() < tamanho().linhas() &&
            0 <= p.coluna() && p.coluna() < tamanho().colunas());
}

// Devolve a cor do fundo:
inline Quadro::Cor Quadro::fundo() const {
    return fundo_;
}

// Devolve a relação largura/altura dos píxeis do quadro:
inline double Quadro::aspecto() const {
    return aspecto_;
}

// Devolve o tamanho do quadro:
inline Tamanho Quadro::tamanho() const {
    return tamanho_;
}

// Devolve o índice na matriz de píxeis correspondente a uma dada posição, que
// se admite estar dentro do quadro:
inline int Quadro::indice(const Posicao& p) const {
    return p.linha() * tamanho().colunas() + p.coluna();
}

#endif // QUADRO_H

Ficheiro quadro.cpp

#include <iostream>

#include "quadro.h"

// Construtor por cópia:
Quadro::Quadro(const Quadro& q)
    : tamanho_(q.tamanho_), fundo_(q.fundo_),
      aspecto_(q.aspecto_), n_pixeis(q.n_pixeis),
      pixeis(new Cor[n_pixeis])
{
    // Copiar os píxeis:
    for(int i = 0; i != n_pixeis; ++i)
        pixeis[i] = q.pixeis[i];
}

// Atribuição por cópia:
Quadro& Quadro::operator = (const Quadro& q) {
    if(this != &q) {
        if(n_pixeis != q.n_pixeis) {
            delete pixeis;
            n_pixeis = q.n_pixeis,
           pixeis = new Cor[n_pixeis];
        }
        tamanho_ = q.tamanho_;
        fundo_ = q.fundo_;
        aspecto_ = q.aspecto_;
        // Copiar os píxeis:
        for(int i = 0; i != n_pixeis; ++i)
            pixeis[i] = q.pixeis[i];
    }
    return *this;
}

// Pinta todo o quadro com a cor do fundo:
void Quadro::apaga() {
    for(int i = 0; i != n_pixeis; ++i)
        pixeis[i] = fundo();
}

// Escreve o quadro num canal, que se presume ligado a um ecrã em modo
// alfanumérico:
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 != n_pixeis - 1; ++i)
        saida << caractere(pixeis[i]);
    saida << flush;
}

1.3.6  Conceito de forma

Ficheiro 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(const 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;

    // Desloca a forma para nova posição:
    virtual void move(const Posicao& 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& q) const = 0;

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

// Construtor:
inline Forma::Forma(const Posicao& posicao)
    : posicao_(posicao) {
}

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

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

#endif // FORMA_H

1.3.7  Quadrados

Ficheiro 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 direito:
class Quadrado : public Forma {
 public:
    // Construtor, dadas a posição e a dimensão do lado (em colunas do quadro):
    Quadrado(const Posicao& posicao, int lado);

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

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

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

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

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

#endif // QUADRADO_H

Ficheiro quadrado.cpp

#include "utils.h"

#include "quadrado.h"

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

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

1.3.7  Círculos

Ficheiro 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(const Posicao& posicao, int raio);

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

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

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

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

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

#endif // CIRCULO_H

Ficheiro circulo.cpp

#include <cmath>
#include <cassert>

#include "circulo.h"
#include "utils.h"

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

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

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

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

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

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

1.3.7  Programa de teste

Ficheiro teste.cpp

#include <cstdlib> // para rand().
#include <list>

#include "quadro.h"
#include "quadrado.h"
#include "circulo.h"

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

    for(int i = 0; i != 10; ++i) {
        int l = rand() % 24;
        int c = rand() % 80;
        lista.push_back(new Quadrado(Posicao(l,c), rand() % 15 + 1));
        l = rand() % 24;
        c = rand() % 80;
        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);  // *i funciona como i.item() nas
              // listas das aulas teóricas.
        quadro.mostra(cout);
        cin.get();
    }
}
 

1.4  Leitura recomendada

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

2  Exercícios

Desenhe a hierarquia de classes para representar os vários tipos de tarefas necessários no trabalho final da cadeira e no problema 4.  A hierarquia deve ter uma classe abstracta Tarefa como base (que membros deve conter? pense primeiro em termos daquilo que um projecto deve poder fazer com uma tarefa).  Faça um programa de teste que lhe assegure que o polimorfismo está "a funcionar".

3  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.