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 substituir uma função virtual, identificando erros caso as assinaturas não correspondam.
Como isso funciona? Quando uma classe tem funções virtuais, o compilador cria uma tabela virtual (vtable). Essa é uma tabela de consulta oculta que contém 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 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. Ele adiciona uma pequena sobrecarga em comparação com chamadas de função comuns, mas possibilita um poderoso comportamento polimórfico.
Uma regra importante: se uma classe tiver funções virtuais e for usada como classe base, seu destrutor também deverá ser virtual. Isso garante a limpeza adequada ao excluir objetos derivados por meio de ponteiros para a classe base:
class Animal {
public:
virtual ~Animal() {} // Destrutor virtual
virtual void speak() {}
};Desafio
FácilVamos criar um sistema de reprodução 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 em que cada um é reproduzido de maneira diferente e verá como as funções virtuais habilitam o comportamento correto mesmo quando acessadas por meio de ponteiros para a classe base.
Você organizará seu código em três arquivos:
Media.h: Defina uma classe baseMediaque represente qualquer mídia reproduzível:- Um membro
std::string titleprotegido - Um construtor que receba um título e o armazene
- Um método virtual
play()que imprima:Playing media: <title> - Um destrutor virtual que imprima:
Media [<title>] destroyed
- Um membro
AudioTrack.h: Defina uma classeAudioTrackque herde publicamente deMedia:- Um membro privado
std::string artist - Um construtor que receba um título e um artista, passe o título para a classe base e armazene o artista
- Substitua
play()usando a palavra-chaveoverridepara imprimir:Playing audio: <title> by <artist> - Um destrutor que imprima:
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 em main.cpp que herde publicamente deMedia:- Um membro privado
int duration(em segundos) - Um construtor que receba um título e uma duração (com duração padrão de 120)
- Substitua
play()para imprimir:Playing video: <title> (<duration>s) - Um destrutor que imprima:
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 o artista informados - Um objeto
VideoClipcom o título do vídeo informado
Percorra o array e chame
play()em cada ponteiro. Em seguida, exclua 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() por meio 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 os destrutores da classe derivada quanto os da classe base são executados ao excluir por meio de um ponteiro para a classe base.
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)
// - Construtor recebe title e duration (duration padrão para 120)
// - Override play() to print: Playing video: <title> (<duration>s)
// - Destructor prints: 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 o 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:
// - A base Media object with title "Generic Media"
// - Um objeto AudioTrack com o título e o artista de entrada
// - Um objeto VideoClip com o título do vídeo de entrada
// 3. Percorra o array e chame play() em cada ponteiro
// 4. Delete todos os objetos em ordem inversa
// TODO: After the block, print: 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 POO
Arquivos externosBuild e compilação em C++Arquivos de cabeçalho e arquivos-fonteNamespaces e escopoIntrodução à POO em C++Classes vs. objetosO ponteiro 'this'Métodos (funções membro)Atributos (membros de dados)Noções básicas de construtores e destruidoresRevisão - calculadora simples4Propriedades de Classe
Membros de Instância vs. EstáticosGetters e SettersFunções Membro ConstPalavra-chave MutableMétodos e Variáveis EstáticosFunções e Classes FriendRevisão - Gerenciador de Contas Bancárias7Herança
Herança básicaNíveis de acesso da herançaOrdem de chamada de construtor e destrutorSobrescrita de métodosFunções virtuais e VTableHerança múltiplaHerança virtualRevisão - Hierarquia de funcionários10Visão geral da STL
Visão geral e filosofia da STLContêineres da STLIteradoresAlgoritmos da STLFunctors e expressões lambdaRecapitulação - Frequência de palavras13Padrões de Projeto — Parte 1
Introdução aos Padrões de ProjetoPadrão SingletonFactory e Abstract FactoryPadrão BuilderPadrão ObserverPadrão Strategy2Gerenciamento de Memória
Memória Stack vs. HeapPonteiros e ReferênciasMemória Dinâmica (new/delete)Ponteiros Inteligentes em C++RAII em C++Revisão - Gerenciador de Array Dinâmico5Encapsulamento
Especificadores de acesso em C++Especificadores de acesso em profundidadeOcultação de informaçõesStruct vs ClasseClasses aninhadas e internasRecapitulação - Sistema de registros de alunos8Polimorfismo
Polimorfismo em compilação vs. execuçãoSobrecarga de funçõesFunções virtuais revisadasFunções virtuais purasClasses abstratasDesign de interfaces em C++Conversão dinâmica e RTTIRecapitulação — Calculadora de formas11Conceitos Avançados de OOP
Composição vs. HerançaMixins via CRTPIdioma PimplApagamento de TiposClasses Enum e Tipagem ForteTratamento de Exceções em OOPHierarquias de Exceções Personalizadas14Padrões de Projeto — Parte 2
Padrão CommandPadrão AdapterPadrão DecoratorPadrão Template MethodPadrão StatePadrão CompositeRAII como Padrão3Construtores e Destrutores
Construtor PadrãoConstrutor ParametrizadoConstrutor de CópiaConstrutor de MovimentoListas de Inicialização do ConstrutorConstrutores DelegantesAprofundamento em DestrutoresRegra dos Três / Cinco / ZeroRevisão - Classe String6Sobrecarga de Operadores
Introdução à Sobrecarga de OperadoresSobrecarga de Operadores AritméticosSobrecarga de Operadores de ComparaçãoOperadores de FluxoSobrecarga do Operador de AtribuiçãoSobrecarga dos Operadores [] e ()Operadores de Conversão de TipoRecapitulação - Classe Matrix9Templates
Templates de FunçõesTemplates de ClassesEspecialização de TemplatesTemplates VariádicosNoções Básicas de SFINAE e Traits de TiposRecapitulação - Contêiner GenéricoPratique por conta própria: Compilador de C++ online