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.