# Classes e objetos

Classes, construtores e destrutor, encapsulamento, métodos const e sobrecarga simples de operadores.

Página: https://resumos.rgo.pt/cadeiras/p/classes-objetos/

Nos [fundamentos](https://resumos.rgo.pt/cadeiras/p/cpp-fundamentos/) separaste dados (variáveis) de comportamento (funções). Uma **classe** junta os dois: descreve um tipo novo com os seus dados (**atributos**) e as operações sobre esses dados (**métodos**). Cada variável desse tipo é um **objeto**. Se programaste com tuplos e dicionários em [Tuplos e listas](https://resumos.rgo.pt/cadeiras/fp/tuplos-listas/), pensa numa classe como um contentor de dados que já traz as suas próprias funções coladas.

## Definir uma classe e criar objetos

Uma classe declara-se com a palavra `class`, e os seus membros dividem-se em **públicos** (quem usa o objeto pode aceder) e **privados** (só os métodos da classe acedem). Os métodos definem-se dentro ou fora da classe:

```
#include <iostream>
#include <string>

class Aluno {
public:
    Aluno(const std::string& n, int nro) : nome(n), numero(nro) {}

    void apresentar() {
        std::cout << nome << " (" << numero << ")\n";
    }

private:
    std::string nome;
    int numero;
};

int main() {
    Aluno a("Ana", 202400123);
    a.apresentar();
}
```

Isto escreve `Ana (202400123)`. O objeto `a` guarda o nome e o número, e o método `apresentar` sabe ir buscar esses dados sem receber argumentos: dentro de um método, os atributos do próprio objeto estão sempre ao alcance. O operador ponto (`.`) escolhe o membro de um objeto concreto.

![Diagrama do objeto a: liga-se aos métodos públicos como apresentar, mas não toca diretamente nos atributos privados nome e numero.](https://resumos.rgo.pt/cadeiras/p/classes-objetos/figura-1.svg)

Métodos curtos definem-se dentro da classe, como acima. Métodos compridos lêem-se melhor fora, com o nome qualificado `Aluno::`:

```
class Aluno {
public:
    void apresentar() {
        std::cout << nome << " (" << numero << ")\n";
    }
    // ...
};
```

Declaração e corpo juntos. Serve para métodos de uma ou duas linhas; a classe continua a ler-se de uma vez.

```
class Aluno {
public:
    void apresentar();
    // ...
};

void Aluno::apresentar() {
    std::cout << nome << " (" << numero << ")\n";
}
```

A classe mostra só a interface e o corpo vive fora, qualificado com `Aluno::`. É a organização dos projetos com ficheiros separados: declarações no `.h`, definições no `.cpp`.

## Construtores e destrutor

O **construtor** é um método especial com o mesmo nome da classe que corre quando o objeto nasce. Garante que nenhum objeto existe num estado inválido: quem cria um `Aluno` é obrigado a dar nome e número logo. O `: nome(n), numero(nro)` a seguir aos parênteses é a **lista de inicialização** e atribui os valores antes de o objeto começar a ser usado; prefere-a sempre a atribuir dentro do corpo.

Uma classe pode ter vários construtores (**sobrecarga**), desde que os parâmetros difiram:

```
#include <iostream>

class Contador {
public:
    Contador() : valor(0) {}
    Contador(int inicial) : valor(inicial) {}

    int ler() { return valor; }
    void incrementar() { valor++; }

private:
    int valor;
};

int main() {
    Contador c1;
    Contador c2(10);
    c2.incrementar();
    std::cout << c1.ler() << " " << c2.ler() << "\n";
}
```

Isto escreve `0 11`. O `c1` usou o construtor sem argumentos e começou em `0`; o `c2` começou em `10` e o incremento levou-o a `11`. O compilador escolhe o construtor pelos argumentos, tal como escolhe entre funções sobrecarregadas.

O simétrico do construtor é o **destrutor** (`~Contador()`), que corre quando o objeto morre, ao sair de âmbito ou por `delete`. Numa classe simples não precisas de o escrever. Mas quando a classe pedir memória com `new` nos [apontadores](https://resumos.rgo.pt/cadeiras/p/apontadores-memoria/), o destrutor é o sítio onde essa memória se liberta: é o mecanismo por trás do RAII.

## Copiar objetos com memória própria

Por omissão, copiar um objeto copia os atributos um a um. Para atributos simples isso chega; para um apontador, copia-se o endereço, e ficam dois objetos a partilhar a mesma memória: o primeiro destrutor liberta-a e o segundo liberta-a outra vez (double-free). Uma classe com `new` precisa de definir a cópia: construtor de cópia, operador de atribuição e destrutor, a chamada **regra dos três**:

```
#include <iostream>

class Bloco {
public:
    Bloco(int n) : tamanho(n), dados(new int[n]) {}
    ~Bloco() { delete[] dados; }

    Bloco(const Bloco& outro)
        : tamanho(outro.tamanho), dados(new int[outro.tamanho]) {
        for (int i = 0; i < tamanho; i++) dados[i] = outro.dados[i];
    }

    Bloco& operator=(const Bloco& outro) {
        if (this != &outro) {
            int* novos = new int[outro.tamanho];
            for (int i = 0; i < outro.tamanho; i++) novos[i] = outro.dados[i];
            delete[] dados;
            dados = novos;
            tamanho = outro.tamanho;
        }
        return *this;
    }

    void escrever(int i, int v) { dados[i] = v; }
    int ler(int i) const { return dados[i]; }

private:
    int tamanho;
    int* dados;
};

int main() {
    Bloco a(2);
    a.escrever(0, 7);
    Bloco b = a;
    b.escrever(0, 9);
    std::cout << a.ler(0) << " " << b.ler(0) << "\n";
}
```

Isto escreve `7 9`: cada bloco tem o seu array e a escrita em `b` não toca em `a`. Sem o construtor de cópia, `Bloco b = a` copiava só o endereço e o programa avariava no fim do `main`, com dois `delete[]` sobre o mesmo array. Repara nos dois cuidados do operador de atribuição: testa a auto-atribuição (`this != &outro`) e aloca a memória nova antes de libertar a antiga, para o objeto nunca ficar a meio. Se a tua classe só tem atributos simples (`string`, `int`, `vector`), não escrevas nada disto: a cópia automática já está certa.

## Encapsulamento

Manter os atributos privados e expor só o necessário chama-se **encapsulamento**. A ideia: quem usa a classe não precisa de saber como os dados estão guardados, só precisa das operações garantidas. Se um dia mudares a representação interna, o código que usa a classe continua igual.

```
#include <iostream>

class Nota {
public:
    Nota(int v) { definir(v); }

    void definir(int v) {
        if (v < 0) v = 0;
        if (v > 20) v = 20;
        valor = v;
    }

    int ler() { return valor; }

private:
    int valor;
};

int main() {
    Nota n(25);
    std::cout << n.ler() << "\n";
    n.definir(-3);
    std::cout << n.ler() << "\n";
}
```

Isto escreve `20` e depois `0`. A regra “nota entre 0 e 20” vive num único sítio, o método `definir`, e o construtor reutiliza-o. Com o atributo público, qualquer parte do programa podia escrever `n.valor = 25` e a regra quebrava-se em silêncio. O encapsulamento troca um pequeno incómodo (escrever métodos de acesso) por uma garantia: o objeto nunca fica inválido.

## Métodos const

Um método que não altera o objeto deve declarar isso com `const` a seguir aos parênteses. Assim ele pode ser chamado sobre objetos constantes, e o compilador impede alterações acidentais, tal como nos parâmetros `const` dos [fundamentos](https://resumos.rgo.pt/cadeiras/p/cpp-fundamentos/):

```
#include <iostream>

class Ponto {
public:
    Ponto(double px, double py) : x(px), y(py) {}

    double soma_coordenadas() const { return x + y; }
    void deslocar(double dx, double dy) {
        x += dx;
        y += dy;
    }

private:
    double x, y;
};

int main() {
    const Ponto p(3.0, 4.0);
    std::cout << p.soma_coordenadas() << "\n";
}
```

Isto escreve `7`. Como `p` é `const`, só aceita métodos `const`: `soma_coordenadas` pode ser chamado, mas `deslocar` sobre `p` nem compilava. Ganha o hábito de marcar todos os métodos de leitura como `const` desde a primeira versão; acrescentar depois obriga a rever a classe toda.

## Sobrecarga de operadores

Em C++ podes dar novo significado aos operadores (`+`, `==`, `<<`) para os teus tipos. É o que permite somar dois objetos com a mesma naturalidade com que somas dois inteiros. Um exemplo pequeno e útil: comparar dois pontos com `==`:

```
#include <iostream>

class Ponto {
public:
    Ponto(double px, double py) : x(px), y(py) {}

    bool operator==(const Ponto& outro) const {
        return x == outro.x && y == outro.y;
    }

    void mostrar() const {
        std::cout << "(" << x << ", " << y << ")\n";
    }

private:
    double x, y;
};

int main() {
    Ponto a(1.0, 2.0);
    Ponto b(1.0, 2.0);
    Ponto c(3.0, 4.0);
    std::cout << (a == b) << " " << (a == c) << "\n";
}
```

Isto escreve `1 0`. O método `operator==` recebe o ponto da direita como parâmetro e compara com o da esquerda, que é o próprio objeto. Repara nos dois `const`: o método não altera nenhum dos dois pontos. Sem esta sobrecarga, `a == b` compararia os objetos como blocos de memória, o que raramente é o que queres.

O `operator<<` para imprimir com `std::cout << p` não pode ser método, porque o objeto da esquerda é o fluxo, não o ponto. Escreve-se como função amiga ou livre que recebe o fluxo e o objeto:

```
#include <iostream>

class Ponto {
public:
    Ponto(double px, double py) : x(px), y(py) {}

    friend std::ostream& operator<<(std::ostream& saida, const Ponto& p);

private:
    double x, y;
};

std::ostream& operator<<(std::ostream& saida, const Ponto& p) {
    return saida << "(" << p.x << ", " << p.y << ")";
}

int main() {
    Ponto p(1.5, 2.0);
    std::cout << p << "\n";
}
```

Isto escreve `(1.5, 2)`. O `friend` dá à função livre acesso aos atributos privados; sem ele, o operador só veria a parte pública. Devolver o fluxo (`return saida`) permite encadear saídas, como `std::cout << p << "\n"`. Este é o operador que os projetos pedem com mais frequência: substitui todos os métodos `mostrar` por esta forma.

## Exemplo completo: uma fração

Vamos construir uma classe `Fracao` com construtor, leitura, soma e comparação, e usá-la num programa completo. Uma fração guarda numerador e denominador; somar $a/b + c/d$ dá $(a d + c b)/(b d)$. Corre o programa:

```cpp
#include <iostream>

class Fracao {
public:
    Fracao(int n, int d) : num(n), den(d) {}

    Fracao operator+(const Fracao& outra) const {
        return Fracao(num * outra.den + outra.num * den, den * outra.den);
    }

    bool operator==(const Fracao& outra) const {
        return num * outra.den == outra.num * den;
    }

    void mostrar() const {
        std::cout << num << "/" << den << "\n";
    }

private:
    int num, den;
};

int main() {
    Fracao a(1, 2);
    Fracao b(1, 3);
    Fracao soma = a + b;
    soma.mostrar();
    std::cout << (soma == Fracao(5, 6)) << "\n";
}
```

Isto escreve `5/6` e depois `1`. Confere a soma: $1/2 + 1/3 = (1 times 3 + 1 times 2)/(2 times 3) = 5/6$. E a comparação evita divisões reais: $5/6 == 5/6$ porque $5 times 6 == 5 times 6$. Repara que `operator+` devolve um objeto novo sem tocar nos operandos, tal como `+` entre inteiros não altera os inteiros.

O que falta para a fração ser perfeita

A soma devia simplificar o resultado (por exemplo, $1/2 + 1/2$ dá `2/4` em vez de `1/1`), o denominador devia ser validado contra zero no construtor e o `mostrar` devia ser um `operator<<`. São três bons exercícios: cada um exercita uma parte desta página, encapsulamento, construtor e sobrecarga.
