Polimorfismo e Funções Virtuais em C++

O polimorfismo é um dos pilares da programação orientada a objetos. Em C++, existem dois tipos principais:

  • Polimorfismo estático – implementado por meio de sobrecarga de funções.
  • Polimorfismo dinâmico – implementado por meio de funções virtuais.

Quando nos referimos simplesmente a "polimorfismo", normalmente estamos falando do dinâmico.

Polimorfismo Dinâmico

Conceito: Utiliza herança e sobrescrita (override) para permitir que uma mesma interface se comporte de diferentes formas.

Característica: Uma interface, múltiplos comportamentos.

Como implementar: Herança e sobrescrita de métodos.

O que é uma função virtual?

Não basta definir uma função na classe base e redefini-la na derivada com corpo diferente. Para que seja virtual, é necessário adicionar a palavra-chave virtual na declaração da classe base.

Pergunta: Um ponteiro da classe base pode acessar apenas as funções virtuais herdadas pela classe derivada, ou todas as funções da classe derivada?

Resposta: Apenas as funções virtuais que foram herdadas da classe base. O ponteiro base enxerga o objeto derivado, mas seu tipo estático é o da base; acessar membros exclusivos da derivada seria violação de limites de memória.

Inicialização de construtores

  • Herança múltipla comum: apenas os construtores das classes base diretas precisam ser chamados.
  • Herança virtual: todas as classes base virtuais devem ser inicializadas pela classe mais derivada, usando lista de inicialização.

Para que servem as funções virtuais?

Permitem acessar tanto as funções da classe base quanto as sobrescritas nas classes derivadas através de um ponteiro ou referência da base.

Uma interface, múltiplas formas: a interface é o ponteiro/referência da base; as formas são as diferentes implementações da mesma função virtual nas classes derivadas.

Exemplo sem virtual

#include <iostream>
using std::cout;

class Animal {
public:
    int patas;
    Animal(int p) : patas(p) {}
    void exibir() {
        cout << "Animal_patas:" << patas << '\n';
    }
};

class Ave : public Animal {
public:
    int peso;
    Ave(int p, int w) : Animal(p), peso(w) {}
    void exibir() {
        cout << "Ave_patas:" << patas << " Ave_peso:" << peso << '\n';
    }
};

class Pardal : public Ave {
public:
    int altura_voo;
    Pardal(int p, int w, int h) : Ave(p, w), altura_voo(h) {}
    void exibir() {
        cout << "Pardal_patas:" << patas << " Pardal_peso:" << peso << " Pardal_altura:" << altura_voo << '\n';
    }
};

int main() {
    Animal a(4);
    Ave b(2, 20);
    Pardal c(2, 20, 300);
    Animal* p = &a;
    p->exibir();
    p = &b;
    p->exibir();
    p = &c;
    p->exibir();
    return 0;
}

Saída: todas as chamadas executam Animal::exibir(), mesmo quando o ponteiro aponta para um objeto derivado. Ocorre vinculação estática (o endereço da função é determinado em tempo de compilação).

Adicionando virtual

Basta inserir a palavra virtual antes da declaração na classe base:

class Animal {
public:
    int patas;
    Animal(int p) : patas(p) {}
    virtual void exibir() {
        cout << "Animal_patas:" << patas << '\n';
    }
};
// classes Ave e Pardal permanecem iguais, sem virtual adicional

Saída: agora cada chamada executa a função correspondente ao tipo real do objeto (derivado). Isso é possível graças à vinculação dinâmica (resolução em tempo de execução).

Conclusão do exemplo

Função virtual permite: que um ponteiro (ou referência) da classe base acesse funções da classe derivada que foram sobrescritas.

Princípio de funcionamento

  • Cada classe que possui funções virtuais (ou herda de uma classe que as possui) tem uma tabela de funções virtuais (vtable).
  • Cada objeto dessa classe contém um ponteiro para a vtable (vptr).
  • O vptr é configurado durante a execução do construtor, apontando para a vtable da classe real do objeto.
  • Ao chamar uma função virtual, o compilador gera código que segue o vptr para a vtable e, de lá, obtém o endereço da função correta.

Três cenários para a vtable de uma classe derivada

  1. Sem sobrescrita: a vtable derivada contém os mesmos endereços que a vtable da base (aponta para as funções da base).
  2. Com sobrescrita: as entradas correspondentes às funções sobrescritas são substituídas pelos endereços das funções da derivada.
  3. Novas funções virtuais: a derivada adiciona novas entradas ao final da vtable para suas próprias funções virtuais.

Localização do vptr e da vtable

  • vptr: geralmente ocupa os primeiros bytes do objeto (4 bytes em sistemas 32 bits, 8 em 64 bits).
  • vtable: pertence à classe, não a objetos individuais. É gerada pelo compilador e armazenada na seção de dados somente leitura (.rdata), pois não deve ser modificada.
  • Código das funções virtuais: fica na seção de código (.text).

Destrutor virtual e vazamento de memória

Quando um objeto derivado é destruído através de um ponteiro da base, o destrutor da base deve ser virtual para que o destrutor da derivada seja chamado. Caso contrário, apenas o destrutor da base executa, causando possível vazamento.

#include <iostream>
using std::cout;

class Base {
public:
    Base() { cout << "Base ctor\n"; }
    virtual ~Base() { cout << "Base dtor\n"; }  // necessário virtual
};

class Derivada : public Base {
public:
    Derivada() { cout << "Derivada ctor\n"; }
    ~Derivada() { cout << "Derivada dtor\n"; }
};

int main() {
    Base* p = new Derivada;
    delete p;   // chama ~Derivada() e depois ~Base()
    return 0;
}

Sem virtual no destrutor da base, apenas ~Base() seria chamado, e os recursos da derivada não seriam liberados.

Acesso à vtable – exemplo detalhado

class Base {
public:
    virtual void f1() { std::cout << "Base::f1\n"; }
    virtual void f2() { std::cout << "Base::f2\n"; }
};

class Derivada : public Base {
public:
    void f1() override { std::cout << "Derivada::f1\n"; }
};

int main() {
    Base* obj = new Derivada;
    obj->f1();   // saída: Derivada::f1
    delete obj;
    return 0;
}

Sequência de chamada:

  1. O compilador gera código para acessar o vptr do objeto (primeiros bytes).
  2. Segue o vptr para a vtable da classe real (Derivada).
  3. Na vtable, a função f1 está na primeira posição (ordem de declaração). Obtém-se o endereço de Derivada::f1.
  4. Executa-se a função.

Layout da vtable

Base:
[RTTI | endereço de Base::f1 | endereço de Base::f2]

Derivada:
[RTTI | endereço de Derivada::f1 | endereço de Base::f2]

Observações:

  • A ordem das funções na vtable segue a ordem de declaração na classe.
  • Funções virtuais herdadas mantêm a posição original; as sobrescritas substituem a entrada.

Funções virtuais privadas podem ser herdadas?

Não. Herança não implica acesso. Embora a classe derivada possua fisicamente a função virtual privada (dentro da subobjeto da base), ela não pode acessá-la diretamente. O código gerado pela vtable, no entanto, pode chamá-la se um ponteiro da base to usado (porque o acesso é verificado em tempo de compilação, e o ponteiro base tem permissão de acesso aos membros públicos/protegidos da base – mas a função privada é inacessível para a classe derivada em si).

Quantas vtables e vpters em herança múltipla?

Quando uma classe deriva de múltiplas bases que possuem funções virtuais, ela terá uma vtable para cada base com funções virtuais. Cada vtable é independente. O objeto derivado conterá um vptr para cada vtable (tantos quantas forem as bases com funções virtuais). Por exemplo:

  • Uma classe que herda de duas bases, cada uma com suas próprias funções virtuais, terá duas vtables e dois vpters.
  • Se uma base não tiver funções virtuais, não haverá vtable extra.

Onde são armazenadas as novas funções virtuais da classe derivada em herança múltipla?

As novas funções virtuais da derivada são adicionadas ao final da primeira vtable (a da primeira classe base na lista de herança). As demais vtables permanecem com os endereços originais (ou sobrescritos) das bases.

Tags: C++ Polimorfismo funções virtuais Herança vtable

Publicado em 7-16 00:47