Coleção UAB−UFSCar Sistemas de Informação Ednaldo Brigante Pizzolato Introdução à programação orientada a objetos com C++ e Java Introdução à programação orientada a objetos com C++ e Java Reitor Targino de Araújo Filho Vice-Reitor Pedro Manoel Galetti Junior Pró-Reitora de Graduação Emília Freitas de Lima Secretária de Educação a Distância - SEaD Aline Maria de Medeiros Rodrigues Reali Coordenação UAB-UFSCar Daniel Mill Denise Abreu-e-Lima Joice Lee Otsuka Valéria Sperduti Lima Coordenadora do Curso de Sistemas de Informação Sandra Abib UAB-UFSCar Universidade Federal de São Carlos Rodovia Washington Luís, km 235 13565-905 - São Carlos, SP, Brasil Telefax (16) 3351-8420 www.uab.ufscar.br [email protected] Conselho Editorial José Eduardo dos Santos José Renato Coury Nivaldo Nale Paulo Reali Nunes Oswaldo Mário Serra Truzzi (Presidente) Secretária Executiva Adriana Silva EdUFSCar Universidade Federal de São Carlos Rodovia Washington Luís, km 235 13565-905 - São Carlos, SP, Brasil Telefax (16) 3351-8137 www.editora.ufscar.br [email protected] Ednaldo Brigante Pizzolato Introdução à programação orientada a objetos com C++ e Java 2011 © 2010, Ednaldo Brigante Pizzolato Concepção Pedagógica Daniel Mill Supervisão Douglas Henrique Perez Pino Equipe de Revisão Linguística Ana Luiza Menezes Baldin André Stahlhauer Andréia Pires de Carvalho Ângela Cristina de Oliveira Jorge Ialanji Filholini Mariucha Magrini Neri Paula Sayuri Yanagiwara Priscilla Del Fiori Sara Naime Vidal Vital Equipe de Editoração Eletrônica Izis Cavalcanti Juliana Greice Carlino Rodrigo Rosalis da Silva Equipe de Ilustração Jorge Luís Alves de Oliveira Thaisa Assami Guimarães Makino Capa e Projeto Gráfico Luís Gustavo Sousa Sguissardi Ficha catalográfica elaborada pelo DePT da Biblioteca Comunitária da UFSCar P695i Pizzolato, Ednaldo Brigante. Introdução à programação orientada a objetos com C++ e Java / Ednaldo Brigante Pizzolato. -- São Carlos : EdUFSCar, 2010. 155 p. -- (Coleção UAB-UFSCar). ISBN – 978-85-7600-204-8 1. Programação orientada a objetos (Computação). 2. C++ (Linguagem de programação de computador). 3. Java (Linguagem de programação de computador). 4. Classes e objetos. I. Título. CDD – 005.11 (20a) CDU – 681.32.06 Todos os direitos reservados. Nenhuma parte desta obra pode ser reproduzida ou transmitida por qualquer forma e/ou quaisquer meios (eletrônicos ou mecânicos, incluindo fotocópia e gravação) ou arquivada em qualquer sistema de banco de dados sem permissão escrita do titular do direito autoral. ........... Sumário Apresentação. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 Unidade 1: Introdução 1.1 Primeiras palavras. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 1.2Problematizando o tema. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 1.3Linguagens imperativas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 1.3.1 Encapsulamento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 1.3.2 Visibilidade . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 1.3.3 Encapsulamento e visibilidade. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 1.4Considerações finais. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 1.5Estudos complementares. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 Unidade 2: Introdução às linguagens de programação C++ e Java 2.1Primeiras palavras. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 2.2Problematizando o tema. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 2.3A História das linguagens . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 2.4C++ e Java – estrutura básica de um programa. . . . . . . . . . . . . . 24 2.5Considerações finais. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32 2.6Estudos complementares. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32 Unidade 3: Introdução à orientação a objetos 3.1Primeiras palavras. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35 3.2Problematizando o tema. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35 3.3O paradigma OO. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36 3.4Classes e objetos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38 3.5Elementos de uma classe. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40 3.6Diagrama de classes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 3.7Relacionamento entre classes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .43 3.8Exemplos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44 3.9Considerações finais. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54 Unidade 4: Introdução à sobrecarga 4.1Primeiras palavras. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57 4.2Problematizando o tema. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57 4.3Sobrecarga de métodos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58 4.4Sobrecarga de operadores. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59 4.5Exemplos em Java. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65 4.6Considerações finais. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 4.7Estudos complementares. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67 Unidade 5: Alocação dinâmica de memória 5.1Primeiras palavras. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71 5.2Problematizando o tema. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71 5.3Revisão. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72 5.4Alocação de atributos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78 5.5Construtor de cópia. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79 5.6Alocação de objetos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82 5.7Exemplo em C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83 5.8Considerações finais. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88 Unidade 6: Composição e herança 6.1Primeiras palavras. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91 6.2Problematizando o tema. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91 6.3Composição . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91 6.4Herança . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92 6.5Exemplo em Java. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93 6.6Exemplo em C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96 6.7Considerações finais. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102 Unidade 7: Polimorfismo 7.1Primeiras palavras. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105 7.2Problematizando o tema. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105 7.3Polimorfismo paramétrico. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106 7.3.1 Templates em C++. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106 7.3.2 Generics em Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109 7.4 Polimorfismo de inclusão . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110 7.4.1 Polimorfismo de inclusão em C++ . . . . . . . . . . . . . . . . . . . . . . . . 110 7.4.2 Polimorfismo de inclusão em Java. . . . . . . . . . . . . . . . . . . . . . . . 114 7.5Considerações finais. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116 Unidade 8: Classes abstratas 8.1Primeiras palavras. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119 8.2Problematizando o tema. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119 8.3Definição de classes abstratas . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119 8.4Classes abstratas em Java e em C++ . . . . . . . . . . . . . . . . . . . . . . 120 8.4.1 Exemplos em Java. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120 8.4.2 Exemplos em C++. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122 8.5Considerações finais. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122 Apêndice A. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123 Apêndice B. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129 Referências. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153 Apresentação Este livro é para quem já sabe algoritmos e já construiu programas em linguagens como C ou Pascal. O leitor deve, portanto, ter familiaridade com os conceitos básicos de programação, tais como o conceito de variáveis, escopo, entrada e saída de dados, estruturas de seleção, estruturas de repetição, subprogramas (funções e procedimentos), passagem de valores por valor ou por referência, entre outros. Apesar de este livro abordar tais conceitos, não tem como objetivo principal a explicação dos mesmos. O leitor já deve saber, também, que existem várias linguagens de programação (C++, C#, Java, Pascal, Delphi, Fortran, Algol, PL/I, Cobol, Lisp, Prolog – só para citar algumas), mas talvez não saiba a razão da existência de tantas linguagens. Algumas foram concebidas por questões de aplicação (para serem científicas ou mais voltadas a transações comerciais ), enquanto outras foram criadas em decorrência do próprio avanço tecnológico. Todas as linguagens têm em comum o fato de terem uma sintaxe e uma semântica bem definida. Podem ser interpretadas ou compiladas e, normalmente, estão relacionadas a um determinado paradigma de programação. Alguns dos paradigmas de programação existentes são: • Programação Imperativa (ou Procedural); • Programação Orientada a Objetos; • Programação Concorrente; • Programação Lógica; • Programação Funcional. Espera-se que o leitor já tenha tido contato com alguma linguagem representante do paradigma imperativo ou procedural (por incluir procedimentos e funções) e tenha tido oportunidade de implementar programas usando tal linguagem. Exemplos de linguagens do paradigma imperativo ou procedural são C, Pascal, Fortran e PL/I. Também se espera que o leitor saiba o que seja programação estruturada. O conhecimento prévio do que são algoritmos e como elaborá-los, bem como o conhecimento prévio de uma linguagem de programação e dos conceitos de programação estruturada contribuirá para o entendimento dos conceitos de orientação a objetos e das linguagens C++ e Java aqui abordadas. A opção por duas linguagens permitirá que o leitor teste os conceitos de orientação a objetos em ambientes diferentes e verifique que alguns conceitos estão presentes em uma linguagem e não em outra. 11 Os principais objetivos deste livro são: • Apresentar ao(à) leitor(a) o paradigma de orientação a objetos e seus principais conceitos; • Fornecer conhecimentos sobre as linguagens de programação C++ e Java; • Mostrar como os conceitos de programação orientada a objetos podem ser implementados em ambas as linguagens (quando possível); • Permitir que o(a) leitor(a) crie aplicativos em Java utilizando os conceitos de orientação a objetos com interface gráfica. Pedirei permissão ao(a) leitor(a) para tratá-lo(a) por você daqui em diante. 12 Unidade 1 Introdução 1.1 Primeiras palavras Para entender o paradigma de orientação a objetos e sua importância é preciso entender qual foi o primeiro paradigma de programação, suas linguagens representativas, seus problemas e como ocorreu a transição (evolução) para o paradigma de orientação a objetos. Esta unidade objetiva apresentar uma visão geral sobre o paradigma imperativo e o orientado a objetos. Mais detalhes podem ser obtidos em livros sobre paradigmas e linguagens de programação. 1.2 Problematizando o tema Por que não utilizamos nossa linguagem para transmitir para a máquina (computador ou robô) nossas ideias ou intenções? Na verdade, é preciso entender que o computador atual trabalha com a linguagem binária (zeros e uns) e que tudo o que queremos que ele faça deve ser especificado em forma de algoritmo e, posteriormente, traduzido para a linguagem da máquina. Um algoritmo especifica quais ações devem ser feitas/tomadas e em qual ordem. As linguagens de programação devem representar de forma clara e objetiva o que está especificado no algoritmo. Queremos que as intenções ou ideias sejam transmitidas de forma a não causar dúvidas ao computador ou robô. Não podemos conceber que um computador pare de executar uma ação e tente adivinhar o que o programador gostaria, na realidade, que fosse feito. Por exemplo: “robô, por favor, tome seu lubrificante agora” e “robô, por favor, tome o ônibus das 3 para ir ao centro da cidade” são frases que utilizam o mesmo verbo (tomar) para situações completamente diferentes. O robô deve entender que a primeira ação significa engolir e a segunda ação significa utilizar o meio de transporte coletivo para se deslocar até o centro da cidade. Também temos uma ideia natural de que as ações devem respeitar uma determinada ordem. Por exemplo, para a troca de um pneu furado é necessário desparafusar o pneu e também é necessário erguer o carro para que outro pneu seja colocado. Entretanto, se dispusermos apenas de equipamento mecânico (chave de roda) para desparafusar a roda, não é aconselhável que o carro seja erguido antes que as porcas sejam afrouxadas. O contato do pneu com o solo impede que a roda gire e facilita o processo. Também não devemos retirar as porcas inteiramente com o carro no solo, sob o risco de que ele tombe e cause algum acidente. Se houver passageiros, também é importante que eles saiam do carro antes de ele ser erguido. Não se pode ordenar que os passageiros saiam se eles não existirem. É preciso que existam comandos que possibilitem a mudança de fluxo 15 (execução de um conjunto de ações se uma determinada condição for satisfeita). Dessa forma, sequência de comandos, estruturas de seleção e de repetição fazem parte das chamadas linguagens imperativas. 1.3 Linguagens imperativas Inicialmente, uma linguagem imperativa era composta de comandos simples, tais como atribuição, comandos condicionais e comandos de desvio de fluxo. Com a combinação desses comandos (o que ocorria, por exemplo, em Fortran-IV) era possível implementar qualquer algoritmo, pois a mudança de valor de uma variável era obtida por meio do comando de atribuição, um conjunto de comandos poderia ser executado a partir da avaliação de uma expressão condicional e a repetição de um conjunto de comandos poderia ser conseguida por intermédio da combinação de comandos de desvio de fluxo com comandos condicionais. O uso combinado de tais comandos possibilita representar qualquer algoritmo que se deseje elaborar. Isso também é conhecido como “Touring Complete”. De acordo com Tucker & Noonan (2007), uma linguagem imperativa é uma linguagem “Touring Complete” e também apresenta as seguintes características: • Estruturas de controle (seleção e repetição); • Entrada e saída de dados; • Tratamento de erro e de exceções; • Abstração procedural; • Expressões e comandos de atribuição; • Bibliotecas de apoio. Deve-se notar que os paradigmas de programação orientada a objetos, programação funcional e programação lógica também contemplam o conceito de “Touring Complete”. Dentro do contexto do paradigma de linguagens imperativas está a programação estruturada. Nela, evita-se que o fluxo de execução seja desviado de forma desorganizada. Isso porque o comando de desvio de fluxo de execução normalmente empregado era o goto (vá para). Com a introdução da programação estruturada, procurou-se organizar o código e deixá-lo mais legível. Entraram em cena os comandos de repetição que estabeleciam regras para o desvio de fluxo de execução sem deixar de satisfazer o “Touring Complete”. 16 Figura 1 Representação de código de repetição estruturado e não estruturado. Do ponto de vista visual, uma repetição, ou loop, poderia ser representada por um colchete que indica na extremidade superior o início da repetição e na extremidade inferior o final desta. A Figura 1 apresenta a visão estruturada à esquerda e a visão não estruturada à direita. São apenas 2 repetições, mas já é possível se ter uma ideia do que pode acontecer com um código não estruturado. Apesar da organização do código, ainda havia um problema central com as linguagens imperativas: o foco nas funções e não nos dados. Isso talvez explicasse os longos prazos de desenvolvimento de sistemas, os elevados orçamentos, a insatisfação dos clientes, etc. Do ponto de vista técnico, o levantamento de requisitos e o projeto de software tinham especificações e representações muito diferentes das utilizadas pelos programadores. Os dados, por outro lado, não eram protegidos (o foco era nas funções), o que dificultava a garantia da integridade das informações. Também era difícil reaproveitar o código, fazendo com que uma mesma ideia fosse implementada novamente ou seu código fosse duplicado e adaptado. Porém, mudanças em uma parte do código poderiam gerar altos custos de manutenção para garantir que não havia inconsistências. Era necessária, ainda, a reformulação da documentação (quando existente). Os profissionais passaram a concentrar os esforços na solução do problema (modificação do código) e não na documentação. A falta de documentação contribuía para a geração de sistemas ineficientes, com custos ainda mais elevados de manutenção. Era preciso um novo conceito, um novo paradigma para superar os desafios da área. É nesse contexto que surgiu o paradigma de orientação a objetos. Mas ele não surgiu de repente. Foi um processo gradual, que se iniciou em 1967 com a linguagem Simula-67. Nela havia um esforço em transferir também para os dados a ideia de abstração de procedimentos. A ideia de abstração de dados procura permitir que o programador crie novos tipos de dados que sejam mais adequados ao problema que está tentando resolver. Envolve, também, os conceitos de encapsulamento e visibilidade. 1.3.1 Encapsulamento Não é preciso recorrer a um dicionário para inferir sobre as ideias envolvidas em encapsulamento. Encapsulamento vem de encapsular, que significa colocar 17 em cápsulas. Cápsulas podem nos remeter à ideia de remédios ou de foguetes tripulados. Uma cápsula de remédio é um envoltório que protege seus componentes contra a incidência de luz, umidade e contato com impurezas que possam comprometer seu desempenho. Também são projetadas para, em contato com nosso estômago, liberar gradativamente o remédio. Cápsulas de foguete são desenhadas para proteger a vida dos integrantes da missão tripulada. Permitem que eles recebam fluxo contínuo de oxigênio e que sejam protegidos contra o frio e o calor intenso, além de outros riscos. Em ambos os casos, pode-se observar que a palavra cápsula está associada a proteção. O conceito pode ser mais abrangente. Em uma busca pela World Wide Web podemos encontrar a seguinte definição de cápsula fonocaptora: Trata-se de um transdutor em miniatura que, ao percorrer as ondulações dos sulcos de vinil, transforma as vibrações mecânicas em impulsos elétricos, que por sua vez serão amplificados, resultando em som audível.1 Assim, podemos aplicar o conceito de cápsula também para ações. Na verdade, a própria medicina aprimorou o conceito de cápsula criando a “cápsula endoscópica”. Ela é um dispositivo similar a uma cápsula de remédio, que contém uma microcâmera para capturar imagens do intestino. A microcâmera fica protegida, pelo envoltório da cápsula, da ação dos ácidos presentes em nosso organismo. A mesma cápsula protege nosso organismo de eventuais perfurações que podem ser causadas pela microcâmera. De uma forma bem simples podemos verificar que uma cápsula tem a função de reunir um conjunto de elementos em um único dispositivo e também de proteger o material interno de ações indevidas do ambiente externo. A proteção está associada à visibilidade, abordada a seguir. A Unidade 3 apresentará exemplos de classes nas quais os conceitos de encapsulamento e visibilidade serão utilizados. 1.3.2 Visibilidade A visibilidade está relacionada à propriedade de ser visto ou não. Algo é visível quando está exposto. Aumentar a visibilidade implica em aumentar a exposição. Diminuir a visibilidade também está relacionado com a diminuição de exposição. Um objeto pode tanto estar ao alcance da visão (e, portanto, ser visível) como pode estar escondido. O conceito de visibilidade, em programação orientada a 18 1 Disponível em: <http://pt.wikipedia.org/wiki/Cápsula_fonocaptora>. Acesso em: 22 fev. 2010. objetos, está mais associado aos componentes que ao próprio objeto. Seria como termos uma cápsula de remédio dividida em duas partes, sendo uma delas transparente e a outra não. Pela parte transparente conseguimos ver o que há dentro daquela parte da cápsula, mas não temos ideia do que há dentro da outra parte (pois está escondida). 1.3.3 Encapsulamento e visibilidade Quando colocamos todos os componentes eletrônicos de um liquidificador em um compartimento, estamos escondendo-os do usuário e, ao mesmo tempo, encapsulando. O usuário terá acesso às funcionalidades do liquidificador por meio dos botões de ligar e desligar, bem como dos botões de velocidade. Outro exemplo seria o aparelho de televisão. Seus componentes eletrônicos estão encapsulados em um compartimento que é responsável pela sintonia dos canais e pela apresentação das imagens recebidas. As funcionalidades, apesar de estarem presentes dentro do compartimento, são acessíveis aos usuários a partir de botões externos ou do controle remoto. O encapsulamento, em ambos os exemplos, permite que os componentes eletrônicos fiquem protegidos do acesso direto do usuário. Não imaginamos alguém sintonizando um canal de televisão agindo diretamente nos dispositivos eletrônicos, utilizando-se de uma chave de fenda. Assim, os projetistas dos equipamentos pensaram nas funcionalidades às quais os usuários teriam acesso e criaram os botões ou controles externos. Somente esses botões são visíveis aos usuários. Outra vantagem do encapsulamento pode ser compreendida por meio dos mesmos exemplos: a composição. Construímos chips eletrônicos a partir da combinação de circuitos em um componente, chamado circuito integrado. Combinamos as funcionalidades de vários circuitos integrados em uma placa. Criamos um dispositivo eletrônico a partir da combinação de várias placas. Se você abrir seu computador, verá várias placas (placa mãe, placa de vídeo, placa de som, etc.). Se você abrir seu aparelho de DVD, também verá várias placas, e assim por diante. Com a composição é possível construir dispositivos de forma mais rápida, como exige a indústria eletroeletrônica. 1.4 Considerações finais Nesta unidade apresentamos o paradigma de linguagem de programação imperativa, bem como alguns problemas relacionados a ela. Indicamos que havia a necessidade de um novo paradigma que focasse não somente o código, mas também os dados. Introduzimos alguns conceitos importantes, como encapsulamento 19 e visibilidade, que serão úteis quando da introdução ao paradigma de orientação a objetos. Na próxima unidade serão introduzidas as linguagens de programação C++ e Java, em preparação para a apresentação dos conceitos de orientação a objetos. Orientações sobre ambientes de programação para as duas linguagens podem ser obtidas no Apêndice A deste livro. 1.5 Estudos complementares O(a) leitor(a) que desejar obter mais informações sobre paradigmas de programação poderá consultar o livro: TUCKER, A. B.; NOONAN, R. E. Programming Languages: Principles and Paradigms. New York: McGraw-Hill, 2007. O paradigma de linguagem de programação imperativa é apresentado no capítulo 12. Outra leitura sugerida é a do livro: MITCHELL, J. C.; APT, K. Concepts in Programming Languages. Cambridge: Cambridge University Press, 2001. No capítulo 1 são apresentadas as linguagens de programação, os principais objetivos e a história das linguagens de programação. Na parte 2 do livro são apresentados os conceitos da programação imperativa em 4 capítulos. 20 Unidade 2 Introdução às linguagens de programação C++ e Java 2.1 Primeiras palavras Nesta unidade você terá contato com os principais comandos das linguagens C++ e Java. A apresentação dos comandos não é detalhada, visto que se supõe que o leitor já tenha tido contato com a linguagem de programação C. Os conceitos de orientação a objetos não serão apresentados ainda. O objetivo principal da unidade é fazer com que o leitor tenha familiaridade com as duas linguagens, para que possa entender os conceitos de orientação a objetos que serão apresentados na próxima unidade. Para melhor compreensão dos programas e conceitos apresentados, é aconselhável que o leitor implemente os códigos desta unidade. Sugere-se a consulta ao Apêndice A deste livro, caso tenha dúvidas sobre os ambientes de programação para ambas as linguagens. 2.2 Problematizando o tema Existem alguns casos em que o conhecimento apenas da teoria é o suficiente para exercer a profissão. Em alguns casos o adjetivo teórico vem associado a ela. Um caso exemplar é o físico teórico. O físico teórico trabalha com as teorias da física e não tem muito diálogo com os físicos práticos. Entretanto, outras profissões requerem a prática do ofício. É o caso de chef de cozinha, garçom, médico, odontologista, etc. Um exemplo clássico é o de piloto de aeronaves, em que as horas de voo contam como experiência e como habilitação para pilotar aviões de maior porte. Apesar de ser possível, é pouco provável que uma pessoa que esteja aprendendo a programar computadores consiga se tornar um(a) exímio(a) programador(a) apenas com as informações teóricas. Em geral, sugere-se que o(a) aprendiz(a) coloque os conceitos em prática. Esta unidade apresentará conceitos seguidos de pequenos exemplos que facilitarão o aprendizado das linguagens C++ e Java. Ah, você notará que as duas linguagens são muito parecidas. Por isso, antes de apresentarmos as estruturas básicas das duas linguagens, cabe apresentar um pouco da história das linguagens de programação que influenciaram ambas. 23 2.3 A História das linguagens Com a Internet é fácil satisfazer nossas curiosidades. Por exemplo, podemos, por meio de uma simples pesquisa na World Wide Web (www), descobrir que Plankalkul2 foi a primeira linguagem de programação, criada em 1945 por Konrad Zuse, na Alemanha. Fortran é a linguagem de programação de computadores mais velha ainda em uso. Foi criada nos anos 1950 por John Backus e seu nome vem de FORmula TRANslation, o que denota sua vocação para a computação que envolve cálculo. Existem alguns dialetos de FORTRAN muito famosos, como o Fortran-77 e o Fortran-90. Em várias árvores genealógicas de linguagens de programação (sim, existem árvores genealógicas que indicam a ordem de aparecimento das linguagens de programação!) a linguagem Fortran aparece lá nas origens. Outra linguagem que influenciou significativamente a sintaxe e a estrutura das linguagens mais recentes foi a Algol. As linguagens de programação orientadas a objeto foram influenciadas por essas linguagens. Infelizmente, a quantidade de linguagens de programação é superior a 200, portanto não conseguiríamos representar corretamente toda essa história. Aqui, nossa intenção é apenas demonstrar que C++ e Java são parentes. Por isso você notará muita semelhança entre elas. Bom, se desejássemos simplificar a história da linguagem C++, poderíamos dizer que ela sofreu grande influência da linguagem C with classes (que originou-se de C) e da Simula. C (1971) originou-se da linguagem B (1968), que se originou de BCPL (1967), que se originou de CPL (1963), que se originou de Algol. Java, por outro lado, originou-se de OAK, que teve influência de várias outras linguagens de programação: ADA, OBJECTIVE-C, C++, CEDAR, SMALLTALK e SCHEME. Note que Java sofre influência de C++ (e, portanto, de C também!). 2.4 C++ e Java – estrutura básica de um programa O C++ foi desenvolvido por Bjarne Stroustrup, dos Bell Labs, durante a década de 1980 com o objetivo de implementar uma versão distribuída do núcleo (kernel) do sistema operacional Unix. Stroustrup percebeu que a linguagem Simula possuía características bastante interessantes para o desenvolvimento de softwares, mas era muito lenta para uso prático. Resolveu combinar suas características com as da linguagem C (rápida e portável para diversas plataformas) de forma a construir uma nova linguagem mais poderosa. Inicialmente denominou-a C with classes, que em português significa C com classes. Depois, mudou seu nome para C++. 24 2 Plankalkul é uma combinação de duas palavras: plan e kalkul. Kalkul significa cálculo e plan significa plano. Mais informações sobre essa linguagem podem ser obtidas em Sebesta (2009). // meu primeiro programa em C++ #include <iostream> using namespace std; int main (){ cout << “Olá pessoal!“ << endl; return 0; } Em 1985 foi lançada a primeira edição do livro sobre a linguagem, chamado The C++ Programming Language. Um exemplo bem simples de código C++ é apresentado no código anterior. É importante destacar que ainda não há uso do conceito de orientação a objetos. A primeira linha do código apresentado é um comentário de linha. Em C++ é possível realizar comentários em uma única linha ou mesmo em parte dela. Todo o texto colocado após as duas barras será ignorado pelo compilador. Um comando válido da linguagem antes das barras duplas será executado normalmente. Diferentemente dos comentários da linguagem C, não há a necessidade de se colocar outras duas barras para indicar o final do comentário. Ele termina automaticamente com o final da linha. Outra possibilidade de comentário é a utilização do /* e */. Todo o texto inserido após a combinação /* será considerado comentário até que a combinação */ seja encontrada. A última combinação indica o final dos comentários. Você já deve saber que comentários ajudam na compreensão do código e fazem parte da chamada documentação interna. Tenha o hábito de fazer comentários em seu código. Isso facilitará a compreensão futura das ideias nele contidas. A próxima linha de código é uma diretiva de pré-processamento. Nela há a indicação de se inserir o arquivo chamado iostream.h. Uma dúvida muito comum é com relação ao uso de <> ou “ ” para delimitar o nome. Se você utilizar o primeiro conjunto de limitadores, estará deixando claro para o compilador que deseja que o arquivo seja procurado na pasta includes do compilador. O uso das aspas indicará para o compilador que o arquivo a ser inserido está na pasta corrente. O arquivo iostream.h em questão é necessário para que sejam utilizados os comandos de entrada e saída de dados. Um exemplo de saída de dados é apresentado na linha anterior ao return 0; e será comentado em breve. A declaração using namespace std; indica ao compilador que estaremos utilizando o namespace std. Isso evitará que tenhamos que escrever, por exemplo, o comando std::cout. É uma linha muito frequente em programas da linguagem C++. 25 O programa principal está em uma função chamada main – assim como em um programa da linguagem C – que pode ou não ter um tipo de retorno. Se você declarar que a função main retorna um inteiro, então a função deverá retornar um inteiro antes de ser finalizada. No código exemplo em questão, o comando return 0; existe exatamente porque há um tipo de retorno para a função main (int main). Se o código tivesse sido escrito com tipo de retorno void, então não haveria necessidade do comando return. O comando de saída cout << “Olá pessoal!” << endl; é uma simplificação do comando printf da linguagem C. O comando em questão imprimirá a mensagem “Olá pessoal!” e em seguida mudará de linha. O comando endl significa final de linha. É equivalente ao “\n” da linguagem C. Na verdade, também é possível utilizar o \n em C++. O comando equivalente ao apresentado anteriormente com \n seria: cout << “Olá pessoal !\n“; Assim como a saída de dados ficou mais simples, a entrada de dados também. O código a seguir calcula a média de 3 notas de um aluno. As notas são digitadas e a média calculada é exibida na tela. // cálculo da média de 3 notas em C++ // diretiva de pré-processamento // indica para incluir o arquivo iostream.h #include <iostream> // declaração do namespace using namespace std; // programa principal int main (){ float media, nota, soma; int i; soma = 0; // loop for idêntico ao da linguagem C for (i=0;i<3;i++) { cout << “Nota da prova“ << i; cin >> nota; soma = soma + nota; } 26 // cálculo da média media = soma / 3; // exibição do resultado na tela cout << “Média = “ << media << endl; return 0; } Quem está familiarizado com um código em C verá que existem poucas diferenças para a linguagem C++ referentes aos comandos básicos. A principal diferença está nos comandos de entrada e saída. Aliás, como a linguagem C é um subconjunto da linguagem C++, quem sabe a linguagem C terá pouco trabalho para aprender a linguagem C++. No novo exemplo, o único comando novo é o de entrada de dados (cin >> nota). Note que, diferentemente da linguagem C, a linguagem C++ não exige formatação dos dados. A entrada de dados tornou-se mais simples. É necessária, no entanto, uma atenção especial aos direcionadores << e >>. O primeiro está associado ao cout e indica saída de dados, ao passo que o segundo está associado ao cin e indica entrada de dados. Assim como o cout permite a escrita de várias informações por meio da combinação de direcionadores de saída, o cin também permite a leitura de vários valores a partir da combinação de direcionadores de entrada. Se houvéssemos escrito o código sem loop, teríamos que ter criado 3 variáveis para nota (chamemos de nota1, nota2 e nota3). O comando para leitura das 3 notas poderia ser: cin >> nota1 >> nota2 >> nota3; Note que cin apenas faz leitura de dados. Se houver a necessidade de que uma informação seja apresentada para o usuário (geralmente isso acontece), então é preciso utilizar o comando cout. Não é possível exibir uma mensagem para o usuário utilizando o cin. Outras diferenças importantes entre C e C++ são a declaração de variáveis lógicas por meio do tipo bool e a declaração de constantes. Para representar variáveis lógicas, o C não tinha um tipo definido. Era preciso utilizar variáveis inteiras e interpretar seus valores como sendo valores lógicos. Em C o 0 (zero) representa o valor booleano falso, enquanto o 1 (um) representa o valor verdadeiro. Em C++ é possível deixar o código mais limpo nesse sentido, pelo uso do tipo de dado bool. Com ele é possível criar uma variável que aceite os valores verdadeiro ou falso. Com relação ao uso de constantes, em C, a declaração de uma constante acontecia com o uso da diretiva #define. Tal diretiva simplesmente substituía, em 27 tempo de compilação, a cadeia de caracteres pelo valor. Por exemplo, se o programa necessitasse utilizar PI, então em C teríamos uma diretiva: #define PI 3.14159 Em todos os locais onde houvesse a palavra PI, a mesma seria substituída por 3.14159. Note que não há tipo de dados associado. É apenas uma substituição simples de um valor por outro. Em C++ é possível definir um tipo para uma posição de memória e informar que a mesma será constante, ou seja, o programa não poderá alterar seu conteúdo dali em diante. Um exemplo de declaração de constante seria: const float PI = 3.14159; Nesse caso o compilador não substitui a cadeia de caracteres PI por 3.14159. Na verdade, o valor é armazenado e PI é utilizado como se fosse uma variável (só que é constante). Vamos, agora, criar um programa em Java semelhante ao nosso primeiro programa em C++. Mas antes, vamos contar um pouco sobre a linguagem Java. Ela foi desenvolvida nos laboratórios da Sun Microsystems no início dos anos 1990, com o objetivo de ser uma linguagem-base de projetos de software para produtos eletrônicos. Sim, o objetivo era colocar código em torradeiras e geladeiras! Mas o programa desenvolvido em linguagem C não era tão genérico, dependia muito da plataforma alvo. Na melhor das hipóteses, tinha que ser recompilado. No início dos anos de 1990, Patrick Naughton, James Gosling e Mike Sheridan, três cientistas da Sun, começaram a definir as bases para o projeto de uma nova linguagem de programação que não contivesse os conhecidos problemas das linguagens tradicionais, como C e C++. Especificaram a linguagem e a denominaram OAK. Entretanto, por problemas de Copyright, tiveram que a renomear para Java, em homenagem ao café que consumiam na Sun. A linguagem criada é mais simples que C++, pois não tem sobrecarga de operadores, structs, unions, aritmética de ponteiros e herança múltipla. Além disso, existe o gerenciamento de memória (garbage collection), que evita o vazamento de memória. Conquistou atenção devido a sua aplicação na World Wide Web, em que programas Java possibilitam a criação de animações (por meio de applets). Mas isso veremos mais tarde. O grande diferencial da linguagem Java é a independência de plataforma. Java consegue tal independência porque seu compilador não gera instruções 28 específicas de uma plataforma, mas sim código intermediário, denominado bytecode. Bytecode pode ser descrito como uma linguagem de máquina destinada a um processador virtual que não existe fisicamente. Para que o código possa ser executado é preciso que exista uma máquina virtual Java (ou JVM), que interpretará o código intermediário e executará as instruções conforme o planejado. Assim, máquinas virtuais devem ser instaladas nas máquinas alvo e serão responsáveis pela “conversa” com o processador. Um código Java compilado gerará um código intermediário, que poderá ser submetido a qualquer máquina virtual e será executado sem a necessidade de recompilação. Isso é diferente de um código escrito em C++ e compilado para um processador específico. Apesar de o programador ter que fazer pouca ou nenhuma alteração no código C++, ele terá, no mínimo, que o recompilar. Isso não será necessário para o código Java. Por outro lado, para executar um programa em Java é necessário que a plataforma alvo tenha uma máquina virtual Java. Vamos, então, examinar um programa simples escrito em Java. O programa deverá ser criado em um arquivo chamado teste.java, pois estamos criando uma classe em Java chamada teste. Os programas em Java devem ser armazenados em arquivos cujos nomes sejam iguais ao da classe com a extensão .java. class teste { // classe teste sem nada // declaração de main() public static void main(String args[]) { System.out.println(“Olá pessoal“); } } Esse código pode ser escrito em um editor de texto simples e compilado com o comando de linha javac. Para executá-lo, basta digitar o comando java teste no prompt de comando. Não se esqueça de que a pasta corrente deve ser a pasta em que o programa foi gravado. Esse exemplo simples mostra que não precisamos incluir arquivo algum. Há uma função principal chamada main (em C++ também há!) que não retorna valor algum. A saída de dados é realizada pelo comando println (escrita com mudança de linha), acionado pela linha de código: 29 System.out.println(“...“); import javax.swing.JOptionPane; class teste { public static void main(String args[]) { String x; x = JOptionPane.showInputDialog(null, “Digite uma msg: “); System.out.println(x); } } As chaves servem para delimitar o início e o fim de uma estrutura ou bloco de código, da mesma forma que acontece em C e C++. Antes de iniciarmos um exemplo do cálculo da média de 3 valores em Java, é conveniente estudar o programa do código anterior. Ele introduz a leitura em Java. Sim, agora temos um comando de importação que é um pouco parecido com o include do C++, visto anteriormente. Como Java é puramente orientado a objetos, tudo nele refere-se a classes. O reuso de classes já escritas é simples. Basta informar apenas onde estão. Assim, o comando import da primeira linha indica que utilizaremos a classe JOptionPane, do pacote swing, que está dentro do pacote javax. Podemos importar classes das seguintes formas: a) import javax.swing.*; b) import javax.swing.nome_da_classe; No primeiro caso (a), todas as classes do pacote swing são visíveis. No segundo caso (b), apenas a classe especificada está visível. Alternativamente, poderíamos ter omitido o comando import e ter escrito o comando de leitura assim: x = javax.swing.JOptionPane.showInputDialog (...); Ou seja, informaríamos o nome completo da classe. Ah, os imports mais utilizados são: import java.awt.*; // GUI. import java.awt.event.*; // GUI event listeners. import javax.swing.*; // Mais GUI. import java.util.*; // estruturas de dados import java.io.*; 30 import java.text.*; // entrada e saída // Classes de formatação. import java.util.regex.*; // Expressões regulares Bom, a declaração de String é algo novo para quem conhece C, mas será algo familiar em breve para os aprendizes de C++ e Java, pois String está presente em ambas as linguagens. String, na verdade, é um tipo de dados utilizado para armazenar uma cadeia de caracteres. Assim, o resultado da chamada da “função” (vamos, por enquanto, chamar showInputDialog de função) será atribuído a uma cadeia de caracteres cujo nome é x. Por fim, imprimimos o que foi lido. Enquanto não estiver familiarizado com ambientes de programação como o NetBeans, você pode testar os códigos aqui apresentados apenas utilizando um editor de texto simples e os comandos javac e java no prompt de comando, como já indicado. Para elaborar o programa do cálculo da média de 3 valores, precisamos fazer a leitura de 3 valores em ponto flutuante (float ou double) e efetuar a divisão por 3. A leitura, assim como no programa apresentado em C++, pode ser feita dentro de um loop for: import javax.swing.JOptionPane; import java.lang.*; class teste { public static void main(String args[]) { float nota, media, soma = 0; int i; for (i=0;i<3;i++) { nota = Float.parseFloat(JOptionPane.showInputDialog( null, “Digite uma nota: “)); soma = soma + nota; } media = soma/3; System.out.println(media); } } Para converter o valor lido por JOptionPane.showInputDialog em float é preciso chamar a “função” (novamente, vamos chamar de função, por enquanto) de conversão de tipo para converter de String para float. O valor convertido é armazenado em nota e o processo é semelhante ao apresentado no exemplo do código C++ para a mesma funcionalidade. Um estudo mais cuidadoso das duas linguagens verificará que existem muitas similaridades entre ambas. As estruturas de seleção (if e switch) são iguais, assim como as de repetição, as expressões aritméticas e lógicas, os operadores aritméticos e lógicos e assim por diante. 31 Existem poucas diferenças entre as duas linguagens. Uma delas é o tipo de dado boolean para armazenar valores lógicos (em C++ é bool). Outra diferença está no trabalho com vetores e matrizes. Em Java, para se declarar um vetor de inteiros chamado dias_do_mes, precisamos fazer as seguintes declarações: int dias_do_mes[ ]; dias_do_mes = new int[12]; A primeira linha apenas indica ao compilador que haverá um vetor. A variável é automaticamente ajustada para null quando o primeiro comando termina. Somente após a alocação (por meio do comando new int seguido do tamanho do vetor) é que o vetor passa a existir. Em Java, não é preciso preocupar-se com a devolução da memória alocada para o sistema operacional. Isso é automaticamente feito pelo sistema interno de coleta de lixo. Uma vantagem da linguagem Java é, portanto, o controle efetivo da memória utilizada. Outros exemplos neste livro permitirão melhor conhecimento de ambas as linguagens. 2.5 Considerações finais Nesta unidade vimos um pouco da história das linguagens C++ e Java. Ambas são muito parecidas e os códigos aqui apresentados comprovam tais semelhanças. Para o leitor familiarizado com a linguagem C, aprender ambas as linguagens não será um grande problema. 2.6 Estudos complementares Existem diversos livros dedicados ao aprendizado de C++ e Java para iniciantes. O livro indicado a seguir apresenta os principais comandos da linguagem Java de forma clara e didática. ARAÚJO, D. L.; REIS, F. G. Desenvolvendo com Java 2: para iniciantes. Rio de Janeiro: Book Express, 2000. 32 Unidade 3 Introdução à orientação a objetos 3.1 Primeiras palavras Com o aumento da complexidade dos sistemas e a necessidade cada vez maior de reuso de códigos já implementados, surgiu a necessidade de um novo paradigma de programação (orientação a objetos) que trouxesse mais flexibilidade e segurança na tarefa de codificação e mais economia para as indústrias de softwares. Considerando-se que um programa de computador tem código e tem informações (também chamadas de dados), há a possibilidade de se focar no código ou nos dados. Focar no código significa considerar importantes os acontecimentos, ao passo que focar nos dados significa considerar importante o que está sofrendo a ação dos acontecimentos. O primeiro enfoque apresenta problemas com o aumento do tamanho dos sistemas. O segundo enfoque, inspirado nos objetos do mundo real, permite a construção de soluções computacionais tendo os dados como elementos centrais. Nesta unidade apresentaremos mais detalhes do segundo enfoque, bem como maneiras de implementar soluções baseadas em orientação a objetos, tanto em C++ como em Java. 3.2 Problematizando o tema A chamada “crise do software” foi um termo utilizado nos anos 1970 para expressar as dificuldades relacionadas com o desenvolvimento de softwares em comparação com a crescente demanda. Um paralelo em economia seria, de um lado, um aumento considerável da demanda (feita pelos consumidores) por um produto (por exemplo, televisor) e, de outro lado, a incapacidade da indústria de fornecer a quantidade solicitada do referido produto. Geralmente isso gera um efeito indesejável, que é o aumento da inflação. O problema com o desenvolvimento dos softwares era ainda maior, pois não existiam técnicas eficientes de desenvolvimento de software. Utilizando ainda o paralelo com a economia, seria o equivalente a ter no mercado produtos (televisores) não muito confiáveis! Apesar da existência do paradigma de programação estruturada, a forma de programar não permitia a reutilização eficiente de código. Além disso, manutenções corretivas podiam causar efeitos indesejáveis no resto do código, uma vez que dados e funções existiam de forma independente. Para que o problema fique claro, vamos imaginar que temos um carrinho de controle remoto. Do ponto de vista da programação, imaginemos que o controle remoto seja uma função. Imaginemos, agora, que a indústria responsável pela confecção do carrinho deseje lançar um novo brinquedo: um avião. Se a indústria houvesse planejado um controle remoto mais geral (prevendo as funcionalidades 35 para carrinhos, helicópteros, aviões, barcos e submarinos), seria bem mais fácil fazer adaptações no projeto do controle remoto para o lançamento do novo produto. Entretanto, sem um projeto geral de controle remoto como base, a indústria teria 3 caminhos: 1.Utilizar o mesmo controle remoto do carrinho no avião; 2.Adaptar o controle remoto do carrinho para o avião; 3.Construir um novo controle remoto para o avião. A utilização do mesmo controle remoto para o avião teria um grave problema: a utilização indevida da funcionalidade da marcha ré, pois aviões não voam de ré! Esse problema poderia causar sérios prejuízos financeiros à indústria. Adaptar o controle remoto do carrinho para o avião poderia ser uma solução interessante. Entretanto, limitações projetadas para o carrinho (como velocidade) poderiam não ser aplicáveis ao avião. Além disso, do ponto de vista de programação, teríamos algumas funcionalidades semelhantes (ir para frente) que apresentariam implementações diferentes (uma para o controle remoto do carrinho e outra para o controle remoto do avião), apesar de serem idênticas. Mais ainda, uma alteração na funcionalidade “ir para frente” em carrinhos implicaria em alterar o código do programa em 2 partes distintas. Imaginemos que a empresa tivesse optado por tal estratégia para todos os produtos lançados e podemos entender o grau de complexidade da tarefa de manutenção. Por fim, a construção de uma nova solução. Seria a mais conveniente, mas certamente teria maior custo e atrasaria o lançamento do produto (o que pode significar perda considerável da fatia do mercado para um concorrente que esteja acompanhando suas tendências). O paradigma de orientação a objetos permite, em desenvolvimento de software, algo similar ao que permitiria para uma indústria um projeto mais genérico de um produto. É o que veremos nesta unidade. 3.3 O paradigma OO Basicamente, o paradigma de orientação a objetos utiliza os mesmos princípios utilizados na construção de hardwares (com o uso de componentes básicos como transistores, resistores, fusíveis, diodos, chips, etc.). Os “objetos” já existentes são utilizados para produzir novos “objetos”. A analogia com o hardware é muito interessante e oportuna. Um chip é, por 36 sua vez, formado por milhões de transistores interligados de forma específica, que depende da funcionalidade para a qual foi projetado. Chips podem ser interconectados formando placas (por exemplo, a placa de modem, a placa de vídeo do computador, a placa de processamento de dados de um carro, etc.). Na verdade, tudo o que você imaginar de equipamento eletrônico atual contém placas, que, por sua vez, contêm chips, que contêm resistores, transistores, etc. O paradigma de orientação a objetos objetiva mimetizar o que ocorre com o hardware, utilizando um conceito bem comum de nosso mundo: objetos. Formalmente, para uma linguagem ser considerada orientada a objetos ela precisa implementar quatro conceitos básicos: 1.Abstração: habilidade de modelar características do mundo real. 2.Encapsulamento: habilidade da unidade de proteger os dados e permitir que apenas suas operações internas tenham acesso a eles. 3.Herança: mecanismo que permite a) a criação de novos objetos por meio da modificação de algo já existente e b) o vínculo do objeto criado com o objeto antigo. É um conceito muito conhecido na natureza. 4.Polimorfismo: capacidade de uma unidade ter várias formas. Para entender abstração, precisaremos utilizar um exemplo: carro. Um carro é um veículo que transporta pessoas. Normalmente tem 4 rodas (existem carros com 3 rodas), uma direção (existem alguns – de instrução – com 2 direções), bancos, vidros, etc. Abstrair é imaginar algo geral que pode ser aplicado a carros. Quando você imagina o seu carro, você está transformando o abstrato em concreto, pois ele tem certas características que o diferem de outros carros e pode ser tocado, dirigido, etc. Retornaremos aos conceitos de abstrato e concreto muito em breve. Encapsulamento já foi abordado anteriormente neste livro. Por favor, recorra ao item 1.3.1 para recordar. Em linguagens orientadas a objeto o encapsulamento acontece quando escondemos os atributos e métodos do programador (usando private). Assim, ele não pode acessar diretamente tais membros da classe. Herança já é um conceito mais fácil de entender. Herdamos a capacidade de nos alimentarmos com leite quando pequenos (pois somos mamíferos), a cor dos olhos, o tamanho do nariz, etc. Se as características são misturadas de pai e mãe, dizemos que a herança é dupla, ao passo que se fossem herdadas de um único ser (clonagem) seria uma herança simples. Uma analogia com a clonagem pode ser explorada para transmitir o conceito de herança em computação. É importante, entretanto, definir o que é clonagem do ponto de vista biológico. A palavra clone (do grego klon, significa “broto”) é 37 utilizada para designar a forma de geração de indivíduos por reprodução assexuada. Poderia ser definida como o processo em que são produzidas cópias fiéis de outro indivíduo, ou seja, um clone. Utilizando o exemplo da ovelha, imaginemos que um cientista deseje criar uma ovelha verde. Uma ovelha verde seria uma ovelha normal com uma característica nova: cor verde. Se o procedimento de clonagem estiver bem dominado e se for possível inserir um gene que determina a cor da pena do papagaio no clone, em poucos dias é possível ter uma ovelha verde. Tal conceito (herança) é muito importante em orientação a objetos, pois é possível criar um novo objeto a partir de um modelo já criado. Por fim, precisamos entender polimorfismo. Imaginemos poder criar uma peça de xadrez que seja especial. Ela pode ora se transformar em cavalo (obedecendo todas as regras da peça cavalo), ora se transformar em torre (obedecendo todas as regras da peça torre), ora em peão e assim por diante. Assim como tal peça seria muito interessante em um jogo de xadrez, um objeto com tal capacidade em computação pode ser muito útil. 3.4 Classes e objetos Precisamos, inicialmente, entender melhor o que é uma classe em computação (e o que seria um objeto). Uma classe seria um modelo (carimbo) de um objeto. Objeto seria sua concretização (quando o carimbo é usado em conjunto com a tinta), ou seja, uma instância da classe. Quando alguém fala em cadeira, temos uma representação abstrata em nossa mente do que é uma cadeira. É uma “coisa” que tem um assento, um encosto e pés. A representação mais simplista é a apresentada na figura a seguir. Entretanto, a cadeira pode ter dois pés, ou mesmo uma estrutura com uma haste e rodinhas. Ela pode ter um projeto diferenciado (e por isso mesmo custar mais). Exemplos de cadeiras podem ser vistos na Figura a seguir: Figura 2 Cadeiras comuns. Note que são cadeiras, mas que são diferentes do modelo apresentado anteriormente. Na verdade, nossa mente tem uma representação complexa de 38 cadeira que aceita todos esses exemplos. Uma classe deve prever um conjunto de características para o objeto a ser criado a partir do molde, bem como um conjunto de ações que o objeto poderá executar. No exemplo da cadeira, isso se aplica ao tipo de pé, ao tipo de encosto, se é uma cadeira com rodinhas, se é uma cadeira de balanço, qual o material de sua estrutura (madeira, aço, metal ou plástico), seu revestimento (pano, couro, verniz), etc. Como você pode perceber, especificar um molde para uma cadeira é algo complexo. Outro exemplo poderia ser uma lâmpada. Ela pode ser incandescente ou fluorescente, pode ser de bastão ou ter o formato clássico, ter várias voltagens, potências, cores, marcas, etc. Mais um exemplo: uma empilhadeira. Ela tem uma cor, uma altura, uma capacidade de carga, um ângulo máximo de giro, tem uma velocidade máxima de movimentação, etc. Tudo isso se configura no conjunto de características do futuro objeto (ainda estamos falando de classe!). Tal empilhadeira pode realizar as seguintes (entre várias) funções: 1) movimentar-se para frente; 2) movimentar-se para trás; 3) girar Z graus para a direita; 4) girar Z graus para a esquerda; 5) pegar a carga; etc. Tais funções se configuram em ações que o futuro objeto poderá realizar. Ações são chamadas de métodos, enquanto as características são chamadas de atributos. A empilhadeira utilizada como exemplo é uma instância (um objeto) da classe empilhadeira (“carimbo”), pois tem características (altura específica, cor, etc.) e ações determinadas. Para deixar bem claro o conceito de classe e objeto, podemos imaginar a classe cachorro. Um molde ou carimbo genérico seria aquele que permitisse “guardar” informações, tais como nome, altura, peso, idade, raça, cor dos olhos, tamanho do rabo, etc. Um exemplo de um cachorro poderia ser o meu. O Rex tem uma raça específica, uma altura, peso, etc. Ele é uma “instância” da classe cachorro. Não consigo tocar na classe cachorro, mas consigo tocar em um exemplo da classe cachorro, que é o Rex. Meu cachorro é um objeto da classe cachorro. Ah, um objeto pode interagir com outro e mudar suas características! Poderíamos utilizar um objeto “spray” para pintar (uma ação de “spray”) a empilhadeira e, assim, mudar um valor de seus atributos (o atributo cor). Quando pensamos em objetos, as coisas ficam mais claras. Existem vários outros exemplos de classes, como “conta bancária”, “declaração de imposto de renda”, etc. 39 3.5 Elementos de uma classe Como explicado anteriormente, a classe tem um nome (empilhadeira, cachorro, spray, etc.) e dois tipos importantes de elementos: atributos e métodos. Quando pensamos em uma caixa, podemos imaginar suas características: material, tamanho, textura, cor, etc. Esses são alguns dos exemplos. Seu estado – aberta ou fechada – também é outro exemplo de atributo. As ações abrir e fechar são exemplos de métodos da classe caixa. Outro exemplo já citado seria o da lâmpada. O tipo de lâmpada, a cor, a voltagem, a potência e o estado da lâmpada (acesa, apagada ou queimada) seriam exemplos de atributos. Acender, apagar e queimar a lâmpada seriam exemplos de métodos. Em breve apresentaremos um exemplo de código para a lâmpada. typedef struct data { int dia, mes, ano; } data; Em termos de linguagem de programação, classes diferem de estruturas (structs) no quesito proteção de dados. No exemplo da data, podemos ver um conjunto de dados (dia, mês e ano) sendo empacotado em um mesmo “volume”. Entretanto, não há nenhum mecanismo que impeça o programador de criar uma variável do tipo data (por exemplo, data_nascimento) e de atribuir o valor 45 para mês. Sim, não faz sentido colocar o valor 45 em mês, visto que existem apenas 12 meses e é por isso que devemos criar “estruturas” que impeçam tal situação. Assim, é interessante que possamos controlar os valores assumidos pelos elementos da estrutura de forma que dia, mês e ano tenham sempre valores válidos. Isso nos faz pensar sobre o uso do controle remoto do ar-condicionado (ou do videocassete, do aparelho de DVD, do televisor, etc.) para realizar algumas funções de nosso interesse. Um controle remoto de um aparelho de ar-condicionado tem teclas para ligar e desligar, controlar a velocidade do ventilador, da pá de distribuição do ar e também para controlar a temperatura (e principalmente para controlar a temperatura!). Mais ainda, o controle remoto não permite que diminuamos a temperatura para menos de um valor pré-determinado (em torno de 15ºC) ou aumentemos para um valor superior a outro valor pré-determinado (em torno de 27ºC). Isso permite que o aparelho funcione corretamente e não congele ou derreta. O fabricante do aparelho projetou um controle remoto que protege o objeto que industrializa e comercializa. Ninguém que utilize o controle remoto poderá 40 modificar a temperatura para um nível inferior a 15ºC ou superior a 27ºC. Assim, temos com classes mais que simplesmente um “empacotamento”. Temos um empacotamento de dados e código que permite a consistência das informações presentes. Mantendo a analogia com o exemplo do ar-condicionado, os dados só poderão ser modificados segundo as “funções” das teclas do controle remoto. A classe esconderá seus dados (visibilidade) e permitirá que apenas alguns métodos sejam disponibilizados para alteração deles. Se criássemos a classe data, precisaríamos de métodos (não chamamos mais de funções) que mantivessem a consistência destes. Um método poderia ser atribuir_dia(int x). O valor x deve, nesse exemplo, ser verificado para saber se é um valor válido. Ele depende do mês e do ano da data. Se o mês for fevereiro e o ano for bissexto, então o dia poderá assumir valores entre 1 e 29. Assim, se x estiver dentro desses limites, dia poderá receber o valor em x. Caso contrário, dia poderá receber um valor padrão (1, por exemplo). Se o mês for agosto, x poderá estar entre 1 e 31. Se for setembro, entre 1 e 30. Também poderíamos ter um método de adicionar um dia à data. Isso faria com que a data fosse sendo modificada conforme percorremos um calendário. Mais ainda, mudaria de mês automaticamente quando um dia fosse acrescentado ao final do último dia do mês anterior. E permitiria, inclusive, o controle da mudança de ano, uma vez que do dia 31/12 de um determinado ano deve-se, automaticamente, mudar para o dia 01/01 do ano seguinte. 3.6 Diagrama de classes Uma forma muito comum de se representar classes é por meio do diagrama de classes da UML (Unified Modeling Language). Com ele, é possível representar visualmente uma classe. Também é possível representar a relação entre elas (o que será visto mais adiante). Uma classe, em UML, é representada por um retângulo com 3 partes. Na parte superior encontra-se o nome da classe, na intermediária os atributos e na inferior os métodos. O nome da classe deve ser em negrito e, geralmente, é um substantivo em que a primeira letra é maiúscula. Na parte intermediária do diagrama são apresentados os atributos ou características da classe. Além do nome do atributo, devemos especificar sua visibilidade, que pode ser: ~: as classes de um pacote podem ser usadas; +: público; #: protegido; -: privado. 41 Normalmente você verá os atributos tendo visibilidade privada (-). Isso significa que somente os métodos da classe podem acessá-los. Visibilidade privada impede que os valores sejam acessados diretamente. Para se alterar um atributo privado é preciso que exista um método que todos possam acionar (acesso público) relacionado a ele. Tal método controlará os valores que o atributo poderá assumir. Assim, acesso público é um acesso irrestrito e acesso privado é um acesso restrito. O modo de acesso protegido está relacionado com herança e será discutido oportunamente. Vale ressaltar que structs em C seriam equivalentes a uma classe sem métodos com atributos de acesso irrestrito. Ou seja, sem proteção alguma. Agora já temos isso bem claro. Bom, voltando aos atributos, estes têm tipo de dado e podem ainda apresentar um valor padrão. Note que na classe a seguir, o atributo Nome é do tipo String e o valor padrão, representado pelo símbolo de igual, é Ednaldo: Pessoa +Nome : String = “Ednaldo“ +Idade : int = 43 ... ... +fazer_aniversario() Em breve falaremos mais a respeito do valor padrão. Agora temos os métodos. Note que ambos estão com acesso público. É importante que a classe tenha métodos públicos para que os dados possam ser modificados. A classe em questão é incompleta e apresenta apenas alguns métodos. O método fazer_aniversário(), quando acionado, altera o valor de idade. Se o método fazer_aniversário fosse privado, então não poderia ser acionado diretamente. Um método será privado apenas em casos especiais. Imagine uma impressora com alarme sonoro para indicar o final da tinta do cartucho. O alarme deverá ser acionado quando a quantidade de tinta chegar a um nível crítico e não quando o usuário desejar escutar o som do alarme. Assim, tal impressora não teria um “botão” chamado alarme para o usuário acionar. O método imprimir_documento da impressora iria, automaticamente, diminuindo a quantidade de tinta do cartucho. Quando chegasse a um nível crítico, acionaria o método alarme. Já que iniciamos a discussão sobre a classe impressora, podemos tentar representá-la. 42 Impressora -Cor : String -Tipo : String -Velocidade : int -Capacidade : int -Qualidade_impressão : char -Nível_reservatório : float -Estado : bool +Imprimir() +Trocar_suprimento() +Ligar() +Desligar() -Alarme() Uma impressora pode ser colorida ou monocromática (preto e branco), pode ser a laser, a jato de tinta, matricial ou a jato de cera, tem determinada velocidade de impressão, capacidade de suportar uma determinada quantidade de papéis em sua bandeja, pode imprimir em qualidade de texto rápido, qualidade alta ou mesmo qualidade de foto, pode armazenar o nível de tinta dos cartuchos (ou de toner), etc. Existem muitos outros atributos de uma impressora! E existem algumas ações que uma impressora realiza, entre elas a mais óbvia: imprimir! E todos sabemos que uma impressora imprime se estiver ligada (estado = ligado), que a impressão consome tinta, etc. 3.7 Relacionamento entre classes Existem vários relacionamentos entre classes. Nosso foco será apenas nas relações de herança e composição. A relação de herança é aquela em que uma classe herda características de uma classe mais genérica. Normalmente é reconhecida por meio da relação é-um. “Um cachorro é um mamífero” indica que cachorro herda características de uma classe mais genérica, que é a classe mamífero. Na verdade, a classe mamífero é mais genérica, pois permite que vários animais herdem características dela (tigre, gato, cavalo, etc.). Outro relacionamento importante é o de composição. Ele é reconhecido pela relação tem. Um carro tem uma direção, tem 4 rodas, etc. Um funcionário tem data de nascimento, departamento, etc. Ou seja, data de nascimento é um atributo do tipo data. 43 Tanto com herança quanto com composição é possível fazer reuso de classes já criadas. Esse é um dos grandes atrativos da orientação a objetos, que faz com que seja fortemente adotada nas fábricas de software. 3.8 Exemplos Agora vamos ver exemplos simples de classes em C++ e em Java. Os relacionamentos do tipo herança e composição serão vistos na Unidade 6. Vamos começar com um exemplo de lâmpada. Construir uma lâmpada genérica não é tão simples assim. Uma lâmpada tem voltagem, potência, tipo (fluorescente, incandescente), marca e cor, entre outros possíveis atributos. Ah, e pode estar acesa, apagada ou queimada. Como o objeto da classe lâmpada deve ter seus valores definidos logo após sua declaração, isso significa que é preciso que a classe tenha um construtor que atribua valores iniciais dos atributos para aquele objeto. Vejamos uma implementação simples em C++ feita no Netbeans: #include <stdlib.h> #include <iostream.h> using namespace std; class lâmpada { private: int voltagem; bool estado; public: lampada(); // construtor sem parâmetro lampada(int v); // construtor com parâmetro void ligar_desligar();// simula o interruptor de ligar/ // desligar bool get_estado(); // obtém o estado da lâmpada int get_voltagem(); // obtém a voltagem da lâmpada void imprime(); // imprime os atributos da lâmpada }; Note que em C++ a definição da classe deve terminar com ponto e vírgula (;). Os atributos normalmente estão na área private da classe e os métodos na área public. Isso acontece porque desejamos proteger o acesso aos atributos, de forma a evitar que o objeto tenha valores indevidos. Imagine que a lâmpada possa ser somente 110 ou 220V. E se alguém desejasse que a lâmpada assumisse o valor 145V para a voltagem? Em C, você se lembra, isso era possível, visto que 44 struct apenas agrupava valores, mas não os protegia. Agora, com a definição do tipo de acesso private para voltagem e estado, se o programador criar um objeto chamado lâmpada_sala, então ele poderá acionar apenas os métodos ligar_desligar( ), get_estado( ), get_voltagem( ) e imprime( ). Note que os construtores não estão na lista. Isso significa que não podem ser acionados pelo programador. Um construtor é acionado apenas quando há a declaração de um objeto e tal acionamento acontece de forma automática. No caso anterior, pode-se observar a existência de dois construtores. Um construtor tem o mesmo nome da classe, mas não tem tipo de retorno. Assim, quando houver uma classe ABC, deverá existir um construtor – cujo nome será ABC – que será o responsável pela inicialização do objeto. Se houver dois ou mais construtores, então, o compilador identificará qual será chamado a partir do número e tipo dos parâmetros que estão na lista de parâmetros. A combinação nome do método e lista de parâmetros é conhecida como assinatura. Métodos com o mesmo nome, mas com assinaturas distintas, são diferentes. Em orientação a objetos isso significa sobrecarregar o método. No caso em tela, ocorre uma sobrecarga de construtor. Veremos mais a respeito de sobrecarga na próxima unidade. Se no programa principal o programador criar o objeto L sem parâmetro, então o construtor sem parâmetros é chamado. Caso contrário, se o programador definir o objeto L(220), então a lâmpada L terá 220V. lampada::lampada(){ voltagem = 110; estado = false; } lampada::lampada(int v){ if (v == 110 || v == 220) voltagem = v; else voltagem = 110; estado = false; } Os dois construtores são apresentados no código anterior. Note que a voltagem será 110 sempre que o valor for diferente de 220. Isso impedirá a criação de um objeto com voltagem diferente de 110 ou 220V e a lâmpada sempre estará desligada (estado = false). A partir do exemplo apresentado pode-se inferir sobre a função do construtor: inicializar os atributos do objeto. Ou seja, após a declaração do objeto, ele poderá ser utilizado imediatamente, pois seus atributos foram preenchidos. O construtor serve, então, para inicializar o objeto. 45 void lampada::ligar_desligar( ){ estado = !estado; } Para acender ou apagar uma lâmpada é necessário apenas acionar o interruptor. O método inverterá o estado da lâmpada. Se estava apagada, ficará acesa. Se estava acesa, ficará apagada. Os métodos get (do inglês pegar, obter) são responsáveis pela obtenção dos valores armazenados nos atributos de acesso restrito (private). Note que não é possível acessar o atributo de forma direta simplesmente pelo fato de que tal ato poderia quebrar toda a segurança do objeto. Lembre-se que, se alguém pode acessar o atributo estado para imprimir seu valor na tela, também pode acessá-lo para modificá-lo. bool lampada::get_estado( ) { return estado;} int lampada::get_voltagem( ){ return voltagem;} void lampada::imprime( ) { cout << “estado = “ << estado << endl; cout << “voltagem = “ << voltagem << endl; } Note, também, que o compilador sabe a qual classe o método pertence por intermédio do operador de escopo ::. Isso significa que, na verdade, o método tem um nome e um sobrenome. Como exemplo, utilizemos o método imprime do último código. É um método cujo nome é imprime e o sobrenome é lâmpada (pertence à classe lâmpada). Seu tipo de retorno é void, ou seja, não há tipo de retorno. Por isso, em seu código, não há o comando return. Os métodos get_voltagem e get_estado também pertencem à classe lâmpada (pelo “sobrenome”). Entretanto, tais métodos retornam valores (o objetivo de um método get é retornar um valor). Note, também, que o retorno ocorre por cópia. Ou seja, uma cópia do valor armazenado em voltagem ou em estado é retornada. E note, também, que o método tem direito (por pertencer à classe) de acessar os dados private. Somente membros da classe podem acessar os dados restritos. Isso faz sentido, pois caso contrário, nada poderia acessar os atributos (ou métodos) de acesso restrito. Poderíamos, então, modificar o tipo de acesso de private para impossible ou forbidden (brincadeira, não existem tais tipos de acesso!). Continuando, precisamos mostrar o uso da classe lâmpada. 46 int main(int argc, char** argv) { lampada sala(9), cozinha(220), escritorio; escritorio.imprime(); cout << “estado das lampadas \n“; cout << “sala “ << sala.get_estado() << endl; cout << “cozinha “ << cozinha.get_estado() << endl; cout << “escritorio “ << escritorio.get_estado() << endl; sala.ligar_desligar(); ... return (EXIT_SUCCESS); } Os objetos sala, cozinha e escritório indicam as lâmpadas criadas para tais cômodos. Toda lâmpada cujo parâmetro seja diferente de 110 ou 220V terá voltagem assumida como 110V. Se não houver parâmetro (caso da lâmpada do escritório), tal lâmpada será 110V. O comando escritório.imprime( ) acionará o método imprime( ), que será responsável por imprimir os dados da lâmpada do escritório. A lâmpada da sala será acesa (pois todas são criadas desligadas) quando o método ligar_desligar( ) for acionado. Como informado anteriormente, a classe poderia ser mais complexa. Poderíamos ter que simular o estado “queimada”. Também poderíamos indicar que uma lâmpada só poderia ser acesa se não estivesse queimada, se seu estado estivesse indicando que está apagada e estivesse conectada na rede de energia elétrica. Sim, porque quando você compra uma lâmpada em uma loja, ela supostamente não está queimada, mas não poderá ser acesa, visto que não está ligada à rede elétrica. Outro fato digno de nota no código é que não há um método set para alterar a voltagem da lâmpada, por exemplo. Se ela é criada com uma voltagem X, permanecerá com tal voltagem até que se queime e seja inutilizada. 47 class horario //nome da classe { //início da declaração da classe private: //especificador de acesso. int hora; //atributo hora int minuto; //atributo minuto int segundo; //atributo segundo public: //especificador de acesso. horario(); //construtor horario(int,int,int); //construtor com 3 parâmetros acerta_hora(int); //método acerta_hora acerta_minuto(int); //método acerta_minuto acerta_segundo(int); //método acerta_segundo ~horario(); //destruidor //fim da classe. }; //atenção: não esqueça o ; Agora vamos utilizar a classe horário para comparar uma implementação feita em C++ com sua equivalente em Java. A definição da classe horário está bem explicada, não? Existem 3 atributos, vários métodos, 2 construtores e um destruidor. A primeira observação é com relação a algo muito simples, mas que, muitas vezes, causa transtornos: o ponto e vírgula no final da declaração. Se você não colocar ponto e vírgula no final da definição do protótipo da classe, isso lhe causará problemas para compilar o programa. A especificação do código dos métodos pode vir logo após a declaração da classe. Alternativamente, um método pode ser definido dentro da declaração da classe. No exemplo a seguir, o construtor sem parâmetros tem sua implementação dentro da declaração da classe. Tal implementação pode ser feita, portanto, tanto dentro como fora da declaração da classe! class horario { private: int hora; int minuto; int segundo; public: 48 horario() { hora = 10; minuto = 5; Segundo = 2;} acerta_hora(int); horario(int,int,int); acerta_minuto(int); acerta_segundo(int); ~horario(); }; horario::horario(int a, int b, int c) { hora = minuto = segundo = 0; if (a >=0 && a < 24) hora = a; ... } O outro construtor, os métodos e o destruidor (ou destrutor) deverão ser implementados fora da classe. Já vimos pelo exemplo da classe lâmpada que para definir o método fora da classe devemos especificar a qual classe ele pertence. Assim, o construtor com parâmetros deverá ter nome e sobrenome, como no exemplo de código apresentado. Provavelmente você já sabe completar o código escondido do construtor horário. Note que no protótipo não havia necessidade de especificar os nomes dos parâmetros. Aqui há. O destruidor (ou destrutor) tem um código muito simples. Na verdade é o código mais fácil já visto: horario::~horario() { } Sim, o código é vazio. Para casos em que não exista alocação dinâmica de memória, os destruidores são simples e não precisam nem de declaração. Para os casos de alocação dinâmica de memória, o destrutor é necessário, pois é ele que devolve a memória emprestada pelo construtor no momento da alocação dinâmica. Vale relembrar que tanto o construtor como o destrutor não são acionados pelo programador. O construtor é acionado no momento da declaração do objeto e o destruidor é acionado quando termina o escopo do objeto. Todos os outros métodos devem ter o especificador da classe seguido do operador de escopo (::) seguido do nome do método. Lembre-se que, dentro do método, é importante verificar se os parâmetros passados satisfazem os critérios da classe. Os métodos são o meio de acesso aos atributos. Se não verificarem os dados passados deixarão que os atributos assumam valores indevidos. E isso é contrário ao “espírito” de orientação a objetos. 49 Agora, a implementação em Java. Como informado, utilizaremos o exemplo da classe Horário, que deve armazenar hora, minuto e segundo. Vamos criar dois arquivos com extensão .java. O primeiro – referente à classe Horário – deverá se chamar Horário.java. O segundo – referente ao programa principal responsável pelo teste do uso da classe Horário – se chamará testeHorario.java. public class Horario { private int hora, minuto, segundo; public Horario() { hora = 0; minuto = 0; segundo = 0;} public Horario(int a, int b, int c) { hora = minuto = segundo = 0; if (a < 24 && a >= 0) hora = a; if (b < 60 && b >= 0) minuto = b; if (c < 60 && b >= 0) segundo = c; } public void setHora(int a) { if (a < 24 && a >= 0) hora = a; } public void setMinuto(int a) { if (a < 60 && a >= 0) minuto = a; } public void setSegundo(int a) { if (a < 60 && a >= 0) segundo = a; } public int getHora() { return hora; } public int getMinuto() { return minuto; } public int getSegundo() { return segundo; } } A classe Horário deverá restringir o acesso aos 3 atributos já mencionados, a partir do especificador de acesso private. Sim, private em C++ e private em Java! Como já foi indicado neste livro, você poderá comprovar que C++ e Java são linguagens muito parecidas. A classe Horário deverá ser delimitada pelo abre chaves ({) e o fecha chaves (}). Mas note que, diferentemente de C++, em Java não há ponto e vírgula após o } que indica final da classe. Outra questão importante a ser observada no código é a verificação dos valores que serão atribuídos aos atributos. Note que todos os valores, quer no cons- 50 trutor quer nos métodos set, devem ser verificados. Hora deve sempre ficar entre 0 e 24; minuto e segundo sempre entre 0 e 60. Mas isso não é novidade! Você já foi alertado sobre isso quando a classe Horário para C++ foi apresentada. Bom, a classe Horário está pronta e pode ser compilada. Na linha de comando, digite: javac Horário.java O comando javac aciona o compilador java e se o código estiver correto gerará o arquivo Horário.class. Precisamos de um programa principal para utilizar a classe Horário. Sim, em Java também é necessário um programa principal (main). public class testeHorario { public static void main(String[] args) { Horario almoco = new Horario(12,0,0); System.out.println(almoco.getHora()); } } O objeto almoço é criado por meio do comando new Horario. No exemplo, o objeto foi criado com os parâmetros 12, 0 e 0. Isso significa que o horário armazenado em almoço será 12h00. Para imprimir o valor hora armazenado no objeto, busca-se o valor a partir do método getHora( ) e promove-se a saída por intermédio do println. Outro exemplo interessante de classe é o jogo da velha. Consiste em um tabuleiro com peças O e X que devem ser colocadas alternadamente pelo jogador 1 e pelo jogador 2. A classe deve controlar as jogadas, saber a vez do jogador, saber se um jogador fez uma jogada incorreta ou não, detectar se o jogo já terminou e se houve um ganhador ou não. Sempre que uma jogada correta for realizada e o jogo não houver terminado, então a classe solicitará que o outro jogador efetue sua jogada. O jogo deve iniciar com as posições vazias. Uma jogada válida é uma jogada dentro do tabuleiro em uma posição ainda não ocupada por nenhuma peça. Vejamos o protótipo da classe. 51 class jogo { private: char tab[3][3]; // armazena jogadas int jogadas; // controla total de jogadas char jogador; // controla de quem é a vez public: jogo( ); // inicializa o jogo bool terminou( ); // jogo terminou? char verifica_vencedor( ); // quem venceu? void jogar( ); // inicia o jogo void limpa_tela( ); // limpa a tela void troca_jogador( ); // troca jogador void desenhar_tabuleiro( ); // desenha tabuleiro bool posicionar_peca(int, int); // posicionar peça }; Deve-se escolher um símbolo – por exemplo * – para indicar que o local ainda não foi preenchido. O atributo jogadas será responsável por controlar o total de jogadas já realizadas. Deve iniciar-se com o valor zero e ter seu valor incrementado em uma unidade a cada nova jogada. Dessa forma, servirá para controlar o final do jogo (pois um jogo da velha tem, no máximo, nove jogadas). Assim, se o tabuleiro houver sido preenchido e não houver vencedor, então o jogo terminou e houve empate. O código do método de controle de fim de jogo será apresentado em breve. Por fim, o atributo jogador. Ele armazenará o símbolo do jogador da vez. jogo::jogo( ) { int i, j; for (i=0;i<3;i++) for (j=0;j<3;j++) tab[i][j] = ’*’; jogadas = 0; srand(time(NULL)); // inicializa semente para sorteio if (rand( ) % 2 == 0) jogador = ’X’; else jogador = ’O’; } Voltando ao construtor, deve-se observar que há a chamada de srand. Para que isso ocorra é preciso incluir time.h no código. Srand permite inicializar a se- 52 mente do gerador de números aleatórios. Isso permitirá que sempre que um “sorteio” for realizado, ele possa ser diferente do sorteio anterior. O parâmetro time(NULL) garante que isso aconteça. Se o parâmetro for um valor constante, então os sorteios serão viciados (sempre a mesma coisa, não importa quantas vezes você rode o programa). Ou seja, se o jogador a iniciar a partida for X quando o parâmetro for 3, então sempre o jogador X iniciará a partida! Com time(NULL) isso não é verdade. Uma vez pode ser o X e outra pode ser o O. O sorteio é feito por meio da chamada da função rand( ). Seu resultado é um número inteiro que será dividido, nesse caso, por 2. Se o resto for 0, então será o jogador X a iniciar a partida, caso contrário será o jogador O. Bom, vamos agora ao código do método de verificação se o jogo terminou ou não. Se já ocorreram 9 jogadas, então não há mais espaço disponível para novas jogadas. Caso o número de jogadas seja menor que 9, então é preciso verificar se já houve um vencedor. Se houve, então o jogo terminou. bool jogo::terminou( ) { if (jogadas < 9 && verifica_vencedor( ) == ’*’) return false; else return true; } O método verifica_vencedor retornará O caso o vencedor seja o jogador cujo símbolo é o O, X caso o vencedor seja o jogador cujo símbolo é o X ou * caso não haja vencedor. O método verifica_vencedor deverá verificar se há repetição do mesmo símbolo (O ou X) em qualquer uma das 3 colunas, em qualquer uma das 3 linhas e nas duas diagonais (principal e secundária). Se o símbolo X – por exemplo – satisfizer tal condição, então ele será o símbolo a ser retornado pelo método, indicando que o vencedor é o X. O método posiciona_peca(int x, int y) deverá, dadas as coordenadas x e y, posicionar a peça do jogador. Se as coordenadas estiverem corretas (dentro do tabuleiro) e se a posição estiver desocupada (tab[x][y] == ‘*’), então a jogada é válida. A referida posição deverá receber o jogador da vez (tab[x][y] = jogador) e será preciso verificar se o jogador da vez ganhou. Sempre após uma jogada válida é preciso trocar o jogador da vez. Uma jogada inválida resultará em uma mensagem de aviso para o usuário e uma solicitação de nova jogada. Você consegue terminar o código do jogo? Bom, mais exemplos de classes em C++ e em Java podem ser vistos no Apêndice B. 53 3.9 Considerações finais Nesta unidade foi possível entrar em contato com alguns conceitos do paradigma de orientação a objetos. Exemplos comentados em C++ e em Java puderam ilustrar os conceitos básicos de orientação a objetos. Outros exemplos no Apêndice B poderão contribuir para melhor entendimento do assunto. 54 Unidade 4 Introdução à sobrecarga 4.1 Primeiras palavras Quem é acostumado a programar em C, por exemplo, não consegue compreender a razão pela qual existem sobrecargas de métodos e operadores em linguagens como C++. Nesta unidade serão vistas as duas formas de sobrecarga: métodos e operadores. Na verdade, você já viu um pouco de sobrecarga de métodos na unidade anterior. Talvez não tenha aceitado com tranquilidade o fato do construtor horário ter tido duas implementações diferentes (uma sem parâmetros e outra com 3 parâmetros). Isso ocorre em C++ (e em Java também) quando um método apresenta o mesmo nome que outro, mas suas assinaturas são diferentes. Assinatura é o binômio nome do método e lista de parâmetros. Duas listas de parâmetros A e B são iguais se contiverem o mesmo número de parâmetros e se o tipo de dado do elemento ai da lista A for igual ao tipo do elemento bi da lista B, considerando-se que A = {a1, a2, ..., an} e B = {b1, b2, ..., bn}. Nesta unidade, além da sobrecarga de métodos, você verá, também, a sobrecarga de operadores (somente para C++, pois Java não apresenta tal característica). 4.2 Problematizando o tema Muitas vezes, nos deparamos com a seguinte situação: temos uma função em C++ que é responsável pelo cálculo do quadrado de um número. E ela funciona para valores inteiros. Mas e se desejássemos que ela funcionasse também para valores decimais? Na verdade, deveríamos criar outra função com o mesmo código, mas tipos de parâmetros diferentes! Replicar o código apenas por causa do tipo de parâmetro não faz muito sentido, faz? Mas a sobrecarga de métodos é mais que isso, e você verá detalhes em breve. Outra questão importante é poder representar operações lógicas e aritméticas de forma natural, como fazemos com dados do tipo inteiro, double ou float. É desejável que a operação C = A + B; tenha significado para inteiros, floats e doubles, mas também para vetores, matrizes, números complexos, frações, etc. Se A, B e C fossem objetos da classe fração, então o comando apresentado seria a atribuição para o objeto C do resultado da soma de duas frações. Lindo, não? É o que também será abordado nesta unidade. 57 4.3 Sobrecarga de métodos Quando um método é declarado, ele tem uma assinatura, ou seja, um nome e um conjunto de parâmetros. Se declaramos dois métodos com o mesmo nome, mas com listas de parâmetros diferentes, dizemos que estamos sobrecarregando o referido método. Vejamos o exemplo a seguir. class vetor { private: float v[10]; public: vetor(); // construtor não tem tipo de retorno! void adiciona(float); // adiciona um float a // todos os elementos void adiciona(vetor); // adiciona um outro vetor ao atual ... }; Os métodos adiciona(float) e adiciona(vetor) são diferentes, pois o primeiro permite que um número seja adicionado a todos os elementos do vetor enquanto o segundo permite que um vetor seja adicionado ao atual. Vamos entender o contexto. Imaginemos o objeto A da classe vetor com os elementos {1, 2, 3, 4, 5, 6, 7, 8, 9 e 10}. A deve ser declarado no programa principal (ou em algum subprograma) da seguinte forma: Vetor A; Em seguida, algum método de atribuição deve existir para trocar os valores iniciais do vetor (vamos supor que o construtor inicialize tudo com 0) para a sequência 1, 2, 3... for (i=0;i<10;i++) A.atribui(i,i+1); Assim, a posição 0 do vetor (lembre-se que os índices iniciam em zero) receberá o valor 1, a posição 1 o valor 2 e assim por diante. Agora, vamos supor que alguém deseje transformar a sequência {1, 2, 3,...,10} 58 em {2, 3, 4,...,11} por meio da chamada do método adiciona: A.adiciona(1); Tal método deverá adicionar o valor 1 a cada elemento do vetor. Mas alguém poderia ter criado um objeto B e, em um determinado momento do código, desejasse fazer com que C = A + B. Para tanto, deveria fazer o seguinte: C.zera(); C.adiciona(A); C.adiciona(B); C.imprime(); Supondo B = {_1, _2, _3,..., _10} o resultado apresentado pelo método imprime para o objeto C seria: {0,0,0,0,0,0,0,0,0,0,0} Note que o método adiciona, nesse caso, passa um vetor e não um único número. Os nomes são iguais, mas as operações não! Ah, e o programa saberá qual método chamar apenas pela assinatura! 4.4 Sobrecarga de operadores Muito interessante (e legal) é poder declarar 3 matrizes A, B e C e fazer a seguinte operação: C = A + B; Isso significa que a linguagem C++ permite que alguns operadores possam ter sua funcionalidade estendida para outros tipos de dados. E com a “inteligência” embutida na linguagem, é possível fazer a seguinte operação: C = A + 1; Sim, é possível misturar tipos! É um pouco mais complicado, mas nada do outro mundo. Nesses casos um cuidado extra deve ser tomado: C = A + 1; e C = 1 + A; são coisas diferentes que devem ter o mesmo comportamento! Vamos ver como fazer a soma de dois vetores (com o operador +) em C++. A declaração de operador de adição em C++ tem a seguinte sintaxe: 59 Tipo_retorno operator+ (parâmetros); 1. Vetor Vetor::adicao(vetor x) 2. { 3. vetor z; 4. int i; 5. for (i=0;i<x.tam;i++) 6. 7. 8. z.elemento[i] = elemento[i] + x.elemento[i]; z.tam = tam; return z; 9. } O tipo de retorno de um operador deverá ser definido pelo programador, assim como os parâmetros. Antes de prosseguir com as explicações da sintaxe, é preciso explicar o funcionamento dos operadores. Um operador funciona de forma semelhante a um método. No código anterior, a linha 1 indica que um objeto do tipo vetor será retornado pela função adicao (sem acentuação), que o método se chama adicao, que pertence à classe vetor e que um parâmetro do tipo vetor (x) é passado para esse método. Pelo código, é possível deduzir que tam é um atributo de vetor (provavelmente armazena o número de elementos do vetor) e elemento é outro atributo (que armazena cada valor do vetor). Assim, o que se deseja é que os elementos de z sejam a soma dos elementos do vetor atual com os elementos do vetor x. Sim, tem um tal de vetor atual! O que acontece é que quando uma chamada de método é feita, ela é feita por um objeto. Provavelmente, em algum lugar do código (que pode ser no programa principal, por exemplo), haveria, em nosso exemplo, a seguinte linha de comando: a.atribui(b.adicao(c)); Ou seja, b (o vetor atual) aciona o método adicao passando o objeto c como parâmetro (que no método tem o nome trocado para x). Os atributos de b (tanto tam como elemento) são acessados sem problemas. Na operação de adição, um objeto temporário z é criado dentro do método para armazenar a soma do objeto atual (agora você entendeu, não?) com o objeto passado por parâmetro. Quem acionou o método? A resposta é b! O resto do código é simples. Todos os elementos de z são a soma dos respectivos elementos do objeto atual com os elementos de x. 60 Não se deve esquecer de atribuir o valor de tam ao objeto z! E como a mágica acontece quando o operador entra no lugar do método? Funciona como se fosse o método adicao, mas com nome diferente: a.operator=(b.operator+(c)); O operador de atribuição é invocado pelo objeto a e o operador de adição é invocado pelo objeto b. A sintaxe fica transparente para o programador: a = b + c; Bom, temos a atribuição! Ela tem algumas explicações extras! Existem duas coisas novas: & (na primeira linha) e o return *this (antes do fecha chaves). Vetor & Vetor::operator=(vetor x) { int i; for (i=0;i<x.tam;i++) elemento[i] = x.elemento[i]; tam = x.tam; return *this; } O & significa que o método retornará uma referência do próprio objeto que chamou. Isso permite alterar o valor atual do objeto. Você deve ter muito cuidado com o uso de referências. Sempre que houver TIPO & na declaração significa que haverá o retorno de uma referência para um objeto do tipo TIPO. E sempre que você fizer uma declaração de um tipo dentro de um método (como o objeto z do método de adição), ele automaticamente desaparecerá após o fim do método. Seu escopo (sua vida) terminará após o encerramento do método. Assim, você não poderá utilizar TIPO & e fazer o retorno de um objeto z criado dentro de um método. Claro?! Bom, o *this significa que o código está retornando o objeto atual. Se você quer atualizar o valor do elemento que está invocando o método de atribuição, então deve utilizar & e *this. Um fato importante é que o método é acionado por um objeto que está à sua esquerda. E isso causa algum problema quando fazemos a seguinte operação: a = 1 + b; 61 Quem aciona o método de adição? Não pode ser o número 1, pois os inteiros trabalham com tipos pré-definidos e o vetor que estamos trabalhando não era conhecido dos inteiros até agora! Existe um truque aqui! Você pode utilizar a declaração friend. Friend indica amizade e permite que um objeto acesse os dados de outro. Se dentro da classe vetor houver uma declaração explícita de que um método X externo à classe é friend dela, então X poderá acessar os dados daquela classe. Exemplo: classe vetor { friend vetor operator+(int, vetor); … // demais declarações da classe }; Nesse caso, operator+ é friend da classe vetor. A chamada do operador seria da seguinte forma: a = operator+(1,b); Sim, o número 1 e o objeto b são passados por parâmetro. Note que, nesse caso, não há um objeto que invoca o método (porque ele não pertence a nenhuma classe!). O método em questão seria definido fora da classe: vetor operator+(int x, vetor z) { vetor k; int i; k.tam = z.tam; for (i=0;i<k.tam;i++) k.elemento[i] = 1 + z.elemento[i]; return k; } É claro que poderia ter sido bem mais simples: vetor operator+(int x, vetor z) { 62 return (z+1); } Não entendeu? Se você já definiu um operador de adição para ser acionado sempre que o objeto pertencente à classe estiver à esquerda do operador, então quando ele estiver à direita acionará o operador externo à classe (aquele do friend), que acionará o operador da classe (apenas trocando a ordem de chamada). Simples, não? Aproveitando a deixa do friend, se for desejável utilizar os operadores de entrada e saída (>> e <<) para fazer a leitura ou impressão de dados de sua nova classe, então será preciso defini-los como “amigos” da classe: class vetor { friend ostream &operator<< (ostream &, vetor); friend istream &operator>>(istream &, vetor &); … }; Nesse caso, a classe vetor está declarando amizade à classe ostream e à classe istream, deixando que elas acessem os dados de vetor de forma a ler ou escrever informações. Os símbolos & são necessários para que operações no formato a seguir sejam possíveis: cin >> a >> b >> c; // leitura de 3 vetores ou cout << a << b << c; // impressão de 3 vetores Friend pode ser utilizado para declaração de amizade de uma classe a um método/função ou a outra classe. Voltando à questão da sobrecarga de operadores, os operadores apresentados no quadro a seguir podem ser sobrecarregados em C++. Quadro 1 Operadores que podem ser sobrecarregados. + - * / % ^ & | ~ ! = < > += -= *= /= %= ^= &= |= << >> >>= <<= == != <= >= && || ++ -- ->* , -> [] () new Delete 63 Ainda existem dois operadores (pré e o pós incremento) que precisam ser explicados. O pré-incremento ocorre quando fazemos a operação ++x. Já o pósincremento ocorre quando o operador aparece após o objeto. Assim, poderíamos ter os valores internos de nosso objeto vetor sendo atualizados tanto por um como por outro operador. A tarefa do pré-incremento é atualizar o objeto antes de utilizá-lo, enquanto a tarefa do pós-incremento é exatamente oposta a esta, ou seja, utilizá-lo antes de incrementá-lo. Para simplificar, vamos supor que estejamos trabalhando com inteiros (vamos voltar aos objetos em breve), e que o valor de x seja 2. A linha de código z = x++ + 2; faz com que z seja 4 e que x se torne 3. Se, entretanto, a linha de código fosse z = ++x + 2; o x também seria 3 após a execução da linha, mas z seria 5! Para reproduzir tal comportamento nas classes que criamos, precisaremos de duas declarações: vetor &operator++(); // pré-incremento vetor operator++(int); // pós-incremento O pré-incremento fornecerá uma referência ao valor (e, como não há parâmetro sendo passado, espera-se que o retorno seja *this), enquanto o pós-incremento retorna uma cópia do valor. O parâmetro passado é chamado de fantasma. Acompanhe o código a seguir. vetor & vetor::operator++(){ int i; for (i=0;i<tam;i++) // poderia ser, também, i<thistam elemento[i] = elemento[i]+1; //ou thiselemento[i]... return *this; } Fácil? Todos os elementos do vetor são incrementados e o *this é retornado. Agora vamos para o pós-incremento. Quem observar com atenção o código a seguir verá que existe um truque genial! O objeto é atualizado (as mudanças ocorrem no this), mas o valor retornado é uma cópia do antigo (que foi devidamente armazenado em aux!). Assim, em uma operação envolvendo o pós-incremento, o valor utilizado será o antigo (porque utilizará a resposta retornada pelo método), mas na próxima oportunidade que o objeto for utilizado, ele já terá o valor novo! 64 vetor vetor::operator++(int z) { vetor aux = *this; int i; for (i=0;i<tam;i++) // poderia ser, também, i<thistam elemento[i] = elemento[i]+1; // ou thiselemento[i]... return aux; } 4.5 Exemplos em Java Em Java não há sobrecarga de operadores. Existem apenas sobrecargas de métodos e de construtores. Assim como em C++, a sobrecarga de métodos e de construtores acontece pela diferenciação da assinatura. No exemplo a seguir, será apresentada a classe caixote, com largura, altura e profundidade. Serão criados 3 construtores para inicializar um objeto da classe: um sem parâmetros, outro com um parâmetro e outro com três parâmetros. class caixote { // atributos private double largura, altura, profundidade; // construtor sem parâmetros // cria um caixote padrão de tamanho 1 caixote( ) { largura = 1; altura = 1; profundidade = 1;} // cria um caixote com tamanho L (se L for positivo) caixote( double L) { if (L > 0) else largura = altura = profundidade = L; largura = altura = profundidade = 1; } // cria um caixote com dimensões L x A x P desde que // cada uma das dimensões seja positiva caixote(double L, double A, double P) { if (L > 0) largura = L; else largura = 1; if (A > 0) altura = A; else altura = 1; if (P > 0) profundidade = P; else profundidade = 1; } 65 // método de cálculo do volume do caixote double volume( ) { return largura * altura * profundidade; } } O programa principal deve acionar cada construtor por meio do operador new. Para se criar um objeto chamado c1 utilizando o construtor com 3 parâmetros, o comando seria: caixote c1 = new caixote(5,6,7); Note que isso é um pouco diferente do que acontece em C++. Em C++ teríamos os mesmos 3 construtores, mas o objeto c1 seria declarado assim: caixote c1(5,6,7); Um código exemplo do programa principal em Java que utiliza caixote seria o apresentado a seguir. class caixoteDemo { public static void main(String args[]) { caixote c1 = new caixote(5,10,15); caixote c2 = new caixote( ); caixote c3 = new caixote(15); double vol1, vol2, vol3; vol1 = c1.volume(); ... System.out.println(“ vol 1 = “ + vol1); } } 4.6 Considerações finais Nesta unidade foram apresentados os principais conceitos relacionados com sobrecarga de construtores e de métodos. Alguns exemplos ilustraram como seria a utilização de sobrecarga, tanto em C++ como em Java. 66 Além disso, foi apresentada a sobrecarga de operadores, que torna o código C++ mais legível e intuitivo. Com ele é possível que operações como A = B + C; ocorram também para classes definidas pelo programador (tais como fração, número complexo, coordenadas polares, matrizes, etc.). Tal facilidade não existe em Java. 4.7 Estudos complementares Alguns livros como os da série How to Program (DEITEL & DEITEL, 2009a, 2009b) abordam o assunto sobrecarga. Em Deitel & Deitel (2009a) há um capítulo inteiramente dedicado à sobrecarga de operadores. Um comando pouco explorado nesta unidade foi o friend, que é explorado com um pouco mais de detalhes em Deitel & Deitel (2009a). 67 Unidade 5 Alocação dinâmica de memória 5.1 Primeiras palavras Ponteiros e alocação dinâmica de memória são dois temas que, normalmente, causam arrepios nos estudantes. Como são assuntos importantes, podemos rever o que são ponteiros, como se comportam e qual a relação entre ponteiros e alocação dinâmica de memória. Assim, nesta unidade, além de abordarmos alocação dinâmica de memória para classes, também faremos uma revisão de ponteiros, passagem de parâmetros (por valor e por referência), ponteiro para funções e alocação dinâmica de memória. 5.2 Problematizando o tema Existem dois conceitos a serem revistos: ponteiros e alocação dinâmica de memória. Vamos começar pela alocação dinâmica! Você já imaginou criar um programa que calcule a média de uma classe com 50 alunos? Simples, não? E se a classe tiver 200 alunos? Ah, é melhor fazer um programa que contemple uma classe muito grande então, não é?! Mas um programa que contemple uma classe com muitos alunos (vamos dizer 1.000 alunos) ainda pode ser inadequado para quem tenha/tem uma classe especial com 1.001 alunos ou para quem tenha/tem um computador com pouca capacidade de memória RAM. Se a instituição que utilizará seu programa criou uma classe especial com 1.001 alunos, seu programa não lhe será útil. Se, por outro lado, a instituição é de pequeno porte e adquiriu um computador usado com pouca capacidade de memória e suas classes são com no máximo 20 alunos, seu programa está superdimensionado. Às vezes, o superdimensionamento é tal que a execução de programas em computadores com pouca capacidade de memória se torna inviável. Sempre que você optar pela escolha de um tamanho fixo antes da execução do programa, o compilador providenciará a alocação prévia da memória a ser utilizada e isso não poderá ser modificado durante a execução de seu programa. Isso se chama alocação estática de memória e se contrapõe ao conceito de alocação dinâmica de memória. A alocação dinâmica, por outro lado, permite que seu programa aguarde até o momento da execução e consulte o usuário para saber qual a quantidade de memória que deverá ser alocada para executar a tarefa. Como cada usuário fornecerá uma resposta e como a decisão da quantidade de memória utilizada ficará adiada para quando o usuário executar o programa, isso se chama alocação dinâmica de memória. Percebe-se que com a alocação dinâmica de memória, o programa se adequará à necessidade do usuário. Ponteiros trabalham em sintonia com a alocação dinâmica de memória (afinal de contas, apontam para uma área de memória), mas não só isso. Ponteiros 71 podem apontar para uma área de dados já alocada (quer dinâmica, quer estaticamente) ou mesmo para uma área de código. Sim, ponteiros podem apontar para uma função, por exemplo! Dessa forma, tornam-se coringas em programação e, se bem empregados, são muito úteis. Um dos empregos de ponteiros é na passagem de parâmetros para funções e procedimentos. 5.3 Revisão Os dados de um programa estão armazenados na memória primária (memória RAM) do computador. Cada dado é referenciado por um endereço, por exemplo, 0 x 3EFF34, 0 x 4AFE52 e assim por diante. Não é conveniente programar utilizando endereços. Além de dificultar a tarefa de criação do programa, este se torna ilegível quando houver a necessidade de se fazer alguma alteração no código. Assim, convencionou-se atribuir um nome significativo (por isso você não deve utilizar xy34 como um nome de variável) para uma posição de memória que armazenará um determinado valor. Por exemplo, uma variável salário pode armazenar um valor decimal (com centavos) que signifique o salário de um funcionário. Não devemos nos esquecer de que uma variável é uma posição de memória e, portanto, tem um endereço. No mundo real, nos referimos às pessoas pelo nome: o Joãozinho, a Mariazinha, etc. É claro que o Joãozinho tem um endereço, assim como a Mariazinha, mas não falamos “a menina que mora na Rua São Paulo, no número 1234 é bonita”. Falamos: “a Ana é bonita”. No ambiente de programação utilizamos os nomes de variáveis para referenciar uma posição de memória. Ou seja, utilizamos nomes no lugar de endereços. Existem situações em que um objeto pode estar com uma pessoa em um determinado momento e com outra em outro momento. Nesses casos é conveniente ter alguém responsável por nos indicar quem está com o objeto. Um exemplo simples é o locutor de rádio acompanhando uma partida de futebol. Ele é o responsável por nos informar quem é que está com a bola no exato instante. Poderia ser o operador de uma câmera de TV acompanhando a mesma partida. O operador “aponta” a câmera para a posição onde se encontra o jogador que está com a bola. Se desejarmos saber quem está com a bola, perguntamos ao locutor ou ao operador. A Figura 3 apresenta uma imagem aérea de um campo de futebol com jogadores. Note que, sem um apontador, não é possível saber quem está com a bola! 72 Figura 3 Representação aérea de um campo de futebol. Não podemos nos esquecer de que o locutor e o operador também são pessoas que têm nome e endereço! Assim, um ponteiro é uma posição de memória que também tem um endereço e que armazena um endereço de outra variável. Pelo fato de armazenar um endereço, dizemos que ele “aponta” para um lugar (Figura 4). Figura 4 Ponteiros em ação. Se existem as áreas de dados reservadas para os inteiros, para os floats, etc., existem as áreas reservadas para os ponteiros para inteiros, ponteiros para floats, entre outros. Poderíamos imaginar tais áreas como vilas. Existe a vila dos inteiros, a vila dos floats, a vila dos doubles e também a vila dos ponteiros para inteiros, ponteiros para floats, ponteiros para char, etc. Em C, uma declaração de ponteiro para inteiro é feita da seguinte forma: int *p; Como o endereço de uma variável é obtido por meio do operador &, em C podemos indicar que um ponteiro aponta para o local onde a variável está, a partir da expressão a seguir. p = &x; /* x é uma variável do tipo inteiro */ Assim, vamos ilustrar os conhecimentos adquiridos até aqui apenas com as variáveis p e x. Suponhamos que p esteja no endereço 0 x 0205FF e que x 73 esteja no endereço 0 x 45AD3E. Quando fazemos uma operação de atribuição (como x = 4), na verdade estamos colocando o valor 4 na posição 0 x 45AD3E. A operação p = &x indica que a variável p (que é um ponteiro e, portanto, armazena um endereço) terá o valor 0 x 45AD3E armazenado. Ou seja, a posição de memória 0 x 0205FF guarda o valor 0 x 45AD3E. Se fizermos a operação *p = 5, na verdade estamos modificando o valor da variável x, porque estamos colocando no endereço dela um novo valor. Se houvesse outra variável inteira y e se o computador tivesse que executar o comando p = &y, isso significaria que qualquer operação de atribuição envolvendo o ponteiro p não mais atingiria x (somente atingiria y). O endereço de p continuaria sendo o mesmo! O que mudaria seria o valor guardado em p. Ah, e se houvéssemos feito a operação p++, isso significaria que p apontaria para a próxima variável inteira vizinha de y. Utilizando a Figura 4 como elemento de apoio, a operação p++ faria com que a seta apontasse para o minirretângulo imediatamente à direita da posição atual. E para saber quem está com a bola agora? Pergunte ao operador da câmera, ou seja, utilize o operador *: z = *p; /* z é uma variável do tipo inteiro */ Como p armazena o endereço de uma variável, não podemos fazer a atribuição z = p! Isso faria com que um endereço fosse armazenado em um local onde só inteiros são aceitos (assumindo que z é do tipo inteiro). E se você fizesse z = &p, isso faria com que o endereço da variável p (do ponteiro) fosse atribuído à variável z (também incorreto). O operador *, quando associado a um ponteiro, permite recuperar o valor armazenado no endereço para o qual ele aponta. Seria o equivalente a saber o nome da pessoa que está com a bola nesse exato momento! Em C, podemos utilizar ponteiros para apontar para uma variável única, para um vetor, para uma matriz bidimensional, etc. Uma aplicação simples seria criar um programa (ou função) para informar se uma cadeia de caracteres é palíndroma ou não. Algo é palíndroma se a leitura feita da direita para a esquerda for igual à leitura feita da esquerda para a direita. “ELE” é uma cadeia de caracteres que satisfaz tal propriedade. Vamos construir a solução para o problema do palíndromo utilizando função e ponteiros. A primeira coisa que notamos é que a cadeia de caracteres é passada como ponteiro. Na verdade, uma cadeia de caracteres é definida como um ponteiro para a posição inicial e terminada com um símbolo especial (‘\0’). Quando passamos a cadeia de caracteres para a função, na verdade estamos 74 passando o endereço inicial da cadeia. int palíndromo(char *cadeia) { /* declaração das variáveis locais */ char *inicio, *fim; /* tamanho da cadeia de caracteres */ int tam; /* strlen fornece o tamanho de cadeia */ tam = strlen(cadeia); /* aponta para a primeira posição de cadeia */ inicio = &cadeia[0]; /* última posição antes do \0 while (inicio < fim && *inicio == *fim) { /* ponteiros */ fim = &cadeia[tam-1]; */ inicio++; fim--; } if (inicio >= fim) return 1; else return 0; } Os ponteiros inicio e fim apontam para a primeira e para a última posição da cadeia de caracteres, respectivamente. Atenção especial neste momento, pois o final real da cadeia de caracteres (‘\0’) não nos interessa no contexto do palíndromo. O que nos interessa é o último caractere da cadeia. O \0 é apenas um símbolo especial da linguagem para controlar cadeias de caracteres! O ponteiro inicio apontará para a primeira posição da cadeia assim que a operação inicio = &cadeia[0] for executada. Tal operação indica que inicio armazenará o endereço da primeira posição da cadeia passada para a função. O mesmo raciocínio vale para a operação fim = &cadeia[tam-1]. Como a cadeia de caracteres é formada por posições consecutivas de memória, o endereço inicial é menor que o endereço final. Seria equivalente a dizer que em uma rua com casas numeradas em ordem crescente, alguém estivesse apontando para a primeira casa da rua e outra pessoa estivesse apontando para a última casa da rua. Como a numeração é crescente, sabe-se que a pessoa no começo da rua apontará para um número (endereço) menor que o apontado pelo colega no final da rua. Daí o significado da primeira parte da expressão lógica (inicio < fim && *inicio == *fim). A segunda parte da expressão é referente ao conteúdo apontado por cada ponteiro. Para ser palíndroma, a cadeia tem que ter a primeira letra igual à última, a segunda igual à penúltima e assim por diante. 75 Em nossa metáfora de rua, seria o equivalente a ter um morador na casa 1 com o mesmo nome do morador da última casa e assim por diante. Por fim, o código interno do loop significa ir para a posição seguinte no caso do ponteiro inicio e para a posição anterior no caso do ponteiro fim. Se mantivermos nossa metáfora da rua, seria o equivalente a fazer com que a pessoa no começo da rua desse um passo em direção à pessoa no final (inicio++) da rua e esta, por sua vez, também desse um passo em direção à pessoa no início da rua (fim_ _). Se eles se cruzarem (inicio > fim), significa que a cadeia é palíndroma. Se não se cruzarem, significa que o conteúdo de um ponteiro não equivalia ao conteúdo de outro ponteiro! Já que sabemos como ponteiros se comportam com variáveis definidas previamente à execução, podemos investigar, agora, seu comportamento quando se utiliza alocação dinâmica de memória. Em alocação dinâmica, o que se deseja é solicitar ao sistema que forneça um endereço a partir do qual se possam armazenar informações. Vamos começar com algo simples: uma variável. A alocação é solicitada a partir do comando malloc (ou calloc). Se a solicitação for satisfeita, um endereço é retornado. Caso contrário, NULL é retornado. p = (int *) malloc(sizeof(int)); Como a função malloc retorna um ponteiro genérico, é preciso realizar a conversão do ponteiro para o tipo desejado. No exemplo, a conversão é feita para inteiro. Como é ponteiro, é preciso ter o símbolo *. Malloc aloca uma quantidade de posições de memória de um determinado tamanho. Assim, a função malloc precisa ser informada do tamanho desejado de memória a ser alocada. No caso, a intenção era utilizar uma posição para inteiro. Se a solicitação não for atendida, então NULL será retornado. Assim, é preciso verificar se obtivemos o que solicitamos: if (p == NULL) { printf(“Solicitação não atendida! \n“); exit(1); } A partir daí a coisa é simples! Basta utilizar o ponteiro da mesma forma que 76 foi utilizado anteriormente. É importante observar que quando uma alocação de memória é feita, um espaço da área de dados é reservada para seu uso. Depois de utilizá-lo, você deve devolvê-lo ao dono para que, caso alguém deseje, possa utilizá-lo. Se você só realizar solicitações de alocação e não fizer nenhuma devolução será equivalente a um aluno que vai à biblioteca e só faz empréstimos sem devolver. Os livros emprestados não estarão disponíveis para que outros possam utilizá-los. Em C a devolução é feita com o comando free. Lembre-se de utilizá-lo! ... int *x; /* vetor com 10 posições */ x = (int *) malloc(10 * sizeof(int)); if (x != NULL) { for (i=0;i<10;i++) x[i] = i; ... /* outras operações */ free(x); x = NULL; /* por questões de segurança */ /* devolver a memória para o sistema */ } ... No exemplo, o ponteiro x recebe um endereço de memória que pode armazenar 10 posições de inteiros. Assim, x passa a ser vetor. Se malloc não conseguir as 10 posições consecutivas que foram solicitadas, ele retornará NULL. O exemplo é esquemático e mostra uma inicialização arbitrária de x (cada elemento recebe um valor equilavente a sua posição no vetor). Várias outras operações podem ser feitas com tal vetor e não foram apresentadas por questão de espaço. Após a utilização, há a devolução do espaço solicitado a partir do comando free. Ah, e se você estiver trabalhando com estruturas? O que acontece? Vamos, primeiramente, declarar uma estrutura. typedef struct data { int dia, mes, ano; } data; 77 Utilizamos data por ser uma estrutura a ser explorada também em orientação a objetos e por servir como elemento de comparação. A estrutura em questão tem 3 elementos (dia, mês e ano). Se houvéssemos declarado uma variável dia_independencia do tipo data, poderíamos fazer a atribuição do dia assim: dia_independencia.dia = 7; Mas, e se houvéssemos criado uma variável feriado como um ponteiro para data (data *feriado;)? Nesse caso, seria necessário fazer uma alocação dinâmica de memória (afinal, o que existe é um ponteiro para armazenar um endereço e não um espaço para armazenar 3 valores!). feriado = (data *) malloc(sizeof(data)); if (feriado != NULL) { /* trabalho com um elemento da estrutura */ feriado->dia = 7; ... free(feriado); /* liberação de memória feriado = NULL; /* questões de segurança */ */ } 5.4 Alocação de atributos Em algumas situações é interessante que o atributo seja alocado dinamicamente. Geralmente isso ocorre com vetores e matrizes. Um atributo em C++ alocado dinamicamente deve ser declarado como ponteiro. Neste item será apresentado o exemplo de uma fila de pessoas, que serão identificadas pelos três primeiros dígitos do documento de identidade. A fila em questão não tem prioridade, ou seja, um idoso ou gestante deve obedecer a ordem cronológica de entrada para ser atendido. Para inserir uma pessoa na fila deve-se utilizar o método inserir (int), em que o parâmetro a ser passado é a identificação da pessoa. Se a fila já estiver cheia, o método deverá retornar o valor booleano falso, que indicará que a pessoa não pôde ser inserida na fila. A retirada de uma pessoa da fila acontece com o uso do método retirar( ). O método deve retornar a identificação da primeira pessoa da fila e também deve 78 promover sua reorganização. Ou seja, o segundo da fila deve ocupar a primeira posição, o terceiro da fila deve ocupar a segunda posição e assim por diante. É uma simulação do que acontece em filas reais. 5.5 Construtor de cópia Toda vez que um método retorna à cópia de um objeto e toda vez que criamos um objeto utilizando outro como exemplo (lembra-se do exemplo da ovelha?), o construtor de cópia entra em ação. Assim, é fácil entender a missão do construtor de cópia: criar uma cópia do objeto em questão. Um construtor de cópia é fácil de ser identificado. Ele tem o mesmo nome da classe, não retorna nenhum valor e tem como parâmetro uma referência para um objeto da mesma classe. Ou seja, se a classe se chama X, então o construtor de cópia seria: X::X(X&) { ... } O primeiro X indica o nome da classe, o segundo o nome do método (construtor) e o terceiro é uma referência a um objeto da classe de mesmo nome. Deve ser, obrigatoriamente, uma referência para a classe X, uma vez que se assim não o fosse, configurar-se-ia em chamada por cópia. E uma chamada com parâmetro passado por cópia obrigaria a chamada do construtor de cópia. Mas se o construtor de cópia fizesse uma chamada por cópia isso não teria fim nunca. Seria um loop infinito. Um construtor de cópia normalmente não chama a atenção, visto que não aparece em muitos códigos. O fato é que se um construtor de cópia não for criado pelo programador, ele será fornecido automática e gratuitamente pelo compilador. Sim, o compilador criará um construtor de cópia para você sempre que for necessário. Um leitor mais curioso fará a seguinte pergunta: “mas se o compilador cria um construtor de cópia para mim, então qual o grande problema?” Por que este item 5.5? Para melhor compreensão, vamos apresentar um exemplo de construtor de cópia. 79 class vetor { private: int elementos[10]; int total; public: vetor( ); // construtor sem parâmetros vetor(vetor &); // construtor de cópia ... }; O vetor em questão armazena um conjunto de valores no conjunto elementos e também controla o total de valores armazenados. Ou seja, se o total for igual a 10, sabe-se, com certeza, que não é possível colocar mais informações no vetor. Pode-se convencionar o vetor como tendo valores positivos. Nesse caso, o construtor poderia inicializá-lo com um valor negativo, por exemplo. Assim, compreendemos como ficaria o construtor: teria um loop para inicializar todos os elementos do vetor e também teria a atribuição total = 0; como demonstrado no exemplo a seguir. vetor::vetor( ) { int i; for (i=0;i<10;i++) elementos[i] = -1; total = 0; } E o construtor de cópia, como seria? Deve fazer a cópia dos elementos passados por referência. Ou seja, todos os elementos de x são copiados. E se o código não for criado, o compilador gerará um código exatamente igual para seu programa. vetor::vetor(vetor &x) { int i; for (i=0;i<10; i++) elementos[i] = x.elementos[i]; total = x.total; } Mas e se o vetor fosse criado com alocação dinâmica? Vamos entender o 80 que o construtor padrão (oferecido pelo compilador) faria em tal situação. Com alocação estática, o construtor padrão simplesmente copia tudo para um novo objeto, conforme a Figura 5: Figura 5 Esquema de cópia de um construtor de cópia padrão. Mas, e se a classe contiver um ponteiro? Em tal situação, o compilador fará o que sempre fez, ou seja, copiar o conteúdo do velho para o novo. Mas isso não é exatamente o que queremos! Copiar o conteúdo do ponteiro significa copiar o endereço para o local que ele aponta. Ou seja, vai apontar para o mesmo local que o original. O mais grave é que se o objeto original for destruído, o vetor original também será destruído! E para onde apontará a cópia? Figura 6 Cópia rasa. No exemplo da Figura 6, os dados da classe original são copiados para a classe cópia, inclusive o ponteiro. Como ele aponta para um endereço X de memória, a cópia também apontará para aquele endereço. Se a classe original for destruída, o vetor também será. Mas a cópia continuará apontando para aquele local (que não está mais disponível). O desejável é uma cópia profunda, ou seja, uma cópia que cria um novo vetor. 81 Figura 7 Cópia profunda. A eliminação tanto do original como da cópia não traria problemas para o código. Do ponto de vista de código, significa fazer uma alocação dinâmica dentro do construtor de cópia, para que seja criado um novo vetor. Na figura anterior é possível ver uma classe cópia que contém um ponteiro (assim como a classe original), mas que aponta para outro endereço de memória. Os dados armazenados no vetor original são copiados para o novo vetor alocado. Se a classe original for destruída, a cópia não será afetada. vetor::vetor(vetor &x) { int i; elementos = new int [10]; for (i=0;i<10; i++) // alocação do novo vetor elementos[i] = x.elementos[i]; total = x.total; } Assim, o novo construtor de cópia vai ter uma cópia de todos os elementos do vetor original, mas não terá a fragilidade de apontar para o mesmo local do original. 5.6 Alocação de objetos Na discussão sobre construtor de cópia foi apresentado um exemplo de alocação dinâmica de um atributo pertencente à classe (para construção de um vetor). Mas a alocação pode ocorrer na declaração do objeto. Imagine que exista 82 um ponteiro *C da classe circulo. Sua declaração seria: circulo *C; Ou seja, *C é um ponteiro para um elemento da classe circulo. Para poder utilizá-lo, é preciso fazer a alocação dinâmica do ponteiro *C. Para criar um único objeto da classe circulo utilizando *C, deve-se utilizar o seguinte comando: *C = new circulo; Se a alocação foi bem-sucedida (havia espaço em memória para a alocação), então, a partir desse momento, C++ pode armazenar as informações da classe. Há um pequeno detalhe: os métodos de C++ só poderão ser acessados pelo operador (o mesmo ocorre quando trabalhamos com struct e ponteiro!). E uma vez que ocorreu a alocação, será preciso devolver a memória alocada ao final de sua utilização. Ou seja, será preciso utilizar o delete! Ah, se a alocação não foi bem-sucedida, então será preciso terminar a execução do código e informar o usuário a respeito do erro encontrado. 5.7 Exemplo em C++ Como exemplo, vamos utilizar a classe fila. Queremos ter uma fila de elementos (vamos utilizar letras) que tenha um tamanho máximo definido pelo programador. Se ele não definir nada, então será criada uma fila padrão de tamanho 10. Na fila, o procedimento é atender aquele que está em primeiro lugar. Quando o primeiro da fila for atendido, todos os outros devem mudar de posição. Ou seja, quem estava em segundo passa a ficar em primeiro, quem estava em terceiro passa para segundo e assim por diante. Resumindo, o funcionamento é igual ao de uma fila de padaria. A seguir serão apresentados os principais trechos do código C++. class fila { private: char *elemento; int tammax, tam_atual; fila(); ~fila(); public: fila(int); bool insere(char); 83 char retira(); void imprime(); }; No exemplo anterior, encontra-se a definição da classe com o ponteiro para os elementos do vetor (chamado elemento), além dos atributos tammax e tam_atual (que controlam o tamanho máximo e o tamanho atual), bem como os métodos insere, retira e imprime. Também estão presentes os construtores (com e sem parâmetro e o destrutor). O método insere deve ter um character como parâmetro (o elemento que será inserido) e deve retornar um valor lógico (bool) caso a operação seja bem-sucedida e o método retira deve apenas retornar o elemento que foi retirado da fila. Deve haver alguma convenção para que um valor seja retornado caso a fila esteja vazia. fila::fila() { cout << “ construtor sem parâmetro“ << endl; elemento = new char[10]; if (elemento != NULL) { tammax=10; tam_atual=0; } else exit (1); } O código do construtor sem parâmetros é bem simples. Há a solicitação de memória (new) com o tamanho indicado (10) e o tipo (char). O resultado da alocação é retornado para o ponteiro elemento. Se não for NULL, então a operação terá sido bem-sucedida e os atributos tammax e tam_atual podem ser atualizados. Tammax indicará o tamanho máximo da fila e tam_atual indicará quantos elementos estão na fila (no momento da criação a fila está vazia). O construtor com parâmetro é muito parecido, mas um detalhe deve ser levado em consideração: o tamanho (que será passado por parâmetro) deve ser testado. Caso contrário, alguém poderá solicitar um vetor de tamanho negativo e isso causará problemas para o programa. Tente fazê-lo. O destrutor também é bem simples. A tarefa dele é devolver (por meio do comando delete) o que foi emprestado. É boa prática apontar o ponteiro que não está mais sendo utilizado para 0. 84 fila::~fila() { cout << “destrutor“ << endl; delete [] elemento; elemento = 0; } O método insere deve receber um caractere e colocá-lo na fila (caso a fila ainda não esteja cheia e caso o elemento a ser inserido seja um elemento alfanumérico). Ele deve entrar na última posição vaga, caso a fila ainda não esteja cheia. A verificação de fila cheia é feita na condição tam_atual < tammax. Se tam_atual ainda não atingiu o tamanho máximo (tammax), então ainda é possível inserir elementos no vetor. E caso seja possível inserir, deve-se sempre lembrar que é preciso atualizar o tamanho atual do vetor. bool fila::insere(char a) { if (isalpha(a) && tam_atual < tammax) { elemento[tam_atual++] = a; return true; } else return false; } O método retira deve retornar o valor que está sendo retirado da fila para quem fez a chamada do método. Muitos confundem o conceito e acreditam que imprimir o número retirado da fila já seja a solução. Não é! O valor deve ser devolvido e não impresso. O valor impresso seria útil para o usuário que estivesse acompanhando a execução do programa (caso a impressão fizesse algum sentido). Números saltando na tela não fazem sentido. O valor retornado é útil para o programador, que poderá utilizar o elemento retirado da fila para fazer testes, etc. Outra questão importante na retirada de um elemento da fila é que todos os elementos que permaneceram na fila devem mudar de posição. A retirada do elemento implicará, ainda, na atualização do tamanho da fila. char fila::retira() { int i; 85 char letra; if (tam_atual > 0) { letra = elemento[0]; for (i=0;i<tam_atual;i++) elemento[i] = elemento[i+1]; tam_atual--; return letra; } else return ‘0’; } O método imprime é responsável por imprimir a fila. Se ela estiver vazia, não imprime nada (só muda de linha). void fila::imprime() { int i; for (i=0;i<tam_atual;i++) cout << elemento[i] << “ “; cout << endl; } Falta demonstrar como a classe pode ser utilizada no programa principal. Vamos criar uma fila de padaria com 15 elementos e oferecer ao usuário um menu de opções para as tarefas a serem realizadas pela fila. int main(int argc, char** argv) { int i; char letra, vez, opção; fila padaria(15); { // abre novo escopo fila estadio_futebol; } // fecha escopo – aciona destrutor da fila // exibir menu e ler opção do usuário cout << “i. inserir um elemento na fila“ << endl; cout << “r. retirar um elemento da fila“ << endl; 86 cout << “p. imprimir a fila“ << endl; cout << “s. sair“ << endl; cout << “Entre com uma das opções do menu!“ << endl; cin >> opcao; while (opcao != ‘s’) { if (opcao == ‘i’) { cout << “Entre com uma letra“; cin >> letra; if (padaria.insere(letra)) cout << “inseriu !“ << endl; else cout << “falha na inserção! “ << endl; } if (opcao == ‘p‘) padaria.imprime(); if (opcao == ‘r‘) { vez = padaria.retira(); if (vez != ‘0‘) cout << “agora é a vez do “ << vez << endl; else cout << “não tem ninguém na fila! “ << endl; } // exibir menu e ler opção do usuário ... } O leitor atento observará logo no começo do código do programa principal que há uma declaração de fila (estádio_futebol) que está entre parênteses. Na verdade serve para provar que o destrutor é acionado automaticamente quando o fim do escopo de um objeto é encontrado. O abrir chaves indica início de um novo escopo e o fechar chaves indica o final de tal escopo. Assim, quando o programa for executado, ele apresentará a mensagem “destrutor” no início do programa e será referente à fila estádio_futebol. Uma confusão que os estudantes fazem é com relação ao acionamento do destrutor. Muitos acham que ele é acionado automaticamente quando o programa termina. O código apresentado é para esclarecer tal dúvida. Também é importante observar que as duas filas terão tamanhos diferentes. Aquela declarada com parâmetro terá o tamanho informado (15), enquanto a outra terá o tamanho padrão (10). 87 5.8 Considerações finais Gerenciamento dinâmico de memória é um assunto muito importante para os programadores em C++. O gerenciamento dinâmico de memória permite que o programador crie programas flexíveis e que satisfaçam as condições do ambiente do usuário. Se houver a necessidade de uma matriz enorme, então o programa tentará fazer a alocação. Caso seja bem-sucedida, seu programa poderá realizar as tarefas pertinentes. Após ter sido utilizada, a área de memória relativa à matriz poderá ser devolvida ao sistema operacional para que outro programa possa utilizá-la. Ah, é preciso entender também o que faz o construtor de cópia e em quais situações é preciso criar o próprio código do construtor de cópia para transformá-lo em cópia profunda. 88 Unidade 6 Composição e herança 6.1 Primeiras palavras Tanto composição como herança são conceitos muito importantes em orientação a objetos e estão completamente associados aos conceitos de reutilização de software. Apesar de a operação “copy & paste” ser muito utilizada, ela traz inconvenientes, do ponto de vista da engenharia de software. A replicação do código sem um adequado gerenciamento provocará efeitos colaterais sempre que houver uma manutenção no sistema. Nesta unidade os dois conceitos serão explorados de forma detalhada e alguns exemplos ilustrarão o uso de ambos. 6.2 Problematizando o tema A indústria de hardware conseguiu atender o aumento da demanda por produtos por meio da criação de componentes com funcionalidades bem definidas, que podem ser aproveitados em outros projetos. Assim, um conjunto de componentes eletrônicos pode ser interligado e encapsulado de modo que forme um circuito eletrônico. Circuitos eletrônicos podem ser combinados de maneira a formar uma placa de circuito eletrônico. A combinação de placas pode produzir um aparelho eletrônico como um monitor de vídeo. A indústria do software, por outro lado, deparou-se com o aumento da demanda de sistemas, o aumento da complexidade destes e a baixa qualidade dos produtos por ela produzidos. Precisou reinventar-se. O paradigma de orientação a objetos permitiu que algo similar ao encapsulamento de componentes eletrônicos e seu reuso, praticado pela indústria do hardware, fosse replicado para a indústria do software. 6.3 Composição No mundo real, um objeto é composto por outros. Uma porta é composta por maçaneta, fechadura, madeira (se for porta de madeira), etc. A própria fechadura é composta por diversos outros componentes. Um monitor de vídeo é composto por placas de circuito integrado, botões (de ligar/desligar, contraste, brilho, etc.), tela, etc. Enfim, o mundo real contém objetos que são obtidos por meio da combinação com outros objetos. Quando dizemos que um carro tem uma direção, estamos indicando que “direção” faz parte de “carro”; quando um empregado tem uma data de admissão, estamos indicando que a data de admissão faz parte da classe “empregado”. 91 A relação tem-um é uma relação importante em orientação a objetos, pois permite que o processo de criação de uma classe mais complexa seja feito de forma similar a um jogo Lego. 6.4 Herança Do ponto de vista biológico, herança é a transmissão de características codificadas nos genes para outros indivíduos da mesma espécie, por meio de um mecanismo chamado reprodução. Assim, a criação de um novo indivíduo é feita segundo um modelo existente. O que ocorre é um reaproveitamento de um código genético já existente. O paradigma de orientação a objetos inspirou-se também no conceito de herança. Se há um código que define uma classe X e há outra classe Y que utiliza a classe X como exemplo, então, segundo o conceito de herança, o código da classe X pode ser reaproveitado. No exemplo, a classe X serve como base para a criação da classe Y. A classe Y é derivada da classe X, assim como queijo, requeijão e iogurte seriam derivados de leite. Alguns outros exemplos podem deixar o conceito de herança mais claro. O primeiro exemplo seria a classe veículo. A definição de veículo é “um meio de transporte qualquer”. Ou seja, tudo aquilo capaz de mover algo de um lugar para outro é considerado veículo. O rádio, a TV e o jornal são, então, veículos de comunicação. O skate, o patinete, o carro, a lancha, o navio, o helicóptero e o foguete são veículos de transporte de pessoas. Uma relação muito clara que emerge dessa relação é a relação é-um. Um carro é um veículo. Portanto, veículo é uma classe mais geral, que permite diferenciar objetos que transportam algo de outros objetos que servem para enfeitar ambientes, por exemplo. Estamos falando de categorias. Quando vamos definir um carro ou um skate podemos emprestar o conceito de veículo já consolidado, e a relação é um é válida. Isso nos faz entender as vantagens da utilização de herança pela indústria de software. Figura 8 Algumas categorias que compartilham o conceito veículo. Na verdade, podemos criar uma hierarquia de classes. Poderíamos subdividir 92 veículos em veículos de comunicação e veículos de transporte de pessoas e/ou cargas. Veículos de transporte de pessoas e/ou cargas poderiam ser terrestres, aquáticos e aéreos. Veículos terrestres poderiam ser sem rodas ou com rodas. Veículos com rodas poderiam ser os de 4 rodas, 3 rodas, 2 rodas ou mesmo os de uma roda. O fato é que a relação é-um se mantém. Um carro de 4 rodas é um veículo. Um carro de 4 rodas é um veículo terrestre de rodas. É importante observar que a relação é-um pode ser direta (como no caso filho-pai) ou indireta (como no caso neto-avô ou bisneto-bisavô). Na herança direta não existe uma classe intermediária. Na indireta há pelo menos uma classe entre as duas relacionadas. A herança pode ser simples ou múltipla. Na herança simples apenas uma classe é considerada para efeitos de transmissão de características. Na herança múltipla mais de uma classe é considerada. No exemplo do veículo, poderíamos definir um veículo anfíbio, que tem características de veículo aquático e também de veículo terrestre. Figura 9 Hierarquia de classes. Outro conceito importante associado com herança é o de sobreposição de método. Sobreposição (ou anulação) é diferente de sobrecarga. Quando dois métodos têm o mesmo nome, mas assinaturas diferentes, dizemos que há sobrecarga de método. Quando um método com mesmo nome e mesma assinatura está presente, tanto na classe base como na classe derivada, o método da classe derivada esconde o método da classe base, ou seja, sobrepõe-se ao método da classe base. A chamada do método por um objeto da classe derivada sempre acionará o método presente na classe derivada. A seguir serão apresentados exemplos práticos nas linguagens C++ e Java para consolidar os conceitos e demonstrar suas utilizações práticas. 6.5 Exemplo em Java Java trabalha apenas com herança simples. Assim, vamos apresentar um exemplo em Java de herança simples em dois níveis. Vamos, primeiramente, apresentar a classe PONTO, que deve ter como atributos as coordenadas x e y. Também é importante que existam as funções de acesso aos atributos, 93 chamadas de funções set e get. As funções set servem para alterar os valores, enquanto as funções get servem para recuperar os valores armazenados. public class PONTO { private int x, y; public PONTO(int a, int b) { x = a; y = b;} public void setx(int a) { x = a;} public void sety(int a) { y = a;} public int getx(){return x;} public int gety(){return y;} public void imprimir(){ System.out.println(“ x = “+ x+ “ y = “+ y); } } A criação da classe circulo pode ser feita levando-se em conta a existência da classe ponto. public class circulo extends PONTO { private double raio; public circulo (int a, int b, double c) { super(a,b); raio = c; } public double getRaio() { return raio; } @Override public void imprimir(){ super.imprimir(); System.out.println(“raio = “+getRaio()); } public double getArea() { return 3.141592*getRaio()*getRaio(); } public double getPerimetro() { return 2*3.141592*getRaio(); } } 94 Podemos dizer que circulo é um ponto com um raio. A classe circulo indica que está herdando características da classe ponto por meio da declaração extends. Dessa forma, os atributos x e y também fazem parte da classe circulo, assim como os métodos set e get daquela classe. Os membros private da classe PONTO não podem ser acessados diretamente pela classe circulo. Dessa forma, x e y são obtidos ou alterados apenas por meio das funções de acesso get e set, respectivamente. O construtor da classe circulo recebe 3 parâmetros, sendo dois deles referentes a PONTO. Assim, o construtor da classe circulo deve acionar – por intermédio da chamada de super – o construtor da classe PONTO. A classe PONTO é a classe base, também chamada superclasse. Por isso o nome super. A classe base deve ser acionada e finalizada antes da classe derivada. Em uma analogia com a construção civil, a classe base seria equivalente ao alicerce de uma obra e a classe derivada, as paredes. Não se pode construir as paredes sem o alicerce. A chamada da classe base acontece, como informado, por meio do comando super. Os parâmetros a e b recebidos pelo construtor de circulo são passados para o construtor PONTO a partir da chamada super(a,b). Uma das funções de super está, então, decifrada. Outra função de super é acessar um método da classe base, o que é demonstrado no método imprimir. Lá, o método imprimir de PONTO é acionado e em seguida o raio é impresso. Isso evita que tenhamos que reescrever o código da impressão de ponto. Isso é especialmente importante se a impressão de ponto obedecer a um formato preestabelecido. Deve-se observar que em uma classe derivada o comando super deve ser sempre o primeiro a ser executado. Se isso não acontecer, o construtor default da classe base será acionado automaticamente. É importante que ele exista. Em Java existe uma anotação (@Override) que permite que programadores assinalem a intenção de sobrescrever em uma classe derivada um método da classe base. Alguns compiladores utilizam tal anotação para alertar o programador quando uma alteração na assinatura de um método é efetuada e não replicada nos métodos das classes relacionadas (base ou derivada). No código em questão, o método imprimir da classe circulo está escondendo o método imprimir da classe PONTO. Anotar no código que essa é a verdadeira intenção pode evitar problemas futuros, no caso de uma mudança de nome, de tipo de retorno ou mesmo de número de parâmetros em um dos métodos. No programa principal, a classe circulo poderia ser utilizada da seguinte forma: circulo c = new circulo(10,10,2); c.imprimir(); 95 Como mencionado, o método imprimir acionado é o da classe circulo. O método imprimir da classe base está escondido. Na verdade, ele é acionado dentro do método imprimir da classe circulo por meio da chamada super.imprimir(). Outra palavra-chave em Java que tem relação com herança é a palavra FINAL. Ela pode ser usada tanto antes de nomes de métodos como antes de nomes de classes. Se um método chamado X da classe base é precedido da palavra final, então tal método não poderá ser sobreposto na classe derivada. Se uma classe é definida como final, então tal classe não poderá ser derivada. 6.6 Exemplo em C++ Outro exemplo interessante seria a classe pessoa e a classe professor. Professor é uma pessoa. Assim, podemos criar a classe pessoa e depois criar a classe professor utilizando a classe pessoa como base. A classe pessoa, na verdade, poderá servir de base para a classe estudante também (pois estudante é uma pessoa). Uma pessoa tem nome, sobrenome e data de nascimento. Seria o conjunto básico de informações, uma vez que uma criança pode não possuir RG e CPF. Como vamos utilizar a data de nascimento, precisamos definir a classe data e uma função para verificar o ano bissexto. bool bissexto(int a) { if (a % 4 == 0 && a % 100 != 0) return true; else if (a % 4 == 0 && a % 100 == 0 && a % 400 == 0) return true; else return false; } class data { private: int dia, mes, ano; public: data(); data(int, int, int); void setDia(int); void setMes(int); 96 void setAno(int); int getDia(); int getMes(); int getAno(); void imprime(); }; data::data() { dia = 1; mes = 1; ano = 2010; } data::data(int d, int m, int a) { if (a > 1900 && a < 2011) ano = a; else ano = 2010; if (m >=1 && m <=12) mes = m; else { mes = 1; if (d > 0 && d < 29) dia = d; else if (d > 28 && d < 32) { dia = 30; if (d == 29 && mes == 2 && bissexto(ano)) dia = d; else if (d == 31 && (mes == 1 || mes == 3 || mes == 5 || mes == 7 || mes == 8 || mes == 10 || mes == 12)) dia = d; } else dia = 1; } } void data::setAno(int a) { if (a > 1900 && a < 2010) 97 ano = a; else ano = 2010; } void data::setMes(int m) { if (m < 13 && m > 0) mes = m; else mes = 1; } void data::setDia(int d) { int x; if (d > 0 && d < 29) dia = d; else if (d > 28 && d < 32) { dia = 30; if (d == 29 && getMes() == 2 && bissexto(getAno())) dia = d; else { x = getMes(); if (d == 31 && (x == 1 || x == 3 || x == 5 || x == 7 || x == 8 || x == 10 || x == 12)) dia = d; } } else dia = 1; } int data::getAno() { return ano; } int data::getMes() { return mes; } 98 int data::getDia() { return dia; } void data::imprime() { cout << getDia() << “/“ << getMes() << “/“ << getAno() << endl; } Como informado, a classe pessoa utiliza a classe data (uma pessoa tem uma data de nascimento), ou seja, uma composição. class pessoa { private: char nome[20]; char sobrenome[20]; data nascimento; public: pessoa(); pessoa(char *, char*); void setNome(char *); void setSobrenome(char *); char *getNome(); char *getSobrenome(); void imprime(); void setData(int, int, int); }; pessoa::pessoa() { strcpy(nome,“Ana“); nascimento.setAno(2010); nascimento.setDia(1); nascimento.setMes(1); strcpy(sobrenome,“Silva“); } pessoa::pessoa(char *n, char *s) { strcpy(nome,n); strcpy(sobrenome,s); nascimento.setAno(2010); nascimento.setDia(1); nascimento.setMes(1); } void pessoa::setNome(char *x){ } strcpy(nome,x); 99 void pessoa::setSobrenome(char *x) { strcpy(sobrenome,x); } char * pessoa::getNome() { return nome; } char * pessoa::getSobrenome() { return sobrenome; } void pessoa::imprime() { cout << getNome() << “,“ << getSobrenome() << endl; nascimento.imprime(); } void pessoa::setData(int d, int m, int a) { nascimento.setDia(d); nascimento.setMes(m); nascimento.setAno(a); } Algumas considerações interessantes: 1.O método imprime de pessoa chama o método imprime de data (nascimento.imprime()); 2.A classe pessoa necessita ter um método que permita modificar a data. Com ele, podem-se acionar os métodos set de data a partir do atributo nascimento; 3.Os atributos de data e de pessoa são privados e não podem ser acessados diretamente. A classe estudante herda características de pessoa. Assim, espera-se que não seja necessário reescrever o código de acesso às informações de pessoa. É o que será apresentado no código a seguir. Note que a classe estudante indica que está herdando as características (atributos e métodos) da classe pessoa por meio do :public. class estudante: public pessoa { private: 100 char curso[20]; char RA[10]; char universidade[20]; public: estudante(); estudante(char *, char *, char *); void setCurso(char *); void setRA(char *); void setUniversidade(char *); char *getCurso(); char *getRA(); char *getUniversidade(); void imprime(); estudante(char *, char *, char *, char *, char *); }; estudante::estudante() { strcpy(curso,“Computacao“); strcpy(universidade,“UFSCar“); strcpy(RA,“123456“); } estudante::estudante(char *c, char *r, char *u) { strcpy(curso,c); strcpy(universidade,u); strcpy(RA, r); } estudante::estudante(char *c, char *r, char *u, char *n, char *s): pessoa(n,s) { strcpy(curso,c); strcpy(universidade,u); strcpy(RA, r); } O primeiro construtor inicializa o objeto com valores padrão e chama o construtor sem parâmetro da classe base. O segundo construtor inicializa com os dados passados por parâmetro, mas os dados de pessoa são os criados pelo construtor sem parâmetro da classe pessoa. O terceiro construtor recebe, também, por meio da lista de parâmetros, os dados de nome e sobrenome e aciona o construtor com parâmetros da classe base, conforme se pode observar a partir do código apresentado anteriormente. O resto do código é bem simples, com destaque para o método imprime, que aciona o método imprime de pessoa. 101 void estudante::setCurso(char *c) { strcpy(curso,c); } void estudante::setRA(char *r) { strcpy(RA, r); } void estudante::setUniversidade(char *u) { strcpy(universidade,u); } char * estudante::getCurso() { return curso; } char *estudante::getRA() { return RA; } char *estudante::getUniversidade() { return universidade; } void estudante::imprime() { cout << “ curso : “ << curso << endl; cout << “ RA : “ << RA << endl; cout << “ Univ. : “ << universidade << endl; pessoa::imprime(); } Tente criar o código do programa principal para utilizar a classe pessoa e a classe estudante. 6.7 Considerações finais A tarefa de copiar, colar e adaptar normalmente gera erros. Erros são muito caros para as fábricas de software. Recriar do zero também é caro (afinal, todo o esforço de planejar, implementar e testar já foi feito por alguém). A melhor solução é poder reutilizar um código pronto. Algumas formas foram vistas nesta unidade, como herança e composição. Herança está associada à relação é-um, enquanto composição está associada à relação tem-um. 102 Unidade 7 Polimorfismo 7.1 Primeiras palavras Você já viu algum tipo de polimorfismo neste livro, mas ele ainda não foi formalmente definido. Nesta unidade você verá vários tipos de polimorfismo existentes em orientação a objetos com exemplos em C++ e em Java. 7.2 Problematizando o tema O termo “polimorfismo” tem suas origens no idioma grego e significa várias (poli) formas (morfos). Há mais de 40 anos – mais precisamente em 1967 – Strachey (2000) identificou 2 tipos de polimorfismo: paramétrico e ad-hoc. Mais tarde, em 1985, Luca Cardelli & Peter Wegner (1985) aprofundaram o estudo sobre o tema e propuseram uma nova visão, em que 4 tipos são claramente definidos. Figura 10 Formas de polimorfismo. O polimorfismo universal pode ser considerado o polimorfismo verdadeiro, enquanto o polimorfismo ad-hoc normalmente é considerado polimorfismo aparente ou de aparência. O polimorfismo ad-hoc pode, ainda, ser subdividido em 2 outros tipos: de sobrecarga e de coerção. Sobrecarga foi um dos assuntos abordados neste livro. Faz sentido entender sobrecarga como uma forma de polimorfismo, visto que um mesmo método pode assumir várias formas dependendo da quantidade e do tipo de parâmetros passados. Coerção permite que um argumento seja convertido para o tipo esperado por uma função, evitando assim um erro de tipo. Um exemplo bem simples seria o do operador de adição, definido para realizar a soma de 2 números reais. Se um dos operandos for inteiro, então ele será forçado a se tornar real. 105 O polimorfismo universal, como já mencionado, é considerado o verdadeiro polimorfismo. Ele é subdividido em 2 outras categorias: paramétrico e de inclusão. O polimorfismo paramétrico é aquele que a partir de uma única definição de um método ou de atributo pode trabalhar de forma genérica com qualquer tipo. O polimorfismo de inclusão está associado com herança. Toda classe derivada (ou subclasse) pode ser usada no contexto da classe base (ou superclasse). Esses tipos de polimorfismo serão abordados com mais detalhes nos itens a seguir. 7.3 Polimorfismo paramétrico Um gabarito, na indústria de confecção, é um molde que pode ser aplicado a qualquer tipo de tecido. Imagine a dificuldade que seria ter que criar moldes semelhantes para tecidos diferentes. O gabarito é para isso mesmo. Serve como molde. Está associado ao conceito de generalização. Em orientação a objetos também é possível explorar esse conceito de forma que o produto (software) tenha qualidade e possa ser produzido mais rapidamente. Imagine que você já tenha uma classe vetor de inteiros. Agora aparece a necessidade de se criar uma classe vetor de floats. Modificar a classe já existente não é uma boa solução, pois você pode ter a necessidade de utilizá-la em um futuro próximo. Uma solução seria copiá-la para outro arquivo, atribuir-lhe um novo nome e modificar-lhe o tipo de dado, mas eventuais ajustes no código original implicarão em ajustes no novo código e nas respectivas documentações. E se, além do tipo float, você tivesse que utilizar a classe vetor para aceitar também doubles? Os riscos de o código ser modificado em alguma das classes e não o ser em outra(s) aumenta. O desejável é que haja uma espécie de molde, em que o tipo de dado é definido como genérico e pode ser modificado conforme o interesse do programador. Assim, as manutenções são feitas em um único código. 7.3.1 Templates em C++ Nada melhor que alguns exemplos para entender o conceito de templates em C++. void troca(int & a, int & b) { int temp(a); a = b; b = temp; 106 } No exemplo anterior, o procedimento troca recebe dois parâmetros inteiros e faz com que os valores sejam trocados, ou seja, o que estava em a vai para b e o que estava em b vai para a. Simples! E se desejarmos algo parecido para a troca de valores de duas variáveis float? Uma solução seria criar outro procedimento, mas já vimos que isso não é vantajoso. Teríamos o mesmo código repetido apenas por necessitar modificar o tipo de dados dos parâmetros? Isso não parece muito inteligente! Precisamos de algo que generalize a utilização do código apresentado (e de outros também!). template class<T> void troca(T & a, T & b) { T temp(a); a = b; b = temp; } Em C++, felizmente, temos os templates! Eles permitem que o código fique geral para tipos diferentes de dados, como o apresentado no exemplo anterior. Em todos os lugares onde encontrávamos int, fizemos uma substituição por T e colocamos uma declaração antes da função, informando que o T será o tipo genérico. O nome T foi escolhido aleatoriamente (na verdade é em homenagem a template). Assim, no código apresentado, a, b e temp são todas variáveis do tipo T. E como utilizar isso? Veja o código exemplo a seguir. int x1, x2; x1 = 10; x2 = 20; troca(x1,x2); // ß primeira chamada float y1, y2; y1 = 10.2; y2 = 33.3; troca(y1,y2); // ß segunda chamada … O compilador sabe qual procedimento chamar pelo fato de saber os tipos das variáveis. A primeira chamada é direcionada para o procedimento troca cujos parâmetros são inteiros. Já na segunda chamada, o procedimento acionado é o troca com floats. Tais procedimentos são criados automaticamente pelo compilador. O programador não precisa fazer essa tarefa repetitiva (e mecânica)! 107 Outro exemplo poderia ser o de acesso a uma posição específica de um vetor. O método at, da classe meu_vetor, indica a posição em que está o valor a ser utilizado. O importante aqui é que o p é um ponteiro para T, em que T é genérico! template <class T> class meu_vetor { T *p; … T & at(unsigned int indice) { return p[indice]; } … }; No programa principal teríamos: meu_vetor <int> v; O tipo que está dentro dos limites < > é que será utilizado para fazer a alocação de memória. No construtor, por exemplo, você faria a alocação new T[x], em que x seria o tamanho do vetor. Note que, no exemplo anterior, o método at está dentro da classe. Se ele estivesse fora, você teria que especificar a qual classe pertence. Bom, isso você já sabe! O que você provavelmente não sabe é que se estivesse fora, e se você estivesse utilizando template, então deveria colocar a declaração de template antes do método! Na verdade, o template deve ser declarado antes de cada método. Com o uso de templates passa a ser possível criar objetos da classe meu_vetor do tipo inteiro, float, double, etc. Muito simples! 108 7.3.2 Generics em Java Generics em Java tem o mesmo objetivo que template em C++. Vamos criar uma classe simples que apenas armazena um valor. public class Obj { private Integer i; public void add(Integer i){ this.i = i; } public Integer get(){ return this.i; } } A classe em questão armazena um valor inteiro. Para utilizá-la podemos, no programa principal, fazer chamadas para add e get, como no exemplo seguinte. public class NewMain { public static void main(String[] args) { Obj integerObj = new Obj(); integerObj.add(10); Integer OutroInteger = integerObj.get(); System.out.println(OutroInteger); } } Mas é fácil notar que tal programa não conseguiria trabalhar com Strings. Para trabalhar com Strings ou qualquer outro tipo precisaríamos que Obj armazenasse um tipo de dado genérico, conforme o exemplo a seguir. public class Obj<T>{ private T t; // T é o tipo do atributo public void add(T t){ this.t = t; } public T get(){ return this.t; } } 109 O trecho de código a seguir apresenta o uso da nova classe Obj. Agora ela aceita tanto Strings como inteiros. Obj<String> ObjInt = new Obj<Integer>(); ObjInt.add(10); String OutroObjInt = (Integer) ObjInt.get(); System.out.println(OutroObjInt); Obj<String> cadeiaCaracteres = new Obj<String>(); cadeiaCaracteres.add(“Teste“); String outraCadeia = (String) cadeiaCaracteres.get(); System.out.println(outraCadeia); 7.4 Polimorfismo de inclusão Como já mencionado, polimorfismo é a capacidade de assumir várias formas. Ele permite que escrevamos programas mais genéricos. Uma das formas vistas nesta unidade é o paramétrico, em que uma classe é projetada independentemente do tipo que seus atributos irão ter. Outra forma é o polimorfismo de inclusão, em que um objeto de uma classe mais genérica (base ou superclasse) assume o papel de um objeto de uma classe mais específica (subclasse ou classe derivada). Um exemplo é quando um objeto A de animal assume o papel de cachorro (cachorro é um animal). 7.4.1 Polimorfismo de inclusão em C++ Vamos supor que você seja gerente de uma equipe em uma fábrica de software. Você, junto com a equipe de projeto de software, estabeleceu as características gerais da classe funcionário_empresa. A classe deve ter a seguinte forma: classe funcionario_empresa { private: 110 char primeiro_nome[20]; char sobrenome[20]; int codigo_funcao; int codigo_departamento; float salario_base; funcionario_empresa(char * , char *); ~funcionario_empresa(); void atribui_salario(float); void promocao(); void imprime_dados(); void imprime_vencimentos(); public: }; Um gerente é um funcionário de empresa, assim como um funcionário do departamento de compras ou do departamento de finanças. Mas tem, pelo menos, uma coisa que o diferencia: um gerente tem subordinados. Assim, a classe gerente é uma subclasse de funcionário_empresa, que tem uma particularidade: tem subordinados. A relação é-um, como vista anteriormente, determina herança. Também poderíamos ter uma classe de vendedores. Eles também são funcionários da empresa (novamente a relação é-um), mas normalmente os vendedores têm um salário fixo e um salário variável (comissão). Os programadores que compõem sua equipe desejam criar um programa genérico que trabalhe com todos os funcionários da empresa. Eles sabem que a forma de imprimir os dados do gerente é diferente da forma de imprimir os dados do funcionário do departamento de finanças. Mesmo assim entendem que ficaria mais fácil para alguém entender o código se simplesmente colocassem o método imprimir_dados() disponível e o programa se encarregasse de acionar o método correto de cada classe. Eles também notaram que a forma de calcular e apresentar os dados sobre vencimentos de um gerente e de um vendedor são diferentes. Mesmo assim, a funcionalidade deveria ser a mesma: imprimir_vencimentos(). O programa se encarregaria de acionar o método correto. Bom, em C++ isso é feito utilizando-se a declaração virtual antes do método. A classe funcionario_empresa ficaria: classe funcionario_empresa { private: char primeiro_nome[20]; char sobrenome[20]; int codigo_funcao; int codigo_departamento; float salario_base; 111 public: funcionario_empresa(char * , char *); ~funcionario_empresa(); void atribui_salario(float); void promocao(); virtual void imprime_dados(); virtual void imprime_vencimentos(); }; Cada classe derivada da classe funcionario_empresa teria seu método imprime_dados() e imprime_vencimentos(). Poderia criar-se um funcionário genérico no programa principal que pudesse assumir o papel tanto de gerente como de vendedor: funcionario_empresa *genérico; A declaração deve ser feita utilizando-se ponteiro. Nesse caso, tem-se um funcionário genérico. Ainda não sabemos se ele assumirá o papel de gerente, funcionário do departamento de compras ou mesmo o papel de um vendedor. Após o usuário informar o papel que deseja assumir, é possível transformar o genérico em específico. Suponha que o usuário tenha escolhido o papel de vendedor: genérico = new vendedor(); Vendedor é uma classe derivada da classe funcionario_empresa e tem os métodos imprime_dados() e imprime_vencimentos(). Agora sim, se o método imprime_dados() for acionado (ver exemplo a seguir), o programa saberá que o referido método pertence à classe vendedor: genericoimprime_dados(); Fica claro, então, que a declaração de um objeto genérico pode implicar em uma utilização específica posteriormente. O programador em questão não deve se esquecer de que o comando new aloca memória do computador e ela deverá ser liberada (por meio do comando delete) tão logo não seja mais utilizada. Com polimorfismo é possível criar um vetor de elementos heterogêneos, por exemplo, um vetor de animais mamíferos. Vamos supor que queiramos construir tal vetor, mas que o usuário vai determinar qual será a posição do cachorro, do 112 gato, do cavalo, etc. Depois, o usuário vai escolher uma posição do vetor e vai acionar o método emitir_som() do animal daquela posição. Veja um exemplo resumido da classe mamífero e da classe gato (derivada de mamífero): classe mamífero { private: ... public: virtual emitir_som(); }; A classe mamífero foi simplificada. O que interessa é apenas a existência de um método comum às classes derivadas (emitir_som). classe gato::public mamífero // herança! { private: ... public: emitir_som(); }; O exemplo anterior apenas representa uma das possibilidades de classe derivada (poderíamos ter cavalo, cachorro, leão, tigre, entre outros). Um vetor de animais mamíferos poderia, então, ser criado da seguinte forma: mamífero *animal[10]; Ou seja, teríamos dez animais mamíferos no vetor animal. Se o usuário houvesse decidido que, na primeira posição, seria colocado um cachorro, então teríamos: animal[0] = new cachorro(“totó“,1, 5); Os dados entre parênteses seriam: nome, meses de idade e peso. Posteriormente, o método emitir_som() do animal[0] poderia ser acionado da seguinte forma: animal[0]emitir_som(); 113 Não é difícil imaginar que o seguinte trecho de código do programa poderia emitir sons distintos, como au au au, miau, ihhhhh, etc.: for (i=0;i<10;i++) animal[i]emitir_som(); É importante notar que no polimorfismo de inclusão não é possível saber a qual classe pertence o método acionado antes que o programa seja executado. Isso porque tudo ocorre dinamicamente (em tempo de execução). 7.4.2 Polimorfismo de inclusão em Java E como seria o polimorfismo de inclusão em Java, já que ele acontece em C++, principalmente pelo fato de existirem ponteiros nessa linguagem? Em Java o processo é muito semelhante. Vamos utilizar como exemplo a classe animal e as classes cachorro, gato e pato que são derivadas (subclasses) de animal. A classe animal tem um construtor que inicializa o tipo de animal criado, um método exibir que imprime o tipo do animal e o método som. Note que o método som é genérico. public class Animal { private String tipo; public Animal(String tipo1){ tipo = new String(tipo1); } public void exibir(){ System.out.println(“Eu sou um“ + tipo);} //Método a ser implementado nas subclasses. public void som(){ System.out.println(“som de animal“); } } public class Cachorro extends Animal { private String nome, raca; public Cachorro(String nome1){ super(“cachorro“); nome = nome1; raca = “Boxer”; } public Cachorro(String nome1, String raca1){ super(“cachorro“); nome = nome1; raca = raca1; 114 } public void som() { System.out.println(“Auau“);} } O método som de Cachorro imprime Auau, enquanto o do Gato imprime miau e o do Pato quaquá. As classes de Gato e Pato não serão repetidas aqui por questões de espaço, mas são muito similares à Cachorro. É importante observar que a classe Cachorro (assim como a Gato e a Pato) são subclasses de Animal e, por isso, utilizam a palavra extends em Java. Para testar o polimorfismo, é preciso criar um programa principal, que deve estar dentro da classe TestePolimorfismo. Tal classe deve estar no arquivo TestePolimorfismo.java (isso você já sabe, não?!). No programa principal criamos um vetor de animais e o chamamos de bichos. O interessante dessa construção é que cada elemento do vetor pode assumir o papel de um animal. Podemos, então, ter cachorro, gato e pato fazendo parte de um mesmo vetor. O código a seguir cria um vetor com os elementos cachorro, gato e pato e faz com que um animal de estimação assuma o papel de um dos animais conforme sorteio (feito por Math.random()). O sorteio é feito 5 vezes. Não é possível, a partir da inspeção do código, informar, por exemplo, a qual classe pertence o método som executado por estimacao. Isso é resolvido em tempo de execução. public class TestePolimorfismo { public static void main(String[] args){ Animal[] bichos = { new Cachorro(“Rex“,“Labrador“), new Gato(“Mimi“), new Pato(“Gertrudes“) }; Animal estimacao; for (int i=0; i<5; i++){ int indice = (int) (bichos.length * Math.random() - 0.001); estimacao = bichos[indice]; System.out.println(“\n Escolha: “); estimacao.exibir(); estimacao.som(); } } 115 7.5 Considerações finais Um código genérico gera menos erros. Copiar e colar um código simplesmente para se alterar o tipo (por exemplo) com o qual ele trabalha não faz muito sentido. Além disso, se houver a necessidade de se realizar uma alteração no código, isso terá que ser feito em todas as versões (int, float, double, long int, etc.). A orientação a objetos propõe soluções baseadas em polimorfismo. Alguns poucos exemplos de polimorfismo foram apresentados. É importante que o leitor aprofunde os conhecimentos sobre o assunto, fazendo uma leitura mais detalhada sobre polimorfismo em Deitel & Deitel (2009a, 2009b). 116 Unidade 8 Classes abstratas 8.1 Primeiras palavras Temos uma ideia do que seja abstrato. Uma forma simples de se definir seria “algo intangível, que não pode ser pego”. Uma ideia, por exemplo, é algo intangível. Mas o que seria classe abstrata em programação? Nesta unidade tal conceito será explorado e alguns exemplos em C++ e em Java serão apresentados. 8.2 Problematizando o tema Pode acontecer que você, como gerente, queira forçar sua equipe a criar métodos específicos para as classes derivadas. Você pode fazer isso de duas formas: 1.Inspeção em todos os códigos; 2.Deixar para o compilador a tarefa de verificar. É claro que você vai preferir a segunda opção! É mais fácil, rápida e segura. Assim, você pode criar uma classe abstrata, que não pode ser utilizada diretamente (daí vem a justificação do uso da palavra abstrato), mas que irá servir como referência para a criação de outras classes. Imagine que você deseje que todas as figuras geométricas em 2 dimensões tenham os métodos calcArea( ), calcPerimetro( ) e desenhar( ). É preciso sinalizar para o compilador que ele deverá controlar o trabalho de seus programadores. O compilador, na verdade deverá: 1.Impedir que alguém crie um objeto da classe abstrata; 2.Acusar erro se algum dos métodos não tiver sido criado em uma classe derivada que será utilizada. Se uma classe derivada não implementar todos os métodos da classe base (ou superclasse), ela também será considerada classe abstrata (por herança) e não poderá, portanto, ser utilizada. 8.3 Definição de classes abstratas Do ponto de vista de programação, uma classe que contenha funções virtuais puras (C++) ou funções abstratas (Java) é chamada de classe abstrata. Essas classes são chamadas assim porque não permitem instanciações (objetos). Na verdade, existem apenas com o propósito de estabelecer parâmetros para outras classes dela derivadas. Lembrando: as classes derivadas devem conter as implementações das funções virtuais puras ou abstratas para que se tornem classes 119 concretas. Caso tais implementações não estejam presentes em tais classes, elas também serão consideradas abstratas. 8.4 Classes abstratas em Java e em C++ Você deve estar achando que classes abstratas e interfaces (de Java) são conceitos parecidos e que podem ser usados com objetivos semelhantes. Cuidado! Uma classe pode estender uma única classe (que pode ser abstrata ou não), mas pode implementar várias interfaces. Além disso, interfaces Java não permitem declaração de atributos, enquanto classes abstratas permitem. 8.4.1 Exemplos em Java Em Java, utiliza-se a palavra chave abstract para definir uma classe abstrata: public abstract class Eletrodomestico { private boolean ligado; private int voltagem; // métodos abstratos public abstract void ligar(); public abstract void desligar(); // construtor public Eletrodomestico(boolean l, int volt) { this.ligado = l; this.voltagem = volt; } // métodos concretos public void setVoltagem(int voltagem) { this.voltagem = voltagem; } public int getVoltagem() { return this.voltagem; } public void setLigado(boolean ligado) { this.ligado = ligado; } public boolean isLigado() { return ligado; 120 } } A classe eletrodoméstico, no exemplo anterior, cria um “modelo” que deve ser compartilhado entre outras classes que herdam as características de eletrodomésticos. Nota-se que ter uma voltagem e estar ligado ou não são características de aparelhos elétricos. Assim, pode-se criar uma classe televisão (televisão é um eletrodoméstico) que implemente os métodos abstratos da superclasse. Por questões de espaço serão apresentados apenas alguns dos principais métodos da classe televisão: public class TV extends Eletrodomestico { private int tamanho; private int canal; private int volume; public TV(int tam, int volt) { super (false, volt); this.tamanho = tam; this.canal = 0; this.volume = 0; } // métodos abstratos implementados public void desligar() { super.setLigado(false); setCanal(0); setVolume(0); } public void ligar() { super.setLigado(true); setCanal(3); setVolume(25); } // outros métodos da classe ... } 121 8.4.2 Exemplos em C++ Em C++ a sinalização de que a classe é abstrata ocorre quando igualamos a zero um método virtual: virtual nome_método() = 0; Assim, quando alguém cria um método xxx() virtual e o iguala a zero, na verdade o que está sendo desejado é que a implementação de tal método ocorra em cada uma das classes derivadas daquela classe à qual xxx pertence. Um método puramente virtual (são assim chamados os métodos igualados a zero) não pode ser instanciado em qualquer parte do programa (você só poderá utilizar ponteiros para a classe). Ou seja, se você criou um método virtual xxx() = 0; na classe Animal, não poderá fazer a seguinte declaração: Animal x; Toda classe derivada de Animal deverá implementar todos os métodos virtuais puros dela, caso contrário, a classe derivada também será considerada abstrata pelo compilador e não autorizará a criação de um objeto daquela classe. Como exemplo, podemos relembrar a classe Animal da unidade anterior. Se a classe fosse reescrita para C++ e o método som( ) fosse declarado como virtual puro, então em todas as classes derivadas de Animal (cachorro, gato, pato, cavalo, etc.) deveria haver uma implementação específica de som(). 8.5 Considerações finais Classes abstratas trazem segurança para quem planeja uma classe base e deseja que todos a utilizem como referência. Como ela não pode ser instanciada, o programador será obrigado a criar classes com implementações corretas de todos os métodos virtuais puros (C++) ou abstratos. Ou seja, com classes abstratas, o trabalho de verificar se os membros da equipe de desenvolvimento implementaram os métodos planejados fica a cargo do compilador. Caso alguma classe não tenha sido contemplada com todos os métodos planejados, ela será automaticamente convertida para abstrata e o compilador impedirá que um objeto daquela classe seja criado. O gerente do projeto saberá (sem ter que investigar o código), então, que o programador responsável pela tarefa não a realizou. 122 Apêndice A Na internet existem vários tutoriais de como instalar os ambientes de desenvolvimento (ou de programação). Aqui serão vistas maneiras simples de como utilizá-los para construir programas em C++ e em Java. No apêndice B serão vistos exemplos de programas voltados para jogos envolvendo conceitos de orientação a objetos. Um dos ambientes mais utilizados em desenvolvimento de programas é o NetBeans (Figura 11). Ele permite construções de programas tanto em C++ como em Java, o que é ideal para quem quer aprender orientação a objetos e testar os conceitos nas duas linguagens. A tela inicial do NetBeans pode ser vista na Figura 11. Figura 11 Tela inicial do NetBeans. Para iniciar um novo aplicativo, o programador deve clicar no menu Arquivo e escolher a opção “Novo Projeto”. Então, uma janela de diálogo para solicitar a informação sobre qual linguagem deseja utilizar se abrirá (Figura 12). O programador poderá notar que as opções Java e C/C++ estão presentes. A escolha de uma opção na parte esquerda do diálogo implica na apresentação de tipos de projetos disponíveis na parte direita do diálogo. 123 Figura 12 Caixa de diálogo do novo projeto. Quando escolher a linguagem Java, escolha o tipo de projeto “Aplicação Java”; quando escolher C/C++, escolha o tipo de projeto “Aplicativo de C/C++”. Figura 13 Dados do projeto. Em ambos os casos, clique no botão “Próximo” para prosseguir com as defi- 124 nições do programa. Uma nova caixa de diálogo aparecerá solicitando informações específicas sobre o projeto, tais como nome e localização dos arquivos (Figura 13). Defina o nome que achar apropriado ou mantenha as sugestões do ambiente. Não é necessário definir como projeto principal, caso seu programa tenha vários arquivos. Finalizada a etapa de preparação do ambiente, vem a parte de elaboração dos códigos. Na parte esquerda do ambiente aparecerá uma lista de aplicativos, entre os quais aquele que acabou de ser criado (em nosso exemplo, aplicativo_4). Para criar um arquivo de cabeçalho em C++ ou um arquivo do programa principal é preciso clicar com o botão direito do mouse sobre o tipo de arquivo desejado. No exemplo da Figura 14, o programador está escolhendo a opção de criar um programa principal em C++. Figura 14 Criação de um programa em C++. Uma nova caixa de diálogo aparecerá para que o programador defina o nome do arquivo que está sendo criado. Em seguida, o NetBeans cria um esqueleto do programa, inclusive com comentários sobre a autoria e a data do programa (Figura 15). Em ambientes como o NetBeans, existem recursos que são muito importantes e que facilitam a vida do programador. Um deles é o aviso de que algo não está correto. No exemplo da Figura 15 pode-se ver uma marca (x) na linha 8 indicando que há um erro. Outro recurso interessante é o code completion (Figura 17), que apresenta sugestões de complemento do código particularmente interessantes para a escolha de um método de uma classe. 125 Figura 15 Ambiente de desenvolvimento do NetBeans. Uma vez escrito o código, basta compilar e executar. Procure as opções em “construir” e “executar” do menu. Para se construir uma aplicação Java, os passos são semelhantes. Deve-se ter cuidado especial com a caixa de diálogo sobre os dados do projeto (Figura 16). Figura 16 Dados do projeto Java. 126 Normalmente ela vem com a opção “criar classe principal” ativada. Desative-a se você estiver criando apenas a classe. Essa opção serve para quando se pretende criar o programa principal. Figura 17 Exemplo de Code Completion em Java. 127 Apêndice B Existem alguns exemplos de problemas que permitem entender melhor os conceitos de orientação a objetos de uma forma bem simples. Aqui serão abordados três exemplos em C++: dois de classes simples (rádio e jogo da forca) e um terceiro de sobrecarga de operadores (valor_monetário). Classe simples Um exemplo de classe simples seria a classe rádio. Um rádio é um equipamento eletrônico que permite receber conteúdos sonoros transmitidos a partir de uma fonte (a estação de rádio). O aparelho permite que possamos escolher a fonte por meio de um botão de sintonia. Além disso, o rádio, como qualquer outro equipamento eletrônico, pode estar ligado ou desligado. Quando desligado não é possível ouvir qualquer transmissão; quando ligado pode-se ouvir o que está sendo transmitido na estação de rádio sintonizada. Também se pode escolher o volume em que se deseja ouvir. O código em C++ a seguir apresenta uma forma de se implementar a classe rádio. A classe rádio, a seguir, tem os seguintes atributos: • Volume; • Memória (para armazenar as estações preferidas, tanto em AM como em FM). É uma matriz com 2 linhas e 5 colunas. A primeira linha armazena as estações AM e a segunda linha as estações FM. São 5 estações por linha; • Indicador de modo AM ou FM; • Indicador de ligado/desligado; • Estações. Para simplificar o problema, não se contemplou a questão da bateria (nível de carga, troca de bateria, uso de energia elétrica, etc.). Os atributos são private, ou seja, não podem ser acessados diretamente. Assim, para alterar os atributos foram criados “botões” de rádio que realizam as funções desejadas. Por exemplo, para o atributo volume existem dois botões (aumenta e diminui); para o modo AM/FM existe o botão muda_tipo_estação e assim por diante. 129 class radio { private: int volume; // volume (0 a 10) int sintonia_AM; // estação AM atual da memória int sintonia_FM; // estação FM atual da memória int memoria[2][5]; bool estacao_FM; // memórias de estações preferidas bool ligado; // indica se está no modo AM ou FM // indica se está ligado ou não float estacoes[2][20]; // total de estações AM e FM public: radio(); // construtor float busca_estacao(); // “botão“ de buscar estação void grava_memoria(int); // “botão“ de gravar memória void sintoniza_memoria(int);// “botão“ de sintonizar // estação gravada em // memória void muda_tipo_estacao(); // troca de AM para FM ou // de FM para AM void liga_desliga(); // “botão“ de ligar e desligar void aumenta_volume(); // “botão“ de aumentar // volume void diminui_volume(); // “botão“ de diminuir volume void display(); // display do rádio }; O construtor é um método que deve inicializar os atributos de forma que eles já tenham valores válidos. É conveniente inicializar o rádio desligado e já colocar as estações possíveis. radio::radio() { estacao_FM = true; sintonia_AM = 0; sintonia_FM = 0; ligado = false; estacoes[0][0] = 650; estacoes[0][1] = 685; estacoes[0][2] = 710; 130 estacoes[0][3] = 730; estacoes[0][4] = 745; estacoes[0][5] = 770; estacoes[0][6] = 810; estacoes[0][7] = 830; estacoes[0][8] = 850; estacoes[0][9] = 870; estacoes[1][0] = 88.1; estacoes[1][1] = 90.2; estacoes[1][2] = 91.6; estacoes[1][3] = 92.1; estacoes[1][4] = 94.0; estacoes[1][5] = 94.7; estacoes[1][6] = 95.1; estacoes[1][7] = 98.3; estacoes[1][8] = 99.1; estacoes[1][9] = 99.8; volume = 5; for (int i=0;i<5;i++) { memoria[0][i] = 0; memoria[1][i] = 0; } } O botão de buscar estação é responsável por sintonizar uma nova estação. Ou seja, se atualmente o rádio está sintonizado na estação 90.2 FM, então o busca estação vai para a próxima estação, ou seja, 91.6. float radio::busca_estacao() { if (ligado) { if (estacao_FM) { sintonia_FM++; if (sintonia_FM == 10) sintonia_FM = 0; } else { sintonia_AM++; if (sintonia_AM == 10) 131 sintonia_AM = 0; } } } O botão de gravar na memória permite que algumas estações preferidas sejam armazenadas para facilitar a troca de estações. O usuário deve indicar qual botão deseja associar à estação (são 5 botões para FM e 5 para AM). Se o botão já armazenava uma estação preferida, então ela será substituída pela nova estação. void radio::grava_memoria(int m) { if (ligado) { if (estacao_FM) { if (m >= 0 && m <= 4) memoria[1][m] = sintonia_FM; } else if (m >= 0 && m <= 4) memoria[0][m] = sintonia_AM; } } Associado ao botão de gravar memória está o botão de sintonizar uma estação preferida. O usuário indica o botão (pressionando) e o rádio faz a sintonia. Claro que isso só acontece se o rádio estiver ligado! void radio::sintoniza_memoria(int m) { if (ligado) { if (estacao_FM) { if (m >= 0 && m <= 4) sintonia_FM = memoria[1][m]; } else if (m >= 0 && m <= 4) sintonia_AM = memoria[0][m]; } } Há também os botões de mudança de estação (que troca de AM para FM e vice-versa), o botão de liga/desliga, e os de controle de volume, conforme mostram os códigos apresentados a seguir. 132 void radio::muda_tipo_estacao() { if (ligado) estacao_FM = !estacao_FM; } void radio::liga_desliga() { ligado = !ligado; } void radio::aumenta_volume() { if (ligado) volume++; if (volume > 25) volume = 25; } void radio::diminui_volume() { if (ligado) volume--; if (volume < 0) volume = 0; } O método display servirá para apresentar as informações correntes sobre o rádio (se está ligado, em qual estação, qual volume, etc.); se estiver desligado aparecerá uma informação indicando apenas que está desligado. void radio::display() { if (ligado) { cout << “volume = “ << volume << endl; if (estacao_FM) cout << “FM = “ << estacoes[1][sintonia_FM] << endl; else cout << “AM = “ << estacoes[0][sintonia_AM] << endl; } else cout << “radio desligado “ << endl; } O programa principal deverá contar com uma função menu que disponibilizará as funcionalidades do rádio. Poderia ser parte da classe, como se fosse a parte externa do rádio com seus botões. int menu() { int opcao; opcao = 0; 133 while (opcao <= 0 || opcao > 8) { cout << “ 1. Botao liga/desliga“ << endl; cout << “ 2. Botao AM/FM “ << endl; cout << “ 3. Botao busca estacao “ << endl; cout << “ 4. Botao grava estacao “ << endl; cout << “ 5. Botao memoria “ << endl; cout << “ 6. Botao aumenta volume “ << endl; cout << “ 7. Botao diminui volume “ << endl; cout << “ 8. finalizar “ << endl; cin >> opcao; } return opcao; } Por fim, o programa principal. Nele declaramos um objeto X e, por meio das escolhas do usuário, fazemos o acionamento dos métodos de X. int main(int argc, char *argv[]) { radio X; int opcao; opcao = menu(); while (opcao != 8) { if (opcao == 1) X.liga_desliga(); if (opcao == 2) X.muda_tipo_estacao(); if (opcao == 3) X.busca_estacao(); if (opcao == 4) { int mem = 10; while (mem < 0 || mem > 4) { cout << “ entre com o numero do botao “ ; cin >> mem; } X.grava_memoria(mem); } if (opcao == 5) { int mem = 10; while (mem < 0 || mem > 4) { cout << “ entre com o numero do botao “ ; cin >> mem; } X.sintoniza_memoria(mem); 134 } if (opcao == 6) X.aumenta_volume(); if (opcao == 7) X.diminui_volume(); X.display(); opcao = menu(); } system(“PAUSE“); return EXIT_SUCCESS; } Outro exemplo interessante é o jogo da forca. Esse jogo é um jogo de adivinhação, em que o jogador joga contra o computador. Ao jogador são dadas algumas chances de escolher letras que permitam formar a palavra escolhida (sorteada) pelo computador. A palavra é sorteada no início do programa e o computador deve exibir o número de letras da palavra e uma dica sobre a mesma. O jogador vence se conseguir decifrar a palavra; perde se for enforcado. Cada letra errada implica em uma parte do corpo acrescida. Ou seja, o jogador tem a chance de cometer 6 erros (duas pernas, dois braços, corpo e cabeça). As letras erradas são apresentadas em um espaço separado; as corretas são mostradas em suas posições corretas sobre o tracejado (que indica o tamanho da palavra). Figura 18 Jogo da forca. O programador, assim como no exemplo anterior, deve se preocupar com as funcionalidades do jogo, bem como seus atributos. O código a seguir (também em C++) apresenta uma implementação do jogo da forca.3 Alguns comandos utilizados no desenvolvimento do jogo podem não ser reconhecidos em algumas plataformas de desenvolvimento. Exemplo: utilizando o CygWin como compilador do NetBeans IDE 6.5.1 o comando “printf(“\033[2J”);” limpa a tela, e para exercer essa mesma função no Dev-C++ o comando utilizado deve ser o “system(“CLS”);”. 3 O exemplo do jogo da forca foi elaborado por alunos de graduação da UFSCar. Os nomes dos autores foram preservados no código. 135 /* * JOGO DA FORCA * Autores: Alan Cesar Laine Bruno Fernando Rodrigues Guilherme Cuppi Jerônimo Guilherme Rigo Recio */ //Inclusão das bibliotecas #include <stdlib.h> #include <iostream.h> //para atribuir e comparar strings #include <string.h> //para utlizar as funções srand() e rand() #include <time.h> //para utilizar a funcão isdigit() #include <ctype.h> using namespace std; //Declaração da classe jogo_da_forca class jogo_da_forca { private: //Matriz das palavras possíveis do jogo char matriz[90][40]; //Vetor para armazenar a dica do jogo char dica[25]; //Vetor para armazenar as letras corretas char letras_corretas[30]; //Vetor para armazenar as letras erradas char letras_erradas[7]; //palavra digitada até o momento char palavra_digitada[40]; int numero_erros, posicao_acertos, public: posicao_erros, sorteio; //Construtor com parâmetro jogo_da_forca(int); 136 //Método para imprimir o jogo void imprimir(); //Método para verificar se a letra digitada //consta na palavra sorteada void letra_correta(char); //Método para verificar se a letra digitada //não consta na palavra sorteada void letra_errada(char); //Método para montar a palavra digitada //até o momento void monta_palavra(); //Método para checar se o jogador acertou a //palavra sorteada bool ganhar(); //Método para checar se o jogador atingiu o //limite de erros bool perder(); //Método para verificar se a letra //já foi digitada bool verifica_letra(char); //Método para controlar o jogo int jogar(); }; //Construtor jogo_da_forca::jogo_da_forca(int nivel) { int i; //Contador //Variável para armazenar o número sorteado //na função srand() int sorteio_parcial; /*Atribuição das 30 palavras do nível fácil, ordenadas de 6 em 6 nas respectivas categorias, “Animal“, “Novela ou Mini-Série“, “Fruta“, “Filme“ e “País“ */ strcpy(matriz[0], “ARARA“); strcpy(matriz[1], “MACACO“); strcpy(matriz[2], “LEAO“); strcpy(matriz[3], “ELEFANTE“); 137 strcpy(matriz[4], “COBRA“); strcpy(matriz[5], “GIRAFA“); strcpy(matriz[6], “VIVER A VIDA“); strcpy(matriz[7], “CARAS E BOCAS“); strcpy(matriz[8], “CAMA DE GATO“); strcpy(matriz[9], “CAMINHO DAS INDIAS“); strcpy(matriz[10], “PARAISO“); strcpy(matriz[11], “NEGOCIO DA CHINA“); strcpy(matriz[12], “BANANA“); strcpy(matriz[13], “ABACATE“); strcpy(matriz[14], “MAMAO“); strcpy(matriz[15], “ABACAXI“); strcpy(matriz[16], “LARANJA“); strcpy(matriz[17], “UVA“); strcpy(matriz[18], “A ERA DO GELO TRES“); strcpy(matriz[19], “BRUNO“); strcpy(matriz[20], “HEROIS“); strcpy(matriz[21], “JOGOS MORTAIS SEIS“); strcpy(matriz[22], “NOIVAS EM GUERRA“); strcpy(matriz[23], “SE EU FOSSE VOCE DOIS“); strcpy(matriz[24], “BRASIL“); strcpy(matriz[25], “MEXICO“); strcpy(matriz[26], “ARGENTINA“); strcpy(matriz[27], “CHILE“); strcpy(matriz[28], “ESTADOS UNIDOS“); strcpy(matriz[29], “URUGUAI“); /*Atribuição das 30 palavras do nível intermediário, ordenadas de 6 em 6 nas respectivas categorias, “Animal“, “Novela ou MiniSérie“, “Fruta“, “Filme“ e “País“ */ strcpy(matriz[30], “BICHO-DA-SEDA“); strcpy(matriz[31], “CARRAPATO“); strcpy(matriz[32], “CAMALEAO“); strcpy(matriz[33], “CROCODILO“); strcpy(matriz[34], “CARPA“); strcpy(matriz[35], “CHIMPANZE“); strcpy(matriz[36], “A CASA DAS SETE MULHERES“); strcpy(matriz[37], “MULHERES APAIXONADAS“); strcpy(matriz[38], “AGORA E QUE SAO ELAS“); strcpy(matriz[39], “KUBANACAN“); 138 strcpy(matriz[40], “CHOCOLATE COM PIMENTA“); strcpy(matriz[41], “CELEBRIDADE“); strcpy(matriz[42], “DAMASCO“); strcpy(matriz[43], “FRUTA-DO-CONDE“); strcpy(matriz[44], “GUABIROBA“); strcpy(matriz[45], “KIWI“); strcpy(matriz[46], “MORANGO“); strcpy(matriz[47], “PITANGA“); strcpy(matriz[48], “MAIS VELOZES MAIS FURIOSOS“); strcpy(matriz[49], “AMERICAN PIE TRES“); strcpy(matriz[50], “AS PANTERAS“); strcpy(matriz[51], “AS TARTARUGAS NINJAS“); strcpy(matriz[52], “BAD BOYS DOIS“); strcpy(matriz[53], “BEM-VINDO A SELVA“); strcpy(matriz[54], “GUIANA FRANCESA“); strcpy(matriz[55], “CISJORDANIA“); strcpy(matriz[56], “AZERBAIJAO“); strcpy(matriz[57], “NIGERIA“); strcpy(matriz[58], “MARROCOS“); strcpy(matriz[59], “PAQUISTAO“); /*Atribuição das 30 palavras do nível difícil, ordenadas de 6 em 6 nas respectivas categorias, “Animal“, “Novela ou Mini-Série“, “Fruta“, “Filme“ e “País“ */ strcpy(matriz[60], “AGUIA CINZENTA“); strcpy(matriz[61], “ALBATROZ“); strcpy(matriz[62], “BUGIO PRETO“); strcpy(matriz[63], “GAROUPA“); strcpy(matriz[64], “GAVIAO-QUIRIQUIQUI“); strcpy(matriz[65], “QUERO-QUERO“); strcpy(matriz[66], “SARAMANDAIA“); strcpy(matriz[67], “ANJO MAU“); strcpy(matriz[68], “VEJO A LUA NO CEU“); strcpy(matriz[69], “O CASARAO“); strcpy(matriz[70], “ESCRAVA ISAURA“); strcpy(matriz[71], “O FEIJAO E O SONHO“); strcpy(matriz[72], “SAPOTI“); strcpy(matriz[73], “ARATICUM“); strcpy(matriz[74], “FRAMBOESA“); strcpy(matriz[75], “MANGABA“); strcpy(matriz[76], “TAMARINDO“); strcpy(matriz[77], “MARMELO“); 139 strcpy(matriz[78], “HARRY POTTER E O ENIGMA DO PRINCIPE“); strcpy(matriz[79], “A NOITE DOS MORTOS-BOBOS“); strcpy(matriz[80], “A FANTASTICA FABRICA DE CHOCOLATE“); strcpy(matriz[81], “O QUEBRA-NOZES E O REI DOS CAMUNDONGOS“); strcpy(matriz[82], “LADRAO QUE ROUBA LADRAO“); strcpy(matriz[83], “CONTOS DE NOVA YORK“); strcpy(matriz[84], “TRINIDAD E TOBAGO“); strcpy(matriz[85], “INDONESIA“); strcpy(matriz[86], “ESLOVENIA“); strcpy(matriz[87], “LETONIA“); strcpy(matriz[88], “GROENLANDIA“); strcpy(matriz[89], “REPUBLICA DEMOCRATICA DO CONGO“); /* Função utilizada para definir o ponto de partida para gerar o número aleatório */ srand(time(NULL)); // Atribuição do número sorteado(0 até 29) sorteio_parcial = rand() % 30; /* Comando para selecionar a palavra sorteada correspondente ao nível escolhido */ sorteio = sorteio_parcial + ((nivel - 1) * 30); // Inicialização do vetor letras_corretas for(i = 0; i < 30; i++) letras_corretas[i] = ‘ ‘; // Inicialização do vetor letras_erradas for(i = 0; i < 7; i++) letras_erradas[i] = ‘ ‘; // Inicialização do vetor palavra_digitada for (i = 0; i < 40; i++) palavra_digitada[i] = ‘ ‘; numero_erros = 0; posicao_erros = 0; 140 posicao_acertos = 0; // Atribuição da dica com relação a sorteio_parcial if(sorteio_parcial >= 0 && sorteio_parcial <= 5) strcpy(dica, “Animal“); else if(sorteio_parcial >= 6 && sorteio_parcial <= 11) strcpy(dica, “Novela ou Mini-Serie“); else if(sorteio_parcial >= 12 && sorteio_parcial <= 17) strcpy(dica, “Fruta“); else if(sorteio_parcial >= 18 && sorteio_parcial <= 23) strcpy(dica, “Filme“); else strcpy(dica, “Pais“); } void jogo_da_forca::imprimir() { int i, j; //Contadores // Comando condicional utilizado para imprimir // o jogo sendo acionado pelo número de erros // 0 indica que a forca está vazia // 1 indica que a forca já tem a cabeça // 2 indica cabeça e pescoço // etc. switch(numero_erros) { case 0: cout << “ _____ “; cout << “ cout << “| “ << “Dica: “ << dica << endl; |“ << endl; cout << “|“ << endl; cout << “|“ << endl; cout << “|“ << endl; cout << “|“ << endl; cout << “|“; break; case 1: cout << “ _____ “; cout << “ “ << “Dica: “ << dica << endl; 141 cout << “| |“; Letras Erradas: “; cout << “ for(i = 0; i < 1; i++) cout << letras_erradas[i] << ‘ ‘; cout << endl; cout << “| O“ << endl; cout << “|“ << endl; cout << “|“ << endl; cout << “|“ << endl; cout << “|“; break; // assim por diante… } //Impressão do formato do jogo for(i = 0; i < strlen(palavra_digitada); i++) if(palavra_digitada[i] == ‘ ‘) cout << “ “; else if(palavra_digitada[i] == ‘-’) cout << “- “; else if(palavra_digitada[i] == ‘_’) cout << “__ “; else cout << palavra_digitada[i] << “ “; cout << endl << endl; } void jogo_da_forca::letra_correta(char letra) { int j = 0; //Contador // Percorre a palavra sorteada para verificar se a // letra digitada é correta while(matriz[sorteio][j] != letra) j++; // Caso a letra seja correta, ela será atribuida ao // vetor letras_corretas 142 if(j < strlen(matriz[sorteio])) letras_corretas[posicao_acertos++] = letra; } void jogo_da_forca::letra_errada(char letra) { int j = 0; // Percorre a palavra sorteada para verificar se a // letra digitada é incorreta while(matriz[sorteio][j] != letra) j++; // Caso a letra seja incorreta, ela será atribuida // ao vetor letras_erradas if(j > strlen(matriz[sorteio])) { letras_erradas[posicao_erros++] = letra; numero_erros++; } } void jogo_da_forca::monta_palavra() { int i, j; //Contadores // Comando de repetição para atribuir os // caracteres ao vetor palavra_digitada for(i = 0; i < 40; i++) if(matriz[sorteio][i] == ‘ ‘) palavra_digitada[i] = ‘ ‘; else if(matriz[sorteio][i] == ‘-‘) palavra_digitada[i] = ‘-‘; else { j = 0; while(matriz[sorteio][i] != letras_corretas[j] && j < strlen(letras_corretas)) j++; 143 if(matriz[sorteio][i] == letras_corretas[j]) palavra_digitada[i] = letras_corretas[j]; else palavra_digitada[i] = ‘_‘; } } bool jogo_da_forca::ganhar() { // Faz a comparação entre a palavra_digitada // e a palavra sorteada if(strcmp(palavra_digitada, matriz[sorteio]) == 0) return true; else return false; } bool jogo_da_forca::perder() { // Verifica se o jogador atingiu o // limite de erros if(numero_erros == 7) return true; else return false; } bool jogo_da_forca::verifica_letra(char letra) { int i; //Contador // Comandos de repetição para verificar se // a letra já foi digitada for(i = 0; i < strlen(letras_corretas); i++) if (letras_corretas[i] == letra) { cout << endl << “Esta Letra Ja Foi Digitada!!!“ << endl << endl; return true; } 144 for(i = 0; i < strlen(letras_erradas); i++) if (letras_erradas[i] == letra) { cout << endl << “Esta Letra Ja Foi Digitada!!!“ << endl << endl; return true; } return false; } int jogo_da_forca::jogar() { char letra; //Letra que o jogador insere monta_palavra(); // Comando de repetição para verificar se o // jogador não acertou a palavra e não atingiu // o limite de erros do { // Comando utilizado para limpar a // tela no NetBeans printf(“\033[2J“); // Comando utilizado para limpar a // tela no Dev C++ // system(“CLS“); imprimir(); // Comando de repetição para verificar // se a letra não foi digitada do { cout << “Digite a letra: “; cin >> letra; letra = toupper(letra); } while(verifica_letra(letra)); 145 letra_correta(letra); letra_errada(letra); monta_palavra(); } while(!ganhar() && !perder()); // Verifica se o jogador acertou a palavra // sorteada ou atingiu o limite de erros if(ganhar()) { printf(“\033[2J“); //system(“CLS“); imprimir(); cout << “Voce acertou!!!“ << endl << endl; return (EXIT_SUCCESS); } else { printf(“\033[2J“); //system(“CLS“); imprimir(); cout << “Voce errou!!!“ << endl << endl; cout << “Resposta: “ << matriz[sorteio] << endl << endl; return (EXIT_SUCCESS); } } int main(int argc, char *argv[]) { // Variável para armazenar o nível // escolhido pelo jogador int nivel; char caracter[1]; // Comando de repetição para verificar // se nível digitado é correto do { // Comando de repetição para verificar 146 // se é um inteiro do { cout << “Escolha o nivel:“ << endl << endl; cout << “ 1 - Facil“ << endl; cout << “ 2 - Intermediario“ << endl; cout << “ 3 - Dificil“ << endl << endl; cout << “Nivel: “; cin >> caracter; cout << endl; if(isdigit(caracter[0]) == 0) cout<< “Nivel Invalido!!!“ << endl << endl; } while(isdigit(caracter[0]) == 0); //Conversão do caracter para um inteiro nivel = atoi(caracter); if(nivel < 1 || nivel > 3) cout<< “Nivel Invalido!!!“ << endl << endl; } while(nivel < 1 || nivel > 3); //Declaração do objeto jogo_da_forca jogo_da_forca jogo(nivel); //Chamada do método jogar jogo.jogar(); } Sobrecarga de operadores (C++) Um exemplo simples para entender os conceitos de orientação a objetos seria o da classe DINHEIRO. A classe DINHEIRO deve conter dois atributos do tipo inteiro: a) unidades e b) centavos. Ambos não podem ser negativos e os centavos não podem ter valores superiores a 99. Assim, em todos os momentos que os atributos possam sofrer alterações, seus possíveis novos valores devem ser verificados. Também é interessante que valores sejam somados, subtraídos, multiplicados e divididos. É possível, ainda, que se deseje somar valores inteiros 147 ao dinheiro (pode-se assumir que seja soma de centavos), bem como fazer operação de pré e pós-incremento. A implementação da classe VM a seguir não contemplou a criação de métodos set e get que possibilitariam a alteração dos valores armazenados e a recuperação dos mesmos. O foco principal foi a sobrecarga de operadores. Os operadores de entrada e saída, bem como a adição (com outro valor monetário ou com inteiro), pré e pós-incremento e a atribuição foram as principais operações. class VM { friend ostream& operator<< (ostream &, VM); friend istream& operator>> (istream &, VM); friend VM operator+ (int, VM); private: int unidade, centavos; char simbolo; public: VM( ); VM(char, int, int); VM& operator++(); VM operator++(int); VM& operator=(VM); VM& operator=(int); VM operator+(int); VM operator+(VM); }; // construtor sem parâmetros // inicializa com o símbolo do real e os valores // são zerados VM::VM() { unidade = 0; centavos = 0; simbolo = ‘R‘; } // construtor com parâmetros // obs.: deve manter a integridade e consistência // dos dados. Deve, portanto, verificar os valores 148 // atribuidos a unidade e centavos VM::VM(char s, int unid, int cent){ unidade = 0; centavos = 0; if (cent < 0) cent = 0; else while (cent > 99) { unidade++; cent -= 100; } if (unid >= 0) unidade += unid; else unidade = 0; centavos = cent; simbolo = s; } // operadores membros // o pré-incremento retorna o valor já alterado // pelo método VM& VM::operator++() { this->centavos++; if (this->centavos == 100) { this->centavos = 0; this->unidade++; } return *this; } // o pós-incremento retorna o valor antes da // alteração. Ou seja, retorna uma cópia do que // era antes de ser modificado VM VM::operator++ (int) { VM temp(*this); this->centavos++; if (this->centavos == 100) { this->centavos = 0; this->unidade++; 149 } return temp; // retorna o valor anterior // às modificações } // operador de atribuição recebe os valores // armazenados em val // deve retornar uma referência a VM VM& VM::operator=(VM val) { this->unidade = val.unidade; this->centavos = val.centavos; return *this; } // operador de atribuição recebe o valor // inteiro armazenados em val // deve retornar uma referência a VM VM& VM::operator=(int val) { this->unidade = val; this->centavos = 0; return *this; } // operador de adição // realiza a operação se os símbolos forem iguais // mantém o cuidado de não ferir a regra // dos centavos VM VM::operator+(VM val) { VM temp; if (simbolo == temp.simbolo){ temp.unidade = val.unidade + unidade; temp.centavos = val.centavos + centavos; if (temp.centavos > 99){ temp.centavos -= 100; temp.unidade++; } 150 } return temp; } // operador de adição // realiza a operação se os símbolos forem iguais // Não há a preocupação com os centavos, pois // o valor alterado é apenas referente à unidade. VM VM::operator+(int val) { VM temp; temp.unidade = val + unidade; temp.centavos = centavos; return temp; } // operadores não membros (friends) ostream& operator<< (ostream &s, VM x) { s << x.simbolo << “$“ << x.unidade << “,“ << x.centavos << endl; return s; } istream& operator>> (istream &i, VM x) { i >> x.unidade >> x.centavos; return i; } // operador de adição para contemplar o caso de um // inteiro ser adicionado a um valor monetário // exemplo: z = 5 + y; // para os casos onde o operando à esquerda for // inteiro, o método a seguir será acionado VM operator+ (int val, VM x) { VM temp; temp.unidade = val + x.unidade; temp.centavos = x.centavos; 151 return temp; } // o programa principal apenas cria alguns // valores e realiza a operação z = x + y; que // contempla dois operadores (+ e =) // também imprime o valor calculado utilizando // a sobrecarga do operador de saída. int _tmain(int argc, _TCHAR* argv[]) { VM x(‘R‘,5,50), y(‘R‘,10,80), z; z = x + y; cout << z; return 0; } 152 Referências CARDELLI, L.; WEGNER, P. On understanding types, data abstraction, and polymorphism. ACM Computing Surveys, New York, v. 17, n. 4, p. 471-523, Dec. 1985. DEITEL, P. J.; DEITEL, H. M. C++: How to Program. 7. ed. Upper Saddle River: Pearson Hall, 2009a. ______. Java: How to Program. 8. ed. Upper Saddle River: Pearson Hall, 2009b. SEBESTA, R. W. Concepts of Programming Languages. Reading: Addison-Wesley, 2009. STRACHEY, C. Fundamental concepts in programming languages. Higher-Order and Symbolic Computation, v. 13, n. 1-2, p. 11-49, Apr. 2000. TUCKER, A. B.; NOONAN, R. E. Programming Languages: Principles and Paradigms. New York: McGraw-Hill, 2007. 153 Sobre O Autor Ednaldo Brigante Pizzolato Bacharel em Ciência da Computação (USP), mestre em Ciência da Computação (UFSCar) e Doutor em Computação (University of Essex – UK), o professor Ednaldo tem larga experiência em programação, com conhecimentos sólidos em C, Pascal, Fortran, C++ e Java. Ministra as disciplinas de construção de algoritmos, programação de computadores e organização e recuperação da informação para os cursos de Ciência da Computação e Engenharia da Computação da UFSCar. Também atua na modalidade a distância no curso de Bacharelado de Sistemas de Informação também da UFSCar. Suas atividades de pesquisa estão associadas ao reconhecimento de fala por computador, visão computacional e interfaces multimodais. Este livro foi impresso em novembro de 2011 pelo Departamento de Produção Gráfica - UFSCar.