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() {}
};Desafio
FácilVamos 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 baseMediaque 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
- Um membro protegido
AudioTrack.h: Defina uma classeAudioTrackque herda publicamente deMedia:- 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-chaveoverridepara imprimir:Playing audio: <title> by <artist> - Um destrutor que imprime:
AudioTrack [<title>] destroyed
- Um membro privado
main.cpp: Leia três entradas (cada uma em uma linha separada):- Título da faixa de áudio (string)
- Nome do artista (string)
- Título do vídeo (string)
Defina uma classe
VideoClipdiretamente no main.cpp que herda publicamente deMedia:- 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
Mediacom o título "Generic Media" - Um objeto
AudioTrackcom o título e artista de entrada - Um objeto
VideoClipcom 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;
}
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
1Fundamentos de OOP
Arquivos ExternosBuild e Compilação em C++Arquivos Header e SourceNamespaces e EscopoIntrodução a OOP em C++Classes vs ObjetosO Ponteiro 'this'Métodos (Funções Membro)Atributos (Membros de Dados)Básico de Ctors e DtorsRecapitulação - Calculadora Simples4Propriedades de Classe
Membros de Instância vs EstáticosGetters e SettersFunções de Membro ConstPalavra-chave MutableMétodos e Variáveis EstáticosFunções e Classes FriendRecapitulação - Gerenciador de Conta Bancária7Herança
Herança BásicaNíveis de Acesso na HerançaOrdem de Chamada de Ctor e DtorSobrescrita de MétodosFunções Virtuais e VTableHerança MúltiplaHerança VirtualRecapitulação - Hierarquia de Funcionários2Gerenciamento de Memória
Memória Stack vs HeapPonteiros e ReferênciasMemória Dinâmica (new/delete)Smart Pointers em C++RAII em C++Recapitulação - Gerenciador de Array Dinâmico5Encapsulamento
Especificadores de Acesso em C++Especificadores de Acesso em DetalhesOcultação de InformaçãoStruct vs ClassClasses Aninhadas e InternasRecapitulação - Sistema de Registros de Alunos8Polimorfismo
Polimorfismo de Compilação vs Tempo de ExecuçãoSobrecarga de FunçõesFunções Virtuais RevisitadasFunções Virtuais PurasClasses AbstratasDesign de Interfaces em C++Dynamic Casting e RTTIRecapitulação - Calculadora de Formas3Construtores e Destrutores
Construtor PadrãoConstrutor ParametrizadoConstrutor de CópiaConstrutor de MovimentaçãoListas de Inicialização do ConstrutorConstrutores DelegadosMergulho Profundo em DestrutoresRegra dos Três / Cinco / ZeroRecapitulação - Classe String6Sobrecarga de Operadores
Introdução à Sobrecarga de OperadoresSobrecarga de Operadores AritméticosSobrecarga de Operadores de ComparaçãoOperadores de StreamSobrecarga do Operador de AtribuiçãoSobrecarga dos Operadores [] e ()Operadores de Conversão de TipoRecapitulação - Classe Matrix9Templates
Templates de FunçãoTemplates de ClasseEspecialização de TemplatesTemplates VariádicosFundamentos de SFINAE e Type TraitsRecapitulação - Container Genérico