Aula 3:
Classes – Parte 2
ECOP03 – Programação Orientada a Objetos
Prof. João Paulo Reus Rodrigues Leite
joaopaulo@[Link]
Classes:
“Estes tipos não são ‘abstratos’; eles são tão reais quanto um int ou um float.”
(Doug Mcllroy)
Alguns pontos importantes da aula passada:
1. Represente conceitos importantes para sua aplicação como classes, criando
novos tipos de dados.
2. Separe a interface da classe (parte pública) de sua implementação (parte
privada): encapsulamento.
3. Utilize structs (dados públicos) somente quando não houver restrições
importantes com relação aos dados.
4. Defina sempre um construtor para a inicialização adequada de seus objetos,
e um destrutor, quando necessário.
5. Separe sua implementação em arquivos de cabeçalho (.h) e código (.cpp).
Algumas vezes, uma função membro pode também ser definida no próprio
arquivo de cabeçalho, mas neste caso, a função passa a ser inline, da mesma
maneira que aprendemos com funções comuns. São chamadas definições In-Class.
Portanto, podemos dizer que este tipo de implementação somente deve ser
utilizado para funções pequenas, raramente modificadas e que são utilizadas
frequentemente.
Um candidato forte seriam membros do tipo get/set, mas especialmente get, uma
vez que membros set frequentemente precisam realizar validação em dados.
Hands-On!
Implemente uma classe que modele um carro. O tanque de combustível do carro armazena no máximo X litros de
gasolina. O carro consome Y km/litro. Deve ser possível, através de funções-membro:
– Abastecer o carro com uma certa quantidade de gasolina (até o limite);
– Mover o carro por uma determinada distância (medida em km);
– Retornar a quantidade total de km percorridos pelo carro;
– Retornar a quantidade de combustível e a distância total a percorrer.
No programa principal, crie 2 carros. Abasteça 20 litros no primeiro e 30 litros no segundo. Desloque o primeiro em
200 km e o segundo em 400 km. Depois, desloque o primeiro em mais 50 km e o segundo em mais 100 km. Exiba
na tela a o total de combustível restante para cada um e uma estimativa da distância que poderá ser percorrida com
essa quantidade de combustível. Veja um exemplo:
Carro 1 abastecido com 20 litros.
Carro 2 abastecido com 30 litros.
Carro 1 percorreu 200 km (Total = 200km).
Carro 2 percorreu 400 km (Total = 400km).
Carro 1 percorreu 50 km (Total = 250km).
Carro 2 percorreu 100 km (Total = 500km).
Carro 1 tem ## litros de combustível = ## km de autonomia.
Carro 2 tem ## litros de combustível = ## km de autonomia.
Mutabilidade:
Nós podemos declarar um objeto de uma classe tanto como uma constante (const)
quanto como uma variável. Portanto, um identificador (nome da variável) pode se
referir a um objeto cujos valores sejam tanto “imutáveis”, quanto “mutáveis”.
A utilização cuidadosa de objetos imutáveis pode levar a códigos mais
compreensíveis, facilitar a depuração do programa e aumentar até mesmo o
desempenho.
Por este motivo, precisamos ser capazes de definir funções membro de classes que
sejam aptas a trabalhar com objetos constantes: funções membro constantes.
Funções membro constantes são definidas utilizando-se a palavra const após a
sua lista (vazia) de parâmetros, e não podem modificar nenhum membro de
dados da classe. Veja exemplos:
Objetos declarados como variável podem acessar funções const e não const,
enquanto objetos imutáveis somente podem acessar funções declaradas como const.
Caso haja uma tentativa de acesso a membro não const, o compilador acusa erro:
Custo físico x custo lógico:
Algumas vezes, veremos que uma função membro é “logicamente const”, mas
ainda assim, é necessário que ela modifique algum membro de dados.
Mas como assim?
Algumas vezes, a chamada da função não resulta em uma modificação visível de
estado para o usuário da classe, mas, internamente, alguma mudança é
necessária. Isso é custo lógico.
Imagine, por exemplo, que haja uma necessidade de se contar quantas vezes um
determinado objeto do tipo Date já tenha sido impresso. A contagem não muda os
aspectos essenciais da data, mas teria que mudar o valor de um membro de dados
count, [Link]..
Custo físico x custo lógico:
Podemos definir um membro de dados que poderá ser modificado
dentro de uma função const com a palavra reservada mutable. Veja:
...
Tenha em mente que esta situação é relativamente incomum, e
que apenas pequenas porções da classe devem ser declaradas
como mutable.
Auto-Referência:
Nossas funções de atualização de valores (set) foram todas definidas como void, ou
seja, sem retorno.
No entanto, pode ser interessante que este tipo de função retorne uma referência
para o próprio objeto, de maneira que possamos encadear chamadas de funções.
Podemos, por exemplo, querer escrever o seguinte:
Para isso, devemos modificar as implementações das funções membro set. Mas
como acessar uma auto-referência, ou seja, uma referência para o próprio objeto
que a chamou? this!
Retornar uma
referência para o
próprio objeto
chamador não fere o
encapsulamento, pois
mantém todas as
relações de acesso.
Auto-Referência:
Um exemplo comum de utilização explícita do ponteiro this pode
ser encontrado na manipulação de listas encadeadas.
Imagine uma classe Link, onde existe um método insert, que insere
um valor antes dele.
pre this suc
Inicialização de membros de dados:
Vocês devem ter reparado em uma nova forma de inicialização dos membros de
dados no construtor da classe Link, que é chamada de lista inicializadora de
membros.
Qual é a sintaxe?
A lista começa com dois pontos (:) e segue com os inicializadores individuais de
cada membro separados por vírgula. Exemplos:
Membros estáticos
Uma variável que faz parte da classe, mas não faz parte dos objetos da classe, é
uma variável estática, e a declaramos com a palavra reservada static.
Neste caso, existe apenas uma cópia do membro estático em todo o programa, ao
invés de uma cópia para cada objeto, como acontece com membros de dados não-
estáticos. Este dado é compartilhado por todas as instâncias daquela classe, ou
seja, por todos os objetos.
Mas qual a necessidade disso?
Imagine que, em um jogo de RPG, você tem a classe
Mago. Cada mago pode lançar um feitiço poderoso,
mas só o faz se houver pelo menos três magos no
círculo mágico, para que a magia seja estável. Assim,
cada mago precisa saber quantos magos estão
reunidos, uma informação compartilhada e estática.
Essa informação será uma variável comum entre
todos os magos.
Membros estáticos
Além de membros estáticos de dados, podem também haver funções membro
estáticas. Este tipo de função poderá ser chamado mesmo quando não houver um
objeto instanciado de sua classe. Ela não poderá acessar membros de dados não-
estáticos da classe.
Veja um exemplo, utilizando a classe Circulo da aula passada:
main()
Hands-On!
Crie uma classe que represente um tipo abstrato de dados Complexo com as
seguintes características:
– Possua duas variáveis do tipo double para representar a parte real e a parte imaginária.
– Possua métodos que permitam que objetos desse tipo sejam somados, subtraídos,
multiplicados e divididos.
– Criar construtores que permitam a inicialização de objetos com e sem parâmetros.
– Construir uma variável que pode ser utilizada para ser um contador do numero de
complexos que estão instanciadas em determinado momento em um programa.
– Crie uma função para calcular e retornar o modulo do numero complexo.
– Criar uma função para imprimir um numero complexo no formato “ a + b i ”
– Criar um programa que teste as funcionalidades implementadas nos itens acima.
Hands-On!
Criar uma classe que represente um triangulo retângulo.
– Criar três membros de dados inteiros para representar o tamanho dos
lados, e verificar se esses dados realmente formam um triangulo
retângulo. Se não for, criar um triângulo padrão com lados 5, 4 e 3.
– Implementar uma função membro estática que imprima na tela o
valor dos lados de todos os possíveis triângulos retângulos formados
por três números inteiros menores que 50.
Composição de Classes
Durante a modelagem de seu sistema, você irá perceber que existe um número
pequeno de classes que trabalham sozinhas. Na maior parte das vezes, o que
acontece é uma colaboração entre várias classes, onde cada uma modela um
aspecto diferente de seu universo.
Portanto, além de identificar os itens que formam o vocabulário do sistema, é
necessário transformar cada um deles em um tipo (classe) e modelar os
relacionamentos entre eles.
Vocês verão outros tipos de relacionamentos em disciplinas de “Engenharia de Software”. Por aqui,
estaremos interessados em apenas dois: composição e generalização (que veremos em breve).
Exemplo: Suponha que desejemos lidar com dados pessoais, entre os quais
nome, endereço e data de nascimento.
• Podemos ter uma classe para lidar com dados pessoais, por exemplo
Personal_data.
• Como endereço e data de nascimento são elementos compostos, eles podem
também ser representados como novos tipos de dados, ou classes Date e
Address.
• Criamos membros de dados dos tipos Date e Address na classe Personal_data
para representar a data de nascimento e o endereço da pessoa.
Este tipo de relacionamento é chamado de composição, e é uma relação do tipo
“tem um”: a classe Personal_data será composta por membros das classes Date e
Address, ou seja, terá um membro de cada tipo.
class Date {
private:
int day, month, year;
public:
Date(int=1, int=1, int=1970);
~Date();
void print();
// ...
};
class Address {
private:
string street, city, state, zip_code;
int number;
public:
Address();
~Address();
void print();
// ...
};
class Personal_data {
private:
string first_name, last_name;
int phone_number;
Date birth_date;
Address address;
public:
Personal_data();
~Personal_data();
void print();
// ...
};
Este tipo de interação entre classes é modelado de maneira
diferente em diagramas de classes. Membros de dados cujo tipo
tenha sido definido também pelo usuário entram como
relacionamentos (conexões entre classes), e não são listadas no
espaço dedicado aos membros de dados.
1
1 1
1
A composição é uma forma forte de agregação que requer que um objeto
declarado como parte da classe seja incluído em, no máximo, um objeto
composto por vez. Caso o objeto composto seja deletado, todos os seus
objetos partes também são deletados com ele (controla o ciclo de vida).
Existem vários detalhes sobre relacionamentos, que serão explorados
propriamente nas disciplinas de Engenharia de Software. Veja um exemplo
mais elaborado:
agregação
No diagrama de
classes da rede social
LinkedIn temos
exemplos de alguns
composição
relacionamentos
entre classes.
associação
Temos alguns tipos de relacionamentos:
• Associação: Uma classe possui um ponteiro (ou referência) para outra como membro de dados,
sem gerenciar o ciclo de vida do objeto apontado. Portanto, a classe “sabe” sobre a outra. É um
vínculo mais fraco e flexível – os objetos podem existir separadamente. Representada por uma
linha simples conectando as classes.
• Exemplo: Um Professor está associado a um Departamento, mas ele pode mudar de departamento, e o
departamento pode existir sem esse professor específico.
• Agregação: Uma classe contém um ponteiro (ou referência) para outra, sem gerenciar
diretamente o ciclo de vida desse objeto. Ela “tem” a outra, mas não é dona dela. As partes
ainda podem existir separadamente. Representada por uma linha que conecta classes, com um
losango aberto na extremidade da classe que contém a referência.
• Exemplo: Uma Turma tem vários Alunos. A turma agrega os alunos, mas não os cria ou destrói – os
alunos existem por si só, podem estar em outras turmas.
Portanto, a diferença entre associação e agregação é apenas semântica, não estrutural.
Estruturalmente, parecem iguais em C++, mas a intenção é o que diferencia.
Temos alguns tipos de relacionamentos:
• Composição: Uma classe possui um objeto da outra como membro de dados (tem-um). Portanto,
a classe composta contém a outra, que não pode existir sem ela. Nesse caso, o “todo” é “dono”
das partes. A classe “todo” é responsável pelo ciclo de vida das partes. É representada por uma
linha que conecta classes, com um losango fechado na extremidade da classe composta.
• Exemplo: Imagine uma classe Casa que tem uma Porta. A Porta só existe enquanto a Casa existir — ou
seja, a Casa é dona da Porta. Isso é composição.
Tipo Analogia
Associação “Conheço Você”
Agregação “Você é parte da minha equipe”
Composição “Você faz parte do meu corpo”
Hands-On!
Implemente um sistema simples para modelar uma Casa Inteligente, composta por um Sistema de Iluminação
e um Sistema de Segurança. Esses dois sistemas não existem fora da casa, portanto a relação entre Casa e os
dois sistemas deve ser de composição. Veja as funções membro de cada uma das classes:
• Classe Iluminacao:
– ligarLuzes(): imprime "Luzes acesas."
– desligarLuzes(): imprime "Luzes apagadas."
• Classe Seguranca:
– Método ativarAlarme(): imprime "Alarme ativado."
– Método desativarAlarme(): imprime "Alarme desativado."
• Classe Casa:
– chegarEmCasa(): deve desligar o alarme e acender as luzes.
– sairDeCasa(): deve apagar as luzes e ativar o alarme.
– status(): imprime "Casa em modo seguro."
No programa principal:
Instancie um objeto Casa e chame os métodos chegarEmCasa(), status() e sairDeCasa().
Hands-On!
Implemente um sistema com três classes para analisar um triângulo 2D.
Classe Ponto: Representa um ponto 2D com coordenadas x e y.
• Construtor com parâmetros x e y.
• distancia(const Ponto& outro) que retorna a distância euclidiana entre dois pontos.
Classe Reta: Representa um lado do triângulo, formado por dois pontos. Os pontos devem ser passados como ponteiros
constantes, portanto o relacionamento é de agregação.
• comprimento(): retorna o comprimento do lado usando o método distancia() dos pontos.
Classe Triangulo: Composta por três objetos Ponto como membros diretos e três objetos Reta construídos a partir dos
pontos. Ambos os relacionamentos são de composição.
• perimetro(): retorna a soma dos três lados.
• area(): calcula a área do triângulo usando a fórmula de Heron.
No programa principal, peça ao usuário para digitar as coordenadas dos três pontos. Instancie um
triângulo com esses pontos. Mostre o perímetro e a área do triângulo.
Hands-On!
Escreva uma classe chamada BigInt que represente um numero inteiro longo com 30
dígitos. Acrescente funções que permitam que estes números possam ser lidos pelo
teclado, impressos na tela, somados e subtraídos.
Declarar a classe completa com todos seus membros incluindo construtores e
destrutor. Implementar a classe em um arquivo separado. Sugestão: utilize um vetor
para armazenar cada um dos dígitos do numero.
Ex. numero [Link].345 seria representado no vetor como
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
5 4 3 2 1 9 8 7 6 5 4 3 2 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
Dicas para Solução
• A classe deve conter um vetor de números inteiros.
• Além disso, pode armazenar o tamanho atual do número guardado.
Por exemplo, 1.234.567 tem 7 dígitos.
• A entrada no programa principal deve ser com string.
• Devemos lembrar os dígitos de ‘0’ a ‘9’ são armazenados na tabela
ASCII a partir da posição 48, portanto devemos descontar o caractere
‘0’ para obter seus valores em inteiros.
• Os métodos de soma e subtração, podem ser implementados seguindo
o processo manual utilizado para realizar uma soma/subtração de
vários dígitos, como por exemplo:
1 1
2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 9 8 7 6 5 4 3 2 1 0
9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 2 3 4 5 6 7 8 9 1 2 3 4 5
+ 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 9 8 7 6 5 4 3 2 1 0
9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 9 1 2 3 4 5
2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 9 8 7 6 5 4 3 2 1 0
9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 2 3 4 5 6 7 9 8 2 4 6 9 0
Ao calcular a soma, devemos levar em conta o “vai um”. soma = vet[i] + [Link][i] + vaiUm;
Armazena-se o soma%10, em cada casa decimal do número.
Atualiza-se o vaiUm com soma/10.