Herança e polimorfismo

Substituição, acesso, construção, funções virtuais, classes abstratas, slicing e destrutores virtuais.

TLDR

Perguntar sobre esta página

ChatGPTClaudePerplexityGrokGeminiCopiar e abrirDeepSeekCopiar e abrir

Um Produto de 100 euros devolve 100 em total(). Uma Promocao com desconto de 25% devolve 75 pela mesma operação.

A classe base Produto define a interface comum. A derivada Promocao acrescenta o desconto e outra implementação do cálculo.

Herdar de uma classe base

class Promocao : public Produto estabelece herança pública: um objeto Promocao contém um subobjeto Produto e os membros próprios, e uma referência ou um apontador para Promocao pode ser convertido para o acesso correspondente à base.

A herança pública deve permitir usar a derivada onde o contrato da base é esperado. Se a derivada recusa operações válidas para a base ou muda o seu significado, a relação precisa de ser revista.

Ter código em comum não é razão suficiente para herdar. Para representar “tem um”, usa composição, com um membro de outro tipo.

Tipo estático, chamada virtual e slicing
#include <iostream>

class Produto {
public:
    explicit Produto(double preco) : preco_(preco) {}
    virtual ~Produto() = default;
    virtual double total() const { return preco_; }
protected:
    double preco() const { return preco_; }
private:
    double preco_;
};

class Promocao : public Produto {
public:
    Promocao(double preco, double desconto)
        : Produto(preco), desconto_(desconto) {}
    double total() const override { return preco() * (1.0 - desconto_); }
private:
    double desconto_;
};

int main() {
    Promocao promocao(100.0, 0.25);
    const Produto& referencia = promocao;
    Produto copia = promocao;
    std::cout << referencia.total() << " " << copia.total() << "\n";
}
Resultado

Executa o código para ver a saída e os gráficos.

Saída:

75 100

Neste exemplo o preço é não negativo e o desconto está entre 0 e 1, por isso o desconto 0.25 reduz 100 para 100 * (1 - 0.25) = 75; uma versão com dados externos teria de validar essas condições.

preco_ continua privado e os membros privados da base não são acessíveis diretamente à derivada, que o consulta através do método protegido preco(). Na herança pública, os membros públicos da base continuam públicos e os protegidos continuam protegidos; herança privada ou protegida altera a acessibilidade e não oferece a mesma substituição pública.

Funções virtuais e override

O tipo estático é o tipo conhecido a partir da declaração. referencia tem tipo estático const Produto&. O tipo dinâmico corresponde ao objeto concreto referido, aqui Promocao.

Numa chamada virtual através de uma referência ou apontador para a base, a implementação é escolhida pelo tipo dinâmico. Por isso referencia.total() chama Promocao::total() e produz 75. Sem virtual, uma chamada pela interface da base escolheria a função da base pelo tipo estático.

// Código incorreto para análise. Falta o const da base.
double total() override { return preco() * (1.0 - desconto_); }

O compilador rejeita: a assinatura sem const não substitui total() const. Sem a palavra override, o erro passaria despercebido e a chamada pela base usaria sempre o preço sem desconto. Sobrecarga escolhe entre assinaturas; substituição virtual escolhe a implementação de uma assinatura herdada. São mecanismos diferentes.

Uma chamada explicitamente qualificada, como promocao.Produto::total(), escolhe a implementação da base. Durante construção e destruição, as chamadas virtuais não chegam a partes derivadas ainda não construídas ou já destruídas. Evita depender desse tipo de despacho para inicializar a derivada.

A armadilha do slicing

Passar uma derivada por valor a um parâmetro de tipo base também corta a parte derivada. Um vector<Produto> guarda objetos base, não preserva objetos derivados completos. Para observar objetos polimórficos usa referências ou apontadores; para os possuir usa donos polimórficos, como vector<unique_ptr<Forma>> da secção seguinte.

Classes abstratas

Uma função virtual double area() const = 0; é virtual pura. A classe é abstrata e não permite construir um objeto diretamente. Define uma operação que as classes concretas devem completar.

Programa completo para compilar localmente:

#include <iostream>
#include <memory>
#include <stdexcept>
#include <vector>

class Forma {
public:
    virtual ~Forma() = default;
    virtual double area() const = 0;
};

class Retangulo : public Forma {
public:
    Retangulo(double largura, double altura) : largura_(largura), altura_(altura) {
        if (!(largura > 0 && largura <= 1000000 && altura > 0 && altura <= 1000000))
            throw std::invalid_argument("Dimensoes invalidas");
    }
    double area() const override { return largura_ * altura_; }
private:
    double largura_;
    double altura_;
};

class Quadrado : public Forma {
public:
    explicit Quadrado(double lado) : lado_(lado) {
        if (!(lado > 0 && lado <= 1000000))
            throw std::invalid_argument("Lado invalido");
    }
    double area() const override { return lado_ * lado_; }
private:
    double lado_;
};

int main() {
    std::vector<std::unique_ptr<Forma>> formas;
    formas.push_back(std::make_unique<Retangulo>(3.0, 4.0));
    formas.push_back(std::make_unique<Quadrado>(5.0));
    double total = 0;
    for (const auto& forma : formas) total += forma->area();
    std::cout << total << "\n";
}

Saída:

37

O vetor guarda donos de Forma para um retângulo e um quadrado, e cada chamada forma->area() escolhe a implementação concreta, por isso a soma é 12 + 25 = 37.

Usamos const auto& no ciclo porque um unique_ptr não se copia. O apontador permite chamar métodos do objeto; a propriedade exclusiva mantém-no vivo. Este desenho não exige expor os campos de cada forma.

A regra do destrutor virtual

virtual ~Forma() = default; permite destruir primeiro a parte derivada e depois a base. Se a interface proíbe destruição pela base, outra opção de desenho é um destrutor protegido não virtual. Para uma coleção dona de formas, o destrutor público virtual é a escolha adequada.

Ordem de construção

Uma derivada constrói primeiro a base, depois os seus membros pela ordem de declaração e finalmente executa o corpo do construtor. Em Promocao, Produto(preco) corre antes de inicializar desconto_. A destruição faz a ordem inversa. A derivada não inicializa diretamente campos privados da base; chama o construtor da base.

Herança ou composição

Um quadrado pode partilhar o cálculo de área com um retângulo, mas uma interface de retângulo com setters independentes de largura e altura cria um problema. Um quadrado precisa de manter as dimensões iguais. Herdar dessa interface alteraria o contrato dos setters. No exemplo, ambos implementam Forma e cada tipo conserva o seu próprio invariante. A relação escolhida depende das operações públicas, não só do desenho geométrico.

Exercícios

Preço por valor e por referência

Usa as classes Produto e Promocao da lição, com total() virtual. Prevê os dois resultados abaixo e identifica o tipo dinâmico do objeto em cada chamada.

double por_valor(Produto p) { return p.total(); }
double por_referencia(const Produto& p) { return p.total(); }
// Em main:
Promocao promocao(80, 0.25);
std::cout << por_valor(promocao) << " " << por_referencia(promocao);

Pista
Decide em que chamada se constrói outro objeto e que parte é copiada.
Solução

A saída é 80 60. O parâmetro de por_valor é outro Produto, construído só da base: o seu tipo dinâmico é Produto. Em por_referencia, a referência observa a promoção original, com tipo dinâmico Promocao, e calcula 80 * (1 - 0.25) = 60. A virtual escolhe pelo objeto que existe; não reconstitui desconto num objeto que perdeu essa parte.

Confere a tua resposta:

  • Obtive 80 por valor e 60 por referência.
  • Identifiquei o objeto novo da primeira chamada e o original da segunda.
  • Expliquei por que virtual não recupera a parte que foi cortada.
Voltar à explicação
Destruir pela interface base

Uma coleção usa std::unique_ptr<Base> para possuir objetos Derivada. Base tem métodos virtuais, mas o seu destrutor público não é virtual. O que deve mudar?

Pista

Considera a operação de destruição efetuada pelo unique_ptr.

Solução

O destrutor da Base deve ser virtual para permitir esta propriedade polimórfica, por exemplo virtual ~Base() = default;. Assim, a destruição percorre Derivada e Base. Só ter outros métodos virtuais não torna automaticamente virtual o destrutor.

Confere a tua resposta:

  • Expliquei que a destruição acontece através de Base*.
  • Indiquei um destrutor público virtual.
  • Distingui gestão automática de validade da destruição polimórfica.

Erros frequentes

unique_ptr automatiza a destruição escolhida, mas não corrige um tipo base inadequado.

Voltar à explicação
Detetar uma assinatura que não substitui

Uma derivada declara double total() sem const nem override, para substituir virtual double total() const da base. O que acontece numa chamada pela referência da base? Como o compilador ajuda?

Pista

Compara as duas assinaturas caráter a caráter.

Solução

A chamada pela base usa a implementação da base e ignora o desconto. Com override na derivada, o compilador rejeita a declaração e indica que nada foi substituído. A sobrecarga escolhe entre assinaturas; a substituição virtual escolhe a implementação de uma assinatura herdada.

Confere a tua resposta:

  • Identifiquei a falta do const na derivada.
  • Expliquei por que sem override o erro passa despercebido.
  • Distingui sobrecarga de substituição virtual.

Erros frequentes

Não acrescentes outro virtual na derivada para corrigir. Acrescenta o const e o override.

Voltar à explicação

À tua maneira

Aparência

Modo de cor
Tema
Cor de destaque

Leitura

Fonte de leitura

Álgebra, lógica e uma ideia de cada vez.

Código

Fonte do código

Navegação

Cursor de leitura, sem edição. h, j, k, l movem; / pesquisa nesta página. v seleciona caracteres; V seleciona linhas; y copia. Tab permite destacar ou comentar. Espaço ou Enter abre blocos recolhíveis. Nos cartões, h, j, k, l mudam o foco.

CSS personalizado

Ativa uma sugestão ou guarda o teu CSS. Só muda este navegador.

Usa --page, --text e --accent para acompanhar o tema.

Seletores e recuperação

.site-header, .prose, .course-card, [data-reading-history], [data-playground] e .page-actions.

Abre a aparência com a tecla vírgula ou no rodapé. Se o CSS esconder os controlos, abre esta página sem CSS personalizado.

Pesquisar

Escreve para pesquisar em todo o site.

para escolher · Enter para abrir · Esc para fechar

Copiar fórmula

A cópia automática não está disponível. Copia o LaTeX abaixo.

Pesquisar nesta página

Enter procura · n / N repetem · Esc cancela. Ctrl / Cmd K pesquisa todo o site.

Atalhos de teclado

Clica numa tecla para a mudar. Esc cancela. Backspace desativa.

PesquisarCtrl / Cmd K

Os atalhos não interferem enquanto escreves. Tab e Enter funcionam sempre.