11. Polimorfismo e sobrecarga de operadores¶
Sem vídeo correspondente
Os vídeos 28–29 cobrem herança, mas não cobrem virtual/polimorfismo — que é o assunto principal deste capítulo, e o que torna herança realmente útil na maioria dos casos.
Herança: revisão rápida¶
Herança permite que uma classe reaproveite campos e métodos de outra:
class Animal {
protected: // como private, mas classes filhas tambem enxergam
std::string nome_;
public:
Animal(std::string nome) : nome_(nome) {}
std::string nome() const { return nome_; }
std::string emite_som() const {
return "...";
}
};
class Cachorro : public Animal {
public:
Cachorro(std::string nome) : Animal(nome) {} // chama o construtor da classe base
std::string emite_som() const {
return "Au au!";
}
};
Cachorro é um Animal — herda nome() sem precisar reescrever, e sobrescreve emite_som() com seu próprio comportamento. protected é como private, mas classes filhas também têm acesso (diferente de private, que nem as filhas enxergam).
O problema: herança sem virtual não é polimórfica de verdade¶
void apresenta(const Animal& a) {
std::cout << a.nome() << " diz " << a.emite_som() << "\n";
}
Cachorro rex("Rex");
apresenta(rex);
O que isso imprime? Você esperaria "Rex diz Au au!" — afinal, rex é um Cachorro, e Cachorro::emite_som() diz "Au au!". Mas sem virtual, o C++ decide em tempo de compilação qual emite_som() chamar, baseado no tipo da referência/ponteiro (const Animal&), não no tipo real do objeto. O resultado real é "Rex diz ..." — a versão de Animal, não a de Cachorro. Isso se chama ligação estática (static binding), e quase nunca é o que você quer quando trabalha com uma hierarquia de classes.
virtual: ligação dinâmica¶
A correção é marcar o método na classe base como virtual, e usar override na classe filha:
class Animal {
protected:
std::string nome_;
public:
Animal(std::string nome) : nome_(nome) {}
std::string nome() const { return nome_; }
virtual std::string emite_som() const {
return "...";
}
virtual ~Animal() {} // destrutor virtual -- explicado abaixo, e ESSENCIAL
};
class Cachorro : public Animal {
public:
Cachorro(std::string nome) : Animal(nome) {}
std::string emite_som() const override { // override confirma que estamos sobrescrevendo
return "Au au!";
}
};
class Gato : public Animal {
public:
Gato(std::string nome) : Animal(nome) {}
std::string emite_som() const override {
return "Miau!";
}
};
Agora apresenta(rex) imprime "Rex diz Au au!" de verdade: com virtual, o C++ decide em tempo de execução qual versão chamar, baseada no tipo real do objeto apontado/referenciado — isso é ligação dinâmica, e é o que a palavra "polimorfismo" significa na prática: o mesmo código (a.emite_som()) se comporta diferente dependendo do objeto real por trás da referência.
override não é obrigatório para o polimorfismo funcionar, mas é fortemente recomendado: se você escrever override num método que na verdade não sobrescreve nada na classe base (erro de digitação no nome, assinatura diferente), o compilador acusa erro em vez de silenciosamente criar um método novo, não relacionado, que nunca é chamado por polimorfismo. Sem override, esse tipo de erro é silencioso e muito difícil de depurar.
Por que isso importa: uma coleção polimórfica¶
O caso de uso mais comum de polimorfismo é guardar objetos de tipos diferentes (mas relacionados por herança) numa mesma coleção, e chamar o mesmo método em todos, cada um respondendo do seu jeito:
#include <vector>
#include <memory>
#include <iostream>
std::vector<std::unique_ptr<Animal>> zoologico;
zoologico.push_back(std::make_unique<Cachorro>("Rex"));
zoologico.push_back(std::make_unique<Gato>("Mia"));
for (const auto& animal : zoologico) {
std::cout << animal->nome() << " diz " << animal->emite_som() << "\n";
}
// Rex diz Au au!
// Mia diz Miau!
Repare: std::vector<std::unique_ptr<Animal>>, não std::vector<Animal>. Isso é essencial — um vector<Animal> guardaria cópias de Animal (um problema chamado "object slicing": a parte Cachorro/Gato de cada objeto seria cortada fora, sobrando só a parte Animal). Guardar ponteiros (aqui, unique_ptr para manter RAII — regra do zero, capítulo anterior) preserva o tipo real de cada objeto, e é isso que faz o polimorfismo funcionar dentro da coleção.
Classe abstrata: uma interface¶
Quando a classe base não faz sentido sozinha — só existe para definir "o que toda classe filha precisa saber fazer" —, ela pode ter um método virtual puro, marcado com = 0:
class Forma {
public:
virtual double area() const = 0; // metodo virtual puro -- sem implementacao aqui
virtual ~Forma() {}
};
class Circulo : public Forma {
private:
double raio_;
public:
explicit Circulo(double raio) : raio_(raio) {}
double area() const override { return 3.14159 * raio_ * raio_; }
};
class Quadrado : public Forma {
private:
double lado_;
public:
explicit Quadrado(double lado) : lado_(lado) {}
double area() const override { return lado_ * lado_; }
};
Forma é uma classe abstrata: você não pode criar um Forma f; diretamente (o compilador barra, porque area() não tem implementação) — só pode criar Circulo, Quadrado, ou qualquer outra classe filha que implemente area(). Forma funciona como uma interface: um contrato que diz "toda forma sabe calcular sua área", sem se comprometer com como. Isso vai ser exatamente o padrão usado na Unidade 7 para desacoplar lógica de hardware (uma interface ISensor, com implementações reais e "falsas" para teste).
Destrutor virtual: por que é essencial¶
Repare que Animal e Forma, acima, têm virtual ~Animal() {}/virtual ~Forma() {}. Isso não é acidente. Considere:
Se ~Forma() não for virtual, delete f chama só ~Forma(), e o destrutor de Circulo nunca roda — se Circulo tivesse algum recurso próprio para liberar, vazaria. Com destrutor virtual, delete f corretamente chama ~Circulo() primeiro (que libera o que for específico de Circulo) e depois ~Forma(). Regra fixa: toda classe pensada para ser usada polimorficamente (com virtual em algum método) precisa de destrutor virtual.
virtual ~Base() {} e virtual ~Base() = default; são a mesma coisa
Você vai encontrar as duas formas por aí — inclusive na
Lista 11 e no simulado da Avaliação 2,
que usam a segunda. = default pede ao compilador o destrutor que ele geraria
de qualquer forma, agora marcado virtual; {} escreve à mão um corpo vazio,
que faz exatamente o mesmo. Para os fins deste curso são equivalentes, e você
pode responder com a que preferir.
Sobrecarga de operadores¶
C++ permite definir o que operadores como ==, <, +, << fazem para seus próprios tipos — é o que faz std::string s1 == s2 ou std::cout << meuObjeto funcionarem de forma natural.
class Fracao {
private:
int numerador_;
int denominador_;
public:
Fracao(int num, int den) : numerador_(num), denominador_(den) {}
bool operator==(const Fracao& outra) const {
return numerador_ * outra.denominador_ == outra.numerador_ * denominador_;
}
Fracao operator+(const Fracao& outra) const {
return Fracao(numerador_ * outra.denominador_ + outra.numerador_ * denominador_,
denominador_ * outra.denominador_);
}
friend std::ostream& operator<<(std::ostream& saida, const Fracao& f) {
saida << f.numerador_ << "/" << f.denominador_;
return saida;
}
};
Fracao a(1, 2);
Fracao b(1, 3);
Fracao c = a + b; // usa operator+
std::cout << c << "\n"; // usa operator<< -- imprime "5/6"
bool iguais = (a == b); // usa operator==
operator<< precisa ser friend (ou uma função livre fora da classe) porque o operando da esquerda é std::ostream&, não Fracao — não é um método que Fracao "chama em si mesma", e sim uma função que recebe os dois lados. friend dá a essa função acesso aos campos privados de Fracao, mesmo não sendo um método da classe.
Regra prática: sobrecarregue operadores quando eles tornam o código genuinamente mais legível e correspondem à semântica matemática/intuitiva esperada (+ para somar frações faz sentido; + para "concatenar" dois Retangulo não faria). Não sobrecarregue por sobrecarregar.
Erros comuns¶
Método sobrescrito não é chamado por polimorfismo ("imprime a versão errada") — geralmente falta virtual na classe base. Sem ele, a chamada é resolvida em tempo de compilação pelo tipo da referência, não pelo tipo real do objeto.
"error: cannot declare variable ‘f’ to be of abstract type ‘Forma’" — você tentou instanciar diretamente uma classe com método virtual puro (= 0). Isso é esperado: crie uma classe filha que implemente o(s) método(s) puro(s).
Vazamento ao usar delete num ponteiro de classe base sem destrutor virtual — descrito acima. Toda classe base usada polimorficamente precisa de virtual ~Classe() {}, mesmo vazio.
Guardar objetos por valor numa coleção polimórfica ("object slicing") — std::vector<Animal> corta a parte específica de cada subclasse. Use std::vector<std::unique_ptr<Animal>> (ou ponteiros/referências, dependendo do caso).
Resumo¶
- Herança sem
virtualusa ligação estática — decidida em tempo de compilação pelo tipo da referência, quase nunca o que você quer. virtualna base +overridena filha: ligação dinâmica — decidida em tempo de execução pelo tipo real do objeto. Isso é polimorfismo.- Classe abstrata (método virtual puro,
= 0) define uma interface — um contrato sem implementação, implementado pelas classes filhas. - Toda classe usada polimorficamente precisa de destrutor virtual, senão
deletepor um ponteiro da classe base vaza o que for específico da classe filha. - Coleção polimórfica:
vector<unique_ptr<Base>>, nuncavector<Base>(object slicing). - Sobrecarga de operadores (
==,+,<<, ...) deixa o código mais natural quando corresponde à semântica esperada do tipo.