Renato Cardoso Mesquita Departamento de Eng. Elétrica da UFMG [email protected] Programação Orientada a Objetos em C++ . . 4. Programação Orientada a Objetos em C++ . . . . . . Implementação dos mecanismos, conceitos e estruturas da Orientação a Objetos, estudados no Capítulo 2; • Abstração: Classes e suas instâncias (objetos). Abstração de dados (atributos de um objeto ou de uma classe) e de comportamento (funções membro (de objeto ou da classe)). Membros static contêm atributos de classe ou métodos da classe; membros não static são atributos e métodos dos objetos; • Encapsulamento: Acessos aos membros das classes: public, protected, private; declaração friend; • Hierarquia: Conceito de Herança, itens 4.1 e 4.2. • Modularidade: Programas em C++ podem ser implementados em vários módulos; além disto, pode-se separar a interface (.h) da implementação (.cpp). Em C++ tem-se também o conceito de Namespaces que pode ser usado para auxiliar na modularização; • Tipificação: um programa não deve aceitar intercâmbio com objetos de tipos diferentes, a não ser onde isto seja efetivado de forma controlada, através de construtores ou através de operadores de conversão de tipos. • Polimorfismo: Múltiplas formas, uma só interface: itens 4.3 e 4.4. • Persistência: Objetos que mantêm seus atributos mesmo quando acabam os seus escopos. C++ não suporta diretamente a persistência. . . Programação Orientada a Objetos em C++ Pg. 2 4.1. Herança Simples A partir de uma classe derivam-se novas classes, definindo-se uma hierarquia, em que no primeiro nível está a classe base (ou superclasse, ou classe pai) e, abaixo desta, estão suas derivações, chamadas classes derivadas (ou subclasses, ou classes filhas) Vantagens: • Modelar relações entre classes: explicitar o que há de comum entre algumas classes. Por exemplo, o que um triângulo e um círculo têm em comum? à ambos são "tipos de formas geométricas"; o que um funcionário e um cliente têm em comum? à ambos são "tipos de pessoas" ... • Aproveitar classes já existentes, implementando as modificações que forem necessárias à especialização de classes, modificando-as e mantendo intacto o código já existente. • Muitas vezes, o código fonte da classe não está disponível (biblioteca de classes) e sua alteração é impossível. Pode-se derivar novas classes, realizando-se novas implementações aproveitando as características desejáveis nas classes da biblioteca e definindo novos comportamentos para o que queremos mudar. Herança simples • Na herança simples, toda classe possui apenas uma classe base. Na herança múltipla, as classes derivadas possuem mais de uma classe base. Sintaxe: class Nome_Derivada : [modificador_de_acesso] Nome_ Base { ... // Declaração dos membros da classe derivada }; class Animal { protected: int idade; Programação Orientada a Objetos em C++ Pg. 3 public: Animal(int id){idade=id;} void mostra_idade(); void modifica_idade(); }; Class Ave : public Animal { protected: char nome[20]; public: Ave (int,char *); mostra_nome(); }; Ave A(2, “Patativa”); A.mostra_idade(); A.mostra_nome(); Animal idade mostra_idade() modifica_idade() Ave nome mostra_nome() • Para que é usada a herança pública? Para modelar estruturas “É um tipo de ...” : “Ave é um tipo de animal” possuindo seus atributos e métodos, podendo substituir a classe base em situações onde se espera receber um objeto da classe base. Se uma função espera um objeto do tipo Animal como parâmetro, pode-se passar um objeto do tipo Ave! • Um ponteiro para Animal pode apontar para um objeto Ave; • Uma lista de Animais pode conter objetos Ave; • etc ... void func1(Animal &bicho); // Protótipo da função ... Animal A; Ave B; ... func1(A) ; // OK, A é um Animal func1(B); // OK, B é uma Ave, que é um tipo de Animal void func2( Animal ani, Ave av) { Animal *ptani = &av; // OK, toda Ave "é um tipo de Animal" Ave *ptav = &ani; // OOPS, erro: nem todo animal é uma ave! ptav->mostra_nome( ); // OOPS, erro: ptav estaria apontando Programação Orientada a Objetos em C++ Pg. 4 // para animal! ptav = (Ave *) ptani; // Conversão na "marra". Funciona // porque ptani aponta para uma Ave ptav->mostra_nome( ); // Agora funciona ... } • Em síntese, um objeto da classe derivada pode ser acessado como se fosse um objeto da classe base, quando for manipulado através de ponteiros ou referências. O contrário não é válido! • Classes obtidas por herança pública herdam todos os membros da classe base. Porém, só têm acesso aos membros protegidos e públicos da classe base, isto é, os membros privados da classe base não são acessíveis na classe derivada! Exemplo: class Empregado { private: String primeiro_nome, ultimo_nome; char inicial_do_nome_do_meio; public: void print( ) const; String nome_completo( ) const ; // ... }; class Gerente : public Empregado { private: short nivel; public: void print( ) const; }; void Gerente :: print( ) const { cout << " O nome é " << nome_completo( ) ; //OK! // ... } void Gerente :: print( ) const { Programação Orientada a Objetos em C++ Pg. 5 cout << " O nome é " << primeiro_nome ; // ERRO! // ... } O mais adequado seria a classe derivada acessar o que é público na classe base, adicionando o que for específico dela. Assim: void Gerente :: print( ) const { Empregado :: print( ) ; // As informacoes de Empregado cout << nivel; // E as informacoes especificas de Gerente } Classes derivadas não herdam os construtores, o operador de atribuição ("=") e o destrutor da classe base. Construtores da classe derivada podem chamar um dos construtores da classe base, através da seguinte sintaxe: Nome_Classe Derivada :: Nome_Classe_Derivada (parâmetros da classe base e da classe derivada) : Nome_Classe_Base (parâmetros da classe base) { ... // código do construtor da classe derivada } Exemplo 1: Ave :: Ave(int id1, char * n1) : Animal(id1) { strcpy (nome,n1); } Exemplo 2: class Empregado { private: String primeiro_nome, ultimo_nome; short departamento; public: Empregado( const String& n, int d); Programação Orientada a Objetos em C++ Pg. 6 // ... }; class Gerente : public Empregado { private: short nivel; public: Gerente( const string& n, int d, int nvl); }; Gerente :: Gerente (const string& n, int d, int nvl) : Empregado( n , d) // Inicializa a base { nivel = nvl; // Inicializa os membros // ... } ou então: Gerente :: Gerente (const string& n, int d, int nvl) : Empregado( n , d), // Inicializa a base nivel(nvl) // Inicializa os membros { // ... } Usando esta sintaxe, o construtor de Empregado poderia ser escrito como Empregado :: Empregado( const String& n, int d) : ultimo_nome(n), departamento(d) //Inicializa os membros { // ... } • Os objetos são construídos "de cima para baixo", isto é, primeiro é construída a base, então os membros e depois é executado o código do construtor da classe derivada. • Quando não se faz uma chamada explícita ao construtor da classe Base, o construtor sem parâmetros da classe base é chamado. Nome_Classe Derivada :: Nome_Classe_Derivada (parâmetros da classe derivada) { Programação Orientada a Objetos em C++ Pg. 7 ... // código do construtor da classe derivada } • Quando a classe derivada não possui construtor, o construtor sem parâmetos da classe base é chamado implicitamente em toda criação de instância da derivada. • Destrutores: Quando os objetos de classes derivadas deixam seu escopo, ocorre a chamada de destrutores existentes ao longo da hierarquia. A ordem de chamada dos destrutores é inversa à ordem de chamada dos construtores à primeiro são chamados os destrutores das classes derivadas, depois os membros da classe derivada são destruídos, o destrutor da classe base é chamado e os membros da classe base são destruídos. Exemplo: #include <string.h> #include <iostream.h> class Animal { protected : long quantidade; public : inline Animal (long qtde); inline ~Animal (); }; class Bovino : public Animal { protected : char raca[20]; public : inline Bovino (char * ra, long qtde); inline ~Bovino (); }; class BovinoLeite : public Bovino { protected: long producao_diaria; public : BovinoLeite (long prod, char * ra, long qtde); Programação Orientada a Objetos em C++ Pg. 8 ~BovinoLeite (); void mostrar (); }; /************** Funcoes da Classe Animal**********/ Animal :: Animal (long qtde) : quantidade(qtde) { cout << "\nEstou construindo um Animal"; } Animal :: ~Animal() { cout << "Apagando um Animal ...\n"; } /*************** Funcoes da Classe Bovino**************/ Bovino :: Bovino (char* ra, long qtde) :Animal(qtde) { strcpy (raca,ra); cout << "\nEstou construindo um Bovino"; } Bovino :: ~Bovino() { cout << "Apagando um Bovino ...\n"; } /********** Funcoes da Classe BovinoLeite ***********/ BovinoLeite :: BovinoLeite(long prod, char * ra, long qtde) : Bovino (ra, qtde), producao_diaria(prod) { cout << "\nEstou construindo um Bovino leiteiro"; } BovinoLeite :: ~BovinoLeite() { cout << "Apagando um Bovino leiteiro ...\n"; } Programação Orientada a Objetos em C++ Pg. 9 void BovinoLeite :: mostrar () { cout << "\n\nQuantidade de Animais : " << quantidade; cout << "\nRaca do Animal : "<< raca; cout <<"\nProducao Diaria:"<< producao_diaria <<"\n\n"; } int main () { BovinoLeite vaca (10,"Holandesa",350); vaca.mostrar(); return 0; } Resultados: Estou construindo um Animal Estou construindo um Bovino Estou construindo um Bovino Leiteiro Quantidade de animais: 350 Raca do animal: Holandesa Producao Diaria: 10 Apagando um Bovino leiteiro ... Apagando um Bovino ... Apagando um Animal ... OBS: O livro, na página 268 mostra a implementação de uma estrutura Todo-Parte (“contem”) através de herança pública. Não se deve fazer isto!!!! A herança pública é utilizada para implementar a estrutura “é um tipo de” e não uma estrutura “contém”. Para se implementar a estrutura Todo-Parte, devem ser utilizadas “camadas”!!! Exemplo de uma classe construída por camadas de objetos: class String { ... }; class Address { ... }; class PhoneNumber { ... }; class Person { private: String name; Address address; Programação Orientada a Objetos em C++ Pg. 10 PhoneNumber voiceNumber; PhoneNumber faxNumber; public: ... }; Quando são usadas camadas (mesmo na herança pública, como já vimos), deve-se fazer a inicialização dos objetos chamando-se os construtores na inicialização e não no código dos construtores. Exemplo: // Ineficiente! Não use! Person :: Person(String &nome, Address &endereco, PhoneNumber &telefone, PhoneNumber &fax) { name = nome; address = endereco; voiceNumber = telefone; faxNumber = fax; // Restante do Código do Construtor } // Maneira eficiente de fazer a inicialização Person :: Person(String &nome, Address &endereco, PhoneNumber &telefone, PhoneNumber &fax) : name(nome), address(endereco), voiceNumber(telefone), faxNumber(fax) { // Restante do Código do Construtor } Porque o segundo construtor é mais eficiente? Porque a construção de objetos ocorre em duas etapas: 1. É feita a inicialização dos atributos, na ordem da declaração destes na classe; 2. Execução do corpo do construtor que foi chamado. à Um construtor para String e Address, além de 2 para PhoneNumber serão chamados, antes mesmo de se entrar no corpo do construtor de Person!!! à Usar a segunda forma é mais eficiente! Programação Orientada a Objetos em C++ Pg. 11 Operador de atribuição: Deve-se lembrar de fazer a atribuição para todos os atributos de uma classe na sobrecarga do operador de atribuição. class A { private: int x; public: A(int val_inicial) : x(val_inicial) { } A& operator = (A &); }; A& A:: operator = (A & atrib) { if (this == &atrib) return *this; x = atrib.x; return *this; } class B : public A { private: int y; public: B(int val_inicial) : A(val_inicial), y(val_inicial) { } B& operator = (B &); }; // Operador de atribuição errado para a classe B B& B:: operator= (B & atrib) { if (this == &atrib) return *this; y = atrib.y; // e x ????? return *this; } // Operador de atribuição corrigido!! B& B:: operator= (B & atrib) { if (this == &atrib) return *this; A::operator=(atrib); // Chamada ao operador = de A Programação Orientada a Objetos em C++ Pg. 12 y = atrib.y; return *this; } // Outra maneira de corrigir a atribuição!! B& B:: operator = (B & atrib) { if (this == &atrib) return *this; ((A&) *this) = atrib; //Atribuição à parte A do objeto y = atrib.y; return *this; } Membros protegidos: • O modelo de membros privados/públicos funciona bem em classes que implementam tipos concretos. Porém, quando uma hierarquia de classes é introduzida, existe a questão: que acesso aos membros fornecer às classes derivadas? • Membros privados de uma classe não são acessíveis por ninguém, a não ser pelos membros da própria classe; • Membros públicos de uma classe são acessíveis por todo mundo; • Membros protegidos de uma classe não são acessíveis pelo "público em geral" mas são acessíveis pelos membros da classe derivada e seus friends. è O objetivo de se declarar membros protegidos em uma classe base é torná-los acessíveis às funções membro das classes derivadas. Pode-se, também, especificar um modificador de acesso na herança, que pode ser privado, protegido ou público: class X : public B { /* .... */}; class Y : protected B { /* .... */}; class Z : private B { /* ..... */}; Programação Orientada a Objetos em C++ Pg. 13 Acesso aos membros da classe base que se terá na classe derivada Tipo de acesso na classe Base public private protected public private protected public private protected Modificador de Acesso da Herança public public public protected protected protected private private private Tipo de Acesso na Classe Derivada public inacessível protected protected inacessível protected private inacessível private Modificador de acesso da Herança Default : Private! class Z : B { /* ..... */}; 4.2. Herança múltipla • A herança múltipla ocorre quando uma classe derivada possui duas ou mais classes base imediatas, ou seja é filha de mais de uma classe; • Obtém-se uma classe “híbrida” que combina características de várias classes; Sintaxe: class Classe_derivada : [modificador] Classe_base1, [modificador] Classe_base2, ..., [modificador] Classe_basen { // declaração dos membros da classe }; Exemplo: class Hibrida : public A, private B, public C { private: .... public: .... }; Programação Orientada a Objetos em C++ Pg. 14 Exemplo: A classe derivada deriva os atributos e pode usar os métodos das classes base. class Task { // ... public: void delay(int); // ... }; class Displayed{ // ... public: void draw( ); }; class Satellite : public Task, public Displayed { //... public: void transmit( ); }; void f( Sattelite& s) { s.draw( ); s.delay(10); s.transmit( ); } // Displayed::draw( ) // Task::delay( ) // Satellite::transmit() • Note que com herança simples as escolhas do programador para implementar as classes seriam limitadas. Satellite poderia ser um tipo de Task ou um tipo de Displayed, mas nunca de ambas as classes simultaneamente. • A classe derivada pode ser passada para funções que esperam receber uma das classes base: Satellite é um tipo de Task, mas também é um tipo de Displayed! void highlight(Displayed*); void suspend (Task*); void g(Satellite* p) { Programação Orientada a Objetos em C++ Pg. 15 highlight(p); // passa um ponteiro para a "porção Displayed" de p suspend(p); // passa um ponteiro para a "porção Task" de p } • Os construtores da classe derivada podem chamar construtores das classes base. Para tal, os parâmetros dos construtores da classe derivada devem ser os que ele mesmo usa e os que deseja passar aos construtores das bases: Classe_derivada :: Classe_derivada (parâmetros da Classe_derivada, parâmetros da Classe_base1, parâmetros da Classe_base2, ..., parâmetros da Classe_basen) : Classe_base1(parâmetros da Classe_base1), Classe_base2(parâmetros da Classe_base2), ... Classe_basen(parâmetros da Classe_basen) { // código do construtor da Classe_derivada } Exemplo (para código completo ver página 271): class Horizontal { char tipoh; protected : int posicao_horizontal(char *); public : Horizontal(char tipo) {tipoh = tipo;} }; class Vertical { char tipov; protected : int posicao_vertical(); public : Vertical (char tipo) {tipov = tipo;}; }; class Mensagem : public Horizontal, public Vertical { char * msg; public : Mensagem (char * texto, char ver, char hor); Programação Orientada a Objetos em C++ Pg. 16 ~Mensagem(); }; /* Construtor de Mensagem */ Mensagem :: Mensagem (char * texto, char ver, char hor) : Vertical(ver), Horizontal(hor) { msg = new char[strlen(texto)+1]; if (!msg) { cout << " Memoria Insuficiente"; return; } strcpy(msg, texto); } Mensagem :: ~Mensagem() { delete [ ] msg; } Ambiguidades em herança múltipla: 1- Classes Base possuem membros homônimos. • Quando o objeto da classe derivada tentar referenciar o membro homônimo, o compilador não saberá a qual das classes se está referindo. class Loteria { public: void draw(); ... }; class Objeto_Grafico { public: void draw(): ... }; class SimulaLoteria : public Loteria, public Objeto_Grafico { Programação Orientada a Objetos em C++ Pg. 17 // Não redeclara draw() !! ... }; SimulaLoteria simul; simul.draw(); // Erro! Ambiguidade! simul.Loteria::draw(); // OK! simul.Objeto_Grafico::draw(); //OK! • No exemplo anterior as funções têm o mesmo protótipo nas classes base. E se tivessem protótipos diferentes, a sobrecarga de funções não resolveria o problema? NÃO!!! class Task { // ... public: void debug (double p); }; class Displayed { // ... public: void debug(int v); }; class Satellite : public Task, public Displayed { // ... }; void g (Satellite* p) { p->debug(1); p->Task::debug(1); p->Displayed::debug(1); } // Erro de ambiguidade ... // OK //OK E se a escolha dos nomes nas diferentes classes base tivesse sido uma decisão deliberada e se quisesse a seleção da função a ser utilizada baseando-se nos tipos de argumentos? Teríamos que usar a declaração using, trazendo as funções para um escopo comum: o da classe derivada. Programação Orientada a Objetos em C++ Pg. 18 class A { public: int f(int); char f(char); // ... }; class B { public: double f(double); // ... }; class AB : public A, public B { public: using A::f; using B::f; char f(char); // esconde A::f(char) AB f(AB); }; void g (AB& ab) { ab.f(1); ab.f('a'); ab.f(2.0); ab.f(ab); } // A::f(int) // AB::f(char) // B::f(double) // AB::f(AB) 2- Duplicidade de atributo, através de classes base replicadas. class ClasA { protected : char nome[31]; public : void mostrar () {cout << "\nNome na Classe\t: " << nome;} }; class ClasB1 : public ClasA { public : ClasB1 (char * n = "Classe B1") {strcpy(nome,n);} void mostrar() {ClasA::mostrar();}; }; Programação Orientada a Objetos em C++ Pg. 19 class ClasB2 : public ClasA { public : ClasB2(char * n = "Classe B2") {strcpy(nome,n);}; void mostrar() {ClasA::mostrar();}; }; class ClasC : public ClasB1, public ClasB2 { public : void saida () { ClasB1::mostrar(); ClasB2::mostrar(); strcpy(nome,"ObjClasseC"); // OOPS!! Ambiguidade }; A A B1 B2 C int main () { ClasC objClasC; objClasC.saida(); return 0; } • A declaração do objeto da classe ClasC aciona o construtor "default" da "ClasC()", criado pelo compilador e sem código, este chama os construtores das classes base de "ClasC", na ordem declarada na herança, usando os valores "default". • ClasB1::mostrar() e ClasB2::mostrar(), MOSTRAM RESULTADOS DIFERENTES à Existem dois atributos nome em ClasC! Isto não é necessariamente um erro e pode até ser o efeito esperado! • Se não se deseja ter dois significados para os atributos da classe base quando são acessados pelas diferentes bases da herança múltipla, deve-se usar a declaração "virtual" nas derivações da raiz. A class ClasB1 : public virtual ClasA { ... }; class ClasB2 : public virtual ClasA { ... }; class ClasC : public ClasB1, public ClasB2 { public : B1 B2 C Programação Orientada a Objetos em C++ Pg. 20 void saida () { ClasB1::mostrar(); // Apresenta Classe B2 ClasB2::mostrar(); // Apresenta Classe B2 strcpy(nome,"ObjClasseC"); // Agora está OK! }; • Restrição no uso de classes virtuais: a inicialização dos atributos em uma classe base virtual deve ser feita em um construtor default pelas classes que declaram a derivação virtual, pois o contrutor vai ser chamado uma única vez e diretamente pelo construtor da classe mais em baixo na hierarquia. Isto significa que a classe base virtual deve ter um construtor default( ou com default para todos os seus parâmetros) • Se modificarmos a classe A do exemplo anterior para: class ClasA { protected : char nome[31]; public : ClasA(char *s) { strcpy(nome,s);} void mostrar () {cout << "\nNome na Classe\t: " << nome;} }; O programa apresentará três erros, um para cada construtor da hierarquia! Portanto, deveríamos ter o construtor modificado para: ClasA(char *s =" ") { strcpy(nome,s);} 4. 3. Polimorfismo e funções virtuais 4.3.1. Problemas com "tipos concretos": • Um tipo concreto, como Date, String, define uma espécie de "caixa preta". Uma vez que a caixa preta é definida, ela não possui mais alguma forma de se adaptar a novos usos, exceto pela modificação de sua definição. Esta situação pode ser a desejada, mas ela também pode levar a uma certa inflexibilidade. • Considere a definição de um tipo como "Shape", a ser usado em um sistema gráfico. Assuma que o sistema deva suportar círculos, triângulos e quadrados, e que tenhamos as seguintes classes: class Point { /* ... */}; Programação Orientada a Objetos em C++ Pg. 21 class Color{ /* ... */}; enum Kind { circle, triangle, square }; class Shape { Kind k; //Tipo Point center; Color col; // ... public: void draw( ); void rotate(float ang); // ... }; No caso desta classe, o membro Kind é necessário para especificar qual é o tipo de Shape que realmente se está armazenando, de modo que operações como draw() e rotate() possam ser executadas de modo correto. A função draw() seria semelhante a: void Shape :: draw( ) { switch (k) { case circle: // desenha um circulo break; case triangle: // desenha um triangulo break; case square: // desenha um quadrado break; } } Quais os problemas com esta estratégia? • Funções como draw() precisam conhecer todos os tipos de Shape que possam existir; • A função deve ser modificada toda vez que uma nova forma for adicionada ao sistema à o código de draw é aumentado a cada inclusão de nova forma; Programação Orientada a Objetos em C++ Pg. 22 • Na realidade, todas as funções que trabalham com shapes terão que ser examinadas e modificadas a cada nova forma adicionada; • Só poderemos adicionar uma nova forma, se tivermos acesso ao código fonte de todas as operações; • A parte de dados de Shape tem que ser escolhida de maneira que acomode todas as possíveis representações de uma forma, seja ela qual for ... Qual a saída? • A saída está em se agrupar o que é comum a todas as Shapes (isto é, elas possuem uma forma, uma cor, podem ser desenhadas, giradas, etc ..) em uma classe, e as propriedades específicas das diversas Shapes em classes derivadas ... OK, isto a gente já sabe fazer: herança! • Mas e se quisermos mais ... Especificar qual deve ser a interface de Shape, fazer com que todas as classes derivadas compartilhem esta interface e que possamos declarar um container de ponteiros para Shape e que possamos escrever funções genéricas que manipulem este container de shapes, sem saber qual shape específica está armazenada em cada região de memória apontada por cada ponteiro do container? à vamos precisar do polimorfismo ... Shape Shape * forma [3]; forma[0] = new Triangle; virtual void draw(); forma[1] = new Square; forma[2] = new Circle; Triangle Square Circle for(int i=0; i < 3; i++) forma[i]->draw(); void draw(); void draw(); void draw(); 4.3.2. Polimorfismo: • Implica funções homônimas para lógicas semelhantes ao longo de uma hierarquia; • Para que haja polimorfismo, a ligação entre o objeto e a função deve ser feita em tempo de execução (ligação dinâmica). Existe também a possibilidade de utilizarmos funções com o mesmo nome com ligação entre o objeto e a função feita em tempo de compilação: neste caso, não temos polimorfismo e sim sobrecarga de função (ligação estática)! Programação Orientada a Objetos em C++ Pg. 23 • A implementação do polimorfismo em C++ pressupõe a utilização de funções virtuais. Quando uma classe base tem funções membro que devem ter comportamentos diferentes para suas derivadas, deve-se declará-las como virtuais na classe base e redefinir seu código nas derivadas. class ClasseBase{ ... virtual protótipo_função_virtual(); ... }; class ClasseDerivada : ClasseBase { ... protótipo_função_virtual(); ... }; • Opcionalmente, pode-se repetir a palavra reservada virtual na declaração da função na classe derivada; • Sempre que o código original da classe base não implementa a lógica que desejamos na classe derivada, basta implementar uma nova lógica em outro código para a função na classe derivada. Exemplo: Sistema para gerenciamento de Transações Comerciais com clientes de uma loja: Classe Base: Transação, derivando Venda e desta Venda_Prazo. #include <iostream.h> #include <string.h> class Transacao { char cliente[31]; public : Transacao(char * nome) {strcpy(cliente,nome);}; virtual void mostrar(); }; class Venda : public Transacao { protected : double valor; public : Programação Orientada a Objetos em C++ Pg. 24 Venda (char *, double); void mostrar(); }; class VendaPrazo : public Venda { float juros; public : VendaPrazo(char *, double, float); void mostrar(); }; int main () { char nome[31]; double preco; float taxa; Transacao * vendas[3]; cout << " Nome : " ; cin.getline(nome,30); cout << " Valor a Vista : "; cin >> preco; cout << " Taxa de Juros (%) : "; cin >> taxa; vendas[0] = new Transacao(nome); vendas[1] = new Venda (nome,preco); vendas[2] = new VendaPrazo(nome,preco,taxa); for (int i = 0; i < 3; i++) { cout << i+1 << " "; vendas[i]->mostrar(); delete vendas[i]; } return 0; } void Transacao :: mostrar() { cout << "Nome do Cliente : " << cliente << '\n'; } Venda ::Venda (char* nome, double preco) :Transacao(nome) { valor = preco; Programação Orientada a Objetos em C++ Pg. 25 } void Venda :: mostrar() { Transacao::mostrar(); cout << " Valor da Compra : " << valor << '\n'; } VendaPrazo :: VendaPrazo (char * nome, double preco, float taxa) : Venda (nome, preco) { juros = taxa; } void VendaPrazo :: mostrar() { Transacao::mostrar(); cout << " Taxa Praticada : " << juros << '\n' << " Valor da Compra : " << valor*(100+juros)/100 << '\n'; } • A ligação objeto-função é efetivada em tempo de execução. Para tal, e' necessário o emprego de função virtual. • Se for removida a declaração "virtual" na definição da classe "Transação", os resultados mostrarão apenas o nome da pessoa, nas três impressões. A declaração "virtual" adia a ligação para a execução do programa. • Pode-se também usar referências para efetuar a ligação em tempo de execução: void func1(Transacao &obj) { obj.mostrar(); } Transacao tr(nome); Venda vv(nome,preco); VendaPrazo vp(nome,preco,taxa); func1(tr); // Chama Transacao::mostrar() func1(vv); // Chama Venda::mostrar() func1(vp); // Chama VendaPrazo::mostrar() • A função deve ser declarada como virtual na classe Base; • A função virtual deve estar definida para a classe Base; Programação Orientada a Objetos em C++ Pg. 26 • Os protótipos das funções nas classes derivadas devem ser idênticos aos protótipos na classe base à protótipos diferentes são considerados sobrecarga de função!!! • Funções virtuais não podem ser estáticas (pois funções estáticas não possuem o ponteiro this!); • Nunca é demais repetir: para se ter comportamento polimórfico é necessário que a função na classe base seja virtual e que estejamos manipulando objetos através de ponteiros ou referências para a classe base. Se não for este o caso, o compilador executa a ligação entre o objeto e a função a ser chamada em tempo de compilação! • Construtores não podem ser funções virtuais; • Destrutores podem ser virtuais: na realidade, se vamos usar polimorfismo, os destrutores DEVEM ser virtuais. Exemplo de problemas causados por destrutores não virtuais: //Código com problema: destrutor de Base não virtual class Base { public : ~Base() {cout << "\n\tDestruindo a Parte da Base!";} }; class Derivada : public Base { public : ~Derivada() {cout << "\n\tDestruindo a Parte da Derivada!";} }; int main() { Base base; Derivada deriv; base = deriv; Base * ptrBase = new Base; cout << "\nDeleta ObjBase : "; delete ptrBase; ptrBase = new Derivada; cout << "\nDeleta ObjDeriv : "; delete ptrBase; return 0; } Resultados: Programação Orientada a Objetos em C++ Pg. 27 Deleta ObjBase : Destruindo a Parte da Base! Deleta ObjDeriv : Destruindo a Parte da Base! Destruindo a Parte da Derivada! Destruindo a Parte da Base! Destruindo a Parte da Base! • O que isto significa???? Se alocarmos objetos da classe derivada a partir de ponteiros da classe base com destrutores não virtuais, somente o destrutor da Parte Base é chamado apesar de ptrBase estar apontando, no segundo caso para um objeto da classe Derivada. Solução: o destrutor deve ser virtual, para ter o comportamento esperado! class Base { public : virtual ~Base() {cout << "\n\tDestruindo a Parte da Base!";} }; Resultado: Deleta ObjBase : Destruindo a Parte da Base! Deleta ObjDeriv : Destruindo a Parte da Derivada! Destruindo a Parte da Base! Destruindo a Parte da Derivada! Destruindo a Parte da Base! Destruindo a Parte da Base! Exemplo:Implementação da Hierarquia de Shape Shape virtual void draw(); Triangle Square Circle void draw(); void draw(); void draw(); Programação Orientada a Objetos em C++ Pg. 28 class Point { /* ... */}; class Color{ /* ... */}; class Shape { Point center; Color col; // ... public: Point where( ) const { return center;} void move( Point to) { center = to; /* .... */ draw(); } virtual void draw( ) { }; // Sim, vazia ... virtual void rotate(float ang) { }; // Tambem ... // ... }; class Circle : public Shape { real radius; public: void draw() { /* .... */} // Não e' preciso redefinir where, move, nem mesmo rotate ... }; class Square: public Shape { real side; public: void draw() { /* .... */} void rotate(float) {/* ... */ } // Redefinida }; Shape* vpshape[30]; ... vpshape[0] = new Circle; vpshape[1] = new Square; real alpha=30; ... for(int i=0; i < 30; i++) { vpshape[i]->draw(); vpshape[i]->rotate(alpha); vpshape[i]->draw(); } Programação Orientada a Objetos em C++ Pg. 29 Agora, se quisermos extender o programa, adicionando qualquer forma nova (por exemplo um donut) , é só criar a classe correspondente (derivada de Shape), programar os métodos que forem necessários na classe derivada e pronto! Todas as partes do programa que aceitavam um ponteiro para Shape ou uma referência para Shape aceitarão a nova forma! Tabela de funções virtuais • Como o programa pode fazer a ligação objeto ó função em tempo de execução? • O C++ efetiva a ligação dinâmica graças à existência de uma tabela contendo um ponteiro para cada função virtual de uma classe, incluindo as funções virtuais herdadas de uma ou mais classes base à Tabela de funções virtuais, TFV. • Todo objeto de uma classe possui um ponteiro que o liga a esta tabela: PTR_TFV. De posse deste ponteiro e da tabela, o programa pode decidir qual função virtual deve chamar quando o objeto solicitar. class ClBase{ public: virtual void f1(int); virtual void f2(); int f3(); }; class ClDeriv : public ClBase { public: void f2(); int f3(); // Não deveria estar fazendo isto ... ! virtual int f4(float); }; void ClBase :: f1(int) { ... } TFV de ClBase void ClBase :: f2() { ... } f1 f2 TFV de ClDeriv f1 f2 f4 void ClDeriv :: f2(int) { ... } void ClDeriv :: f4(float) { ... } f3() não é virtual. Ela foi sobrecarregada em ClDeriv, mas não podemos identificar a chamada em tempo de execução, porque ela não está na tabela de funções virtuais Programação Orientada a Objetos em C++ Pg. 30 4.4. Classes Abstratas: • No capítulo 2 vimos que existem Classes para as quais não podemos declarar objetos: Classes Abstratas à servem somente como classes base, para derivar outras classes. • A classe Shape do item anterior é um exemplo típico de uma classe cuja função é esta: servir somente para derivar as classes Circle, Square, etc ... • As Classes Abstratas são aquelas em que pelo menos uma de suas funções não possui definição: funções virtuais puras. class Classe_Abstrata{ ... virtual protótipo_da_função_virtual_pura() = 0; }; • Qualquer tentativa de declaração de um objeto de uma classe Abstrata gera um erro de compilação. Entretanto, é permitido declarar referências e ponteiros para classes abstratas, desde que estes apontem para objetos de classes derivadas na hierarquia e que, nelas, as funções virtuais puras tenham todas sido definidas. • Qual o propósito de se ter uma função virtual pura? à 1- Criar uma classe abstrata; 2- Fazer com que as classes derivadas herdem somente a interface da função; • Qual o propósito de se ter funções virtuais (impuras)? à Fazer com que as classes derivadas herdem a interface da função e uma implementação default para aquela função; • Qual o propósito de se ter funções não virtuais na classe Base? à Fazer com que as classes derivadas herdem a interface da função e uma implementação obrigatória para aquela função; Nunca redefina uma função não virtual herdada, pois isto pode trazer sérios problemas, quando estamos trabalhando com objetos dinâmicos: o polimorfismo não funcionaria! B x; class A{ A *pA = &x; // Ponteiro para x; public: pa->mf(); // OOPS! Chama A::mf() void mf(); ... B *pB = &x; }; pb->mf(); // Chama B::mf() class B: public A{ public: void mf(); // Esconde A::mf() !!! Programação Orientada a Objetos em C++ Pg. 31 ...}; • è Devemos sempre fazer diferença entre herança da interface e herança da implementação. Classe Shape, revisitada ... class Point { /* ... */}; class Color{ /* ... */}; class Shape { // Classe Abstrata Point center; Color col; // ... public: Point where( ) const { return center;} void move( Point to) { center = to; /* .... */ draw(); } virtual void draw( ) = 0 // Virtual pura virtual void rotate(float ang)= 0; // Tambem ... virtual bool is_closed ( ) = 0; // Tambem // ... }; class Circle : public Shape { real radius; public: void draw() { /* .... */} void rotate (float) { } // Vazia, precisa definir a funcao pura bool is_closed( ) { return true;} Circle (Point p, int r); }; Uma classe que não redefine uma função virtual pura continua abstrata herdada de uma classe base continua abstrata. Isto nos permite construir a implementação em etapas: class Poligon: public Shape { public: bool is_closed( ) { return true; } // draw e rotate não foram redefinidas è Polígono é abstrata }; Poligon b; // Erro: declaracao de um objeto de classe abstrata class Irregular_polygon : public Poligon { Programação Orientada a Objetos em C++ Pg. 32 list <Point> lp; // Template que existe na STL public: void draw(); void rotate(float); // Não se tem mais funções virtuais puras // ... }; Irregular_polygon poly(alguns_pontos); // OK, se existir o construtor Um uso importante de classes abstratas é no fornecimento de uma interface sem expor qualquer detalhe da implementação. Por exemplo, um sistema operacional poderia esconder os detalhes dos seus dispositivos físicos em uma classe abstrata: class Device { public: virtual int open(char*, int, int) = 0; virtual int close(int) = 0; virtual int read(int, char*, int) = 0; virtual int write(int, const char*, int) = 0; virtual int ioctl(int, int ...) = 0; virtual ~Device(); // Destrutor virtual }; Poderíamos especificar drivers para dispositivos físicos como classes derivadas de Device e manipular uma grande variedade de drivers através da interface comum a todos eles class Disk : public Device { public: int open(char*, int, int); int close(int); int read(int, char*, int); int write(int, const char*, int); int ioctl(int, int ...); Disk(); ~Disk(); }; Poderíamos também definir uma classe para os processos: class Proc { Programação Orientada a Objetos em C++ Pg. 33 public: virtual int sleep (Sleepq*, int); virtual void wakeup(); virtual int save_runstate(); virtual void resume_runstate(); virtual int send_msg(int); Proc(int); Proc (Proc &); ~Proc(); }; è Pela introdução de classes abstratas temos os mecanismos básicos para escrever um programa completo de forma modular, utilizando classes como nossos blocos de construção. Exemplo de Projeto de Hierarquia de Classes Seja um problema de projeto simples: fornecer uma maneira de um usuário receber um número inteiro a partir da interface com o usuário. • De modo a isolar o nosso programa da grande variedade de opções de entrada que um sistema possa ter, e também para explorar as escolhas de projeto, vamos começar definindo nosso modelo desta operação simples de entrada. • Atributos e métodos básicos: uma classe Ival_box que conheça a faixa de valores de entrada que ela irá aceitar: um programa poderá pedir a Ival_box o seu valor, pedir que ela apresente um prompt para o usuário, se necessário, o programa poderá perguntar a Ival_box se o usuário modificou o valor desde que o programa o consultou pela última vez, etc ... • Devido ao fato de que existem n maneiras de se implementar esta idéia, assume-se que poderemos ter muitos tipos diferentes de Ival_boxes, como controles deslizantes, caixas de texto, interação de voz, etc ... • A idéia básica é construir um sistema de interface virtual com o usuário, que fornecerá vários dos serviços dos sistemas de interface com o usuário existentes. Ele vai poder ser implementado numa ampla variedade de formas, de modo a garantir a portabilidade do código de aplicação. Primeira abordagem: uma hierarquia de classes tradicional: class Ival_box { Programação Orientada a Objetos em C++ Pg. 34 protected: int val; int low, high; bool changed; public: Ival_box(int ll, int hh) { changed = false; val = ll; high = hh;} virtual int get_value() { changed=false; return val;} virtual void set_value(int i) (changed=true; val =i;} virtual void prompt( ) { } virtual bool was_changed( ) const { return changed;} }; Um programador usaria esta classe de forma semelhante a: void interact (Ival_box* pb) { pb->prompt(); // alerta o usuário // ... int i = pb->get_value(); if(pb->was_changed()) { // Novo valor; faça alguma coisa } else // O valor antigo está OK, faça outra coisa } // ... } void fct() { Ival_box *p1 = new Ival_slider(0,5); // Ival_slider derivada //de Ival_box interact(p1); Ival_box *p2 = new Ival_dial(1,12); // Idem Ival_dial interact(p2); } • Não se está preocupado em saber como um programa espera pela entrada do usuário: talvez se tenha uma função get_value(), talvez o programa associe Ival_box com um evento e prepara-a para responder a uma função callback, ou talvez o programa dispare uma thread para o sistema de interface com o usuário... Isto é um detalhe de implementação e não interfere no nosso projeto! Programação Orientada a Objetos em C++ Pg. 35 class Ival_slider : public Ival_box{ // Parte gráfica, que define a aparência de slider, etc ... public: Ival_slider (int, int); int get_value(); void prompt(); }; • Outras classes derivadas poderiam ser definidas: Ival_dial, popup_ival_slider, flashing_ival_slider, etc ... • De onde pegaríamos a parte gráfica: de uma biblioteca de classes gráficas, por exemplo da “Big Bucks Inc” . Poderíamos fazer nossas classes um tipo de BBwindow. Então: class Ival_box : public BBwindow{/*...*/}; //agora usa BBwindow class Ival_slider : public Ival_box{ /* ...*/}; class Ival_dial: public Ival_box {/* ... */}; class Flashing_ival_slider : public Ival_slider {/* ... */}; class Popup_ival_slider : public Ival_slider {/*...*/}; BBwindow Ival_box Ival_dial Popup_ival_slide r Crítica a este projeto: Ival_slider Flashing_ival_slider • O projeto funciona bem, porém tem alguns detalhes que podem ser motivo de se procurar formas alternativas de implementação. 1. BBwindow como base de Ival_box: isto não está correto... Ival_box implementa um conceito diferente do que é implementado por Programação Orientada a Objetos em C++ Pg. 36 BBwindow. O fato de Ival_box ser uma janela de BBwindow é, na realidade, um detalhe de implementação. Derivar Ival_box de BBwindow eleva um detalhe de implementação a uma decisão de projeto de primeiro nível! E se mudássemos a nossa classe de janelas para as fornecidas por “Imperial Bananas” ou para “Liberated Software” ou para “Compiler Whizzes”? Teríamos que manter versões distintas do programa ... Pesadelo ! class Ival_box: public BBwindow {/*...*/}; class Ival_box: public CWwindow {/*...*/}; class Ival_box: public IBwindow {/*...*/}; class Ival_box: public LSwindow {/*...*/}; // versao BB // versao CW // versao IB // versao LS 2. Toda classe derivada compartilha os dados básicos declarados em Ival_box. Estes dados são um detalhe de implementação, e não deveriam estar na interface de Ival_box. Por exemplo, um Ival_slider não precisa do valor inteiro armazenado, pois este pode ser calculado diretamente da posição do controle deslizante quando alguém executa get_value(). Ter dois valores relacionados, mas diferentes, é fonte permanente de erros! É melhor manter os dados privados, de forma que os programadores das classes derivadas não possam criar confusão com eles. Melhor ainda, porque não colocar os dados somente nas classes derivadas, onde eles podem ser definidos de forma a atingir os requisitos destas classes de forma exata? Segunda abordagem: uma hierarquia com classes abstratas: Objetivos: • O sistema de interface com o usuário deve ser um detalhe de implementação escondido dos usuários da classe que não precisam saber sobre ele; • A classe Ival_box não deve conter dados, fornecendo apenas uma interface; • Nenhum tipo de recompilação de código usando a família de classes deve ser necessária depois de uma mudança no sistema de interface com o usuário; • Ival_boxes para diferentes sistemas de interface devem coexistir no nosso sistema. Ival_box como uma interface pura: class Ival_box { public: virtual int get_value() = 0; Programação Orientada a Objetos em C++ Pg. 37 virtual void set_value(int i) =0; virtual void prompt( ) =0; virtual bool was_changed( ) =0; virtual ~Ival_box() { } // Destrutor virtual }; class Ival_slider : public Ival_box, protected BBWindow { public: Ival_slider (int, int); ~Ival_slider(); int get_value(); void prompt(); bool was_changed() const; protected: // Funcoes que sobrescrevem funções virtuais de BBwindow // p.ex. BBwindow::draw(), BBwindow::mouse1hit() private: //Dados necessários para slider }; • BBwindow é somente um detalhe de implementação à protected • Ival_box fornece a interface da implementação à pública A hierarquia agora é definida da seguinte forma: class Ival_box {/*...*/}; class Ival_slider : public Ival_box, protected BBwindow { /* ...*/}; class Ival_dial: public Ival_box, protected BBwindow {/* ... */}; class Flashing_ival_slider : public Ival_slider {/* ... */}; class Popup_ival_slider : public Ival_slider {/*...*/}; BBwindow BBwindow Ival_box Ival_slider Ival_dial “Implementado usando” ou “Detalhe de implementação” Popup_ival_slide r Flashing_ival_slider Programação Orientada a Objetos em C++ Pg. 38 Terceira abordagem: uma implementação alternativa: A implementação anterior está melhor, mas ainda não resolve o problema de controle de diversas versões class Ival_box {/*...*/}; // o que é comum class Ival_slider : public Ival_box, protected BBwindow {/* ... */} class Ival_slider : public Ival_box, protected CWwindow {/*... */} A solução óbvia é definir várias classes Ival_slider, com nomes separados: class Ival_box {/*...*/}; // o que é comum class BB_ival_slider : public Ival_box, protected BBwindow {/* ... */} class CW_ival_slider : public Ival_box, protected CWwindow{/*... */} Pode-se ainda refinar da seguinte forma: class Ival_box {/*...*/}; class Ival_slider : public Ival_box {/* ... */ }; // o que é comum class BB_ival_slider : public Ival_slider, protected BBwindow {/*...*/} class CW_ival_slider :public Ival_slider, protected CWwindow{/*. */} Ival_box BBwindow Ival_slider BB_ival_slider CWwindow Cw_ival_slider Pode-se ainda refinar a solução, usando classes mais específicas na hierarquia de implementação. Por exemplo, se a “Big Bucks Inc” possuir sua própria classe slider, podemos derivar a nossa Ival_slider diretamente de BB_slider: class BB_ival_slider : public Ival_slider, protected BBslider {/*...*/} class CW_ival_slider :public Ival_slider, protected CWslider{/*. */} Programação Orientada a Objetos em C++ Pg. 39 BBwindow Ival_box CWwindow BBslider Ival_slider CWslider BB_ival_slider Cw_ival_slider Este melhoramento se torna significativo onde nossas abstrações não são muito distintas das utilizadas pelo sistema utilizado para implementação. A programação se transforma em um mapeamento entre conceitos similares. A hierarquia então se transformará em: class Ival_box {/*...*/}; class Ival_slider : public Ival_box{ /* ...*/}; class Ival_dial: public Ival_box {/* ... */}; class Flashing_ival_slider : public Ival_slider {/* ... */}; class Popup_ival_slider : public Ival_slider {/*...*/}; seguida pelas implementações desta hierarquia para vários sistemas de interface com o usuário, expressas como classes derivadas: class BB_ival_slider: public Ival_slider, protected BBslider {/* ... */}; class BB_flashing_ival_slider : public Flashing_ival_slider, protected BBslider {/* ... */}; class BB_popup_ival_slider: public Popup_ival_slider, protected BBslider {/* ... */}; class CW_ival_slider: public Ival_slider, protected CWslider {/* ... */}; // ... • A hierarquia chegou em um ponto quase ideal. Conseguimos jogar para a parte mais baixa as possíveis dependências de implementação . • Um problema que resta é que a criação de objetos ainda deve ser feita utilizando nomes dependentes da implementação, como CW_ival_dial e BB_flashing_ival_slider. Se mudarmos de sistema de janelas, deveremos “catar” no nosso código todas as ocorrências de declarações de objetos de um tipo e substituí-los por outros. Programação Orientada a Objetos em C++ Pg. 40 Quarta abordagem: introduzindo uma classe para as operações de criação de objetos: class Ival_maker { public: virtual Popup_ival_slider* popup_slider(int, int)=0; //cria popup slider virtual Ival_dial* dial(int, int) = 0; // cria um dial // ... }; class BB_maker : public Ival_maker { // cria as versões BB public: Popup_ival_slider* popup_slider(int, int); //cria popup slider virtual Ival_dial* dial(int, int); // cria um dial // ... }; class LS_maker : public Ival_maker { // cria as versões LS public: Popup_ival_slider* popup_slider(int, int); //cria popup slider virtual Ival_dial* dial(int, int); // cria um dial // ... }; Cada função cria um objeto do tipo desejado (interface e implementação): Ival_dial* BB_maker ::dial(int a, int b) { return new BB_ival_dial(a,b); } Ival_dial* LS_maker :: dial(int a, int b) { return new LS_ival_dial(a,b); } Dado um ponteiro para um Ival_maker, um usuário pode agora criar objetos sem saber exatamente qual sistema de interface com o usuário está sendo utilizado. Por exemplo: void user(Ival_maker* pim) { Ival_box = pim->dial(0,99); // cria um objeto dial apropriado // ... } BB_maker BB_impl; // para usuários BB Programação Orientada a Objetos em C++ Pg. 41 LS_maker LS_impl; // para usuários LS void driver() { user(&BB_impl); // usa BB user(&LS_impl); // usa LS } Variante para a criação de objetos: “Construtores virtuais”. class Expr { public: Expr(); // construtor default Expr(const Expr&); //construtor de cópia virtual Expr* new_expr() {return new Expr();} virtual Expr* clone() { return new Expr(*this);} // ... }; • new_expr() e clone() são virtuais e indiretamente constroem objetos: por isto, são chamadas de “construtores virtuais” • Uma classe derivada pode sobrescrever new_expr() e/ou clone() para retornar um objeto de seu próprio tipo: class Cond : public Expr { public: Cond(); Cond(const Cond&); Cond* new_expr() {return new Cond();} Cond* clone() { return new Cond(*this);} // ... }; Então void user (Expr* p) { Expr* p2 = p->new_expr(); } Se o objeto apontado por p for da classe Expr, p2 apontará para um objeto da classe Expr; se o objeto apontado por p for da classe Cond, p2 apontará para um objeto da classe Cond! Os objetos também podem ser copiados sem perda de informação, usando o método clone().