Herança em C ++: iniciante, intermediário, avançado

Este artigo descreve a herança em três níveis: iniciante, intermediário e avançado. Especialista no. E nem uma palavra sobre o SOLID. Sinceramente.


Iniciante


O que é herança?


A herança é um dos princípios fundamentais da OOP. Segundo ele, uma classe pode usar as variáveis ​​e métodos de outra classe como seus.


Uma classe que herda dados é chamada de subclasse, classe derivada ou classe filho. Uma classe da qual os dados ou métodos são herdados é chamada de superclasse, classe base ou classe pai. Os termos "pai" e "filho" são extremamente úteis para entender a herança. Como uma criança recebe as características de seus pais, a classe derivada recebe os métodos e variáveis ​​da classe base.


A herança é útil porque permite estruturar e reutilizar o código, que por sua vez pode acelerar significativamente o processo de desenvolvimento. Apesar disso, a herança deve ser usada com cautela, pois a maioria das alterações na superclasse afetará todas as subclasses, o que pode levar a conseqüências imprevistas.


Neste exemplo, o método turn_on() e a variável serial_number não serial_number declarados ou definidos na subclasse Computer . No entanto, eles podem ser usados ​​porque são herdados da classe base.


Nota importante : Variáveis ​​e métodos privados não podem ser herdados.


 #include <iostream> using namespace std; class Device { public: int serial_number = 12345678; void turn_on() { cout << "Device is on" << endl; } private: int pincode = 87654321; }; class Computer: public Device {}; int main() { Computer Computer_instance; Computer_instance.turn_on(); cout << "Serial number is: " << Computer_instance.serial_number << endl; // cout << "Pin code is: " << Computer_instance.pincode << endl; // will cause compile time error return 0; } 

Tipos de herança


Existem vários tipos de herança no C ++:


  • público - dados públicos ( public ) e protegidos ( protected ) são herdados sem alterar o nível de acesso a eles;
  • protegido ( protected ) - todos os dados herdados ficam protegidos;
  • privado - todos os dados herdados se tornam privados.

Para a classe base do Device , o nível de acesso a dados não muda, mas como a classe derivada do Computer herda dados como privados, os dados se tornam privados para a classe Computer .


 #include <iostream> using namespace std; class Device { public: int serial_number = 12345678; void turn_on() { cout << "Device is on" << endl; } }; class Computer: private Device { public: void say_hello() { turn_on(); cout << "Welcome to Windows 95!" << endl; } }; int main() { Device Device_instance; Computer Computer_instance; cout << "\t Device" << endl; cout << "Serial number is: "<< Device_instance.serial_number << endl; Device_instance.turn_on(); // cout << "Serial number is: " << Computer_instance.serial_number << endl; // Computer_instance.turn_on(); // will cause compile time error cout << "\t Computer" << endl; Computer_instance.say_hello(); return 0; } 

A classe Computer agora usa o método turn_on() como qualquer método particular: turn_on() pode ser chamado de dentro da classe, mas tentar chamá-lo diretamente do main resultará em um erro no momento da compilação. Para a classe base Device , o método turn_on() permaneceu público e pode ser chamado de main .


Construtores e destruidores


No C ++, construtores e destruidores não são herdados. No entanto, eles são chamados quando a classe filho inicializa seu objeto. Os construtores são chamados hierarquicamente um após o outro, começando com a classe base e terminando com a última classe derivada. Os destruidores são chamados em ordem inversa.


Nota importante: Este artigo não cobre destruidores virtuais. Material adicional sobre este tópico pode ser encontrado, por exemplo, neste artigo no Habr .


 #include <iostream> using namespace std; class Device { public: // constructor Device() { cout << "Device constructor called" << endl; } // destructor ~Device() { cout << "Device destructor called" << endl; } }; class Computer: public Device { public: Computer() { cout << "Computer constructor called" << endl; } ~Computer() { cout << "Computer destructor called" << endl; } }; class Laptop: public Computer { public: Laptop() { cout << "Laptop constructor called" << endl; } ~Laptop() { cout << "Laptop destructor called" << endl; } }; int main() { cout << "\tConstructors" << endl; Laptop Laptop_instance; cout << "\tDestructors" << endl; return 0; } 

Construtores: Device -> Computer -> Laptop .
Destrutores: Laptop -> Computer -> Device .


Herança múltipla


A herança múltipla ocorre quando uma subclasse possui duas ou mais superclasses. Neste exemplo, a classe Laptop herda o Monitor e o Computer ao mesmo tempo.


 #include <iostream> using namespace std; class Computer { public: void turn_on() { cout << "Welcome to Windows 95" << endl; } }; class Monitor { public: void show_image() { cout << "Imagine image here" << endl; } }; class Laptop: public Computer, public Monitor {}; int main() { Laptop Laptop_instance; Laptop_instance.turn_on(); Laptop_instance.show_image(); return 0; } 

Vários problemas de herança


A herança múltipla requer um design cuidadoso, pois pode levar a consequências imprevistas. A maioria dessas conseqüências é causada por ambiguidade na herança. Neste exemplo, o Laptop herda o método turn_on() de ambos os pais e não está claro qual método deve ser chamado.


 #include <iostream> using namespace std; class Computer { private: void turn_on() { cout << "Computer is on." << endl; } }; class Monitor { public: void turn_on() { cout << "Monitor is on." << endl; } }; class Laptop: public Computer, public Monitor {}; int main() { Laptop Laptop_instance; // Laptop_instance.turn_on(); // will cause compile time error return 0; } 

Apesar de os dados privados não serem herdados, é impossível resolver a herança ambígua alterando o nível de acesso aos dados para privado. Ao compilar, primeiro uma pesquisa por um método ou variável ocorre e, depois disso, verifica o nível de acesso a eles.


Intermediário


Problema de losango



O problema do diamante é um problema clássico em idiomas que suportam herança múltipla. Esse problema ocorre quando as classes B e C herdam A e a classe D herda B e C


Por exemplo, as classes A , B e C definem o método print_letter() . Se print_letter() for chamado pela classe D , não está claro qual método deve ser chamado - um método da classe A , B ou C Idiomas diferentes têm abordagens diferentes para resolver problemas em forma de diamante. Em C ++, a solução para o problema é deixada a critério do programador.

O problema em forma de diamante é principalmente um problema de design e deve ser fornecido no estágio de design. No estágio de desenvolvimento, ele pode ser resolvido da seguinte maneira:


  • chame o método de uma superclasse específica;
  • referem-se ao objeto da subclasse como um objeto de uma superclasse específica;
  • substitua o método problemático na última classe filho (no código, turn_on() na subclasse Laptop ).

 #include <iostream> using namespace std; class Device { public: void turn_on() { cout << "Device is on." << endl; } }; class Computer: public Device {}; class Monitor: public Device {}; class Laptop: public Computer, public Monitor { /* public: void turn_on() { cout << "Laptop is on." << endl; } // uncommenting this function will resolve diamond problem */ }; int main() { Laptop Laptop_instance; // Laptop_instance.turn_on(); // will produce compile time error // if Laptop.turn_on function is commented out // calling method of specific superclass Laptop_instance.Monitor::turn_on(); // treating Laptop instance as Monitor instance via static cast static_cast<Monitor&>( Laptop_instance ).turn_on(); return 0; } 

Se o método turn_on() não turn_on() sido substituído no Laptop, chamar Laptop_instance.turn_on() resultará em um erro de compilação. Um objeto Laptop pode acessar duas turn_on() método turn_on() simultaneamente: Device:Computer:Laptop.turn_on() e Device:Monitor:Laptop.turn_on() .


O Problema do Diamante: Construtores e Destrutores


Como no C ++, quando o objeto da classe filho é inicializado, os construtores de todas as classes pai são chamados, surge outro problema: o construtor da classe base Device será chamado duas vezes.


 #include <iostream> using namespace std; class Device { public: Device() { cout << "Device constructor called" << endl; } }; class Computer: public Device { public: Computer() { cout << "Computer constructor called" << endl; } }; class Monitor: public Device { public: Monitor() { cout << "Monitor constructor called" << endl; } }; class Laptop: public Computer, public Monitor {}; int main() { Laptop Laptop_instance; return 0; } 

Herança virtual


A herança virtual impede que vários objetos da classe base apareçam na hierarquia de herança. Portanto, o construtor da classe base Device será chamado apenas uma vez e uma chamada ao método turn_on() sem substituí-lo na classe filho não causará um erro de compilação.


 #include <iostream> using namespace std; class Device { public: Device() { cout << "Device constructor called" << endl; } void turn_on() { cout << "Device is on." << endl; } }; class Computer: virtual public Device { public: Computer() { cout << "Computer constructor called" << endl; } }; class Monitor: virtual public Device { public: Monitor() { cout << "Monitor constructor called" << endl; } }; class Laptop: public Computer, public Monitor {}; int main() { Laptop Laptop_instance; Laptop_instance.turn_on(); return 0; } 

Nota : a herança virtual nas classes Computer e Monitor não permitirá herança romboide se a classe filho Laptop não herdar a classe Device virtualmente ( class Laptop: public Computer, public Monitor, public Device {}; ).


Classe abstrata


Em C ++, uma classe na qual existe pelo menos um método virtual puro é considerada abstrata. Se o método virtual não for substituído na classe filho, o código não será compilado. Além disso, no C ++ é impossível criar um objeto de uma classe abstrata - uma tentativa também causará um erro de compilação.


 #include <iostream> using namespace std; class Device { public: void turn_on() { cout << "Device is on." << endl; } virtual void say_hello() = 0; }; class Laptop: public Device { public: void say_hello() { cout << "Hello world!" << endl; } }; int main() { Laptop Laptop_instance; Laptop_instance.turn_on(); Laptop_instance.say_hello(); // Device Device_instance; // will cause compile time error return 0; } 

Interface


O C ++, diferentemente de algumas linguagens OOP, não fornece uma palavra-chave separada para indicar uma interface. No entanto, a implementação da interface é possível criando uma classe abstrata pura - uma classe na qual existem apenas declarações de método. Tais classes também são frequentemente chamadas de Abstract Base Class (ABC).


 #include <iostream> using namespace std; class Device { public: virtual void turn_on() = 0; }; class Laptop: public Device { public: void turn_on() { cout << "Device is on." << endl; } }; int main() { Laptop Laptop_instance; Laptop_instance.turn_on(); // Device Device_instance; // will cause compile time error return 0; } 

Avançado


Embora a herança seja um princípio fundamental da POO, ela deve ser usada com cautela. É importante pensar que qualquer código que será usado provavelmente será alterado e poderá ser usado de uma maneira que não seja óbvia para o desenvolvedor.


Herança de uma classe implementada ou parcialmente implementada


Se a herança não vem de uma interface (uma classe abstrata pura no contexto de C ++), mas de uma classe na qual existem implementações, vale a pena considerar que o herdeiro está conectado à classe pai pela conexão mais próxima possível. A maioria das mudanças na classe dos pais pode afetar o herdeiro, o que pode levar a um comportamento inesperado. Tais mudanças no comportamento do herdeiro nem sempre são óbvias - um erro pode ocorrer no código já testado e em funcionamento. Essa situação é exacerbada pela presença de uma hierarquia de classes complexa. É sempre bom lembrar que o código pode ser alterado não apenas pela pessoa que o escreveu, e os caminhos de herança óbvios para o autor podem não ser levados em consideração por seus colegas.


Por outro lado, vale ressaltar que a herança de classes parcialmente implementadas tem uma vantagem inegável. Bibliotecas e estruturas geralmente funcionam da seguinte maneira: elas fornecem ao usuário uma classe abstrata com vários métodos virtuais e muitos implementados. Assim, a maior quantidade de trabalho já foi realizada - a lógica complexa já foi escrita e o usuário pode personalizar apenas a solução pronta para atender às suas necessidades.


Interface


A herança de uma interface (classe abstrata pura) apresenta herança como uma oportunidade para estruturar o código e proteger o usuário. Como a interface descreve o trabalho que a classe de implementação fará, mas não descreve como, qualquer usuário da interface está protegido contra alterações na classe que implementa essa interface.


Interface: Exemplo de Uso


Antes de tudo, vale ressaltar que o exemplo está intimamente relacionado ao conceito de polimorfismo, mas será considerado no contexto da herança de uma classe abstrata pura.


Um aplicativo que executa lógica de negócios abstrata deve ser configurado a partir de um arquivo de configuração separado. No estágio inicial de desenvolvimento, a formatação desse arquivo de configuração não estava totalmente formada. Passar a análise de arquivo atrás de uma interface oferece várias vantagens.


A falta de clareza em relação à formatação do arquivo de configuração não atrasa o processo de desenvolvimento do programa principal. Dois desenvolvedores podem trabalhar em paralelo - um na lógica de negócios e outro no analisador. Como eles interagem por essa interface, cada um deles pode trabalhar de forma independente. Essa abordagem facilita a codificação de testes de unidade com código, uma vez que os testes necessários podem ser escritos usando simulação para essa interface.


Além disso, ao alterar o formato do arquivo de configuração, a lógica de negócios do aplicativo não é afetada. A única coisa que requer uma transição completa de uma formatação para outra é escrever uma nova implementação da classe abstrata já existente (classe analisadora). Além disso, retornar ao formato de arquivo original requer um trabalho mínimo - substituindo um analisador existente por outro.


Conclusão


A herança oferece muitos benefícios, mas deve ser cuidadosamente projetada para evitar os problemas pelos quais apresenta uma oportunidade. No contexto da herança, o C ++ fornece uma ampla gama de ferramentas que abrem inúmeras possibilidades para o programador.


E o SOLID é bom.

Source: https://habr.com/ru/post/pt445948/


All Articles