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 em nome, morada 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 é parte de). Por exemplo, é claro que uma moeda (no sentido físico do termo, uma chapa metálica) tem uma nacionalidade, tal como tem um valor. Mas uma moeda, na mesma acepção, não é uma nacionalidade, tal como não é um valor. 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 liguagem 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.
A utilização de várias classes parece portanto ser melhor solução. Seja então a classe Empregado*:
class Empregado {A classe 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():
public:
Empregado(string const& nome, string const& morada, Sexo sexo)
: nome_(nnome), morada_(morada), sexo_(sexo) {
}
string nome() const {
return nome_;
}
string morada() const {
return morada_;
}
Sexo sexo() const {
return sexo_;
}
void mostra() const {
cout << "Nome: " << nome() << endl
<< "Morada: " << morada() << endl
<< "Sexo: " << sexo() << endl;
}
private:
string nome_;
string morada_;
Sexo sexo_;
};
class Chefe {Podem-se identificar pelo menos dois problemas nesta implementação. O primeiro é a repetição de código. Tudo o que consta na classe 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.).
public:
Chefe(string const& nome, string const& morada, Sexo sexo, int nível)
: nome_(nome), morada_(morada), sexo_(sexo), nível_(nível) {
}
string nome() const {
return nome_;
}
string morada() const {
return morada_;
}
Sexo sexo() const {
return sexo_;
}
int nível() const {
return nível_;
}
void mostra() const {
cout << "Nome: " << nome() << endl
<< "Morada: " << morada() << endl
<< "Sexo: " << sexo() << endl
<< "Nível: " << nível() << endl;
}
private:
string nome_;
string morada_;
Sexo sexo_;
int nível_;
};
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,
Chefe chefe("Zé", "Rua A", masculino, 2);é uma inicialização inválida (para o C++ as classes Empregado e Chefe não têm qualquer relação). O que se pretendia com esta inicialização era que a variável e ficasse com a informação do chefe como mero empregado, i.e., sem aquilo que é específico de um chefe.Empregado empregado = chefe;
A estas operações de retirar o que é específico de uma classe, chama-se em inglês slicing, um vez que tiram 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:
Chefe chefe("Zé", "Rua A", masculino, 2);Infelizmente, tal não é possível.Empregado* ponteiro_empregado = &chefe;
De igual forma seria desejável que referências para Empregados pudessem ser sinónimos de Chefes. Ou seja:
Chefe chefe("Zé", "Rua A", masculino, 2);Mais uma vez, tal ainda não é possível.Empregado& referência_empregado = c;
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 Empregadoinclui toda a informação sobre o chefe! Isto é, não há 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 pratico 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 numa única lista de ponteiros para Empregado. Por exemplo, admitindo que ListaPonteiroEmpregado é uma classe de lista em que os itens são do tipo Empregado*, o seguinte código seria possível:
ListaPonteiroEmpregado pessoal;Infelizmente, tal ainda não é possível.
// Inserção dos empregados:
pessoal.põeFim(new Empregado("Manela", "Rua B", feminino));pessoal.põeFim(new Chefe("Zé", "Rua A", masculino, 2));
...
// Visualização dos empregados:
for(ListaPonteiroEmpregado::Iterador i = pessoal.primeiro();
i != pessoal.fim();
++i)
(*i)->mostra();
* 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");
}
class Chefe {Esta é apenas uma solução parcial. Por um lado não se conseguiu evitar reescrever as funções membro nome(), morada() e sexo(), mesmo que muito simples e simplesmente delegando nas respectivas funções da classe Empregado. Por outro lado o acrescento à posteriori do número de contribuinte na classeEmpregado obrigaria a reescrever não só essa classe mas também a classe Chefe, para que esta possua uma função membro para lhe aceder, e todas as outras criadas à semelhança da classe Chefe: Motorista, Secretário, etc..
public:
Chefe(string const& nome, string const& morada, Sexo sexo, int nível)
: empregado(nome, morada, sexo), nível_(nível) {
}
string nome() const {
return empregado.nome();
}
string morada() const {
return empregado.morada();
}
Sexo sexo() const {
return empregado.sexo();
}
int nível() const {
return nível_;
}
void mostra() const {
empregado.mostra();
cout << "Nível: " << nível() << endl;
}
private:
Empregado empregado;
int nível_;
};
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 variáveis membro é mais indicada para representar relações tem um ou de composição (e.g., uma moeda tem um valor e uma nacionalidade, ou é composta por um nome e uma nacionalidade).
class Empregado {Esta solução obriga a alterar a classe Empregado, acrescentando um novo construtor, sempre que se cria uma nova especialização do conceito de empregado.
public:
Empregado(string const& nome, string const& morada, Sexo s)
: nome_(nome), morada_(morada), sexo_(sexo) {
}
Empregado(const Chefe& chefe)
: nome_(chefe.nome()), morada_(chefe.morada()), sexo_(chefe.sexo())
{
}
string nome() const {
return nome_;
}
string morada() const {
return morada_;
}
Sexo sexo() const {
return sexo_;
}
void mostra() const {
cout << "Nome: " << nome() << endl
<< "Morada: " << morada() << endl
<< "Sexo: " << sexo() << endl;
}
private:
string nome_;
string morada_;
Sexo sexo_;
};
Além disso, só introduz a possibilidade de slicing, que na realidade é pouco interessante. Não resolve o problema de criar uma lista de empregados que contenha empregados normais, chefes, secretárias, etc. Para isso teria de ser possível colocar endereços de chefes, etc. num ponteiro para Empregado.
class Chefe : public Empregado {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
public:
Chefe(string const& nome, string const& morada, Sexo sexo, int nível);
int nível() const {
return nível_;
}
void mostra() const;
private:
int nível_;
};
...cria uma nova classe D derivada das classes base A, B e C.
class D : public A, public B, public C {
...
};
A classe derivada herda todas os membros das classes base. No entanto, os membros privados da classe base não ficam acessíveis directamente às funções membro 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:
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:
Chefe chefe("Zé", "Rua A", masculino, 2);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 static_cast<tipo>():
Empregado empregado = chefe; // slicing, pouco útil.
Empregado* ponteiro_empregado = &chefe;
Empregado& referência_empregado = chefe;
Chefe* ponteiro_chefe = static_cast<Chefe*>(ponteiro_empregado);
Chefe& referência_chefe = static_cast<Chefe&>(referência_empregado);
inline Chefe::Chefe(string const& nome, string morada,Se não se invocar explicitamente o construtor de uma classe base, o compilador gerará automaticamente uma chamada ao construtor por omissão dessa classe. Claro que para isso deverá existir um construtor por omissão na classe base, de outro modo ocorrerá um erro de compilação.
Sexo sexo, int nível)
: Empregado(nome, morada, sexo), nível_(nível) {
}
A ordem pela qual as inicializações têm lugar é a que se segue:
class Z {são invocados, por ordem:
...
};
class D : A, B, C {
public:
D(int ii, const Z& zz)
: i(ii), z(zz), pi(new pi) {
for(int i = 0; i != dim; ++i)
m[i] = 0;
}
~D() {
delete pi;
}
...
private:
static const int dim = 10;
int i;
const Z z;
float m[dim];
int* pi;};
Como para qualquer outra classe, a linguagem tentará sempre fornecer automaticamente:
inline void Chefe::mostra() const {Repare-se que, como o procedimento com o mesmo nome da classe 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 chamar recursivamente o procedimento Chefe::mostra().
Empregado::mostra();
cout << "Nível: " << nível() << endl;
}
Dada a definição da classe, o seguinte código
Chefe chefe("Zé", "Rua A", masculino, 2);resulta em
chefe.mostra();
Nome: ZéPor outro lado, se se pretendesse mostrar o chefe como mero empregado, poder-se-ia usar
Morada: Rua A
Sexo: masculino
Nível: 2
chefe.Empregado::mostra();que resultaria em
Nome: Zé
Morada: Rua A
Sexo: masculino
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 {
...
};
ListaPonteiroEmpregado pessoal;O único problema é que, como o procedimento mostra() é invocado 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 apresenta a noção de polimorfismo e sua implementação em C++.
// Inserção dos empregados:
pessoal.põeFim(new Empregado("Manela", "Rua B", feminino));
pessoal.põeFim(new Chefe("Zé", "Rua A", masculino, 2));
...
// Visualização dos empregados:
for(ListaPEmpregado::Iterador i = pessoal.primeiro();
i != pessoal.fim();
++i)
(*i)->mostra();
Suponha-se que se pretende, mais uma vez, implementar o conceito de pilha de inteiros, na forma da classe PilhaInt, de novo sem quaisquer limitações quanto ao número de itens, excepto a memória disponível. Uma vez que a classe ListaInt está já desenvolvida, não impõe restrições quanto ao comprimento das listas, e possui todos os métodos necessários 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 PinhaInt funciona como uma ListaInt, mas...
Então uma primeira implementação das pilhas poderia ser:
class PilhaInt {Esta solução usa composição para representar a relação entre as duas classes, e as funções e procedimentos membro definidos delegam na classe ListaInt as suas tarefas.
public:
typedef ListaInt::Item Item;
void põe(Item item) {
lista.põeFim(item);
}
void tira() {
lista.tiraÚltimo();
}
Item topo() const {
return lista.últimoItem();
}
Item& topo() {
return lista.últimoItem();
}
bool vazia() const {
return lista.vazia();
}
int altura() const {
return lista.comprimento();
}
private:
ListaInt lista;
};
Um resultado muito semelhante pode ser obtido por herança privada:
class PilhaInt : private ListaInt {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 Item e vazia(). Eles são redefinidos apenas porque a herança foi feita de forma privada, e portanto o utilizador da classe PilhaInt de outra forma não tem acesso aos membros correspondentes da classe base. Nos outros casos, como os nomes das funções e procedimentos das listas usados directamente pela pilha não têm os nomes que se pretendiam, a definição de novas funções e procedimentos justifica-se, desde que sejam inline, para não incorrer nos custos da invocação de funções.
public:
typedef ListaInt::Item Item;
void põe(Item item) {
põeFim(item);
}
void tira() {
tiraÚltimo();
}
Item topo() const {
return últimoItem();
}
Item& topo() {
return últimoItem();
}
bool vazia() const {
return ListaInt::vazia();
}
int altura() const {
return comprimento();
}
};
Esta solução pode ser vista como um alteração da interface da classe ListaInt de modo a proporcionar 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 PilhaInt pode ser definida como:
class PilhaInt : private ListaInt {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:
public:
using ListaInt::Item;
void põe(Item item) {
põeFim(item);
}
void tira() {
tiraÚltimo();
}
Item topo() const {
return últimoItem();
}
Item& topo() {
return últimoItem();
}
using ListaInt::vazia;
int altura() const {
return comprimento();
}
};
class PilhaInt : private ListaInt {o que corresponde simplesmente a reduzir a interface da classe ListaInt de modo a proporcionar apenas os serviços de uma pilha.
public:
using ListaInt::Item;
using ListaInt::põeFim;
using ListaInt::tiraÚltimo;
using ListaInt::últimoItem;
using ListaInt::últimoItem;
using ListaInt::comprimento;
};
A herança privada deve ser usada com muita parcimónia, pois introduz uma ligação muito forte entre a classe PilhaInt e a classe ListaInt. 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.
class A {Mais tarde se estudarão as propriedades de uma terceira categoria de acesso: protected.
private:
int i;
public:
int u;
};class B : private A {
public:
void f() {
i = 1;// erro: i é privada de A.
u = 1;
}
};class C : public A {
public:
void f() {
i = 1;// erro: i é privada de A.
u = 1;
}
};void g() {
A a;
B b;
C c;
a.i = 1;// erro: i é privada de A.
a.u = 1;
b.i = 1;// erro: i é privada de A.
b.u = 1;// erro: u é privada de B embora seja públic de A (herança privada).
c.i = 1;// erro: i é privada de A.
c.u = 1;
a = b;// erro: um B não é um A (herança privada).
b = a;// erro: um A não é um B.
a = c;
c = a;// erro: um A não é um C.
}
2. Dada a classe Filme no módulo filme que se segue:
filme.H
#ifndef FILME_Hfilme_impl.H
#define FILME_H#include <iostream>
#include <string>class Filme {
public:
Filme(std::string const& nome, std::string const& realizador,
int duracao);
Filme(istream& entrada);std::string nome() const;
std::string realizador() const;
int duracao() const;void carrega(std::istream&);
// void guarda(std::ostream&);void mostra(std::ostream&);
// void le(std::istream&)private:
std::string nome_;
std::string realizador_;
int duracao_;
};#include "filme_impl.H"
#endif
inline Filme::Filme(std::string const& nome,filme.C
std::string const& realizador,
int duracao)
:nome_(nome), realizador_(realizador), duracao_(duracao) {
}inline Filme::Filme(std::istream& entrada) {
carrega(entrada);
}inline std::string Filme::nome() const {
return nome_;
}inline std::string Filme::realizador() const {
return realizador_;
}inline int Filme::duracao() const {
return duracao_;
}inline void Filme::mostra(std::ostream& saida) {
saida << "Nome:\t\t" << nome_ << std::endl
<< "Realizador:\t" << realizador_ << std::endl
<< "Duracao:\t" << duracao_ << std::endl;
}
#include "filme.H"e o programa:using namespace std;
void Filme::carrega(istream& entrada)
{
getline(entrada, nome_ );
getline(entrada, realizador_);
entrada >> duracao_;
char c;
while(entrada.get(c) && c != '\n')
;
}
teste.C
#include <iostream>construa as classes FilmeEstrangeiro e EdicaoDoRealizador por herança pública de Filme. A classe FilmeEstrangeiro deve ter informação sobre o país de origem e a língua em que o filme é falado. A classe EdicaoDoRealizador deve ter informação (uma string) sobre as alterações feitas ao original.
#include <fstream>using namespace std;
#include "filme.H"
int main()
{
ifstream entrada("filmes.txt");Filme filme(entrada);
filme.mostra(cout);
}
Ambas as classes devem ter procedimentos carrega() e mostra().
Altere o ficheiro teste.C de modo a que carregue de um ficheiro (usando o procedimento carrega()) um Filme, um FilmeEstrangeiro e uma EdicaoDoRealizador e os mostre no ecrã (usando o procedimento mostra()). Cada classe derivada apenas carrega e mostra a sua própria informação específica, delegando sempre que necessário nos procedimentos da classe base para carregar e mostrar a informação genérica relativa a um Filme.
3. Trabalhe no Problema 2 da avaliação.
# Existem 10 exemplares na biblioteca do ISCTE.