Conceito de Polimorfismo
O polimorfismo representa a capacidade de um objeto assumir múltiplas formas. No contexto da programação orientada a objetos em C++, ele permite que diferentes classes derivadas respondam de maneiras distintas a uma mesma chamada de função, dependendo do tipo do objeto que a invoca em tempo de execução.
Um exemplo clássico é o sistema de processamanto de pagamentos: uma classe base define um método de pagamento, mas as subclasses (Cartão de Crédito, Boleto, PIX) implementam regras de taxas e processamento específicas para cada modalidade.
Requisitos para o Polimorfismo Dinâmico
Para que o polimorfismo ocorra em C++, dois critérios técnicos devem ser atendidos:
- A chamada da função deve ser feita através de um ponteiro ou uma referência da classe base.
- A função chamada deve ser declarada como
virtualna classe base e sobrescrita (override) na classe derivada.
Funções Virtuais e Sobrescrita
O uso da palavra-chave virtual sinaliza ao compilador que a resolução da função deve ser feita de forma dinâmica (Late Binding). Veja o exemplo abaixo:
#include <iostream>
class Conta {
public:
virtual void calcularJuros() {
std::cout << "Juros padrão: 1%" << std::endl;
}
};
class ContaPoupanca : public Conta {
public:
// Sobrescrita da função virtual
virtual void calcularJuros() override {
std::cout << "Juros Poupança: 0.5%" << std::endl;
}
};
void ProcessarJuros(Conta& c) {
c.calcularJuros();
}
int main() {
Conta c1;
ContaPoupanca cp1;
ProcessarJuros(c1); // Saída: Juros padrão: 1%
ProcessarJuros(cp1); // Saída: Juros Poupança: 0.5%
return 0;
}
A sobrescrita (override) exige que a assinatura da função (nome, parâmetros e retorno) seja idêntica à da classe base. Caso contrário, ocorrerá uma "ocultação" (hiding) em vez de polimorfismo.
Exceções na Sobrescrita: Destrutores e Covariância
Destrutores Virtuais
É uma boa prática definir o destrutor da classe base como virtual. Se um objeto de uma classe derivada for deletado através de um ponteiro da classe base, e o destrutor não for virtual, apenas o destrutor da base será chamado, causando vazamento de memória (memory leak).
class Base {
public:
virtual ~Base() { std::cout << "Destrutor Base" << std::endl; }
};
class Derivada : public Base {
public:
~Derivada() { std::cout << "Destrutor Derivada" << std::endl; }
};
Covariância
A covariância permite que a função sobrescrita na classe derivada retorne um ponteiro ou referência para um objeto da própria classe derivada, enquanto a função base retorna um ponteiro ou referência para a classe base.
Palavras-chave: final e override
- override: Garante que a função está realmente sobrescrevendo um método virtual da base. Se a assinatura não bater, o compilador gera um erro.
- final: Quando aplicado a uma função, impede que ela seja sobrescrita por classes posteriores. Quando aplicado a uma classe, impede que ela seja herdada.
Classes Abstratas e Funções Virtuais Puras
Uma classe torna-se abstrata quando possui pelo menos uma função virtual pura, declarada com = 0. Essas classes não podem ser instanciadas e servem como interfaces.
class Forma {
public:
virtual void desenhar() = 0; // Função virtual pura
};
class Circulo : public Forma {
public:
void desenhar() override {
std::cout << "Desenhando um círculo" << std::endl;
}
};
Princípios de Implementação: O vptr e a vtable
Internamente, o polimorfismo é gerenciado por dois mecanismos:
- vtable (Virtual Functon Table): Um array estático criado pelo compilador para cada classe que contém funções virtuais. Ele armazena os endereços dessas funções.
- vptr (Virtual Table Pointer): Um ponteiro oculto adicionado a cada instância da classe. Ele aponta para a
vtablecorrespondente ao tipo real do objeto.
Ao realizar uma chamada polimórfica, o programa segue o vptr do objeto, localiza a vtable e executa a função no endereço ali armazenado. Esse processo ocorre em tempo de execução, garantindo a flexibilidade do código.
Polimorfismo em Herança Múltipla
Na herança múltipla, uma classe derivada pode conter múltiplos ponteiros vptr, um para cada classe base que possui funções virtuais. Isso exige que o compilador realize um ajuste no ponteiro this (this-pointer adjustment) para garantir que o método da classe correta seja acessado, especialmente quando há sobreposição de assinaturas de métodos nas bases.
class Motor {
public:
virtual void ligar() { std::cout << "Motor ligado" << std::endl; }
};
class Radio {
public:
virtual void sintonizar() { std::cout << "Rádio sintonizado" << std::endl; }
};
class Carro : public Motor, public Radio {
public:
void ligar() override { std::cout << "Carro pronto para rodar" << std::endl; }
};
Neste cenário, um objeto da classe Carro terá estruturas internas para referenciar as tabelas virtuais tanto de Motor quanto de Radio.
Vinculação Estática vs. Dinâmica
- Vinculação Estática (Static Binding): Resolvida durante a compilação. Exemplos incluem sobrecarga de funções e templates. É mais eficiente em termos de performance.
- Vinculação Dinâmica (Dynamic Binding): Resolvida durante a execução através da
vtable. Proporciona maior flexibilidade e extensibilidade ao sistema.