C++ Herança
A lojinha da Ana vende canetas, cadernos e livros. Todo produto tem nome e preço; mas o livro tem também um autor. Você poderia copiar a classe Produto inteira, colar com o nome Livro e acrescentar o autor. Funciona, até o dia em que você corrige um erro no cálculo do desconto em uma e esquece da outra. A herança resolve isso: você diz "um Livro é um Produto, com algo a mais", e a classe nova recebe tudo da antiga, sem copiar uma linha.
- Como criar uma classe a partir de outra:
class Livro : public Produto. - O que a classe filha herda, o que ela acrescenta e o que ela não pode tocar.
- Como chamar o construtor da classe base na lista de inicialização.
- O nível de acesso
protected, entre opublice oprivate. - A ordem real em que as partes nascem e morrem.
- Como redefinir um método e ainda usar a versão da base (
Produto::mostrar()). - A ideia da herança pública × privada, e como proibir a herança com
final.
Antes, é bom conhecer Classes e Objetos, Construtores e public e private. Os programas foram compilados com o g++ 15 na máquina da Ana, na pasta ~/cpp, com g++ -std=c++23 -Wall -Wextra arquivo.cpp -o programa.
A ideia: "é um"
Na escola você aprendeu que "todo cachorro é um mamífero": ele tem tudo o que um mamífero tem (respira, mama quando filhote) e mais algumas coisas só dele (late). Herança é isso em código:
| Nome | Também chamada de | No exemplo |
|---|---|---|
| Classe base | classe mãe, superclasse | Produto: nome, preço, desconto |
| Classe derivada | classe filha, subclasse | Livro: tudo do Produto + autor |
Antes de usar herança, faça o teste do "é um": a frase "um Livro é um Produto" faz sentido? Sim: herança serve. "Um Carro é um Motor"? Não: o carro tem um motor. Nesse caso, o motor vira um atributo do carro (isso se chama composição), e não uma classe base.
1. A primeira herança
#include <iostream>
#include <string>
class Produto {
public:
Produto(std::string nome, double preco) : nome_{nome}, preco_{preco} {}
std::string nome() const { return nome_; }
double preco() const { return preco_; }
void aplicar_desconto(double pct) { preco_ -= preco_ * pct / 100; }
void mostrar() const {
std::cout << nome_ << " — R$ " << preco_ << '\n';
}
private:
std::string nome_;
double preco_;
};
class Livro : public Produto {
public:
Livro(std::string nome, double preco, std::string autor)
: Produto{nome, preco}, autor_{autor} {}
std::string autor() const { return autor_; }
private:
std::string autor_;
};
int main() {
Produto caneta{"Caneta azul", 3.5};
Livro livro{"Dom Casmurro", 40, "Machado de Assis"};
caneta.mostrar();
livro.mostrar(); // herdado de Produto
livro.aplicar_desconto(10); // herdado de Produto
std::cout << livro.nome() << ", de " << livro.autor()
<< ", agora custa R$ " << livro.preco() << '\n';
}
- class Livro
- Uma classe nova, como qualquer outra.
- :
- Os dois-pontos depois do nome querem dizer "derivada de".
- public
- O tipo de herança: o que era
publicnoProdutocontinuapublicnoLivro. É o que você vai usar em 99% dos casos (a seção 6 mostra a outra opção). - Produto
- A classe base.
- : Produto{nome, preco}
- Na lista de inicialização do construtor do
Livro: chama o construtor doProduto, para montar a "parte produto" do livro. - autor_{autor}
- Depois, inicializa o que é só do livro.
- std::string autor() const
- Um método novo, que só o
Livrotem.
$ g++ -std=c++23 -Wall -Wextra livro.cpp -o livro $ ./livro Caneta azul — R$ 3.5 Dom Casmurro — R$ 40 Dom Casmurro, de Machado de Assis, agora custa R$ 36
A classe Livro tem só um atributo e um método escritos nela, mas o objeto livro usou mostrar(), aplicar_desconto(), nome() e preco(), todos herdados. Por dentro, todo Livro carrega um Produto completo, com mais um pedaço no fim:
┌─────────── Livro ────────────┐ │ ┌──────── Produto ─────────┐ │ │ │ nome_ = "Dom Casmurro" │ │ │ │ preco_ = 36 │ │ │ └──────────────────────────┘ │ │ autor_ = "Machado de Assis" │ └──────────────────────────────┘
Experimente: rode como está. Depois chame livro.aplicar_desconto(50); e mostre de novo. Para ir além, crie uma classe Caderno : public Produto com um atributo int folhas_ e um método folhas().
#include <iostream>
#include <string>
class Produto {
public:
Produto(std::string nome, double preco) : nome_{nome}, preco_{preco} {}
std::string nome() const { return nome_; }
double preco() const { return preco_; }
void aplicar_desconto(double pct) { preco_ -= preco_ * pct / 100; }
void mostrar() const {
std::cout << nome_ << " — R$ " << preco_ << '\n';
}
private:
std::string nome_;
double preco_;
};
class Livro : public Produto {
public:
Livro(std::string nome, double preco, std::string autor)
: Produto{nome, preco}, autor_{autor} {}
std::string autor() const { return autor_; }
private:
std::string autor_;
};
int main() {
Livro livro{"Dom Casmurro", 40, "Machado de Assis"};
livro.mostrar();
std::cout << "Autor: " << livro.autor() << '\n';
}
2. O construtor da classe base
Construtores não são herdados como os outros métodos: o Livro precisa do seu próprio. E ele tem a obrigação de construir a parte Produto que carrega por dentro. O lugar para isso é a lista de inicialização, antes dos atributos:
Livro(std::string nome, double preco, std::string autor)
: Produto{nome, preco}, // 1º: a parte Produto
autor_{autor} // 2º: o que é só do Livro
{}
Se você esquecer o Produto{…}, o C++ tenta usar um construtor sem parâmetros do Produto. Como ele não existe, o g++ reclama (o erro completo está em Quando dá errado):
$ g++ -std=c++23 -Wall -Wextra h3.cpp -o h3
h3.cpp: In constructor ‘Livro::Livro(std::string, double, std::string)’:
h3.cpp:15:23: error: no matching function for call to ‘Produto::Produto()’
15 | : autor_{autor} {}
| ^
Se a classe filha não acrescenta atributos que precisem de valor, ela pode reaproveitar os construtores da base com uma linha: using Produto::Produto;. Aí Brinde b{"Chaveiro", 0}; chama direto o construtor do Produto.
3. O que a filha pode tocar: protected
A classe Livro herdou preco_, mas ele é private no Produto. Privado quer dizer "só a própria classe", e a filha é outra classe. Tente usar preco_ num método do Livro:
class Livro : public Produto {
public:
// ... construtor
void etiqueta() const {
std::cout << nome() << " (" << autor_ << ") R$ " << preco_ << '\n';
}
private:
std::string autor_;
};
$ g++ -std=c++23 -Wall -Wextra h1.cpp -o h1
h1.cpp: In member function ‘void Livro::etiqueta() const’:
h1.cpp:19:61: error: ‘double Produto::preco_’ is private within this context
19 | std::cout << nome() << " (" << autor_ << ") R$ " << preco_ << '\n';
| ^~~~~~
h1.cpp:10:12: note: declared private here
10 | double preco_;
| ^~~~~~
"Produto::preco_ é privado neste contexto". A parte Produto existe dentro do livro, mas o código do Livro não pode mexer nela diretamente. Há dois caminhos:
- Usar os métodos públicos da base:
preco()em vez depreco_. É o caminho mais seguro, porque oProdutocontinua no controle das próprias regras. - Marcar o atributo como
protected("protegido"): fica invisível para o resto do programa, mas visível para as classes filhas.
class Produto {
public:
Produto(std::string nome, double preco) : nome_{nome}, preco_{preco} {}
std::string nome() const { return nome_; }
private:
std::string nome_;
protected:
double preco_; // filhas podem usar; o main, não
};
Com isso, etiqueta() compila. Mas o main continua barrado:
$ g++ -std=c++23 -Wall -Wextra h2.cpp -o h2
h2.cpp: In function ‘int main()’:
h2.cpp:29:11: error: ‘double Produto::preco_’ is protected within this context
29 | livro.preco_ = 0;
| ^~~~~~
h2.cpp:11:12: note: declared protected here
11 | double preco_;
| ^~~~~~
| Acesso | A própria classe | Classes filhas | Resto do programa |
|---|---|---|---|
public | sim | sim | sim |
protected | sim | sim | não |
private | sim | não | não |
protected com parcimônia
Um atributo protected pode ser alterado por qualquer classe filha, inclusive as que outras pessoas escreverem daqui a um ano. Se o Produto tem uma regra ("preço nunca negativo"), ela só é garantida enquanto o atributo for private. A recomendação comum: atributos private; métodos protected quando forem ferramentas só para as filhas.
4. A ordem de construção e destruição
Se o livro carrega um produto por dentro, quem nasce primeiro? Vamos perguntar ao programa:
#include <iostream>
#include <string>
class Produto {
public:
Produto(std::string nome) : nome_{nome} {
std::cout << " Produto(" << nome_ << ")\n";
}
~Produto() {
std::cout << " ~Produto(" << nome_ << ")\n";
}
private:
std::string nome_;
};
class Livro : public Produto {
public:
Livro(std::string nome, std::string autor) : Produto{nome}, autor_{autor} {
std::cout << " Livro(" << autor_ << ")\n";
}
~Livro() {
std::cout << " ~Livro(" << autor_ << ")\n";
}
private:
std::string autor_;
};
int main() {
std::cout << "Criando o livro:\n";
Livro livro{"Dom Casmurro", "Machado de Assis"};
std::cout << "Fim do main:\n";
}
$ g++ -std=c++23 -Wall -Wextra ordem.cpp -o ordem $ ./ordem Criando o livro: Produto(Dom Casmurro) « primeiro a base (o alicerce)... Livro(Machado de Assis) « ...depois a filha Fim do main: ~Livro(Machado de Assis) « na destruição, ao contrário: primeiro a filha... ~Produto(Dom Casmurro) « ...depois a base
Pense numa casa: primeiro o alicerce, depois as paredes; na demolição, primeiro as paredes, depois o alicerce. A base nasce primeiro porque a filha pode precisar dela já no próprio construtor; e morre por último porque o destrutor da filha ainda pode usá-la. É a mesma regra de "ordem inversa" da aula Destrutores e RAII.
5. Redefinir um método da base
O mostrar() herdado não sabe do autor. A filha pode escrever um método com o mesmo nome; para o Livro, ele passa a valer no lugar do da base. E, dentro dele, Produto::mostrar() chama a versão original, para não repetir código:
class Livro : public Produto {
public:
Livro(std::string nome, double preco, std::string autor)
: Produto{nome, preco}, autor_{autor} {}
void mostrar() const {
Produto::mostrar(); // a versão da classe base
std::cout << " autor: " << autor_ << '\n';
}
private:
std::string autor_;
};
void imprimir_etiqueta(const Produto& p) {
p.mostrar();
}
int main() {
Livro livro{"Dom Casmurro", 40, "Machado de Assis"};
livro.mostrar();
std::cout << "Pela função imprimir_etiqueta:\n";
imprimir_etiqueta(livro);
}
$ g++ -std=c++23 -Wall -Wextra redefinir.cpp -o redefinir $ ./redefinir Dom Casmurro — R$ 40 autor: Machado de Assis Pela função imprimir_etiqueta: Dom Casmurro — R$ 40 « o autor sumiu!
Duas coisas aconteceram:
- Um
Livropode ser passado onde se espera umProduto. A funçãoimprimir_etiquetarecebeconst Produto&, e aceitou o livro sem reclamar. Faz sentido: todo livro é um produto. - Mas, lá dentro, rodou o
mostrar()doProduto, e o autor não apareceu. Para a função,pé "um produto qualquer", e o C++ escolheu o método olhando o tipo da referência, não o objeto de verdade.
Fazer o C++ olhar o objeto de verdade é o assunto da próxima aula, Polimorfismo (virtual), e é onde a herança mostra toda a sua força.
6. Herança pública × privada (só a ideia)
A palavra depois dos dois-pontos decide como o que é público na base aparece na filha:
| Forma | O public da base vira, na filha… | Significado |
|---|---|---|
class Livro : public Produto | public | "Livro é um Produto". Todo mundo pode tratar o livro como produto. |
class Livro : protected Produto | protected | Raríssimo. |
class Livro : private Produto | private | "Livro usa um Produto por dentro", mas não quer que ninguém saiba. |
A herança privada é rara: quase sempre um atributo (composição) faz o mesmo de forma mais clara. Ela importa porque é o padrão de class: se você esquecer a palavra public, a herança vira privada sem aviso, e os métodos herdados ficam inacessíveis no main (cenário em Quando dá errado). Com struct, o padrão é public.
O C++ permite herança múltipla: class Ebook : public Livro, public Arquivo. É poderosa e cheia de detalhes (o que acontece se as duas bases tiverem um método com o mesmo nome?). O uso mais comum e seguro é herdar de uma classe e de várias "interfaces", que você vai conhecer em Classes Abstratas.
7. final: ninguém herda daqui C++11
Às vezes uma classe não foi pensada para ser base de outras. Escreva final depois do nome, e o compilador recusa qualquer tentativa de herdar dela:
class Livro final : public Produto {
// ...
};
class Ebook : public Livro { // tentativa de herdar de Livro
// ...
};
$ g++ -std=c++23 -Wall -Wextra final.cpp -o final
final.cpp:20:7: error: cannot derive from ‘final’ base ‘Livro’ in derived type ‘Ebook’
20 | class Ebook : public Livro {
| ^~~~~
"Não é possível derivar da base final Livro no tipo derivado Ebook". O Clang do editor diz base 'Livro' is marked 'final'. O final também vale para um método só, como você vai ver em Polimorfismo.
Quando dá errado
$ g++ -std=c++23 -Wall -Wextra h3.cpp -o h3
h3.cpp: In constructor ‘Livro::Livro(std::string, double, std::string)’:
h3.cpp:15:23: error: no matching function for call to ‘Produto::Produto()’
15 | : autor_{autor} {}
| ^
h3.cpp:15:23: note: there are 3 candidates
h3.cpp:6:5: note: candidate 1: ‘Produto::Produto(std::string, double)’
6 | Produto(std::string nome, double preco) : nome_{nome}, preco_{preco} {}
| ^~~~~~~
h3.cpp:6:5: note: candidate expects 2 arguments, 0 provided
h3.cpp:4:7: note: candidate 2: ‘constexpr Produto::Produto(const Produto&)’
4 | class Produto {
| ^~~~~~~
h3.cpp:4:7: note: candidate expects 1 argument, 0 provided
h3.cpp:4:7: note: candidate 3: ‘constexpr Produto::Produto(Produto&&)’
h3.cpp:4:7: note: candidate expects 1 argument, 0 provided
h3.cpp:14:23: warning: unused parameter ‘nome’ [-Wunused-parameter]
14 | Livro(std::string nome, double preco, std::string autor)
| ~~~~~~~~~~~~^~~~
h3.cpp:14:36: warning: unused parameter ‘preco’ [-Wunused-parameter]
14 | Livro(std::string nome, double preco, std::string autor)
| ~~~~~~~^~~~~
O que significa: o construtor do Livro não disse como construir a parte Produto, então o C++ procurou um Produto() sem argumentos. Os três "candidatos" são os construtores que existem: o seu (2 argumentos), o de cópia e o de movimento (1 argumento cada). Nenhum aceita 0. Os avisos de parâmetros não usados são a pista: nome e preco chegaram e ninguém os usou. O Clang resume: constructor for 'Livro' must explicitly initialize the base class 'Produto' which does not have a default constructor.
Como consertar: acrescente Produto{nome, preco} no começo da lista de inicialização.
O que significa: a filha tentou usar um atributo private da base (o h1.cpp da seção 3). O Clang diz 'preco_' is a private member of 'Produto'.
Como consertar: use um método público da base (preco()), ou, se fizer sentido, troque o atributo para protected.
public: "is inaccessible within this context"
class Livro : Produto { // faltou o public
// ...
};
int main() {
Livro livro{"Dom Casmurro", 40, "Machado de Assis"};
livro.mostrar();
}
$ g++ -std=c++23 -Wall -Wextra h4.cpp -o h4
h4.cpp: In function ‘int main()’:
h4.cpp:23:18: error: ‘void Produto::mostrar() const’ is inaccessible within this context
23 | livro.mostrar();
| ~~~~~~~~~~~~~^~
h4.cpp:7:10: note: declared here
7 | void mostrar() const { std::cout << nome_ << " — R$ " << preco_ << '\n'; }
| ^~~~~~~
h4.cpp:23:18: error: ‘Produto’ is not an accessible base of ‘Livro’
23 | livro.mostrar();
| ~~~~~~~~~~~~~^~
O que significa: "mostrar é inacessível aqui" e "Produto não é uma base acessível de Livro". Sem a palavra public, uma class herda de forma privada: o mostrar(), que era público, virou privado dentro do Livro. (Clang: 'mostrar' is a private member of 'Produto'.)
Como consertar: class Livro : public Produto.
O que significa: a classe de que você tentou herdar foi marcada com final (seção 7).
Como consertar: se a classe é sua e você decidiu que ela pode ter filhas, tire o final. Se não é sua (uma biblioteca), use composição: guarde um objeto dela como atributo.
O que significa: você chamou o método por uma referência ou ponteiro do tipo da base (const Produto&, Produto*), como no imprimir_etiqueta da seção 5. Sem virtual, o C++ escolhe o método pelo tipo da referência.
Como consertar: marque o método da base como virtual. A explicação completa está em Polimorfismo.
Missão
Missão: crie a classe Revista, filha de Produto, com um atributo int edicao_. O construtor recebe nome, preço e edição e chama o construtor do Produto. Redefina mostrar() para chamar o Produto::mostrar() e depois mostrar Edição 42. Por fim, tire as barras // das duas linhas do main.
#include <iostream>
#include <string>
class Produto {
public:
Produto(std::string nome, double preco) : nome_{nome}, preco_{preco} {}
void mostrar() const {
std::cout << nome_ << " — R$ " << preco_ << '\n';
}
private:
std::string nome_;
double preco_;
};
// escreva a classe Revista aqui
int main() {
Produto caneta{"Caneta azul", 3.5};
caneta.mostrar();
// Revista revista{"Superinteressante", 25, 42};
// revista.mostrar();
}
#include <iostream>
#include <string>
class Produto {
public:
Produto(std::string nome, double preco) : nome_{nome}, preco_{preco} {}
void mostrar() const {
std::cout << nome_ << " — R$ " << preco_ << '\n';
}
private:
std::string nome_;
double preco_;
};
class Revista : public Produto {
public:
Revista(std::string nome, double preco, int edicao)
: Produto{nome, preco}, edicao_{edicao} {}
void mostrar() const {
Produto::mostrar();
std::cout << " Edição " << edicao_ << '\n';
}
private:
int edicao_;
};
int main() {
Produto caneta{"Caneta azul", 3.5};
caneta.mostrar();
Revista revista{"Superinteressante", 25, 42};
revista.mostrar();
}
A saída da solução no laboratório:
$ ./revista Caneta azul — R$ 3.5 Superinteressante — R$ 25 Edição 42
Referência
| Código | O que faz |
|---|---|
class Filha : public Base { … }; | Herança pública: "Filha é uma Base" |
Filha(…) : Base{…}, atributo_{…} {} | Constrói a parte da base, depois os atributos da filha |
using Base::Base; | Herda os construtores da base |
protected: | Visível para a classe e as filhas; invisível para o resto |
Base::metodo(); | Chama a versão da base de um método redefinido |
class X final : public Base | Proíbe que alguém herde de X |
class Filha : Base | Herança privada (padrão de class) |
| Ordem | Constrói base → filha; destrói filha → base |
Exercícios
1. A declaração
Complete para declarar Cachorro como filha pública de Animal:
2. Chamar o construtor da base
O construtor de Animal recebe o nome. Complete a lista de inicialização do Cachorro:
3. O nível certo
Qual palavra deixa um membro visível para as classes filhas, mas não para o main?
4. Ao criar um Livro (filho de Produto) e deixá-lo sair do escopo, em que ordem rodam construtores e destrutores?
5. Qual destes pares não passa no teste do "é um"?
6. Você escreveu class Livro : Produto (sem public) e livro.mostrar() no main deu erro. Por quê?
Resumo
class Filha : public Basecria uma classe que recebe tudo da base e pode acrescentar atributos e métodos.- Use herança quando a frase "Filha é uma Base" faz sentido; quando é "tem um", use um atributo.
- O construtor da filha chama o da base na lista de inicialização:
: Base{…}, atributo_{…}. privateda base é proibido até para a filha;protectedlibera só para as filhas.- A base nasce primeiro e morre por último.
- Um método com o mesmo nome na filha substitui o da base;
Base::metodo()chama o original. Por uma referência da base, semvirtual, roda o da base. - Sem
public, a herança de umaclassé privada.finalproíbe herdar.