Resumo da Aula 8

Sumário

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 da construção protected:.  Membros com esta categoria de acesso funcionam como privados para o público em geral, mas como públicos para as classes derivadas:
 
Categoria de acesso dos membros Acesso
privado (private:) Só membros e amigos* da classe.
protegido (protected:) Membros e amigos* da classe e de classes derivadas.
público (public:) Sem restrições.

Se a utilização da palavra chave const podia ser vista como introduzindo duas interfaces distintas para a mesma classe (correspondentes ao acesso através de uma variável e ao acesso através de uma constante), a utilização da categoria de acesso protegido divide cada uma dessas interfaces em duas.  Os membros públicos fazem parte da interface geral.  Os membros públicos e os membros protegidos fazem parte de uma interface estendida fornecida a quem pretende derivar a classe.

Quando se deriva uma classe a partir de outra, a derivação pode também ser pública, protegida ou privada.  A derivação protegida é muito pouco utilizada na prática.  No entanto, faz-se aqui uma breve descrição das suas características comparativamente com as derivações pública e privada, já estudadas.

Sendo B uma classe derivada de A e C uma classe derivada de B:

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 que B faz de 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 esta tabela e não se preocupe se não a perceber à primeira.  Recorra a ela quando sentir dificuldades com as políticas de acesso das classes.

*   A amizade não é hereditária.  Se uma classe A for declarada como amiga de uma classe B, classes que sejam derivadas da classe A não herdarão a amizade relativamente a B.

Polimorfismo e métodos virtuais

Na aula anterior viu-se que o seguinte código

list<Empregado*> empregados;

//
Inserção dos empregados:
empregados.push_back(new Empregado("João Maria", masculino));
empregados.push_back(new Chefe("Ana Maria", feminino, 4));

...

// Visualização dos empregados:
for(list<Empregado*>::iterator i = empregados.begin();
    i != empregados.end(); ++i)

    (*i)->mostra();

embora compilasse correctamente, tinha um comportamento pouco desejado: todos os objectos presentes 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, como se pode ver no respectivo diagrama:

Ponteiros de uma classe base pública podem conter endereços de objectos de classes derivadas, devido à relação é um estabelecida entre as classes.  Ao se invocar, através de um ponteiro para a classe base, uma operação que a classe derivada ocultou fornecendo a sua própria versão, dois tipos de comportamento são portanto plausíveis:

  1. É executado o método da classe base.
  2. É executado o método da classe derivada.
O mesmo se passa quando o acesso se faz através de referências.

O primeiro comportamento é o que ocorre, por omissão, no C++.  Ao segundo dá-se o nome de comportamento polimórfico, pois conseguem-se diferentes comportamentos através de ponteiros ou referências uniformes.  Os métodos que proporcionam comportamento polimórfico à respectiva classe dizem-se polimórficos ou virtuais.  Basta que uma classe tenha uma operação polimórfica para se dizer ela também polimórfica.

Em rigor, como se verá, existem vários tipos de polimorfismo, sendo o polimorfismo associado à derivação de classes conhecido por polimorfismo de subtipos.

2.1  Operações e métodos

Em programação orientada para objectos é importante distinguir dois conceitos: o de operação e o de método.  Uma operação invoca-se através de um objecto na esperança de despoletar determinado comportamento.  Um método é uma implementação particular de uma dada operação.  Assim, ao se invocar uma operação, será executado um método.

Até agora a cada operação correspondia unicamente um método, pelo que esta distinção não era muito importante.  O comportamento polimórfico das classes altera este quadro.  Suponha-se de novo a situação indicada no diagrama apresentado.  Se o comportamento do código fosse o que se pretendia, a invocação da operação mostra() para todos os itens da lista levaria à execução do método apropriado à classe do objecto em causa, ou seja,  Empregado::mostra() ou Chefe::mostra(), consoante o caso.  Ter-se-ia pois, nesse caso, uma operação e dois métodos que a implementam.

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

Na realidade aquilo que se consegue ao usar polimorfismo é que os métodos a executar quando se invoca uma operação sejam determinados durante a execução do programa, e não durante a compilação, como é habitual.  O método a executar depende do tipo do objecto apontado (ou referenciado) e não do tipo do ponteiro (ou referência), e portanto só pode ser conhecido durante a execução do programa.

A este comportamento, em que o método 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).  O termo ligação tem a ver com a ligação estabelecida entre a operação invocada e o método realmente executado.

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

// Visualização dos empregados:
for(list<Empregado*>::iterator i = empregados.begin();
    i != empregados.end(); ++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 a decisão de qual o método a executar em cada caso tem de ser tomada durante a execução do programa, se se pretender que exista polimorfismo, ou seja, se se pretender que a operação mostra() seja polimórfica.

O método realmente executado ao invocar uma operação polimórfica pode pertencer a uma classe que nem sequer existia quando a classe base foi definida.  Esta é uma das enormes vantagens das operações polimórficas 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 operações polimórficas seja um pouco menos eficiente do que a invocação de operações normais, para as quais há apenas um método por operação, mas isso não deve servir de justificação para a não utilizar o polimorfismo onde ele for apropriado!

2.3  Operações polimórficas

Em C++ o comportamento polimórfico consegue-se declarando as operações da classe base como polimórficas através do especificador virtual.  Basta que exista uma operação polimórfica ou virtual, declarada na própria classe ou herdada de uma classe base, para a classe se dizer também polimórfica.

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

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

    string const& nome() const;

    Sexo sexo() const;
    virtual void mostra() const;

  private:

    string nome_;
    Sexo sexo_;
};

inline Empregado::Empregado(string const& nome, Sexo 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;
}

O especificador aplicou-se apenas na declaração, que funciona como a declaração da operação.  A definição corresponde apenas a um dos possíveis métodos que implementa essa operação, pelo que não inclui o especificador.

A classe Empregado é polimórfica.  No entanto, as operações inspectoras nome(), morada() e sexo() não o são: não é suposto que estas operações, para as quais se fornecem métodos na classe base, tenham métodos especializados definidos em classes derivadas!

Se uma classe derivada de uma classe polimórfica definir um método que implementa uma operação polimórfica de uma classe base, diz-se que sobrepôs a sua própria implementação da operação.  Note-se que só é uma sobreposição se a operação original for polimórfica!  Caso contrário trata-se de uma simples ocultação.  Por exemplo, o método Chefe::mostra() sobrepõe-se ao método Empregado::mostra(), pois ambos implementam a operação polimórfica mostra() da classe Empregado.

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

    int nível() const;

    void mostra() const;

  private:

    int nível_;
};

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

    : Empregado(nome, sexo), nível_(nível) 
{

}

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

    return nível_;
}

inline void Chefe::mostra() const 
{

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

A sobreposição não é obrigatória.  Uma classe derivada pode-se limitar a herdar um método associado a uma operação polimórfica sem lhe sobrepor uma versão própria.  Por exemplo:

class A {
  public:
    virtual void f();
    ...
};

void A:: f() 
{

    ...
}

class B : public A {

    ...
};

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

void C::f()
{

    ...
}

Neste exemplo existem três classes, todas elas polimórficas.  Todas possuem uma operação polimórfica void f(), herdada da classe base A.  A classe base define o método 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.  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 a operação f() ser polimórfica:

class C : public B {

    ...

  public:
    virtual void f();

    ...

};

Aliás, mesmo sendo redundante, é boa prática deixar claro que uma operação é polimórfica, 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 operação é ou não polimórfica.  No caso da classe Chefe isso levaria à definição:

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

    int nível() const;

    virtual void mostra() const;

  private:

    int nível_;
};

Quando uma classe derivada declara uma operação com o mesmo nome que uma operação de uma classe base (directa ou indirecta), a operação da classe base fica oculta.  Isto passa-se mesmo que a operação da classe derivada tenha uma assinatura diferente da assinatura da operação 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;
    }
};

int A::f(int i) 
{

    return i;
}

class B : public A {
public:
    void f(); // ocultação de A::f()
};

void B::f() 
{            

    int j = f(10);   // erro: compilador não encontra A::f() porque está oculto.
    int k = A::f(10);// ok: o prefixo A:: permite ao compilador encontrar a
                     // operação pretendida.
}

Uma ocultação só é uma sobreposição quando:
  1. A operação declarada na classe derivada tiver a mesma assinatura da operação ocultada.
  2. Ambos tenham a mesma assinatura de uma operação polimórfica declarada na (ou herdada pela) classe base.

Além disso, as operações têm de ter o mesmo tipo de devolução*.  É um erro tentar sobrepor um método com um tipo de devolução incompatível com o da operação polimórfica que implementa.  Por exemplo:

class A {
  public:
    virtual int g(int);
};

class B : public A {
  public:
    double g(int); // erro: mesma assinatura que o operação polimórfica A::g()
                   // (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.3.1  Invocação estática explícita

Se se desejar, por alguma razão, invocar explicitamente um dado método, ignorando portanto o mecanismo de ligação dinâmica, tem de se qualificar o nome do método com o nome da classe onde está definido, usando o operador de resolução de âmbito.  Por exemplo, se se pretendesse listar os empregados da empresa como simples empregados, poder-se-ia usar:

// Visualização dos empregados:
for(list<Empregado*>::iterator i = empregados.begin();
    i != empregados.end(); ++i)

    (*i)->Empregado::mostra();

A qualificação explícita numa invocação dá sempre origem à ligação estática: o compilador fica a saber imediatamente o método a executar.  De resto, é isto que permite, durante a definição do método Chefe::mostra(), invocar o método Empregado::mostra() usando ligação estática (se a ligação também neste caso fosse dinâmica, acabar-se-ia com uma execução recursiva do método Chefe::mostra()!):

void Chefe::mostra() const 
{

    Empregado::mostra(); // invocação com ligação estática.
    cout << "Nível: " << nível() << endl;
}

2.4  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).

Este 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();
    ~B();

    ...

  private:
    float* pf;

    ...

};

B:: B() 
{

    pf = new float(0.0f);
}

B::~B()
{

    delete 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 é polimórfico, é 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 polimórficos!  Ou seja, a classe A deveria ter sido definida como:

class A {
  public:

    ...

    // Com destrutor explícito polimórfico, mesmo que vazio:
    virtual ~A() {}
};

Logo, a base da hierarquia de classes representando tipos de empregados na empresa deve ter também um destrutor polimórfico:

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

    string const& nome() const;

    Sexo sexo() const;
    virtual void mostra() const;

  private:

    string nome_;
    Sexo sexo_;
};

Note-se que é usual que se defina o destrutor virtual de uma classe directamente na definição dessa classe, em vez de se declarar simplesmente, colocando a definição fora da definição da classe.

Classes e operações abstractas

Suponha-se que se pretendia construir um sistema de edição de figuras em que formas bidimensionais pudessem ser manipuladas pelo utilizador.  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 são brancos.

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.

/** 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 PretoEBranco Cor;

    /**
Constrói um quadro com dimensão dada (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 dimensão.numeroDeLinhas() - 1 (para a linha) e
       
entre 0 e dimensão.colunas() - 1 (para a coluna).  A razão
       
entre a largura e a altura dos píxeis é dada por aspecto.
       
@pre 0 < dimensao.numeroDeLinhas() e 0 < dimensao.numeroDeColunas() e
            
0,0 <= aspecto.
       
@post dimensao() = dimensao e fundo() = fundo e aspecto() = aspecto. */
    Quadro(Dimensao const& dimensao = Dimensao(24, 80), 
           Cor const fundo = preto,
           double const aspecto = 1.0);

    /**
Devolve a dimensao do quadro.
       
@pre V.
       
@post dimensao = dimensão do quadro. */
    Dimensao dimensao() const;

    /**
Devolve a razão largura/altura (i.e., o aspecto) dos píxeis
       
do quadro.
        @pre
V.
        @post aspecto
= aspecto dos píxeis no quadro. */
    double aspecto() const;

    /**
Devolve a cor do fundo.
       
@pre V.
       
@post fundo = cor do fundo do quadro. */
    Cor fundo() const;

    /**
Indica se uma dada posição está dentro do quadro.
       
@pre V.
       
@post inclui = (0 <= posicao.linha() e 
                        posicao
.linha() < dimensao().numeroDeLinhas() e
                       
0 <= posicao.coluna() e
                        posicao
.coluna() < dimensao().numeroDeColunas()). */
    bool inclui(Posicao const& posicao) const;

    /**
Devolve a cor do pixel na posição dada.  Se a posição estiver
       
fora do quadro devolve a cor do fundo. 
       
@pre V.
       
@post (cor = fundo() e ¬inclui(posicao)) ou 
             
(cor = cor do píxel em posicao e inclui(posicao)). */
    Cor cor(Posicao const& posicao) const;

    /**
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).
       
@pre V.
       
@post saida.fail() ou (¬saida.fail() e saida contém uma 
             
representação do quadro no modo texto). */
    void mostraEm(std::ostream& saida) const;

    /**
Pinta o píxel numa dada posição com uma dada cor.
       
@pre V.
       
@post ¬inclui(posicao) ou cor(posicao) = cor. */
    void pinta(Posicao const& posicao, Cor const cor);

    /**
Pinta o pixel numa dada posição com um cor contrastante com o
       
fundo do quadro.
       
@pre V.
       
@post ¬inclui(posicao) ou cor(posicao) = contrasta(fundo()). */
    void pinta(Posicao const& posicao);

    /**
"Despinta" o pixel numa dada posição pintando-o com a cor do
       
fundo.
       
@pre V.
       
@post cor(posicao) = fundo(). */
    void limpa(Posicao const& posicao);

    /**
Pinta todo o quadro com a cor do fundo.
       
@pre V.
       
@post (Q p : : cor(p) = fundo()). */
    void apaga();

  private:
    ...
};

Esta classe recorre a dois TAD usados para representar posições e dimensões no quadro:

/** Representa uma posição. */
class Posicao {
  public:
    /**
Constrói uma nova posição.
       
@pre V.
       
@post linha() = linha e coluna() = coluna. */
    Posicao(int const linha = 0, int const coluna = 0);

    /**
Devolve a linha.
       
@pre V.
       
@post linha = linha da posição. */
    int linha() const;

    /**
Devolve a coluna.
       
@pre V.
       
@post coluna = coluna da posição. */
    int coluna() const;

  private:
    ...
};

/** Representa uma dimensão no ecrã. */
class Dimensao {
  public:
    /**
Constrói nova dimensão.
       
@pre 0 <= numero_de_linhas e 0 <= numero_de_colunas.
       
@post numeroDeLinhas() = numero_de_linhas e
              numeroDeColunas
() = numero_de_colunas. */
    Dimensao(int const numero_de_linhas = 0, int const numerdo_de_colunas = 0);

    /**
Devolve o número de linhas.
       
@pre V.
       
@post numeroDeLinhas = número de linhas. */
    int numeroDeLinhas() const;

    /**
Devolve o número de colunas.
       
@pre V.
       
@post numeroDeColunas = número de colunas. */
    int numeroDeColunas() const;

  private:
    ...
};

3.2  Figuras: listas de formas

Definida a classe Quadro para representar o conceito de quadro, é preciso concretizar o conceito de figura.  Numa abordagem simples, pode-se dizer que uma figura consiste num conjunto 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 Circunferencia) derivadas da classe base Forma, é possível pensar numa figura como contendo 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, como se viu.  Ou seja, um Quadrado é uma Forma, uma Circunferência é 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 abstracto.  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 exemplos ou instâncias 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 no Resumo da 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 subespé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 subespé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 abstracto que generaliza as características das várias espécies da ordem dos primatas, essas sim representadas por classes concretas como Homo_sapiens_sapiens ou Pan_troglodytes.

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:

/** Representa o conceito de forma como algo que tem uma posição, que se pode
   
mover, e que se sabe desenhar num quadro. 
   
@invariant V. */
class Forma {
  public:
    /**
Constrói uma forma dada uma posição.
       
@pre V.
       
@post posicao() = posicao. */
    Forma(Posicao const& posicao);

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

    /**
Devolve a posição da forma.
       
@pre V.
       
@post posicao = posição da forma. */
    Posicao posicao() const;

    /**
Desenha a forma num quadro.
       
@pre V.
       
@post quadro está pintado na zona correspondente à forma. */
    virtual void desenhaEm(Quadro& quadro) const;

    /**
Desloca a forma para nova posição.
       
@pre V.
       
@post posicao() = nova_posicao. */
    virtual void movePara(Posicao const& nova_posicao);

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

    /**
Indica se a condição invariante da classe se verifica.
       
@pre V.
       
@post cumpreInvariante = V. */
    bool cumpreInvariante() const;
};

inline Forma::Forma(Posicao const& posicao)
    : posicao_(posicao) 
{

    assert(cumpreInvariante());
    assert(this->posicao() == posicao);
}

inline Forma::~Forma()
{
    assert(cumpreInvariante());

}

inline Posicao Forma::posicao() const 
{
    assert(cumpreInvariante());

    return posicao_;
}

inline void Forma::movePara(Posicao const& nova_posicao) 
{
    assert(cumpreInvariante());

    posicao_ = nova_posicao;

    assert(cumpreInvariante());
    assert(posicao() == nova_posicao);
}

inline bool Forma::cumpreInvariante() const
{
    return true;
}

A classe possui um atributo posicao_ que guarda a posição corrente da forma e que é inicializado através do construtor.  O inspector posicao() devolve a posição da forma.  É uma operação normal, não-polimórfica, por parecer pouco plausível que alguma classe derivada necessite de sobrepor uma versão especializada ao respectivo método (embora o assunto seja discutível).  Pelo contrário, pode haver necessidade de sobrepor especializações do método movePara() em classes derivadas, pois, por exemplo, para determinadas formas pode haver movimentos que não se limitem a provocar uma alteração da posição de origem.  Assim, a correspondente operação movePara() foi declarada polimórfica ou virtual, para que a classe forma apresente comportamento polimórfico durante invocações desta operação por intermédio de ponteiros ou referências, ou seja, para que formas diferentes se possam mover de forma diferente.

A operação desenhaEm() é um pouco mais delicada.  É óbvio que desenhaEm() deve ser declarada como polimórfica ou virtual, pois cada classe derivada concreta saberá como desenhar-se e portanto sobreporá a sua própria especialização do respectivo método.  Mas como definir o método desenhaEm() na classe Forma?  Sendo Forma uma representação duma abstracção, conclui-se que não tem qualquer sentido definir esse método: é 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.

A linguagem C++ obriga a fornecer uma implementação para todas as operações polimórficas declaradas.  Que fazer, então, para que seja possível não definir o método desenhaEm() na classe Forma?  É possível, nestes casos, declarar a operação não apenas polimórfica ou virtual mas abstracta ou puramente virtual.  Para isso coloca-se a estranha construção = 0 após a declaração da operação em causa.  Operações abstractas ou puramente virtuais não precisam de ser implementadas na própria classe!

Claro está que uma operação abstracta para a qual não tenha sido fornecida implementação não pode ser invocada!  Que método seria invocado?  Só pode ser invocada se o for de uma forma polimórfica, isto é, através de um ponteiro que endereça na realidade um objecto de uma classe derivada que, essa sim, forneça uma implementação para a operação invocada.  Isto implica, naturalmente, que não podem existir instâncias de classes com operações abstractas.  De facto, para a linguagem C++  todas as classes com operações abstractas são elas próprias classes abstractas, i.e., são classes das quais não se podem construir 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 {
  public:
    ...
    /** 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.  Esta operação é, portanto, abstracta,
        sendo a sua existência que torna a classe Forma abstracta. */
    virtual void desenhaEm(Quadro& quadro) const = 0;
    ...
};

Classes derivadas são abstractas se declararem alguma operação como puramente virtual ou se não fornecerem implementações para todas as operações abstractas herdadas das classes base.  Assim, as formas concretas, como circunferências ou quadrados, derivadas por herança pública da classe base Forma, deverão sempre fornecer um método que implementem a operação desenhaEm(), declarada na classe base, 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));

Dois pontos têm ainda de ser frisados relativamente a operações e classes abstractas.  

O primeiro ponto é que, curiosamente, uma classe pode simultaneamente declarar uma operação como abstracta e fornecer o respectivo método.  Embora seja invulgar, pode ser útil fazê-lo quando se pretende fornecer uma implementação parcial da operação e impor às classes concretas derivadas o fornecimento de uma implementação completa dessa operação, eventualmente recorrendo ao método definido na classe base para realizar parte das suas tarefas.  

O segundo ponto tem a ver com o facto de não ser muito simpático obrigar o programador consumidor de uma classe a percorrer todas as operações de uma classe para indagar se ela é ou não, abstracta (basta que haja uma operação abstracta para que a classe o seja também).  É típico, para lhe simplificar a vida, definir-se o destrutor de uma classe como abstracto sempre que a classe o for.  Esta prática tem a vantagem adicional de permitir definir uma classe abstracta sem que nenhuma das suas operações precise de ser abstracta.  Note-se que, ao contrário do que acontecia para as operações normais de uma classe, tem sempre de se fornecer uma implementação para o destrutor de uma classe, mesmo que este seja abstracto.  Por outro lado, numa classe derivada o destrutor ou é declarado explicitamente pelo programador, ou é fornecido implicitamente pela linguagem.  Assim, um destrutor abstracto tem com único efeito tornar a classe abstracta, ou seja, não instanciável.

class Forma {
  public:
   
...
    virtual ~Forma() = 0;
   
...
}
...
/* Não é possível definir um método durante a declaração da respectiva operação
   quando a operação é abstracta: */
Forma::~Forma()
{
}

A notação usada para representar uma classe abstracta é idêntica à das restantes classes, embora o seu nome seja mostrado em itálico, o mesmo acontecendo com todas as suas operações abstractas:

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 atributos e operações específicos e fornecimento de uma implementação específica (sobreposição) para a operação desenhaEm() herdada como operação abstracta a partir da classe base:

/** Representa um quadrado aberto com lados paralelos aos eixos
   
coordenados.  Posição é o canto superior direito.
   
@invariant 0 <= lado. */
class Quadrado : public Forma {
  public:
    /**
Constrói novo quadrado dadas a posição do canto superior esquerdo
       
e a dimensão do lado (em colunas do quadro).
       
@pre V.
       
@post posicao() = posicao e lado() = lado. */
    Quadrado(Posicao const& posicao, int lado);

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

    /** Devolve dimensão do lado (em colunas do quadro).
       
@pre V.
       
@post lado = lado do quadrado. */
    int lado() const;

    /**
Desenha o quadrado no quadro, compensando o facto dos píxeis serem
       
rectangulares.
       
@pre V.
       
@post quadro está pintado na zona correspondente ao quadrado. */
    virtual void desenhaEm(Quadro& quadro) const;  

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

    /**
Indica se a condição invariante de classe se verifica.
       
@pre V.
       
@post cumpreInvariante = 0 <= lado. */
    bool cumpreInvariante() const;
};

inline Quadrado::Quadrado(Posicao const& posicao, int lado)
    : Forma(posicao), lado_(lado) 
{
    assert(0 <= lado);

    assert(cumpreInvariante());
    assert(this->posicao() == posicao);
    assert(this->lado() == lado);
}

inline int Quadrado::lado() const 
{
    assert(cumpreInvariante());

    return lado_;
}

inline bool Quadrado::cumpreInvariante() const 
{
    return 0 <= lado_;
}

void Quadrado::desenhaEm(Quadro& quadro) const 
{
    assert(cumpreInvariante());

    ...
}

A implementação da operação desenhaEm(), que é a única abstracta herdada (com excepção do destrutor, ver secção anterior),  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 circunferências 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.  É isso que permite garantir que a invocação da operação desenhaEm() através dos ponteiros na lista leva sempre à execução de um método existente.

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

  1. Em primeiro lugar, permite guardar 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 operações virtuais, que a invocação de desenhaEm() através desses ponteiros (ou referências) invoca a o método apropriado à classe efectiva do objecto apontado.
  2. Em segundo lugar, consegue-se garantir que não podem ser criadas instâncias de uma classe que representa um conceito abstracto como o de forma, uma vez que existem operações abstractas ou puramente virtuais que tornam a classe abstracta.
  3. 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.

As representações da classe derivada Quadrado e de uma classe adicional Circunferência, bem como a representação das suas relações com a classe base Forma apresentam-se abaixo:

Módulos físicos completos

As classes apresentadas no texto anterior estão disponíveis na sua versão completa.  O código inclui ainda a classe Circunferência e um pequeno programa de teste que permite verificar o correcto funcionamento das classes.

Os módulos físicos correspondentes a este programa são:

O conjunto dos ficheiros encontra-se em codigo_resumo.zip.

!!Falta aqui secção sobre auto_ptr!  Ver Josuttis.

!!Falta discutir aqui a noção de objecto e de classe propriamente dita (vs. valor e TAD)!

Leitura recomendada

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

Referências

[1]  Stanley B. Lippman e Josée Lajoie, "C++ Primer", 3ª edição, Addison-Wesley, Reading, Massachusetts, 1998.

[2]  Bjarne Stroustrup, "The C++ Programming Language", 3ª edição, Addison-Wesley, Reading, Massachusetts, 1997.