Baixe o app para aproveitar ainda mais
Prévia do material em texto
Escola Politécnica da Universidade de São Paulo Escola Politécnica da Universidade de São Paulo Laboratório de Programação Orientada a Objetos para Engenharia Elétrica Aula 6: Herança e Polimorfismo I PCS3111 Agenda 1. Herança 2. Modo Protegido 3. Polimorfismo 4. Princípio da Substituição 5. Casting 2 Agenda 1. Herança 2. Modo Protegido 3. Polimorfismo 4. Princípio da Substituição 5. Casting 3 Herança em OO ! Herança é uma das características essenciais da Orientação a Objetos! ! Em um domínio, é comum que as classes tenham características semelhantes. ! O termo expressa a transmissão de características, como na herança genética: 4 Avó e neta Pai e filha Tarcísio-pai e Tarcísio Filho Herança (Inheritance) ! Herança é o processo pelo qual uma nova classe, denominada classe derivada, é criada a partir de uma classe já existente, chamada de classe base. ! A nova classe derivada “pega emprestado” o comportamento da classe base. ! A nova classe derivada é chamada de subclasse. ! A classe base original é chamada de superclasse. 5 Nomenclatura São equivalentes os seguintes pares: Classe base e classe derivada ou Classe mãe e classe filha (obs: classe ancestral se refere a qualquer nível de herança) ou Superclasse e subclasse 6 A subclasse herda o comportamento da superclasse Todos os membros (atributos e métodos) da superclasse são comuns a todas as subclasses dela derivadas. A derivação não altera a classe base, isto é, a classe base preserva seus métodos e atributos. 7 Herança Ao se criar uma classe B, ! Se já existir uma classe A que representa um conceito mais geral ! Se a classe B é um tipo de A e, por isso, possui os mesmos atributos e métodos, Então, nesse caso, pode-se definir a classe B como uma classe derivada da classe A. A classe A será a superclasse de B. 8 A B Subclasse Superclasse A subclasse pode adicionar seu comportamento específico ! Subclasses também podem adicionar seus próprios atributos e métodos: • A classe B pode ter atributos próprios, além dos atributos de A. • A classe B pode ter métodos próprios, além dos métodos de A. 9 Exemplo 10 Todos os atributos e métodos de Empregado se transmitem por herança a Pesquisador e Vendedor. Vendedor é um tipo de Empregado. Além de nome e salario, ele é caracterizado pela comissao. Pesquisador é um tipo de Empregado. Além de nome e salario, ele é caracterizado pelo numeroDeArtigos. Todas as classes da hierarquia podem ter instâncias! (Por enquanto...) No exemplo: 11 Manoela é uma instância de Pesquisador Otávio é uma instância de Vendedor Ana é uma instância de Empregado (i.e., não é nem pesquisadora nem vendedora!) Por que usar herança? ! Porque o mecanismo de herança possibilita o reuso de código: • Com a derivação de uma classe a partir de outra, atributos e métodos são herdados: não precisam ser reescritos! (Mas podem, como vamos ver) • Se muitos desenvolvedores usam a mesma classe, aumenta a chance de descobrirem erros => maior confiabilidade do código. ! Porque com herança podemos organizar o projeto em hierarquias, tornando o código mais inteligível. 12 Vantagens e desvantagens da herança ! Vantagem: diminuição de esforço de programação e depuração: • Pode-se evoluir um projeto que tenha código já testado e em funcionamento, ao invés de fazer código novo. • Nem sempre se tem acesso ao código da classe base, mas isso não impede de criar uma classe derivada a partir dela! ! Desvantagens • Velocidade de execução • Complexidade estrutural do código 13 Generalizar x especializar A herança é usada num projeto em duas situações: ! Especialização: quando se deseja criar um comportamento especial a partir do mais geral. • Ex.: um ExameDeGlicemia é um tipo de ExameDeSangue, com características específicas. ! Generalização: quando existe um comportamento comum a várias classes, e torna-se interessante ter uma classe base, eliminando redundâncias. • Ex.: tanto os exames de sangue quanto os de imagem estão associados a um médico e um paciente. Fica mais claro ter uma classe Exame, que se associa a Médico e Paciente, porque ExamesDeSangue e ExamesDeImagem são tipos de Exame. 14 Não confundir herança com composição! Tanto na herança e na composição, as classes se estruturam numa hierarquia! A composição é uma relação entre todo e partes Por exemplo, um ventilador é composto de motor, pás e grade. A herança é uma relação entre tipos. Por exemplo, há diferentes tipos de ventilador: de mesa, de parede, de pé. 15 Como declarar subclasses em C++ 16 class NomeSuperClasse { // atributos da classe public: // métodos da classe }; class NomeSubClasse: public NomeSuperClasse { // novos atributos da subclasse public: // novos métodos da subclasse }; indica herança Agenda 1. Herança 2. Modo Protegido 3. Polimorfismo 4. Princípio da Substituição 5. Casting 17 Acesso aos membros da classe base ! Os métodos de uma classe sempre podem acessar os atributos e métodos desta classe, independente se declarados públicos ou privados. ! Já os objetos externos da classe só podem acessar os membros públicos da classe. ! Quais membros da classe base podem ser usados pela classe derivada desta classe base? ! Depende... 18 Controle de acesso ! Escopo Privado (private) • Nomes podem ser usados apenas nos métodos próprios da classe. ! Escopo Protegido (protected) • Nomes podem ser usados em métodos próprios e de classes (tipos) derivadas. ! Escopo Público (public) • Nomes podem ser usados em quaisquer métodos. 19 Acesso aos membros da classe 20 class NomeSuperClasse { private: // acessíveis só aos objetos desta classe protected: // acessíveis às subclasses public: //acessíveis às classes externas à hierarquia }; class NomeSubClasse: public NomeSuperClasse { private: // acessíveis só aos objetos desta classe protected: // acessíveis às subclasses public: //acessìveis às classes externas à hierarquia }; Atributos Protegidos na Superclasse Exemplo 1 Membros protegidos são acessíveis aos objetos das subclasses 21 Notação: # Elemento protegido + Elemento público Classe Empregado (.h) 22 18 class Empregado { 19 protected: 20 string nome ; 21 double salario; 22 23 public: 24 Empregado (string nome); 25 ~Empregado (); 26 27 string getNome(); 28 void setNome (string nome); 29 double getSalario(); 30 void setSalario (double salario); 31 32 double calcularSalarioLiquido(); 33 }; Atributos podem ser acessados por objetos das subclasses No exemplo: ! Todo Empregado recebe um salário padrão de R$ 5.000,00 ! Um Pesquisador recebe um adicional de salário, por ser pesquisador, de R$1.000,00 ! Todos os empregados têm desconto de 15% de IRPF. 23 Classe Empregado (.cpp) 24 14 double const salarioPadrao = 5000.; 15 16 Empregado::Empregado (string nome) { 17 this->setNome (nome); 18 this->setSalario ( 20 salarioPadrao); // salário padrão da empresa 21 } 22 23 Empregado::~Empregado () { 24 cout << "Empregado removido" << endl; 25 } 26 27 string Empregado::getNome () { 28 return nome; 29 } 30 Classe Empregado (.cpp) 2531 void Empregado::setNome (string nome) { 32 this->nome = nome; 33 } 34 35 void Empregado::setSalario (double salario) { 36 this->salario = salario; 37 } 38 39 double Empregado::getSalario() { 40 return this->salario; 41 } 42 double Empregado::calcularSalarioLiquido () { 43 // desconto do irpf 44 return this->getSalario() * 0.85; 45 } 46 Subclasse Pesquisador (.h) 26 14 #include "Empregado.h" 15 16 using namespace std; 17 18 class Pesquisador : public Empregado { 19 private: 20 int numeroDeArtigos; 21 22 public: 23 Pesquisador (string nome, int numeroDeArtigos); 24 ~Pesquisador (); 25 26 int getNumeroDeArtigos(); 27 void setNumeroDeArtigos (int numeroDeArtigos); 28 29 }; 30 Subclasse getNome() não é redeclarado. Usa-se o método da superclasse (herança). Antes de rodar o programa, vamos ver os construtores e destrutores das subclasses 27 Construtores das subclasses ! Um objeto da subclasse é também um objeto da superclasse! ! Para construir o objeto da subclasse, deve-se chamar o construtor da superclasse no construtor da subclasse. ! Se não chamar, o compilador usará automaticamente o construtor padrão da superclasse. ! Se a superclasse não tiver construtor (isto é, usar o construtor padrão), naturalmente, será usado o construtor padrão da superclasse. 28 Subclasse::Subclasse (<params>) : SuperClasse (<args>) 14 Pesquisador::Pesquisador (string nome, 15 int numeroDeArtigos) : Empregado (nome) Destrutores das subclasses ! Como o objeto da subclasse é também um objeto da superclasse, é necessário destruir não só o objeto da subclasse, mas também o objeto da superclasse! 29 Subclasse Pesquisador (.cpp) 30 10 #include "Pesquisador.h" 11 12 double const adicionalPesquisador = 1000.0; 13 14 Pesquisador::Pesquisador (string nome, 15 int numeroDeArtigos) : Empregado (nome) { 16 this->numeroDeArtigos = numeroDeArtigos; 17 this->salario = this->salario + adicionalPesquisador; 18 } 19 20 Pesquisador::~Pesquisador () { 21 cout << "Pesquisador removido" << endl; 22 } 23 24 int Pesquisador::getNumeroDeArtigos () { 25 return this->numeroDeArtigos; 26 } Chamando o construtor da superclasse salario é protected Herança de métodos 31 Herança de métodos ! Assim como os atributos, os métodos da superclasse são herdados pelas subclasses. ! Seja f um método na superclasse A (A::f()). Na subclasse B, há três alternativas: 1. Herdar o método f exatamente como definido na superclasse, simplesmente não declarando f na subclasse. 2. Estender f fornecendo uma nova implementação para f na subclasse B que chama a função f definida em A (A::f()). 3. Substituir f da superclasse, fornecendo uma nova implementação na subclasse B, não relacionada à implementação original. 32 Alternativa 1. herdar o método f exatamente como definido na superclasse 33 Por exemplo, getNome() na classe Empregado é herdado pelas subclasses Pesquisador e Vendedor Exemplo 1 34 9 #include "Empregado.h" 10 #include "Pesquisador.h" 11 #include "Vendedor.h" 12 13 int main (int argc, char** argv) { 14 Empregado *e = new Empregado ("Ana"); 15 Pesquisador *p = new Pesquisador ("Manoela", 2); 16 Vendedor *v = new Vendedor ("Otavio", 500.); 17 // mostra o nome de cada empregado 18 cout << "Empregado: " << e->getNome() << endl; 19 cout << "Pesquisador: " << p->getNome() << endl; 20 cout << "Vendedor: " << v->getNome() << endl; 21 delete e; 22 delete p; 23 delete v; 24 } getNome() na classe Empregado é herdado pelas subclasses Pesquisador e Vendedor Observe, neste exemplo, que: ! Na construção das instâncias das três classes, o atributo nome foi definido pelo construtor da superclasse. ! No main, o método getNome foi invocado a partir de objetos das subclasses. ! O destrutor da superclasse foi chamado ao se destruir o objeto da subclasse. 35 Alternativa 2. Estender o método da superclasse 36 O método calcularSalarioLiquido() é estendido na subclasse Vendedor Neste caso, estende-se f fornecendo uma nova implementação para f na subclasse B que chama a função f definida em A (A::f()) Exemplo 2 ! Suponha que o vendedor receba uma comissão acrescida ao seu salário base, isenta de IRPF. 37 18 class Vendedor : public Empregado { 19 private: 20 double comissao; 21 22 public: 23 Vendedor (string nome, double comissao); 24 ~Vendedor (); 25 26 double getComissao(); 27 void setComissao (double comissao); 28 29 double calcularSalarioLiquido(); 30 }; 31 vendedor.h Cálculo do salário do vendedor 38 vendedor.cpp 12 Vendedor::Vendedor (string nome, 13 double comissao) : Empregado (nome) { 14 this->setComissao (comissao); 15 } 16 17 Vendedor:: ~Vendedor () { 18 } 19 20 double Vendedor::getComissao() { 21 return this->comissao; 22 } 23 24 void Vendedor::setComissao (double comissao) { 25 this->comissao = comissao; 26 } 27 28 double Vendedor::calcularSalarioLiquido() { 29 return (Empregado::calcularSalarioLiquido() + 30 this->getComissao() ); 31 } O método calcularSalarioLiquido() de Empregado é usado na subclasse. 39 13 int main (int argc, char** argv) { 14 15 double comissao; 16 17 cout << "Entre a comissao do vendedor: " << endl; 18 cin >> comissao; 19 20 Empregado *e = new Empregado ("Ana"); 21 Vendedor *v = new Vendedor ("Otavio", comissao); 22 23 cout << "Empregado: " << e->getNome() << endl; 24 cout << "Salario liquido = " << e->calcularSalarioLiquido() << 25 endl << endl; 26 cout << "Vendedor: " << v->getNome() << endl; 27 cout << "Salario liquido = " << v->calcularSalarioLiquido() << 28 endl << endl; 29 30 delete e; 31 delete v; 32 } 33 main.cpp Observe, neste exemplo: ! O método getSalarioLiquido() foi declarado em Vendedor ! Por isso, na classe Vendedor.cpp o método é definido. ! Na definição, para usar o método da superclasse tivemos que usar o operador de resolução de escopo. ! Por quê? O que aconteceria se não tivéssemos usado? Experimente!!!! 40 Caso 3. Dar uma nova implementação para o método da superclasse Neste caso, f na subclasse B é independente de f definida em A 41 O método calcularSalarioLiquido() é redefinido na subclasse Pesquisador. ! Suponha que o salário do pesquisador seja incrementado com um bônus proporcional ao número de artigos publicados, incidindo IRPF sobre o total. 18 class Pesquisador : public Empregado { 19 private: 20 int numeroArtigos; 21 22 public: 23 Pesquisador (string nome, int numeroDeArtigos); 24 ~Pesquisador (); 25 26 int getNumeroDeArtigos(); 27 void setNumeroDeArtigos (int numeroDeArtigos); 28 29 double calcularSalarioLiquido(); 30 31 }; Exemplo 3 42 pesquisador.h 12 double const adicionalPesquisador = 1000.0, bonus = 300; 13 14 Pesquisador::Pesquisador (string nome, 15 int numeroDeArtigos) : Empregado (nome) { 16 this->numeroDeArtigos = numeroDeArtigos; 17 this->salario = this->salario + adicionalPesquisador; 18 } 19 20 Pesquisador::~Pesquisador() { 21 cout << "Pesquisador removido" << endl; 22 } 23 24 int Pesquisador::getNumeroDeArtigos() { 25return this->numeroDeArtigos; 26 } 27 28 void Pesquisador::setNumeroDeArtigos (int numeroDeArtigos) { 29 this->numeroDeArtigos = numeroDeArtigos; 30 } 31 32 double Pesquisador::calculaSalarioLiquido() { 33 return (this->salario + (this->getNumeroDeArtigos() * bonus) * 34 0.85); 35 } 43 pesquisador.cpp 44 main.cpp 13 int main (int argc, char** argv) { 14 15 Pesquisador *p = new Pesquisador ("Manoela", 3); 16 17 cout << "Pesquisadora: " << p->getNome() << endl; 18 cout << "Salario liquido = " << p->calcularSalarioLiquido() << 19 endl << endl; 20 21 delete p; 22 } ! A implementação de getSalarioLiquido() da subclasse Pesquisador é completamente independente da implementação da superclasse Empregado. ! Existe uma implementação específica do destrutor da classe Pesquisador mas mesmo assim, o destrutor da superclasse é acionado! Observe, neste exemplo, que: 45 ! Membros privados não são acessíveis aos objetos das subclasses! ! Exemplo 4: Se a declaração de Empregado for: Atributos Privados na Superclasse 18 class Empregado { 19 private: 20 string nome ; 21 double salario; 22 23 public: ... 46 In constructor 'Pesquisador::Pesquisador(std::string, int)':| include\Empregado.h|21|error: 'double Empregado::salario' is private| A compilação resultará em Atributos Privados na Superclasse Para aumentar o desacoplamento, declaram-se atributos como private e se usam os métodos de acesso nas classes derivadas! 47 18 class Empregado { 19 private: 20 string nome ; 21 double salario; 22 23 public: 24 Empregado (string nome); 25 ~Empregado (); 26 27 string getNome(); 28 void setNome (string nome); 29 double getSalario(); 30 void setSalario (double salario); 31 32 double calcularSalarioLiquido(); 33 }; Para acessar os atributos privados empregado.h Exemplo 4 ! Os métodos de Pesquisador ficam: 48 13 double const adicionalPesquisador = 1000.0, bonus = 300; 14 15 Pesquisador::Pesquisador (string nome, 16 int numeroDeArtigos) : Empregado (nome) { 17 this->numeroDeArtigos = numeroDeArtigos; 18 this->setSalario (Empregado::getSalario() + 19 adicionalPesquisador); 20 } 21 ... 34 double Pesquisador::calcularSalarioLiquido() { 35 return ( (this->getSalario() + (this->getNumeroDeArtigos() * 36 bonus) ) * 0.85); 37 } Agenda 1. Herança 2. Modo Protegido 3. Polimorfismo 4. Princípio da Substituição 5. Casting 49 Polimorfismo ! Polimorfismo é o nome que se dá à propriedade de um mesmo nome ter diferentes significados. ! Nos exemplos, o método getSalarioLiquido é polimórfico, porque há diferentes implementações para o mesmo nome. ! O compilador precisa resolver qual o método mais adequado para cada momento. ! Esse assunto será tratado com rigor na próxima aula! 50 Agenda 1. Herança 2. Modo Protegido 3. Polimorfismo 4. Princípio da Substituição 5. Casting 51 Princípio da substituição de Liskov “ Se S é um subtipo declarado de T, objetos do tipo S devem se comportar como se espera que objetos de T se comportem, se forem tratados como objetos do tipo T. ” 52 O que isso significa? ! Que em todos os contextos em que um objeto de T for requerido, um objeto de S é admitido. ! Que as funções que usam ponteiros ou referências a objetos de T podem usar objetos de S sem qualquer modificação. ! Por isso, podemos fazer: Empregado *p = new Pesquisador ("Manoela", 3); 53 Exemplo 5 ! Suponha que precisemos de uma aplicação que cadastre os empregados contratados pela empresa, mantendo uma lista de empregados ! Só vamos saber o tipo de cada empregado durante a execução do programa. ! Podemos fazer uma listaDeEmpregados como uma lista de ponteiros para Empregado, independente de qual o tipo de empregado. ! Pelo princípio da substituição, onde se pede um objeto do tipo Empregado pode-se usar um objeto de uma de suas subclasses. 54 55 14 int main (int argc, char** argv) { 15 16 Empregado *listaDeEmpregados [10]; 17 int numeroDeEmpregados = 0; 18 int tipoDeEmpregado; 19 int numeroDeArtigos = 0; 20 double comissao = 0.; 21 string nome; 22 bool sair = false; 23 24 while (!sair) { 25 cout << "Informe tipo: 1 para Empregado, 2 para Pesquisador, 3 para Vendedor, outro para sair" 26 << endl; 27 cin >> tipoDeEmpregado; 28 29 switch (tipoDeEmpregado) { 30 case 1: 31 cout << "nome do Empregado: "; 32 cin >> nome; 33 listaDeEmpregados [numeroDeEmpregados] = new Empregado (nome); 34 break; 35 36 case 2: 37 cout << "nome do Pesquisador: "; 38 cin >> nome; 39 listaDeEmpregados [numeroDeEmpregados] = new Pesquisador (nome, 40 numeroDeArtigos); 41 break; 42 43 case 3: 44 cout << "nome do Vendedor: "; 45 cin >> nome; 46 listaEmpregados [numeroDeEmpregados] = new Vendedor (nome, 47 comissao); 48 break; 49 50 default: 51 sair = true; 52 break; 53 } 54 55 // incrementa o número de empregados na lista 56 numeroDeEmpregados++; 57 } 56 29 switch (tipoDeEmpregado) { 30 case 1: 31 cout << "nome do Empregado: "; 32 cin >> nome; 33 listaDeEmpregados [numeroDeEmpregados] = new Empregado (nome); 34 break; 35 36 case 2: 37 cout << "nome do Pesquisador: "; 38 cin >> nome; 39 listaDeEmpregados [numeroDeEmpregados] = new Pesquisador (nome, 40 numeroDeArtigos); 41 break; 42 43 case 3: 44 cout << "nome do Vendedor: "; 45 cin >> nome; 46 listaEmpregados [numeroDeEmpregados] = new Vendedor (nome, 47 comissao); 48 break; 49 50 default: 51 sair = true; 52 break; 53 } 54 55 // incrementa o número de empregados na lista 56 numeroDeEmpregados++; 57 } 57 58 59 // imprime o nome de cada empregado 60 cout << endl << "Imprimindo a lista de empregados..." << 61 endl; 62 63 for (int i = 0; i < (numeroDeEmpregados - 1); i++) { 64 cout << listaDeEmpregados[i]->getNome() << endl; 65 66 } 67 } Observe, neste exemplo, que: ! Não sabemos, a priori, qual o tipo de Empregado que será criado. ! listaDeEmpregados é uma lista de ponteiros para Empregados. No entanto, ela pode receber ponteiros para as subclasses de Empregado. 59 Exemplo 6 O que aconteceria se... ! ... quiséssemos imprimir também o salário de cada Empregado? ! Neste caso, o método calcularSalarioLiquido é diferente para cada subclasse. 60 61 59 // imprime o nome de cada empregado 60 cout << endl << "Imprimindo a lista de empregados..." << 61 endl; 62 63 for (int i = 0; i < (numeroDeEmpregados - 1); i++) { 64 cout << "O salario de " << listaDeEmpregados[i]->getNome() << 65 " e R$" << listaDeEmpregados[i]->calcularSalarioLiquido() << 66 endl; 67 } 68 } O que houve com o salário líquido do vendedor Riquinho??? Agenda 1. Herança 2. Modo Protegido 3. Polimorfismo 4. Princípio da Substituição 5. Casting 62 Casting 63 Casting ! É a conversão de tipos controlada pelo desenvolvedor. ! Em C++• Conversão de C ! (tipo) valor: • int a = (int) b; // b é do tipo float. • Conversão estática (tipos relacionados) ! static_cast<tipo>(valor) Inseguro (unsafe) • Conversão dinâmica ! dynamic_cast<tipo>(valor) Seguro (safe) 64 Casting em hierarquia de tipos ! Upcasting: Tipos genéricos (acima, na hierarquia) sempre podem receber objetos de suas subclasses. • Há garantia que subclasses possuem pelo menos os mesmos métodos que a classe base (Princípio da substituição). ! Downcasting: Tipos específicos (abaixo, na hierarquia) não podem receber explicitamente objetos que foram declarados como referências de suas superclasses. • É preciso converter a referência. 65 Casting em hierarquia de tipos ! Casting estático: • refFilha = static_cast<Filha *>(refPai); ! O código acima funciona mas não é exatamente o que queremos. Pode resultar em problemas depois. 66 1 Empregado* e = new Vendedor ("João“, 300); 2 Pesquisador* p = e; // Erro, “e” não é do tipo esperado 3 4 Pesquisador* p = static_cast<Pesquisador *>(e); // Ok! 5 int numeroDeArtigos = p->getNumeroDeArtigos(); Cast (unsafe) 1 double calcularSalarioLiquido(Empregado* e) { 2 if (Pesquisador* p = dynamic_cast<Pesquisador *>(e)) { 3 return p->calcularSalarioLiquido (); 4 } else if (Vendedor* v = dynamic_cast<Vendedor *>(e)) { 5 return v->calcularSalarioLiquido (); 6 } else { 7 return e->calcularSalarioLiquido (); 8 } 9 } Casting em hierarquia de tipos ! Casting dinâmico: • if (refFilha = dynamic_cast<Filha *>(refPai)) { ... }; 67 Cast (safe) Bibliografia ! Budd, T. An Introduction to Object-Oriented Programming. Addison-Wesley, 3rd ed. 2002. ! Lafore, Robert. Object Oriented Programming in C++. Sams, 2002. ! Savitch, W. C++ Absoluto. Pearson, 1st ed. 2003. 68
Compartilhar