typedef int* Item;A operação i++ é um erro porque i é uma referência constante para um ponteiro. Assim, o ponteiro não pode ser alterado. Mas o seu conteúdo pode, daí que a operação (*i)++ seja totalmente válida.void f(const Item& i) {
i++; // erro!
(*i)++; // ok!
}void g(const int* p) {
f(p); // erro!
}
Finalmente, a invocação de f() a partir de g() é uma erro, dado que implica a inicialização de uma referência constante para int* (a referência i parâmetro de f()) a partir de um const int*, o que descarta o const, sendo esta operação proibida pelo C++!
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 duma 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 dum projecto, por exemplo, as tarefas 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.
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 variável membro adicional, à alteração do construtor, à criação da função membro nível() para aceder ao nível de chefia e à alteração do procedimento membro mostra():
std::string nome_;
std::string morada_;
Sexo sexo_;
public:
Empregado(std::string n, std::string m, Sexo s)
: nome_(n), morada_(m), sexo_(s) {
}
std::string nome() const {
return nome_;
}
std::string morada() const {
return morada_;
}
Sexo sexo() const {
return sexo_;
}
void mostra() const {
std::cout << "Nome: " << nome() << std::endl
<< "Morada: " << morada() << std::endl
<< "Sexo: " << sexo() << std::endl;
}
};
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.).
std::string nome_;
std::string morada_;
Sexo sexo_;
int nível_;
public:
Chefe(std::string n, std::string m, Sexo s, int ní)
: nome_(n), morada_(m), sexo_(s), nível_(ní) {
}
std::string nome() const {
return nome_;
}
std::string morada() const {
return morada_;
}
Sexo sexo() const {
return sexo_;
}
int nível() const {
return nível_;
}
void mostra() const {
std::cout << "Nome: " << nome() << std::endl
<< "Morada: " << morada() << std::endl
<< "Sexo: " << sexo() << std::endl
<< "Nível: " << nível() << std::endl;
}
};
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 c("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 dum chefe. A estas operações de retirar o que é específico duma classe, chama-se em inglês "slicing", um vez que tiram apenas as "fatias" de informação relevantes.Empregado e = c;
Seria também desejável que se pudesse colocar o endereço duma instância da classe Chefe num ponteiro para Empregado. Ou seja:
Chefe c("Zé", "Rua A", masculino, 2);De igual forma seria desejável que referências para Empregados pudessem ser sinónimos de Chefes. Ou seja:Empregado* pe = &c;
Chefe c("Zé", "Rua A", masculino, 2);Note-se que nestes dois casos, ao contrário da inicialização dum 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á "slicing".Empregado& re = c;
Este comportamento permitiria, por exemplo, guardar todo o pessoal da empresa numa única lista de ponteiros para Empregado. Por exemplo, admitindo que ListaPEmpregado é uma classe de lista em que os itens são do tipo Empregado*, o seguinte código seria possível:
ListaPEmpregado pessoal;
// Inserção dos empregados:
pessoal.põeFim(new Empregado("Manela", "Rua B", feminino));pessoal.põeFim(new Chefe("Zé", "Rua A", masculino, 2));
...
// Visualização dos empregados:
for(ListaPEmpregado::Iterador i = pessoal.primeiro();
i != pessoal.fim();
++i)
i.item()->mostra();
* Assume-se definido um tipo enumerado
Sexo
e respectivo operador de escrita num canal:
enum Sexo {masculino, feminino};
inline std::ostream& operator << (std::ostream& saída, Sexo s) {
return saída << (s == masculino? "masculino" : "feminino");
}
class Chefe {Esta é apenas uma solução parcial porque por um lado não se conseguiu evitar reescrever as funções membro nome(), morada() e sexo(), e por outro lado o acrescento à posteriori do número de contribuinte na classe Empregado obrigará a reescrever não só essa classe mas também a classe Chefe, para que esta possua uma função membro para lhe aceder.
Empregado e;
int nível_;
public:
Chefe(std::string n, std::string m, Sexo s, int ní)
: e(n, m, s), nível_(ní) {
}
std::string nome() const {
return e.nome();
}
std::string morada() const {
return e.morada();
}
Sexo sexo() const {
return e.sexo();
}
int nível() const {
return nível_;
}
void mostra() const {
e.mostra();
std::cout << "Nível: " << nível() << std::endl;
}
};
Este tipo de solução não é pois o mais indicado para representar relações é um, embora o seja para representar relações tem um.
class Empregado {Mas 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. Além disso, não resolve o problema de criar uma lista de empregados que contenha empregados normais, chefes, secretárias, etc.
std::string nome_;
std::string morada_;
Sexo sexo_;
public:
Empregado(std::string n, std::string m, Sexo s)
: nome_(n), morada_(m), sexo_(s) {
}
Empregado(const Chefe& c)
: nome_(c.nome()), morada_(c.morada()), sexo_(c.sexo()) {
}
std::string nome() const {
return nome_;
}
std::string morada() const {
return morada_;
}
Sexo sexo() const {
return sexo_;
}
void mostra() const {
std::cout << "Nome: " << nome() << std::endl
<< "Morada: " << morada() << std::endl
<< "Sexo: " << sexo() << std::endl;
}
};
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
int nível_;
public:
Chefe(std::string n, std::string m, Sexo s, int ní);
int nível() const {
return nível_;
}
void mostra() const;
};
...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. Isto deve-se a que proceder de outro modo "abriria a porta" da parte privada duma classe a quem quer que definisse uma classe sua derivada, violando assim o princípio do encapsulamento.
A herança ou derivação fez-se usando a palavra chave public. Isso significa que:
Chefe c("Zé", "Rua A", masculino, 2);No entanto, se se souber, como no exemplo acima, que um ponteiro ou referência duma classe base de facto referenciam um objecto (ou instância) duma dada classe derivada, é possível fazer atribuições no sentido inverso, embora apenas por intermédio do operador static_cast<tipo>():
Empregado e = c;
Empregado* pe = &c;
Empregado& re = c;
Chefe* pc = static_cast<Chefe*>(pe);
Chefe& rc = static_cast<Chefe&>(re);
inline Chefe::Chefe(std::string n, std::string m,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 s, int ní)
: Empregado(n, m, s), nível_(ní) {
}
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 {
static const int dim = 10;
int i;
const Z z;
float m[dim];
int* pi;
public:
D(int ii, const Z& zz) : i(ii), z(zz) {
for(int i = 0; i != dim; ++i)
m[i] = 0;
pi = new int;
}
~D() {
delete pi;
}
...
};
Como para qualquer outra classe, o compilador tentará sempre criar automaticamente um construtor por omissão (se não se tiver definido qualquer construtor na classe), que invoca os construtores por omissão de todas as classe base, um construtor por cópia (desde que não seja definido explicitamente na classe), que invoca os construtores por cópia das classe base, e um operador de atribuição por cópia (desde que não seja definido explicitamente na classe), que invocará os operadores de atribuição por cópia das classes base.
De igual forma é criado automaticamente um destrutor (desde que não seja definido explicitamente) que invoca os destrutores das classes base. A ordem pela qual as operações ocorrem é a seguinte:
void Chefe::mostra() const {Repare-se que, como a versão original do procedimento já fazia parte do trabalho, a primeira operação do procedimento que se lhe sobrepôs é invocar a versão original. Para o fazer 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.
Empregado::mostra();
std::cout << "Nível: " << nível() << std::endl;
}
Dada a definição da classe, o seguinte código
Chefe c("Zé", "Rua A", masculino, 2);resulta em
c.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
c.Empregado::mostra();que resultaria em
Nome: Zé* O tipo de devolução pode ser diferente, mas tem de obedecer a algumas regras. Ver [2, pág. 425].
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 {
...
};
ListaPEmpregado pessoal;O único problema é que, como o procedimento mostra() é executado a partir dum ponteiro para Empregado, todos os empregados são mostrados como empregados básicos, apesar de entre eles haver empregados que são chefes, secretárias, ou motoristas. A solução para este problema será vista na Aula 8.
// Inserção dos empregados:
pessoal.põeFim(new Empregado("Manela", "Rua B", feminino));
pessoal.põeFim(new Chefe("Zé", "Rua A", masculino, 2));
...
// Visualização dos empregados:
for(ListaPEmpregado::Iterador i = pessoal.primeiro();
i != pessoal.fim();
++i)
i.item()->mostra();
Suponha-se que se pretende, mais uma vez, implementar o conceito de pilha de inteiros, na forma da classe PilhaInt, mas agora 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ões 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.
Admita-se que a classe ListaInt, definida na Aula 5, Secção 1.3, possuia os seguintes métodos adicionais:
class PilhaInt {Mas um resultado muito semelhante poderia ser obtido por herança privada:
ListaInt l;
public:
typedef ListaInt::Item Item;
void põe(Item item) {
l.põeFim(item);
}
void tira() {
l.tiraÚltimo();
}
Item topo() const {
return l.trás();
}
Item& topo() {
return l.trás();
}
bool vazia() const {
return l.vazia();
}
int altura() const {
return l.comprimento();
}
};
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 sobrepostos às definições originais apenas porque a herança foi feita de forma privada, e portanto o utilizador da classe PilhaInt de outra forma não teria acesso a esses membros. 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 trás();
}
Item& topo() {
return trás();
}
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 duma forma privada, basta usar uma declaração de utilização do respectivo nome na parte pública da classe derivada. Assim, a classe PilhaInt poderia ser definida como:
class PilhaInt : private ListaInt {
public:
using ListaInt::Item;
void põe(Item item) {
põeFim(item);
}
void tira() {
tiraÚltimo();
}
Item topo() const {
return trás();
}
Item& topo() {
return trás();
}
using ListaInt::vazia;
int altura() const {
return comprimento();
}
};
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 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::trás;
using ListaInt::vazia;
using ListaInt::comprimento;
};
class A {Mais tarde se estudarão as propriedades de uma terceira classe 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.i = 1;// erro: i é privada de B (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] Bjarne Stroustrup, "The C++ Programming Language", 3ª edição, Addison-Wesley, Reading, Massachusetts, 1997. *