Herança e polimorfismo
Substituição, acesso, construção, funções virtuais, classes abstratas, slicing e destrutores virtuais.
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.
#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";
}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
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.
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.
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.