Coleção UAB−UFSCar Introdução à programação orientada a

Propaganda
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<thistam
elemento[i] = elemento[i]+1; //ou thiselemento[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<thistam
elemento[i] = elemento[i]+1; // ou thiselemento[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:
genericoimprime_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.
Download