Suponha-se que se pretendia construir uma aplicação para gestão do pessoal duma empresa. Uma primeira análise do problema poderia levar à identificação de várias classes de empregado: empregados normais, secretários, chefes, motoristas, chefes dos motoristas, etc. A mesma análise poderia indicar que a informação a guardar para um empregado normal consistia, para simplificar, apenas em nome e sexo. Todas as outras classes de empregados possuiriam essa mesma informação adicionada de informação específica. Por exemplo, para os secretários seria necessário guardar uma referência para o respectivo chefe, para os motoristas uma referência para os veículos à sua guarda, para os chefes informação sobre o nível de chefia e sobre o respectivo departamento, incluindo por exemplo uma lista de referências para os respectivos subordinados. A análise de problemas desta índole leva também frequentemente à conclusão de que as várias classes identificadas estão relacionadas entre si. Por exemplo, um chefe é um empregado (embora nem todos os empregados sejam chefes), um secretário é um empregado, um motorista é um empregado, um chefe dos motoristas é um motorista e é um chefe também, etc.
Esta relação é um é extremamente importante e deve ser claramente distinguida da relação tem um (ou é composto por um). Por exemplo, é claro que um humano é composto, entre outras partes, por um coração, e pode ter, por exemplo, um anel. Mas um humano, na mesma acepção, não é um coração, tal como não é um anel. O mesmo se passa, por exemplo, com uma turma de uma universidade. Uma turma tem um conjunto de alunos, tal como tem um nome, um delegado, etc. Uma turma não é simplesmente um conjunto (ou lista) de alunos. Esta diferença deve ser estabelecida mesmo em casos em que as classes identificadas parecem ter apenas um item de informação. No âmbito de um projecto, por exemplo, as tarefas a realizar podem consistir inicialmente apenas numa duração. Mas isso não significa que uma tarefa é uma duração. Uma tarefa tem uma duração, tal como poderá vir a ter outros tipos de informação no futuro.
A relação é um que se referiu é na
realidade apenas a forma habitual de referir uma relação
mais particular: pode substituir um ou pode ser tratado como
um. É que, na linguagem corrente, também se usa
a expressão "é um" para denotar a pertença a uma classe.
Por exemplo, pode-se dizer que o Sr. Fulano é um chefe. Em
C++ isso significaria que fulano seria uma instância da
classe Chefe. Mas a relação é um
a que se alude nesta secção é uma relação
entre classes, e não entre objectos e as respectivas classes!
Assim, é mais preciso dizer que "um chefe pode substituir um
empregado" ou que "um chefe pode se tratado como um empregado".
Normalmente não se usam estas expressões, mas apenas porque
elas são demasiado longas e pesadas.
Como se representam relações é um em C++? O que se ganha com esse tipo de relação? É o assunto da próxima secção.
Empregado, admitindo-se que empregados
com nível de chefia zero (0) são empregados normais.Empregado.
Além disso, a escrita de uma simples operação para
mostrar a informação de um empregado no ecrã, exigiria
uma lógica de programação que, não sendo difícil,
levaria a código difícil de perceber e alterar: consoante se trate
de um empregado normal ou um chefe, diferente informação
necessita ser mostrada. Além disso, o reconhecimento de que no pessoal da empresa
existe um tipo adicional de empregado, não previsto inicialmente,
levaria forçosamente à reescrita total da classe Empregado,
complicando ainda mais todas as suas operações.
A utilização de várias classes parece portanto
ser melhor solução. Seja então a classe Empregado*:
A classe
class Empregado {public:Empregado(string const& nome, Sexo sexo);
string const& nome() const;Sexo sexo() const;void mostra() const;
private:string nome_;Sexo sexo_;};
inline Empregado::Empregado(string const& nome, Sexo sexo): nome_(nome), sexo_(sexo)
{}
inlinestring const&Empregado::nome() const
{return nome_;}
inlineSexoEmpregado::sexo() const
{return sexo_;}
inlinevoidEmpregado::mostra() const
{cout << "Nome: " << nome() << endl<< "Sexo: " << sexo() << endl;}
Chefe, por outro lado, acrescenta o nível de chefia
como informação adicional, o que obriga à definição
de uma nova variável membro, à alteração do
construtor, à criação da função membro
nível()
para inspeccionar o nível de chefia e à alteração
do procedimento membro mostra():
Podem-se identificar pelo menos dois problemas nesta implementação. O primeiro é a repetição de código. Tudo o que consta na classe
class Chefe {public:Chefe(string const& nome, Sexo sexo, int nível);
string const& nome() const;Sexo sexo() const;int nível() const;void mostra() const;
private:string nome_;Sexo sexo_;int nível_;};
inline Chefe::Chefe(string const& nome, Sexo sexo, int nível): nome_(nome), sexo_(sexo), nível_(nível)
{}
inlinestring const&Chefe::nome() const
{return nome_;}
inlineSexoChefe::sexo() const
{return sexo_;}
inlineintChefe::nível() const
{return nível_;}
inlinevoidChefe::mostra() const
{cout << "Nome: " << nome() << endl<< "Sexo: " << sexo() << endl<< "Nível: " << nível() << endl;}
Empregado consta também na classe
Chefe.
Isto, para além do desperdício de esforço, representa
também dificuldades acrescidas de manutenção.
Por exemplo, se se identificar à posteriori que o número
de contribuinte é informação relevante para todos
os empregados, então ter-se-á de alterar não só
a classe empregado mas também em todas as classes relacionadas (chefes,
secretárias, motoristas, etc.).
O segundo problema é que não há qualquer tipo de relação, do ponto de vista da linguagem C++, entre as duas classes definidas. Ou seja, não é possível tratar um chefe como se de um empregado apenas se tratasse. Ou seja,
é uma inicialização inválida (para o C++ as classes
Chefe o_zé("Zé", masculino, 2);
Empregado outro_zé_como_mero_empregado = o_zé;
Empregado e Chefe não têm qualquer
relação). O que se pretendia com esta inicialização
era que a variável outro_zé_como_mero_empregado ficasse com a informação
d'o_zé como mero empregado, i.e., sem aquilo que é específico
de um chefe.A estas operações de retirar o que é específico
de uma classe, chama-se em inglês slicing, e chamar-se-há aqui corte,
uma vez que cortam informação deixando apenas as "fatias" de informação relevantes. Esta operação
é menos útil do que parece à primeira vista (na realidade
até pode ser perigosa). Bastante mais útil seria que
se pudesse colocar o endereço de uma instância ou objecto
da classe Chefe num ponteiro para Empregado. Ou
seja:
Infelizmente, tal também não é possível.
Chefe o_zé("Zé", masculino, 2);Empregado* ponteiro_para_o_zé_visto_como_mero_empregado = &o_zé;
De igual forma seria desejável que referências para Empregados
pudessem ser sinónimos de Chefes. Ou seja:
Mais uma vez, tal ainda não é possível.
Chefe o_zé("Zé", masculino, 2);Empregado& também_sou_o_zé_mas_visto_como_mero_empregado = o_zé;
Note-se que
nestes dois casos, ao contrário da inicialização
de um empregado a partir de um chefe, a informação acessível
através do ponteiro ou referência para Empregado inclui
toda a informação sobre o chefe! Isto é,
não há corte (slicing). No primeiro caso existiam dois
objectos: um Chefe e um Empregado, que funcionava
como um sósia despromovido do Chefe (daí o pouco
interesse prático do slicing). Nos últimos casos existe
apenas um objecto, da classe Chefe, que é acessível
através de um ponteiro ou de uma referência.
Este comportamento permitiria, por exemplo, guardar todo o pessoal da
empresa através de um única lista de ponteiros para Empregado.
Por exemplo,
o seguinte código seria possível:
Infelizmente, tal ainda não é possível.
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();
* Assume-se definido um tipo enumerado
Sexo
e respectivo operador de escrita num canal:
enum Sexo {masculino, feminino};
inline ostream& operator << (ostream& saída, Sexo s)
{return saída << (s == masculino? "masculino" : "feminino");}
Chefe à custa da classe Empregado
usando uma variável membro de instância dessa classe.
Ou seja:
Esta é apenas uma solução parcial. Por um lado não se conseguiu evitar rescrever os inspectores
class Chefe {public:Chefe(string const& nome, Sexo sexo, int nível);
string const& nome() const;Sexo sexo() const;int nível() const;void mostra() const;
private:Empregado empregado;int nível_;};
inline Chefe::Chefe(string const& nome, Sexo sexo, int nível): empregado(nome, sexo), nível_(nível)
{}
inlinestring const&Chefe::nome() const
{return empregado.nome();}
inlineSexoChefe::sexo() const
{return empregado.sexo();}
inlineintChefe::nível() const
{return nível_;}
inlinevoidChefe::mostra() const
{empregado.mostra();cout << "Nível: " << nível() << endl;}
nome() e sexo(), mesmo que duma forma muito simples e que se limita a
delegar no respectivo inspector da classe Empregado. Por
outro lado, o acrescento à posteriori do número de contribuinte
na classe Empregado obrigaria a rescrever não só
essa classe mas também a classe
Chefe, para que esta possuisse o respectivo inspector, o mesmo
acontecendo a todas as outras classes criadas
à semelhança da classe Chefe: Motorista,
Secretário,
etc..Este tipo de solução não é, pois, o mais indicado para representar a relação é um existente entre chefes e empregados. A utilização de atributos é mais indicada para representar relações de agregação (tem um) ou, melhor ainda, de composição (é composto por um). Acontece que um chefe não tem um empregado (bom, pelo menos se formos precisos com a linguagem, pois um chefe pode chefiar vários empregados), nem muito menos é composto por um empregado (a teoria do homúnculo há muito que foi abandonada por absurda).
Empregado aceitando
um Chefe como argumento. Por exemplo:
Esta solução obriga a alterar a classe
class Empregado {public:Empregado(string const& nome, Sexo s);Empregado(Chefe const& chefe);
string const& nome() const;Sexo sexo() const;void mostra() const;
private:string nome_;Sexo sexo_;};
inline Empregado::Empregado(Chefe const& chefe): nome_(chefe.nome()), sexo_(chefe.sexo()){}
Empregado,
acrescentando um novo construtor, sempre que se cria uma nova especialização
do conceito de empregado. Além disso, só introduz a possibilidade de
corte (slicing),
que na realidade é pouco interessante e mesmo perigosa. Não resolve
o problema de criar uma lista de ponteiros para empregados que contenha
endereços de empregados normais,
chefes, secretárias, etc. Para isso teria de ser possível
colocar endereços de chefes, etc. em ponteiros para a classe Empregado.
Chefe pode ser derivada a partir da
classe Empregado da seguinte forma:
A relação é um introduzida pela derivação acima pode ser representada pelo seguinte diagrama:
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):...
{...
}
inline int Chefe::nível() const
{return nível_;}
inline void Chefe::mostra() const
{...
}

A sintaxe para definir classes por derivação corresponde
a colocar : após o nome da classe derivada seguida de uma lista
de classes base, que podem ser em número arbitrário.
Por exemplo, a definição
cria uma nova classe...
class D : public A, public B, public C {...};
D derivada das classes base A, B
e C.A classe derivada herda todos os membros das classes base. No entanto, os membros privados da classe base não ficam acessíveis directamente a partir dos métodos da classe derivada: se não fosse assim, estar-se-ia a "abrir a porta" da parte privada de uma classe a quem quer que definisse uma classe sua derivada, violando-se com isso o princípio do encapsulamento.
A herança ou derivação fez-se usando a palavra
chave public. Isso significa que:
nome()
e sexo()). Este facto resolve o primeiro problema identificado,
pois acrescentar o número do contribuinte e respectivo inspector na classe Empregado leva à sua inclusão automática
na classe Chefe.Empregado.A derivação poderia ter sido feita também usando
a palavra chave private (que é o tipo de herança
por omissão para as classes, sendo a herança pública
por omissão para as estruturas). Nesse caso:
No entanto, se se souber, como no exemplo acima, que um ponteiro ou referência de uma classe base de facto referenciam um objecto (ou instância) de uma dada classe derivada, é possível fazer atribuições no sentido inverso, embora apenas por intermédio do operador
Chefe o_zé("Zé", masculino, 2);Empregado um_novo_zé_mero_empregado = o_zé; // corte (slicing), pouco útil.Empregado* ponteiro_para_o_zé_como_empregado = &o_zé;Empregado& também_sou_o_zé_mas_visto_como_mero_empregado = o_zé;
static_cast<tipo>():
Chefe* ponteiro_para_o_zé_agora_como_chefe =
static_cast<Chefe*>(ponteiro_para_o_zé_como_empregado);Chefe& também_sou_o_zé =
static_cast<Chefe&>(também_sou_o_zé_mas_visto_como_mero_empregado);
Raramente tais conversões são úteis. Aliás, normalmente são sinal de fraco desenho das classes usadas para resolver o problema.
Se não se invocar explicitamente o construtor de uma classe base, o compilador gerará automaticamente uma invocação ao construtor por omissão dessa classe. Para isso deverá existir um construtor por omissão na classe base, de outro modo ocorrerá um erro de compilação.
inline Chefe::Chefe(string const& nome, Sexo sexo, int nível): Empregado(nome, sexo), nível_(nível)
{}
A ordem pela qual as inicializações têm lugar num construtor é a que se segue:
D que
se segue
são invocados, por ordem:
class Z {...};
class D : A, B, C {public:D(int i, Z const& z);~D();
...
private:static int const dim = 10;int i;Z const z;float m[dim];int* pi;
};
D::D(int i, Z const& z): i(i), z(z), pi(new int(10))
{for(int i = 0; i != dim; ++i)m[i] = 0;}
D::~D()
{delete pi;}
A, para inicialização dos atributos
herdados de A.B, para inicialização dos atributos
herdados de B.C, para inicialização dos atributos
herdados de C.int, para inicialização do atributo i.Z, para inicialização do atributo constante
z.pi, para inicialização do atributo pi com o endereço de uma nova variável dinâmica.m.m.dim é de classe, e não
de instância, pelo que é inicializada no início do
programa. Recorda-se que uma variável ou constante diz-se
membro de classe se for partilhada por todas as instâncias
dessa classe.
Como para qualquer outra classe, a linguagem tentará sempre fornecer implicitamente:
D invoca por ordem:
pi.m e pi, não
têm quaisquer destrutores invocados. Um ponteiro não tem destrutor
qualquer que seja o tipo apontado. Uma matriz tem um destrutor que
consiste em destruir cada um dos seus membros, pela ordem inversa à da
construção, excepto quando, como neste caso, os elementos da matriz são de
tipos básicos do C++.Z, para a constante membro z.C, para os atributos herdados de C.B, para os atributos herdados de B.A, para os atributos herdados de A.mostra() para a classe Chefe:
Repare-se que, como o procedimento com o mesmo nome da classe
inline void Chefe::mostra() const
{Empregado::mostra();cout << "Nível: " << nível() << endl;}
Empregado
faz parte daquilo que se pretende fazer, a primeira instrução
do procedimento é a sua invocação. Para isso
foi necessário preceder o nome do procedimento de Empregado::
(operador de resolução de âmbito), de modo a ficar
claro que é a versão original que deve ser executada.
De outro modo estar-se a invocar recursivamente a operação Chefe::mostra().
Dada a definição da classe, o seguinte código
resulta em
Chefe chefe("Zé", masculino, 2);chefe.mostra();
Por outro lado, se se pretendesse mostrar o chefe como mero empregado, poder-se-ia usar
Nome: ZéSexo: masculinoNível: 2
que resultaria em
chefe.Empregado::mostra();
Nome: ZéSexo: masculino
Ou seja, é possível invocar uma operação de uma classe base, mesmo que oculta.
class SerVivo {...};class Animal /*reino*/ : public SerVivo {...};class Vegetal /*reino*/ : public SerVivo {...};class Cordado /*filo*/ : public Animal {...};class Vertebrado /*subfilo*/ : public Cordado {...};class Mamífero /*classe*/ : public Vertebrado {...};class Primata /*ordem*/ : public Mamífero {...};class Hominídeo /*família*/ : public Primata {...};class Homo /*género*/ : public Hominídeo {...};class Homo_sapiens /*espécie*/ : public Homo {...};class Homo_sapien_sapiens /*subespécie*/: public Homo_sapiens {...};
Na figura que se segue pode-se ver um possível diagrama para as relações entre classes de veículos das forças armadas:

São subclasses de uma classe todas as classes que
dela derivem, directa ou indirectamente. São superclasses de uma
classe as classes de que ela derive, directa ou indirectamente. Assim, as
subclasses de VeículoAéreo no diagrama acima são Helicóptero, Avião
e Hidroavião.
Fica como exercício para o leitor desenhar o diagrama do primeiro exemplo e o esqueleto da definição em C++ das classes do segundo exemplo.
Chefe
derivada publicamente da classe Empregado, torna-se possível
o código que antes era inválido:
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();
A situação é ilustrada pelo seguinte diagrama:

O único problema é que, como a operação mostra()
é invocada através de um ponteiro para Empregado,
todos os empregados são mostrados como simples empregados, apesar
de entre eles haver empregados que são chefes, secretárias,
ou motoristas. A solução para este problema será
vista na próxima aula, onde se apresentam as noções de
polimorfismo, ligação estática e dinâmica e sua implementação em C++.
Suponha-se que se pretende, mais uma vez, implementar o conceito de
pilha de inteiros, na forma da classe PilhaDeInt, de novo sem quaisquer
limitações quanto ao número de itens, excepto as impostas
pela memória
disponível e pelos limites do tipo int. Uma vez que a classe ListaDeInt está
já desenvolvida, não impõe restrições
quanto ao comprimento das listas, e possui todas as operações necessárias
para implementar uma pilha, é natural pensar em implementar o conceito
de pilha com base na implementação das listas já existentes.
Neste caso a relação que existe entre as duas classes é:
a classe
PinhaIntfunciona como umaListaDeInt, mas apenas permite...
Então, uma primeira implementação das pilhas poderia ser:
Esta solução usa composição para representar a relação entre as duas classes, e os métodos definidos delegam na classe
class PilhaDeInt {public:typedef ListaDeInt::Item Item;
int altura() const;bool estáVazia() const;Item const& topo() const;
Item& topo();void põe(Item const& novo_item);void tiraItem();
private:ListaDeInt lista;};
int PilhaDeInt:: altura() const
{return lista.comprimento();}
bool PilhaDeInt::estáVazia() const
{return lista.estáVazia();}
Item const& PilhaDeInt:: topo() const
{return lista.trás();}
Item& PilhaDeInt::topo()
{return lista.trás();}
void PilhaDeInt::põe(Item const& novo_item)
{lista.põeAtrás(novo_item);}
void PilhaDeInt::tiraItem()
{lista.tiraDeTrás();}
ListaDeInt as
suas tarefas.
Um resultado muito semelhante pode ser obtido por herança privada:
Esta solução é porventura demasiado parecida com a anterior para se perceberem as vantagens imediatamente. Mas observe-se o que se passa com os membros
class PilhaDeInt : private ListaDeInt {public:typedef ListaDeInt::Item Item;
int altura() const;bool estáVazia() const;Item const& topo() const;
Item& topo();void põe(Item const& novo_item);void tiraItem();};
int PilhaDeInt::altura() const
{return comprimento();}
bool PilhaDeInt::estáVazia() const
{return ListaDeInt::estáVazia();}
Item const& PilhaDeInt::topo() const
{return trás();}
Item& PilhaDeInt::topo()
{return trás();}
void PilhaDeInt::põe(Item const& novo_item)
{põeAtrás(novo_item);}
void PilhaDeInt::tiraItem()
{tiraDeTrás();}
Item e estáVazia(). Eles
são redefinidos apenas porque a herança foi feita de forma
privada, e portanto o utilizador da classe PilhaDeInt de outra forma
não tem acesso aos membros correspondentes da classe base.
Nos outros casos, como os nomes das operações das listas usados directamente pela pilha não têm os nomes
que se pretendiam, a definição de novas operações justifica-se, desde que sejam inline, para não
incorrer nos custos da invocação de rotinas.
Esta solução pode ser vista como uma alteração
da interface da classe ListaDeInt de modo a proporcionar apenas os
serviços de uma pilha.
O C++ proporciona uma forma simples de abrir excepções
quanto ao tipo de herança. Se se pretender tornar públicos
membros cuja herança foi feita de uma forma privada, basta usar
uma declaração de utilização do respectivo
nome na parte pública da classe derivada. Assim, a classe PilhaDeInt
pode ser definida como:
Note-se que usar herança pública neste caso seria o mesmo que afirmar que um pilha é uma lista, o que manifestamente não é verdade. Note-se ainda que, se se considerarem os nomes usados na lista como apropriados para a pilha, pode-se fazer simplesmente:
class PilhaDeInt : private ListaDeInt {public:using ListaDeInt::Item;
int altura() const;
using ListaDeInt::estáVazia;
Item const& topo() const;
Item& topo();void põe(Item item);void tiraItem();};
int PilhaDeInt::altura() const
{return comprimento();}
Item const& PilhaDeInt::topo() const
{return trás();}
Item& PilhaDeInt::topo()
{return trás();}
void PilhaDeInt::põe(Item item)
{põeAtrás(item);}
void PilhaDeInt::tiraItem()
{tiraDeTrás();}
o que corresponde simplesmente a reduzir a interface da classe
class PilhaDeInt : private ListaDeInt {public:using ListaDeInt::Item;
using ListaDeInt::comprimento;using ListaDeInt::estáVazia;using ListaDeInt::trás;
using ListaDeInt::põeAtrás;using ListaDeInt::tiraDeTrás;};
ListaDeInt
de modo a proporcionar apenas os serviços de uma pilha.A herança privada deve ser usada com muita parcimónia,
pois introduz uma ligação muito forte entre a classe PilhaDeInt
e a classe ListaDeInt. Regra geral, portanto, deve-se usar a composição
para este tipo de relações e usar herança pública
para representar relações é um.
Mais tarde se estudarão as propriedades de uma terceira categoria de acesso:
class A {private:int i;public:int u;};
class B : private A {public:void f();};
void B::f()
{i = 1; //erro:ié privada deA.u = 1;}
class C : public A {public:void f();};
void C::f()
{i = 1; //erro:ié privada deA.u = 1;}
void g() {A a;B b;C c;
a.i = 1; //erro:ié privada deA.a.u = 1;b.i = 1; //erro:ié privada deA.b.u = 1; //erro:ué privada deBembora seja pública deA(herança privada).c.i = 1; //erro:ié privada deA.c.u = 1;
a = b; //erro: umBnão é umA(herança privada).b = a; //erro: umAnão é umB.a = c;c = a; //erro: umAnão é umC.}
protected.Recomenda-se a leitura do Capítulo 14 de [1].
[1] Michael Main e Walter Savitch, "Data Structures and Other Objects Using C++", Addison-Wesley, Reading Massachusetts, 1997.