Classes e objetos
Estado, invariantes, encapsulamento, construção, destruição, métodos const e operadores.
Dois campos públicos para horas e minutos permitem atribuir 70 aos minutos. O relógio seguinte controla a construção e o avanço para que cada objeto represente sempre uma hora válida.
A classe define a representação e as operações autorizadas sobre ela.
Definir uma classe e criar objetos
A interface pública diz o que o chamador pode fazer. Os membros privados guardam a representação e só são acessíveis aos membros e amigos da classe. Encapsular é manter uma fronteira que preserva o significado dos dados, não apenas escrever getters e setters para cada campo.
Este relógio guarda minutos desde a meia-noite, numa única representação.
O seu invariante é 0 <= minutos_ < 1440. O chamador conhece horas e
minutos; não precisa de saber a representação escolhida.
Guardar hora_, minuto_ e minutos_ em simultâneo obrigaria a atualizar
três representações da mesma informação.
A construção rejeita valores fora do intervalo e o avanço rejeita quantidades fora de 0 a 1440. Cada operação pública conserva o invariante ou recusa a alteração sem a aplicar. Os detalhes de como um erro interrompe a operação ficam na página Exceções e testes; aqui basta saber que um relógio nunca fica num estado inválido.
Programa completo para compilar localmente:
#include <iostream>
#include <stdexcept>
class Relogio {
public:
Relogio(int hora, int minuto) {
if (hora < 0 || hora >= 24 || minuto < 0 || minuto >= 60)
throw std::invalid_argument("Hora invalida");
minutos_ = 60 * hora + minuto;
}
int hora() const { return minutos_ / 60; }
int minuto() const { return minutos_ % 60; }
void avancar(int quantidade) {
if (quantidade < 0 || quantidade > 1440)
throw std::invalid_argument("Avanco invalido");
minutos_ = (minutos_ + quantidade) % 1440;
}
bool operator==(const Relogio& outro) const {
return minutos_ == outro.minutos_;
}
private:
int minutos_ = 0;
};
int main() {
Relogio a(23, 50);
Relogio b = a;
a.avancar(25);
const Relogio& leitura = a;
std::cout << leitura.hora() << ":" << leitura.minuto() << "\n";
std::cout << b.hora() << ":" << b.minuto() << "\n";
std::cout << std::boolalpha << (a == Relogio(0, 15)) << "\n";
}
Saída:
0:15
23:50
true
O construtor transforma 23:50 em 60 * 23 + 50 = 1430. Avançar 25 produz 1455, cujo resto por 1440 dá 15, ou seja 0:15. A cópia b conserva 1430 (23:50), porque cada objeto tem o seu próprio minutos_. Consulta por const Relogio& não copia o relógio nem o altera.
Encapsulamento
O construtor rejeita horas fora de 0 a 23 e minutos fora de 0 a 59. avancar aceita 0 a 1440, por isso a soma continua representável.
Nenhuma operação pública permite escrever minutos_ = -1.
A validação tem de acontecer antes de alterar o estado. Se avancar(-5) lança uma exceção, o relógio fica com o mesmo valor. Uma classe que permite um estado inválido e espera que cada chamador o corrija não está a proteger o seu invariante.
Construtores e destrutor
Um construtor tem o nome da classe e não tem tipo de retorno. Uma lista de inicialização, como Marca(const std::string& nome) : nome_(nome), constrói os membros diretamente. Atribuir no corpo acontece depois da inicialização e não a substitui. Membros const e referências precisam de ser inicializados, não podem receber a primeira atribuição no corpo.
As bases são construídas primeiro; os membros são construídos pela ordem de declaração na classe, mesmo que a lista do construtor use outra ordem, e depois corre o corpo do construtor. O destrutor tem o nome ~Classe() e não recebe parâmetros. Depois do seu corpo, os membros são destruídos na ordem inversa.
#include <iostream>
#include <string>
class Marca {
public:
explicit Marca(const std::string& nome) : nome_(nome) {
std::cout << "Criar " << nome_ << "\n";
}
~Marca() { std::cout << "Destruir " << nome_ << "\n"; }
private:
std::string nome_;
};
int main() {
Marca a("A");
{
Marca b("B");
Marca c("C");
std::cout << "Dentro\n";
}
std::cout << "Fora\n";
}Executa o código para ver a saída e os gráficos.
Saída:
Criar A
Criar B
Criar C
Dentro
Destruir C
Destruir B
Fora
Destruir A
| Ponto do programa | Objetos vivos | Efeito |
|---|---|---|
| Antes do bloco interior | A | construir A |
Depois de criar c | A, B, C | construir B e depois C |
| Saída do bloco interior | A | destruir C e depois B |
Fim de main | nenhum | destruir A |
A ordem inversa permite que os objetos mais recentes sejam limpos antes dos que já existiam ao entrarem no bloco. Não chames o destrutor de um objeto automático; um objeto dinâmico é destruído pelo dono, por exemplo através de unique_ptr.
explicit num construtor evita certas conversões implícitas para o tipo. Neste caso, exige que a construção de uma Marca seja intencional. O exemplo escreve mensagens para tornar a ordem visível; em código habitual, o destrutor trata recursos e evita lançar exceções.
Métodos const
O const depois dos parâmetros, como em int hora() const, permite chamar o método num objeto constante ou por uma referência constante. Dentro do método, this dá acesso constante ao objeto. avancar não é constante porque altera o estado.
Uma consulta que devolve um apontador modificável para um membro pode permitir alterar estado indiretamente. Por isso, const-correctness também depende dos tipos devolvidos, não apenas da palavra const no método. Usa resultados por valor ou acessos de leitura quando a interface promete só consultar.
this, static e visibilidade
Numa chamada a.avancar(25), this aponta para a, por isso this->minutos_ e minutos_ acedem ao mesmo membro dentro do método; usa this quando ajuda a distinguir nomes ou a devolver o próprio objeto.
Um membro static pertence à classe e não a um objeto específico. Neste exemplo, conta chamadas ao construtor sem argumentos:
// Excerto: numera as chamadas ao construtor sem argumentos.
struct Contador {
static int criados;
int id;
Contador() : id(++criados) {}
};
int Contador::criados = 0;
Depois de Contador a; Contador b;, criados vale 2, com a.id == 1 e b.id == 2. A cópia implícita Contador c = a copiaria o identificador sem chamar esse construtor nem incrementar criados. Por isso, este contador não mede todos os objetos vivos nem garante identificadores únicos perante cópias.
Uma função estática não tem this; só pode aceder a membros de instância se receber um objeto. Uma variável estática local tem duração até ao fim do programa, embora o seu nome só seja visível na função.
Em class, os membros são privados por defeito. public expõe o contrato. protected dá acesso também a classes derivadas e aumenta a quantidade de código capaz de alterar a representação. Preferir estado privado facilita manter o invariante, inclusive quando existe herança.
Sobrecarga de operadores
operator== define a comparação entre dois relógios e devolve bool. Como ambos usam a mesma representação normalizada, basta comparar minutos_. Um operador deve conservar o significado habitual. Não uses + para uma operação surpreendente que destrói ou altera o operando esquerdo. Quando o operador precisa de simetria entre tipos diferentes, prefere uma função livre que recebe os dois operandos; um operador membro recebe o operando esquerdo através de this.
A sobrecarga não permite criar operadores novos, mudar a precedência ou modificar operações entre dois tipos fundamentais. Pelo menos um dos operandos deve ter tipo de classe ou enumeração. friend concede acesso privado a uma função específica, não torna todos os campos públicos.
Copiar objetos com memória própria
Quando todos os membros gerem os seus recursos, a cópia gerada costuma servir. Se um membro é um apontador dono, copiar só o endereço não duplica o recurso. A página Cópia, movimento e propriedade constrói essa cópia independente.
Exercícios
Preservar o estado quando há erro
No Relogio da lição, cria-se Relogio r(23, 55). Chama-se
r.avancar(10) e depois tenta-se r.avancar(-1),
apanhando a exceção. Qual é o estado final e porquê? Considera depois
acrescentar void set_minutos(int n) { minutos_ = n; }
à interface pública. Dá uma chamada que viole o invariante e explica por que
tornar o campo privado não basta se esse método existir.
Pista
Segue os minutos desde a meia-noite e repara onde está a validação.
Solução
O estado final é 0:5: 23 * 60 + 55 = 1435,
1435 + 10 = 1445 e 1445 % 1440 = 5. O primeiro
avanço atravessa a meia-noite. O segundo não altera o objeto porque testa
o argumento antes da atribuição. O invariante continua válido e a falha
conserva o estado anterior. A impressão é 0:5, sem zero à
esquerda. Um r.set_minutos(-1) introduziria um estado
inválido por um método público. O encapsulamento depende de todos os
caminhos de escrita conservarem o invariante, não apenas do acesso privado
aos campos.
Confere a tua resposta:
- Converti 23:55 para 1435 minutos.
- Obtive 0:5 depois do avanço.
- Justifiquei que a exceção deixa o estado intacto.
- Mostrei uma chamada ao setter que viola o invariante.
Erros frequentes
Não apliques a quantidade inválida e só depois tentes corrigir o relógio.
Construir e destruir membros
Uma classe declara os membros A primeiro_; e depois
B segundo_;, mas o construtor lista
segundo_(…), primeiro_(…). Qual é a ordem de construção e
destruição dos membros?
Pista
A ordem de declaração na classe define a construção, não a lista do construtor.
Solução
Constrói primeiro primeiro_, depois segundo_ e
só então corre o corpo do construtor. Na destruição, o corpo do destrutor
corre antes de destruir os membros, que são destruídos como
segundo_ e primeiro_. A lista deve seguir a
ordem de declaração para não sugerir outra ordem.
Confere a tua resposta:
- Usei a ordem de declaração para a construção.
- Inverti a ordem para a destruição.
- Distingui os corpos do construtor e destrutor dos membros.
Erros frequentes
Não inicializes um membro a partir de outro que ainda não foi construído.