Uma Classe que Nasce de Outra — Herança e a Cadeia de Construção

Uma Classe que Nasce de Outra — Herança e a Cadeia de Construção

Fazer uma classe nascer de outra é a parte fácil; saber quando não fazer é o que separa hierarquia útil de acoplamento permanente. O artigo percorre a mecânica da derivação e a cadeia de construção em que a base nasce primeiro, e fecha com o critério honesto de quando preferir composição.
Linguagem C++

11 min de leitura

No artigo A Vida Começa Aqui — Inicialização de Membros a Fundo dominamos como um objeto nasce, incluindo a ordem em que seus membros são construídos. Hoje esticamos essa ideia para além de uma classe só: vamos fazer uma classe nascer de outra. A herança é o mecanismo pelo qual uma classe (a derivada) recebe automaticamente os membros de outra (a base), podendo reaproveitá-los e acrescentar os seus. É um dos pilares da orientação a objetos — e também um dos mais mal utilizados na história da programação. Por isso serei honesto desde o início: herança é poderosa, mas frequentemente é a ferramenta errada, e saber quando não usá-la é tão importante quanto saber a sintaxe. Hoje aprendemos as duas coisas.

A mecânica: derivar uma classe de outra

Herança expressa uma relação "é um": um Gato é um Animal, um Botao é um Widget. A classe derivada ganha os membros públicos e protegidos da base:

#include <iostream>
#include <string>

class Animal {
public:
    Animal(const std::string& nome) : nome_(nome) {}
    void respira() const { std::cout << nome_ << " respira\n"; }
    const std::string& nome() const { return nome_; }
protected:
    std::string nome_;   // 'protected': visível para as DERIVADAS, não para o mundo
};

// Cachorro deriva de Animal: "um Cachorro é um Animal".
class Cachorro : public Animal {
public:
    // O construtor da derivada precisa construir a parte-base PRIMEIRO.
    Cachorro(const std::string& nome) : Animal(nome) {}
    void late() const { std::cout << nome_ << " faz au au\n"; }  // usa nome_ herdado
};

int main() {
    Cachorro rex("Rex");
    rex.respira();   // método herdado de Animal → Rex respira
    rex.late();      // método próprio de Cachorro → Rex faz au au
    std::cout << rex.nome() << '\n';   // getter herdado → Rex
    return 0;
}

Três coisas para notar. Primeiro, class Cachorro : public Animal estabelece a herança pública — a forma padrão, que preserva a interface da base. Segundo, o novo nível de acesso protected: membros protegidos são invisíveis de fora (como private), mas visíveis para as classes derivadas — é o meio-termo que permite à Cachorro usar nome_ diretamente. Terceiro, e crucial, o construtor de Cachorro chama Animal(nome) na lista de inicialização.

A cadeia de construção: a base nasce primeiro

Aqui a aula de inicialização do artigo A Vida Começa Aqui — Inicialização de Membros a Fundo se estende entre classes. Quando você cria um Cachorro, a parte Animal dele precisa existir antes que a parte Cachorro possa se apoiar nela. Por isso o construtor da base roda primeiro, depois o da derivada. E a destruição segue a ordem inversa — a derivada morre antes da base, como quem desmonta um andaime de cima para baixo:

#include <iostream>

class Base {
public:
    Base()  { std::cout << "Base construída\n"; }
    ~Base() { std::cout << "Base destruída\n"; }
};

class Derivada : public Base {
public:
    Derivada()  { std::cout << "Derivada construída\n"; }
    ~Derivada() { std::cout << "Derivada destruída\n"; }
};

int main() {
    std::cout << "-- criando --\n";
    Derivada d;
    std::cout << "-- destruindo --\n";
    return 0;
}

Saída:

-- criando --
Base construída
Derivada construída
-- destruindo --
Derivada destruída
Base destruída

Construção de dentro para fora (base → derivada); destruição de fora para dentro (derivada → base). Essa simetria não é decorativa: garante que, durante a construção da derivada, a base já está pronta para uso, e que, durante a destruição da derivada, a base ainda está intacta. Se a base tem um construtor com parâmetros, a derivada é obrigada a chamá-lo explicitamente na lista de inicialização — como Cachorro fez com Animal(nome).

Reaproveitar comportamento e acrescentar o seu

A derivada pode adicionar membros e também chamar métodos da base a partir dos seus, compondo comportamento. Um exemplo com uma pequena hierarquia de formas geométricas, onde a derivada estende a descrição da base:

#include <iostream>
#include <string>

class Forma {
public:
    Forma(const std::string& cor) : cor_(cor) {}
    void descreve() const { std::cout << "forma " << cor_; }
protected:
    std::string cor_;
};

class Circulo : public Forma {
public:
    Circulo(const std::string& cor, double raio) : Forma(cor), raio_(raio) {}
    double area() const { return 3.14159 * raio_ * raio_; }
    void descreve_completo() const {
        Forma::descreve();                         // chama o método da BASE explicitamente
        std::cout << ", círculo de raio " << raio_ << ", área " << area() << '\n';
    }
private:
    double raio_;
};

int main() {
    Circulo c("azul", 2.0);
    c.descreve_completo();   // forma azul, círculo de raio 2, área 12.5664
    return 0;
}

Forma::descreve() invoca explicitamente o método da base pelo operador de escopo :: (o mesmo do artigo A Filosofia por Trás do C++ e a Saída Elegante do printf, agora entre classes). Isso permite à derivada reusar e estender o que a base já faz, em vez de reescrever.

A honestidade central: quando NÃO usar herança

Prometi franqueza, e este é o parágrafo mais importante da aula. Herança cria o acoplamento mais forte que existe entre dois tipos: a derivada depende dos detalhes da base, e mudanças na base podem quebrar todas as derivadas. Por isso, a diretriz moderna é clara: use herança só quando houver uma genuína relação "é um" e você precisar de polimorfismo (o assunto do próximo artigo). Se você só quer reaproveitar código, quase sempre a composição — ter um objeto da outra classe como membro — é melhor. A pergunta-teste: "X é um Y, ou X tem um Y?". Um Carro não é um Motor; ele tem um Motor. Herdar Carro de Motor seria um erro de modelagem. Compare:

#include <iostream>

class Motor {
public:
    void ligar() const { std::cout << "motor ligado\n"; }
};

// ERRADO conceitualmente: Carro NÃO é um Motor.
// class Carro : public Motor { };

// CERTO: composição — Carro TEM um Motor.
class Carro {
public:
    void dar_partida() const {
        motor_.ligar();               // delega ao membro
        std::cout << "carro andando\n";
    }
private:
    Motor motor_;                     // Carro possui um Motor
};

int main() {
    Carro c;
    c.dar_partida();
    return 0;
}

A composição é mais flexível (você pode trocar o motor, ter vários, mudar sem afetar a interface do carro) e mais fraca em acoplamento. A regra de ouro que carregaremos pelo resto do curso: prefira composição a herança, e reserve a herança para os casos em que precisar tratar objetos derivados através de uma interface comum da base — que é exatamente o poder que destravamos no próximo artigo.

Herança é a ferramenta de reuso mais fácil de usar mal, porque funciona igual quando é adequada e quando não é. O teste que envelhece bem não é "a derivada aproveita código da base?", e sim se ela pode ocupar o lugar da base sem que quem usa perceba. Quando a resposta é não — e com frequência é — composição entrega o mesmo reuso sem o acoplamento permanente que uma hierarquia impõe a todo mundo que vier depois.

Fontes e leituras recomendadas

  • Bjarne Stroustrup, A Tour of C++ (3ª ed.), capítulo sobre hierarquias de classes: a apresentação de herança e composição pelo criador da linguagem.
  • cppreference.com/w/cpp/language/derived_class: a referência técnica sobre herança, especificadores de acesso e a ordem de construção/destruição.
  • ISO C++ Core Guidelines, regras C.120 ("Use class hierarchies to represent concepts with inherent hierarchical structure") e C.129 (herança de interface vs implementação): as diretrizes sobre quando herdar.
  • Scott Meyers, Effective C++ (3ª ed.), Item 32 ("Make sure public inheritance models 'is-a'") e Item 38 ("Model 'has-a' through composition"): a discussão canônica de "é um" vs "tem um".
  • ISO C++ Core Guidelines, regra C.135 e a máxima "prefer composition over inheritance": o respaldo oficial para a regra de ouro da aula.

Exercícios

Exercício 1

Crie uma classe base Veiculo com um membro protegido rodas_ e um método info() const, e uma derivada Moto que passe rodas_ = 2 para a base e adicione um método empina() const. Demonstre que a Moto acessa membros herdados.

Ver resposta

✓ Resposta: A hierarquia:

#include <iostream>
class Veiculo {
public:
    Veiculo(int rodas) : rodas_(rodas) {}
    void info() const { std::cout << "veículo de " << rodas_ << " rodas\n"; }
protected:
    int rodas_;
};
class Moto : public Veiculo {
public:
    Moto() : Veiculo(2) {}                       // passa rodas_ = 2 para a base
    void empina() const { std::cout << "empinando sobre " << rodas_ - 1 << " roda\n"; }
};
int main() {
    Moto m;
    m.info();      // veículo de 2 rodas  (método herdado)
    m.empina();    // empinando sobre 1 roda (usa rodas_ herdado)
    return 0;
}

Moto acessa rodas_ (protegido) e info() (público) herdados, e construiu a parte-base passando 2 ao construtor de Veiculo.

Exercício 2

Adicione mensagens nos construtores e destrutores de uma base e uma derivada e preveja, antes de rodar, a ordem exata das quatro mensagens ao criar e destruir um objeto derivado. Explique por que essa ordem é necessária.

Ver resposta

✓ Resposta: A ordem das quatro mensagens ao criar e destruir uma Derivada:

Base construída
Derivada construída
Derivada destruída
Base destruída

Construção de dentro para fora: a base nasce primeiro para que a derivada possa se apoiar numa base já pronta. Destruição de fora para dentro: a derivada morre primeiro para que, enquanto ela se desmonta, a base ainda esteja íntegra (a derivada pode usar membros da base em seu próprio destrutor). Inverter qualquer das ordens deixaria uma das partes usando a outra em estado inconsistente.

Exercício 3

O código abaixo não compila. Explique por quê e corrija.

#include <string>
class Base {
public:
    Base(int x) : x_(x) {}
protected:
    int x_;
};
class Derivada : public Base {
public:
    Derivada() {}   // por que isto falha?
};
Ver resposta

✓ Resposta: Não compila porque Base só tem um construtor que exige um int, e o construtor de Derivada não o chama — então o compilador não sabe como construir a parte-base. A base precisa ser construída explicitamente:

class Derivada : public Base {
public:
    Derivada() : Base(0) {}   // chama o construtor da base com um valor
};

Como Base não tem construtor padrão (sem argumentos), a derivada é obrigada a invocar Base(algum_int) na lista de inicialização.

Exercício 4

Para cada par, diga se a relação correta é herança ("é um") ou composição ("tem um"), justificando: (a) Quadrado e Retangulo; (b) Casa e Porta; (c) Gerente e Funcionario; (d) Playlist e Musica.

Ver resposta

✓ Resposta: As relações: - (a) Quadrado e Retangulo: conceitualmente parece "é um" (um quadrado é um retângulo com lados iguais), mas na prática essa herança é famosa por causar problemas de substituição (o problema quadrado-retângulo); frequentemente é melhor não herdar. A resposta "livresca" é herança, mas com a ressalva de que este é um caso onde "é um" matemático não vira bom "é um" de código. - (b) Casa e Porta: composição — uma casa tem portas, não é uma porta. - (c) Gerente e Funcionario: herança — um gerente é um funcionário (com atribuições extras). - (d) Playlist e Musica: composição — uma playlist tem músicas (uma coleção delas), não é uma música.

Exercício 5

Refatore o design equivocado abaixo (que usa herança para reaproveitar código) para usar composição, e explique por que a composição é superior neste caso.

#include <vector>
#include <iostream>
// ERRADO: Pilha NÃO é um vetor; ela apenas usa um por dentro.
class Pilha : public std::vector<int> {
public:
    void empurra(int v) { push_back(v); }
    int  topo() const { return back(); }
};
Ver resposta

✓ Resposta: Refatoração para composição:

#include <vector>
#include <iostream>
class Pilha {
public:
    void empurra(int v) { dados_.push_back(v); }
    int  topo() const { return dados_.back(); }
    void desempilha() { dados_.pop_back(); }
    bool vazia() const { return dados_.empty(); }
private:
    std::vector<int> dados_;   // Pilha TEM um vetor; não É um vetor
};
int main() {
    Pilha p;
    p.empurra(1);
    p.empurra(2);
    std::cout << p.topo() << '\n';   // 2
    return 0;
}

A composição é superior aqui porque herdar de std::vector exporia toda a interface do vetor (insert, operator[], resize...) como se fizesse parte da pilha, permitindo violar o próprio conceito de pilha — alguém poderia inserir no meio, quebrando a disciplina LIFO. Com composição, a Pilha expõe apenas as operações que fazem sentido (empurra, topo, desempilha, vazia) e mantém o vetor escondido como detalhe de implementação, que pode até ser trocado depois sem afetar quem usa a pilha. std::vector não tem destrutor virtual, o que torna herdar dele tecnicamente arriscado — um ponto que ficará claro no próximo artigo.

Comentários

Mais em Linguagem C++

Menu, Jogo, Fim — Estados de Jogo e o Snake Completo
Menu, Jogo, Fim — Estados de Jogo e o Snake Completo

Controlar telas com um punhado de booleanos admite estados impossíveis, e o…

Ensinando Símbolos ao seu Tipo — Sobrecarga de Operadores
Ensinando Símbolos ao seu Tipo — Sobrecarga de Operadores

Somar com mais, comparar com igual, imprimir direto no cout: operadores são…

Buscas Instantâneas — map, set e suas Versões Hash
Buscas Instantâneas — map, set e suas Versões Hash

Nem todo problema é sobre sequências; muitos são sobre associações, e em C…