Aula 16 - Relações entre classes
Conceitos-Chave:
- generalização, relação entre classes, onde existe uma classe-mãe (geral) e outras mais específicas, que herdam os membros da classe-mãe (classes-filhas). Relação "is a" ("é um")
- protected, palavra-chave que actua na acessibilidade dos membros de uma classe. Não podem ser acedidos fora desta classe (à semelhança dos membros privados), com a excepção do caso de associação, em que as classes-filhas podem aceder aos membros protegidos da classe mãe
- agregação, relação entre classes, onde uma classe tem como membro um objecto de outra classe. Implica uma relação "has a" ("tem um").
- friend, uma classe que seja "friend" (amiga) de outra pode aceder aos seus membros privados
- polimorfismo, é a abilidade de objectos de diferentes tipos (classes) poderem responder a métodos com o mesmo nome, cada um deles tendo um comportamento específico para cada tipo (classe) (definição da Wikipedia Inglesa)
Relações entre classes
Em grandes projectos (como um jogo) começa-se por planear o código. Esta planificação é feita ao nível da escolha das classes e da escolha das relações entre elas.
O paradigma OOP (Programação Orientada a Objectos) prevê dois grandes tipos de relações entre classes:
- Generalização
- Agregação
Esta aula tentará explicar o uso e importância destas relações.
Generalização (através da Herança)
Imaginemos que estamos a desenvolver um RPG e que existem diferentes raças que, embora com as suas características individuais, partilham entre si várias características. Neste caso poderiamos criar uma classe geral, chamada "Ser", e várias classes mais específicas. Ao criarmos uma generalização estaremos a implicar uma relação "is a" ("é um", por exemplo, Humano é um Ser) Imaginem que esta é a classe Ser:
class Ser
{
public:
void mover(); //todos os seres (animais) se movem
void dormir(); //todos dormem
void falar() { cout << "... Parece que nao fala" << endl; }
/* Se uma determinada raça não falar aparecerá esta mensagem... verão já a
seguir o seu uso */
private:
int hp; //Health Points ou Pontos de Saúde
};
Ok, sei que é uma representação um pouco fraca, mas ando com falta de ideias :)
O que é importante nesta classe é que será ela a geral, através da qual todas as outras a seguir expostas vão ser generalizadas.
Agora alguns exemplos possíveis de classes generalizadas da classe Ser:
class Humano : public Ser //classe Humano, derivada da classe Ser
{
public:
//mover e dormir já estão na geral
void falar() { cout << "Chamaste?" << endl; }
/* Como podem vêr, embora já exista um método "falar" na classe geral,
este não afecta este método */
void atacar();
private:
int forca; // a forca do personagem.
};
class Magico : public Ser
{
public:
void falar() {
cout << "Em que podem as forcas dos meus feiticos te ajudar?" << endl;
}
void lancarFeitico();
private:
int mp; // Magic Points ou Pontos de Magia
int magia; //a força da magia da personagem
};
class Orc : public Ser
{
public:
void atacar();
private:
int forca; // força com que o Orc ataca.
};
Vêm como é facil generalizar? A única coisa que têm de fazer é acrescentar duas palavras de código:
class ClasseGeneralizada : public ClasseGeral
Vejam agora um exemplo do uso das classes acima (programa compilável):
#include <iostream>
using namespace std;
class Ser
{
public:
void mover(); //todos os seres (animais) se movem
void dormir(); //todos dormem
void falar() { cout << "... Parece que nao fala" << endl; }
/* Se uma determinada raça não falar aparecerá esta mensagem... verão já a
seguir o seu uso */
private:
int hp; //Health Points ou Pontos de Saúde
};
class Humano : public Ser //classe Humano, derivada da classe Ser
{
public:
//mover e dormir já estão na geral
void falar() { cout << "Chamaste?" << endl; }
/* Como podem vêr, embora já exista um método "falar" na classe geral,
este não afecta este método */
void atacar();
private:
int forca; // a forca do personagem.
};
class Magico : public Ser
{
public:
void falar() {
cout << "Em que podem as forcas dos meus feiticos te ajudar?" << endl;
}
void lancarFeitico();
private:
int mp; // Magic Points ou Pontos de Magia
int magia; //a força da magia da personagem
};
class Orc : public Ser
{
public:
void atacar();
private:
int forca; // força com que o Orc ataca.
};
int main()
{
Humano hum;
Magico mag;
Orc orc;
hum.falar();
mag.falar();
orc.falar();
cin.get();
}
Nota:
Cada classe tem o seu próprio construtor e destrutor, ou seja, uma classe generalizada não pode herdar o construtor da classe-mãe.
O tipo protected
Antes de avançar mais vou-vos falar sobre o tipo de acesso protected.
Neste ponto das aulas já devem saber bem o que é o tipo privado (private) e público (public) das classes. Muito bem, agora, com a introdução da herança, devem aprender o tipo protected.
Basicamente:
Os membros privados não podem ser acedidos por nenhum objecto, a não ser através da própria classe.
Os membros protegidos podem ser acedidos apenas pela própria classe e pelas classes derivadas, através da herança.
Os membros públicos podem ser acedidos tanto dentro, como fora da classe.
Assim, no exemplo acima, as classes derivadas da função ser não poderiam aceder à variável hp. Podemos resolver facilmente essa questão:
class Ser
{
public:
void mover(); //todos os seres (animais) se movem
void dormir(); //todos dormem
void falar() { cout << "... Parece que nao fala" << endl; }
/* Se uma determinada raça não falar aparecerá esta mensagem... verão já a
seguir o seu uso */
protected:
int hp; //Health Points ou Pontos de Saúde
};
Agregação
A agregação implica uma relação "has a" (tem um). Por exemplo, um Humano tem roupa e duas armas (faca e espada).
class Humano : public Ser //classe Humano, derivada da classe Ser
{
public:
//mover e dormir já estão na geral
void falar() { cout << "Chamaste?" << endl; }
/* Como podem vêr, embora já exista um método "falar" na classe geral,
este não afecta este método */
void atacar();
private:
int forca; // a forca do personagem.
Roupa roupa;
Arma espada;
Arma faca;
};
Generalização vs Agregação
Em alguns casos, poderá parecer difícil distinguir entre as duas relações, o que leva ao uso desapropriado da herança. A herança deve-se usar apenas em casos óbvios, para se evitar confusões futuras. Por exemplo, uma classe Carro não deverá ser uma generalização da classe Motor, só porque a classe Motor é a classe mais importante (que contém a potência, km/h...). Um carro certamente que não é um motor, apenas tem um motor.
Esta explicação poderá parecer um bocado confusa, mas no futuro irão ver que a má distinção entre estes dois tipos de relações pode levar à reescrita completa do código (eu sei por experiência própria)
Classes Friend
Em C++ é possível fazer com que uma classe aceda aos membros privados e protegidos de uma função. Basta declará-la como friend (amiga).
#include <iostream>
using namespace std;
class UM
{
private: //para dizer a verdade, nem precisávamos deste private:
int x;
friend class DOIS; //a classe DOIS é amiga da classe UM, por isso a classe
//DOIS pode aceder aos membros privados da classe UM
};
class DOIS
{
public:
void setX(int x) { classe.x = x; }
void printX() { cout << classe.x << endl; }
private:
UM classe;
};
int main()
{
DOIS t;
t.setX(30);
t.printX();
cin.get();
return 0;
}
Polimorfismo
Polimorfismo pode-se definir como "a abilidade de objectos de diferentes tipos (classes) poderem responder a métodos com o mesmo nome, cada um deles tendo um comportamento específico para cada tipo (classe) (traduzido da definição da Wikipedia Inglesa)
Mas que significa isto na prática? Já vimos que uma classe derivada partilha com a classe-mãe, os membros públicos e protegidos da mesma. Mas não é só isso: o tipo de apontador de uma classe derivada é compatível com o da classe mãe, ou seja, nos exemplos acima (classe Ser), poderíamos fazer algo como:
Magico jog1;
Ser *jogador = &jog1;
Como estão a ver, estamos a pegar no apontador para a classe geral (Ser) e a fazer com que aponte para uma classe generalizada (Magico). Ou seja, o jogador vai ser um magico. Do mesmo modo, poderia-se fazer:
Ser *jogador = new Magico;
O objecto jogador vai assim pertencer à classe Magico. Isto é bastante útil quando queremos alterar o tipo de classes em runtime, ou seja, por acção do utilizador, enquanto o jogo corre.
Também se pode utilizar isto em funções, por exemplo:
#include <iostream>
using namespace std;
class Ser
{
public:
void mover(); //todos os seres (animais) se movem
void dormir(); //todos dormem
void falar() { cout << "... Parece que nao fala" << endl; }
/* Se uma determinada raça não falar aparecerá esta mensagem... verão já a
seguir o seu uso */
private:
int hp; //Health Points ou Pontos de Saúde
};
class Humano : public Ser //classe Humano, derivada da classe Ser
{
public:
//mover e dormir já estão na geral
void falar() { cout << "Chamaste?" << endl; }
/* Como podem vêr, embora já exista um método "falar" na classe geral,
este não afecta este método */
void atacar();
private:
int forca; // a forca do personagem.
};
class Magico : public Ser
{
public:
void falar() {
cout << "Em que podem as forcas dos meus feiticos te ajudar?" << endl;
}
void lancarFeitico();
private:
int mp; // Magic Points ou Pontos de Magia
int magia; //a força da magia da personagem
};
class Orc : public Ser
{
public:
void atacar();
private:
int forca; // força com que o Orc ataca.
};
void falar(Ser *pessoa); //função falar. Notem que leva como atributo um
//apontador para um objecto da classe Ser
int main()
{
Humano hum;
Magico mag;
Orc orc;
falar(&hum); //é óbvio que temos de passar o endereço do objecto
//pois queremos que seja processado o objecto em si...
falar(&mag);
falar(&orc);
cin.get();
}
void falar (Ser *pessoa)
{
pessoa->falar();
}
Está portanto cumprido o primeiro requisito do polimorfismo. Um único objecto pode assumir várias classes. No entanto, ao fazermos jogador->falar() (não se esqueçam que é um apontador, e por isso precisa do '->' em vez do '.') não é chamada o método da classe específica, mas sim o método da classe geral (Ser). Isto faz com que, no exemplo acima, mostre três vezes a expressão "... Parece que nao fala".
Falta então ser cumprido o segundo requisito do polimorfismo, que pode ser facilmente resolvido recorrendo a uma única palavra-chava: virtual:
#include <iostream>
using namespace std;
class Ser
{
public:
void mover(); //todos os seres (animais) se movem
void dormir(); //todos dormem
virtual void falar() { cout << "... Parece que nao fala" << endl; }
/* Se uma determinada raça não falar aparecerá esta mensagem... verão já a
seguir o seu uso */
private:
int hp; //Health Points ou Pontos de Saúde
};
class Humano : public Ser //classe Humano, derivada da classe Ser
{
public:
//mover e dormir já estão na geral
void falar() { cout << "Chamaste?" << endl; }
/* Como podem vêr, embora já exista um método "falar" na classe geral,
este não afecta este método */
void atacar();
private:
int forca; // a forca do personagem.
};
class Magico : public Ser
{
public:
void falar() {
cout << "Em que podem as forcas dos meus feiticos te ajudar?" << endl;
}
void lancarFeitico();
private:
int mp; // Magic Points ou Pontos de Magia
int magia; //a força da magia da personagem
};
class Orc : public Ser
{
public:
void atacar();
private:
int forca; // força com que o Orc ataca.
};
void falar(Ser *pessoa); //função falar. Notem que leva como atributo um
//apontador para um objecto da classe Ser
int main()
{
Humano hum;
Magico mag;
Orc orc;
falar(&hum); //é óbvio que temos de passar o endereço do objecto
//pois queremos que seja processado o objecto em si...
falar(&mag);
falar(&orc);
cin.get();
}
void falar (Ser *pessoa)
{
pessoa->falar();
}
A palavra chave virtual é o que faz o mecanismo de polimorfismo funcionar. Ao definirmos o método falar da classe-mãe (Ser) como virtual, este método não será chamado pelas classes-filhas (sendo chamado o método específico de cada uma).
O polimorfismo é muito importante em programação orientada a objectos, pois fornece-nos um meio de alterarmos o completo total de um objecto, dinamicamente.
Próxima aula -> C++ 17 - Técnicas Avançadas de C++ - Parte I
Fazer o download do código fonte dos exemplos da aula
Fazer o download da aula em PDF (Brevemente)
Ir para o topo da página