Menu
Coddy logo textTech

Funções Virtuais e VTable

Parte da seção Programação Orientada a Objetos do Journey de C++ da Coddy — lição 52 de 104.

A palavra-chave virtual resolve o problema que vimos na lição anterior. Quando você declara um método como virtual, o C++ determina qual versão chamar com base no tipo real do objeto em tempo de execução, não no tipo do ponteiro.

class Animal {
public:
    virtual void speak() {
        std::cout << "Some sound" << std::endl;
    }
};

class Dog : public Animal {
public:
    void speak() override {
        std::cout << "Woof!" << std::endl;
    }
};

Dog d;
Animal* ptr = &d;
ptr->speak();  // Saída: Woof!

Agora o método correto é chamado! A palavra-chave override é opcional, mas recomendada. Ela informa ao compilador que você pretende sobrescrever uma função virtual, capturando erros se as assinaturas não coincidirem.

Como isso funciona? Quando uma classe possui funções virtuais, o compilador cria uma tabela virtual (vtable). Esta é uma tabela de busca oculta contendo ponteiros para as funções virtuais da classe. Cada objeto com funções virtuais armazena um ponteiro oculto (vptr) para a vtable de sua classe.

Quando você chama uma função virtual através de um ponteiro, o programa procura o endereço correto da função na vtable em tempo de execução. Isso é chamado de despacho dinâmico. Isso adiciona uma pequena sobrecarga em comparação com chamadas de função regulares, mas permite um comportamento polimórfico poderoso.

Uma regra importante: se uma classe possui funções virtuais e será usada como uma classe base, seu destrutor também deve ser virtual. Isso garante a limpeza adequada ao excluir objetos derivados por meio de ponteiros base:

class Animal {
public:
    virtual ~Animal() {}  // Destrutor virtual
    virtual void speak() {}
};
challenge icon

Desafio

Fácil

Vamos construir um sistema de player de mídia que demonstra o poder das funções virtuais e do polimorfismo em tempo de execução. Você criará uma hierarquia de tipos de mídia onde cada uma é reproduzida de forma diferente e verá como as funções virtuais permitem o comportamento correto, mesmo quando acessadas por meio de ponteiros da classe base.

Você organizará seu código em três arquivos:

  • Media.h: Defina uma classe base Media que representa qualquer mídia reproduzível:
    • Um membro protegido std::string title
    • Um construtor que recebe um título e o armazena
    • Um método virtual play() que imprime: Playing media: <title>
    • Um destrutor virtual que imprime: Media [<title>] destroyed
  • AudioTrack.h: Defina uma classe AudioTrack que herda publicamente de Media:
    • Um membro privado std::string artist
    • Um construtor que recebe um título e um artista, passa o título para a classe base e armazena o artista
    • Sobrescreva play() usando a palavra-chave override para imprimir: Playing audio: <title> by <artist>
    • Um destrutor que imprime: AudioTrack [<title>] destroyed
  • main.cpp: Leia três entradas (cada uma em uma linha separada):
    1. Título da faixa de áudio (string)
    2. Nome do artista (string)
    3. Título do vídeo (string)

    Defina uma classe VideoClip diretamente no main.cpp que herda publicamente de Media:

    • Um membro privado int duration (em segundos)
    • Um construtor que recebe um título e uma duração (duração padrão de 120)
    • Sobrescreva play() para imprimir: Playing video: <title> (<duration>s)
    • Um destrutor que imprime: VideoClip [<title>] destroyed

    Crie um array de três ponteiros Media* dentro de um escopo de bloco. Aloque dinamicamente:

    • Um objeto base Media com o título "Generic Media"
    • Um objeto AudioTrack com o título e artista de entrada
    • Um objeto VideoClip com o título do vídeo de entrada

    Percorra o array e chame play() em cada ponteiro. Em seguida, delete todos os objetos na ordem inversa. Após o bloco, imprima: Playback complete!

Por exemplo, com as entradas Bohemian Rhapsody, Queen e Nature Documentary:

Playing media: Generic Media
Playing audio: Bohemian Rhapsody by Queen
Playing video: Nature Documentary (120s)
VideoClip [Nature Documentary] destroyed
Media [Nature Documentary] destroyed
AudioTrack [Bohemian Rhapsody] destroyed
Media [Bohemian Rhapsody] destroyed
Media [Generic Media] destroyed
Playback complete!

Observe como chamar play() através de ponteiros Media* invoca o método correto da classe derivada graças às funções virtuais. Observe também como o destrutor virtual garante a limpeza adequada — tanto o destrutor da classe derivada quanto o da classe base são executados ao deletar através de um ponteiro base.

Folha de consulta

A palavra-chave virtual habilita o polimorfismo em tempo de execução, determinando qual método chamar com base no tipo real do objeto, não no tipo do ponteiro:

class Animal {
public:
    virtual void speak() {
        std::cout << "Some sound" << std::endl;
    }
};

class Dog : public Animal {
public:
    void speak() override {
        std::cout << "Woof!" << std::endl;
    }
};

Dog d;
Animal* ptr = &d;
ptr->speak();  // Output: Woof!

A palavra-chave override é opcional, mas recomendada. Ela informa ao compilador que você pretende sobrescrever uma função virtual, capturando erros se as assinaturas não coincidirem.

Como as funções virtuais funcionam: O compilador cria uma tabela virtual (vtable) contendo ponteiros para as funções virtuais da classe. Cada objeto com funções virtuais armazena um ponteiro oculto (vptr) para a vtable de sua classe. Ao chamar uma função virtual por meio de um ponteiro, o programa procura o endereço correto da função na vtable em tempo de execução. Isso é chamado de despacho dinâmico (dynamic dispatch).

Destrutores virtuais: Se uma classe possui funções virtuais e será usada como classe base, seu destrutor também deve ser virtual. Isso garante a limpeza adequada ao excluir objetos derivados por meio de ponteiros da base:

class Animal {
public:
    virtual ~Animal() {}  // Destrutor virtual
    virtual void speak() {}
};

Experimente você mesmo

#include <iostream>
#include <string>
#include "Media.h"
#include "AudioTrack.h"

using namespace std;

// TODO: Defina a classe VideoClip aqui que herda publicamente de Media
// - Membro privado int duration (em segundos)
// - O construtor recebe title e duration (duration padrão para 120)
// - Sobrescreva play() para imprimir: Playing video: <title> (<duration>s)
// - O destrutor imprime: VideoClip [<title>] destroyed

class VideoClip : public Media {
private:
    int duration;

public:
    // TODO: Implemente o construtor
    VideoClip(const std::string& t, int d = 120) : Media(t) {
        // TODO: Armazene a duration
    }

    // TODO: Sobrescreva o método play()
    void play() override {
        // TODO: Implemente este método
    }

    // TODO: Implemente o destrutor
    ~VideoClip() {
        // TODO: Implemente este destrutor
    }
};

int main() {
    // Leia as entradas
    string audioTitle;
    string artist;
    string videoTitle;
    
    getline(cin, audioTitle);
    getline(cin, artist);
    getline(cin, videoTitle);

    // TODO: Crie um escopo de bloco com chaves
    // Dentro do bloco:
    // 1. Crie um array de três ponteiros Media*
    // 2. Aloque dinamicamente:
    //    - Um objeto Media base com o title "Generic Media"
    //    - Um objeto AudioTrack com o title e artist de entrada
    //    - Um objeto VideoClip com o title do vídeo de entrada
    // 3. Percorra o array e chame play() em cada ponteiro
    // 4. Delete todos os objetos em ordem inversa

    // TODO: Após o bloco, imprima: Playback complete!
    
    return 0;
}
quiz iconTeste seus conhecimentos

Esta lição inclui um quiz rápido. Comece a lição para respondê-lo e acompanhar seu progresso.

Todas as lições de Programação Orientada a Objetos