Skip to content

Orientação a objetos

Assim como na seção de introdução à linguagem, orientação a objetos já deve ser algo que vocês dominam razoavelmente bem. Sendo assim, vou focar em mostrar como esses conceitos se manifestam em C++.

Em C++, a única diferença entre class e struct é o acesso padrão dos membros. Em uma class, todos os membros são private por padrão, enquanto em um struct são public. Na prática, usamos struct para pacotes simples de dados e class quando precisamos de encapsulamento e lógica.

class Pessoa {
private:
std::string nome;
int idade;
public:
// Construtor com lista de inicialização
Pessoa(std::string n, int i) : nome(std::move(n)), idade(i) {}
// Método const: garante que não altera o objeto
void saudar() const {
std::cout << "Olá, meu nome é " << nome << "\n";
}
};

C++ moderno favorece a lista de inicialização no construtor por ser mais eficiente. Ela evita a construção padrão seguida de atribuição, inicializando diretamente os membros.

Use explicit em construtores de um único parâmetro para impedir conversões implícitas indesejadas. O destrutor, marcado como ~Classe(), é chamado automaticamente quando o objeto sai de escopo graças ao princípio RAII.

class Recurso {
public:
explicit Recurso(int id) { /* ... */ }
~Recurso() { /* limpeza automática aqui */ }
};

A Regra do Zero estabelece que você não deve escrever destrutores, construtores de cópia ou operadores de atribuição manualmente se puder compor sua classe com tipos da biblioteca padrão que já gerenciam esses aspectos corretamente.

struct Registro {
std::string nome;
std::vector<int> notas;
// O compilador gera automaticamente cópia e movimento corretos aqui.
};

Para usar polimorfismo em C++, você precisa marcar métodos como virtual, sempre definir um destrutor virtual na classe base, e acessar objetos através de ponteiros ou referências.

O destrutor virtual é essencial para evitar vazamentos quando deletamos um objeto derivado através de um ponteiro para a classe base. A palavra-chave override garante que você está realmente sobrescrevendo um método virtual da classe base, gerando erro de compilação se não for o caso.

class Forma {
public:
// Essencial para evitar vazamentos em classes derivadas
virtual ~Forma() = default;
virtual double area() const = 0; // Método puro (classe abstrata)
};
class Quadrado : public Forma {
private:
double lado;
public:
Quadrado(double l) : lado(l) {}
double area() const override { return lado * lado; }
};

Se você passar um objeto por valor em uma hierarquia de classes, ocorre o slicing (fatiamento): a parte específica da classe derivada é descartada e apenas a porção da classe base é copiada. Para evitar isso, sempre passe objetos polimórficos por referência (Base&) ou usando smart pointers.

void imprimirArea(const Forma& f) { // Correto: usa referência
std::cout << f.area() << "\n";
}
int main() {
auto q = std::make_unique<Quadrado>(5.0);
imprimirArea(*q);
}

A forma mais comum e segura de gerenciar coleções de objetos heterogêneos é usando std::vector combinado com std::unique_ptr. Isso permite armazenar diferentes tipos derivados em um mesmo container com gerenciamento automático de memória.

#include <vector>
#include <memory>
int main() {
std::vector<std::unique_ptr<Forma>> formas;
formas.push_back(std::make_unique<Quadrado>(10.0));
// formas.push_back(std::make_unique<Circulo>(5.0));
for (const auto& f : formas) {
std::cout << f->area() << "\n";
}
}

Ao escrever classes em C++, prefira sempre usar listas de inicialização nos construtores e marque construtores de um argumento como explicit. Quando sobrescrever métodos virtuais, use override para garantir a correção.

Sempre defina um destrutor virtual em classes que servem de base para outras. Prefira composição em vez de herança quando possível, e nunca esqueça que polimorfismo só funciona corretamente quando você usa ponteiros ou referências, evitando assim o problema do slicing.