Um aspecto fundamental do C++ é a de ser uma linguagem de programação orientada a objetos, e portanto é quase impossível evitar o uso das classes.
Com as classes, normalmente surge a necessidade de se modificar alguns aspectos da classe original sem alterar sua essência – aparecem então as classes derivadas (ou filhas), e com elas entramos no campo da herança em C++. Mas fear not, my friend, porque ao fim deste artigo você entenderá ao menos um pouquinho mais acerca desse tema um tanto espinhoso.
O que é herança em C++?
A herança é um dos elementos fundamentais das linguagens de programação orientadas a objetos como o C++ (também conhecidas pelo acrônimo POO, do inglês OOP – Object Oriented Programming). A herança permite a criação de uma nova classe (classe derivada) a partir de uma classe existente (classe de base).
A classe derivada herda as suas características da classe de base, incluindo atributos, métodos etc, e pode ainda adicionar – além dos atributos herdados – outros elementos que lhe são próprios.
class User { public: std::string getUserType(); // Rule of five - this may cause slicing, though User(const User& user) = default; User(User&& user) = default; User& operator=(const User& user) = default; User& operator=(User&& user) = default; virtual ~User() = default; }; class Admin : public User { void deleteUser(); };
No bloco anterior, a classe Admin
é derivada da classe de base User
. Note que após os dois-pontos (:), vemos a palavra public
, que indica o nível de acesso da herança entre Admin
e User
, e poderia ser substituído por protected
ou private
– mas não se preocupe, irei falar sobre isso mais à frente.
No diagrama acima, a classe Admin
“herda” (ou seja, é derivada) da classe User
, e portanto tem acesso ao seus métodos marcados como public
ou protected
; neste caso há apenas um, o getUserType()
. Além disso, a classe Admin
possui um método deleteUser
, e vale notar que a herança funciona apenas de cima para baixo, ou seja: a classe User
não tem acesso ao método deleteUser
da classe Admin
.
Exemplo 1 – Herança em C++ com duas classes derivadas
#include <iostream> using namespace std; class User { public: // Construtor por parâmetros da classe User User(const unsigned int id, const std::string& userType = "Normal user", const bool adminRights = false): _userId(id), _userType(userType), _adminRights(adminRights) {} inline std::string getUserType() { return _userType; } // Rule of five - this may cause slicing, though User(const User& user) = default; User(User&& user) = default; User& operator=(const User& user) = default; User& operator=(User&& user) = default; virtual ~User() = default; protected: void accessDb() { cout << "Acesso ao banco de dados realizado com sucesso." << endl; } private: const unsigned int _userId; std::string _userType; bool _adminRights; }; class Admin : public User { public: void deleteUser(unsigned int id) { accessDb(); // operacoes para deletar o usuario do id em questão } }; class NormalUser : public User { }; int main() { // Criação de um objeto da classe User User usuario {0}; // Chamada dos métodos membros da classe User cout << "O tipo do usuario eh: " << usuario.getUserType() << endl; // Criação de um objeto das classe NormalUser e Admin NormalUser usuarioNormal {1}; Admin usuarioAdmin {2}; // Chamada dos métodos (herdados) da classe NormalUser cout << "O tipo do usuarioNormal eh: " << usuarioNormal.getUserType() << endl; }
Como podemos ver no exemplo acima, é possível utilizar as funções public
da classe de base com objetos de uma classe derivada em qualquer lugar do programa.
Um outro exemplo dessa capacidade é que o construtor da classe User
é chamado automaticamente pelo compilador quando um objeto das classes NormalUser
e Admin
é criado – se mudássemos o construtor de public
para private
, por exemplo, o código não compilaria mais porque o compilador não poderia chamar o construtor para criar os objetos. Se quiser saber mais sobre as palavras chave public
, protected
e private
, visite o meu artigo sobre como criar classes em C++.
Quando usar herança em C++?
Normalmente, usa-se o mecanismo de herança quando houver uma relação de pertencimento; quando a classe derivada é uma entidade da mesma categoria que a classe de base.
Esse tipo de relação é conhecido como uma relação “Is-a“, que significa “é um”, em inglês, indicando que o tipo da classe derivada “é um” elemento da mesma categoria da classe base.
Vejamos alguns exemplos a seguir:
- Um administrador é um usuário;
- Um corredor é um atleta;
- Um gato é um felino;
- Uma maçã é uma fruta;
Métodos e atributos protected
Na definição de uma classe os métodos e atributos membros podem ser declarados como protected
se desejamos que eles sejam acessíveis a classes derivadas, sem expô-los como públicos.
Ao contrário do especificador de acesso public
, que permite acessar membros da classe em qualquer lugar, o protected
deixa que os membros da classe sejam acessados apenas dentro dos métodos da própria classe ou de classes derivadas.
Isso é bastante útil quando se deseja, por exemplo, proibir a modificação de dados da classe livremente pelo usuário, mais permitir a modificação controlada através de funções dedicadas a tal fim.
Vejamos um exemplo do uso de membros protected
da classe de base por objetos de uma de suas classe derivadas em um cenário simples de herança em C++.
Exemplo 2 – Membros protected na herança em C++
#include <iostream> using namespace std; class User { public: // Construtor por parâmetros da classe User User(const unsigned int id, const std::string& userType = "Normal user", const bool adminRights = false): _userId(id), _userType(userType), _adminRights(adminRights) {} inline std::string getUserType() { return _userType; } // Rule of five - this may cause slicing, though User(const User& user) = default; User(User&& user) = default; User& operator=(const User& user) = default; User& operator=(User&& user) = default; virtual ~User() = default; protected: void accessDb() { cout << "Acesso ao banco de dados realizado com sucesso." << endl; } private: const unsigned int _userId; std::string _userType; bool _adminRights; }; class Admin : public User { public: void deleteUser(unsigned int id) { accessDb(); // operacoes para deletar o usuario do id em questão cout << "Usuario com id " << id << " removido do banco de dados." << endl; } }; class NormalUser : public User { }; int main() { Admin usuarioAdmin {0}; usuarioAdmin.deleteUser(10); }
Como vemos no programa acima, ao chamar a função deleteUser()
no objeto usuarioAdmin
da classe Admin
, a função accessDb()
, usada para simular o acesso a um banco de dados, é invocada dentro de deleteUser()
; isso é possível porque accessDb()
foi declarada como protected
, o que permite que ela seja usada dentro de métodos das classes derivadas.
Contudo, se eu tentasse executar o código abaixo, o compilador reclamaria que “o método accessDb() é protected em User, portanto é inacessível fora dos métodos da classe User e de suas classes derivadas” (a mensagem de erro real é bem mais curta, mais eu a incrementei um pouco para torná-la mais compreensível).
// Error: 'accessDb' is a protected member of 'User' usuarioAdmin.accessDb()
Especificadores de acesso public, protected e private
No início do artigo falei que para criar uma classe derivada é preciso usar a seguinte sintaxe:
class Derivada : public Base { };
Eu disse também que a palavra public
poderia ser substituída por protected
ou private
. O que isso mudaria na herança em C++?
Essa palavra-chave determina o nível de acesso que a classe derivada tem aos membros da classe de base. Em outras palavras, ela determina quanto da classe de base a classe derivada poderá “enxergar”. A coisa funciona assim:
- public: os membros da classe de base mantém seus especificadores de acesso na classe derivada, ou seja, quem era
public
se mantémpublic
, quem eraprotected
continuaprotected
, e o mesmo vale para private; - protected: os membros da classe de base herdados tornam-se todos
protected
na classe derivada, com exceção daqueles que eramprivate
na classe de base; - private: todos os membros da clase de base herdados na classe derivada tornam-se
private
, ou seja, tornam-se inacessíveis para a própria classe derivada.
Em resumo, o nível de acesso aplicado aos membros herdados é sempre o mais restritivo entre a palavra usada na definição da classe derivada após os dois-pontos, e o especificador de acesso presente na classe de base. Assim sendo, é impossível alterar o especificador de acesso private
para qualquer outro valor (public
ou protected
).
Próximos passos
Agora que você entendeu o que é herança em C++ e como ela funciona, quais são os próximos passos para que domine melhor esse assunto? A próxima etapa no estudo da herança em C++ é a da sobrecarga de métodos. Fique de olho nos próximos artigos (se preferir, se inscreva na nossa newsletter) para não perder o artigo sobre esse tema.
Sou apaixonado por tecnologia, literatura e também filosofia. O cultivo dessas paixões ao longo da minha trajetória me inspiraram a compartilhar aquilo que aprendo com os outros da maneira mais clara que eu possa. Sou formado em Engenharia Elétrica no Brasil, e também sou engenheiro formado na França. Trabalho atualmente como programador C++ em uma multinacional francesa, uma das maiores empresas de TI do mundo.