Herança em C++: uma introdução detalhada com exemplos

Texto "Herança em C++, uma introdução detalhada" sobre fundo azul.

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.

Diagrama de classes de herança simples em C++
Diagrama de classes com uma herança simples em C++

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;
}

O tipo do usuario eh: Normal user
O tipo do usuarioNormal eh: Normal user
Acesso ao banco de dados realizado com sucesso.

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);
}

Acesso ao banco de dados realizado com sucesso.
Usuario com id 10 removido do banco de dados.

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ém public, quem era protected continua protected, 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 eram private 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.

Foto de perfil de Emanoel

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.

Leave a Reply

Your email address will not be published. Required fields are marked *