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:
onde categoria pode ser
class A {public:...protected:...private:...};
class B : categoria A {public:...protected:...private:...};
class C : categoria B {...};
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 |
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.
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
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();
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:
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.
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.
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:
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
//Visualização dos empregados:for(list<Empregado*>::iterator i = empregados.begin();
i != empregados.end(); ++i)(*i)->mostra();
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!
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:
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.
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;}
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:
Neste exemplo existem três classes, todas elas polimórficas. Todas possuem uma operação polimórficaclass A {public:virtual void f();...};
void A:: f()
{...}
class B : public A {...};
class C : public B {public:void f();...};
void C::f()
{...}
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:
A::f() se o objecto apontado pertencer à classe
A.A::f() se o objecto apontado pertencer à classe
B.C::f() se o objecto apontado pertencer à classe
C.
Note-se que, na classe C, teria sido possível explicitar
o facto de a operação f() ser polimórfica:
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 especificadorclass C : public B {
...
public:virtual void f();
...
};
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:
Uma ocultação só é uma sobreposição quando:
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 deA::f()
};
void B::f()
{int j = f(10); //erro: compilador não encontraA::f()porque está oculto.int k = A::f(10);//ok: o prefixoA::permite ao compilador encontrar a
//operação pretendida.}
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:
* Na realidade o tipo de devolução pode ser diferente, desde que seja covariante (ver [2, pág. 425]).
class A {public:virtual int g(int);};
class B : public A {public:double g(int); //erro: mesma assinatura que o operação polimórficaA::g()//(sobreposição), mas com tipo de devolução incompatível.};
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
//Visualização dos empregados:for(list<Empregado*>::iterator i = empregados.begin();
i != empregados.end(); ++i)(*i)->Empregado::mostra();
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;}
Este facto coloca alguns problemas quando se destroem variáveis dinâmicas através de ponteiros para uma classe base. Por exemplo:
O que sucede quando, na funçãoclass 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;}
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:
Logo, a base da hierarquia de classes representando tipos de empregados na empresa deve ter também um destrutor polimórfico:
class A {
public:
...
//Com destrutor explícito polimórfico, mesmo que vazio:virtual ~A() {}};
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.
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.
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.
*/Constrói um quadro com dimensão dada (24x80 por omissão) e com
class Quadro {
public:
// Usa-se preto e branco:
typedef PretoEBranco Cor;
/**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) eentre 0 e
dimensão.colunas() - 1 (para a coluna). A razãoentre a largura e a altura dos píxeis é dada por
aspecto.@pre 0 <
dimensao.numeroDeLinhas() e 0 <dimensao.numeroDeColunas() e0,0 <=
aspecto.@post
dimensao() =dimensaoefundo() =fundoeaspecto() =aspecto.*/Devolve a dimensao do quadro.
Quadro(Dimensao const& dimensao = Dimensao(24, 80),
Cor const fundo = preto,
double const aspecto = 1.0);
/**@pre V.
@post
dimensao= dimensão do quadro.*/Devolve a razão largura/altura (i.e., o aspecto) dos píxeis
Dimensao dimensao() const;
/**do quadro.
V.
@pre= aspecto dos píxeis no quadro.
@post aspecto*/Devolve a cor do fundo.
double aspecto() const;
/**@pre V.
@post
fundo= cor do fundo do quadro.*/Indica se uma dada posição está dentro do quadro.
Cor fundo() const;
/**@pre V.
@post
inclui= (0 <=posicao.linha() e.
posicaolinha() <dimensao().numeroDeLinhas() e0 <=
posicao.coluna() e.
posicaocoluna() <dimensao().numeroDeColunas()).*/Devolve a cor do pixel na posição dada. Se a posição estiver
bool inclui(Posicao const& posicao) const;
/**fora do quadro devolve a cor do fundo.
@pre V.
@post (
cor=fundo() e ¬inclui(posicao)) ou(
cor= cor do píxel emposicaoeinclui(posicao)).*/Escreve o quadro no canal
Cor cor(Posicao const& posicao) const;
/**saida(que se presume ligado a umecrã em modo texto), com conversão para os caracteres ' ' e
'×' (preto e branco respectivamente).
@pre V.
@post
saida.fail() ou (¬saida.fail() esaidacontém umarepresentação do quadro no modo texto).
*/Pinta o píxel numa dada posição com uma dada cor.
void mostraEm(std::ostream& saida) const;
/**@pre V.
@post ¬
inclui(posicao) oucor(posicao) =cor.*/Pinta o pixel numa dada posição com um cor contrastante com o
void pinta(Posicao const& posicao, Cor const cor);
/**fundo do quadro.
@pre V.
@post ¬
inclui(posicao) oucor(posicao) =contrasta(fundo()).*/"Despinta" o pixel numa dada posição pintando-o com a cor do
void pinta(Posicao const& posicao);
/**fundo.
@pre V.
@post
cor(posicao) =fundo().*/Pinta todo o quadro com a cor do fundo.
void limpa(Posicao const& posicao);
/**@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.*/Constrói uma nova posição.
class Posicao {
public:
/**@pre V.
@post
linha() =linhaecoluna() =coluna.*/Devolve a linha.
Posicao(int const linha = 0, int const coluna = 0);
/**@pre V.
@post
linha= linha da posição.*/Devolve a coluna.
int linha() const;
/**@pre V.
@post
coluna= coluna da posição.*/
int coluna() const;
private:
...
};
/**Representa uma dimensão no ecrã.*/Constrói nova dimensão.
class Dimensao {
public:
/**@pre 0 <=
numero_de_linhase 0 <=numero_de_colunas.@post
numeroDeLinhas() =numero_de_linhase() =
numeroDeColunasnumero_de_colunas.*/Devolve o número de linhas.
Dimensao(int const numero_de_linhas = 0, int const numerdo_de_colunas = 0);
/**@pre V.
@post
numeroDeLinhas=número de linhas.*/Devolve o número de colunas.
int numeroDeLinhas() const;
/**@pre V.
@post
numeroDeColunas= número de colunas.*/
int numeroDeColunas() const;
private:
...
};
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:
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
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 {...};
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.A classe possui um atributo
/**Representa o conceito de forma como algo que tem uma posição, que se podemover, e que se sabe desenhar num quadro.
@invariant V.
*/Constrói uma forma dada uma posição.
class Forma {
public:
/**@pre V.
@post
posicao() =posicao.*/Destrutor virtual. Garante que os destrutores das classes derivada
Forma(Posicao const& posicao);
/**ssão invocados mesmo através dum ponteiro para a classe
Forma.@pre V.
*/Devolve a posição da forma.
virtual ~Forma() {}
/**@pre V.
@post
posicao= posição da forma.*/Desenha a forma num quadro.
Posicao posicao() const;
/**@pre V.
@post
quadroestá pintado na zona correspondente à forma.*/Desloca a forma para nova posição.
virtual void desenhaEm(Quadro& quadro) const;
/**@pre V.
@post
posicao() =nova_posicao.*/A posição da forma (um ponto particular, como um centro ou canto):
virtual void movePara(Posicao const& nova_posicao);
private:
//Indica se a condição invariante da classe se verifica.
Posicao posicao_;
/**@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;
}
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:
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
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ãoderivadas 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 classeFormaabstracta. */virtual void desenhaEm(Quadro& quadro) const = 0;
...
};
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:

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:
A implementação da operação
/**Representa um quadrado aberto com lados paralelos aos eixoscoordenados. Posição é o canto superior direito.
@invariant 0 <=
lado.*/Constrói novo quadrado dadas a posição do canto superior esquerdo
class Quadrado : public Forma {
public:
/**e a dimensão do lado (em colunas do quadro).
@pre V.
@post
posicao() =posicaoelado() =lado.*/
Quadrado(Posicao const& posicao, int lado);
/**Destrutor virtual. Garante que os destrutores das classesderivadas 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.*/Desenha o quadrado no quadro, compensando o facto dos píxeis serem
int lado() const;
/**rectangulares.
@pre V.
@post quadro está pintado na zona correspondente ao quadrado.
*/Dimensão do lado (em colunas):
virtual void desenhaEm(Quadro& quadro) const;
private:
//Indica se a condição invariante de classe se verifica.
int lado_;
/**@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());
...
}
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:
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
Quadrado quadrado(Posicao(10, 20), 7); //quadrado em (10, 20) com lado 7.
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.
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.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:

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:
util (usa rotinas genéricas, que serão
vistas mais tarde)
cor
posicao
dimensao
quadro
forma
quadrado
circunferência
teste
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)!
Recomenda-se a leitura do Capítulo 17 de [1].
[2] Bjarne Stroustrup, "The C++ Programming Language", 3ª edição, Addison-Wesley, Reading, Massachusetts, 1997.