1
Engenharia De Sistemas C
Conteúdo Programático
1. Orientação a Objetos;
2. A Linguagem C++;
3. Especificação e Documentação de Software;
4. Qualidade de Software.
Critério de Avaliação
1. Duas provas parciais;
2. Uma prova final;
3. Uma prova de reposição parcial;
4. Uma prova de reposição final;
5. A prova final valerá também como reposição;
6. Um trabalho contribuirá com 20% da nota parcial.
2
1. Orientação a Objetos
1.1 Conceitos Teóricos
1.1.1 Conceitos básicos da orientação a objetos
A orientação a objetos possui três conceitos básicos: objetos, classes e
herança.
Pode-se dizer que a orientação a objetos é uma volta à infância. No
jardim de infância, aprendemos a entender os seguintes conceitos:
•
Objetos e suas propriedades: os objetos do mundo real possuem
características estáticas e dinâmicas. Por exemplo, uma caneta é
fina e comprida (características estáticas) e serve para escrever
(característica dinâmica).
•
Classes e seus membros: as classes são abstrações que
capturam características dos objetos. Objetos com características
semelhantes são membros da mesma classe. Como exemplo,
temos a classe dos seres vivos. O gato e o cachorro são seres
vivos diferentes, mas possuem características semelhantes, logo
fazem parte da mesma classe (classe dos mamíferos).
•
O todo e suas partes: um objeto pode ser composto por vários
outros. Um objeto composto é considerado um objeto de nível
mais alto. Os objetos componentes de um outro objeto é
considerado um objeto de nível mais baixo. Um exemplo da vida
real é um motor. Ele é um objeto composto de vários outros
objetos menores (suas peças).
3
1.1.1.1. Objetos
Abstraindo, então, os objetos do mundo real, caminhamos para a
definição dos objetos de dados, que são compostos por duas partes:
dados (propriedades estáticas) e de seus procedimentos (propriedades
dinâmicas). Dados e procedimentos são expressões comumente
usadas na análise estruturada. Essas expressões podem se
correlacionar respectivamente com atributos e métodos na
metodologia orientada a objetos e com informação e consultas e
atualizações nos bancos de dados relacionais.
(ref. Shlaer, S.; Mellor, S. J., “Object lifecycles - Modeling the word in states”,
Yourdon Press Computing Series.)
Os objetos podem:
•
ser criados e, por conseqüência, destruídos;
•
se comunicar, via passagem de mensagens (síncronas e
assíncronas) ou via chamada de rotinas (síncronas);
•
servir de base para comunicação como conteúdo de mensagens
ou parâmetros de rotinas;
•
ser compartilhado por outros objetos;
•
ser protegidos.
1.1.1.2. Classe
Classe é o conjunto de objetos de mesma classificação. Podemos
encarar uma classe como um módulo que encapsula suas operações.
Uma das principais conseqüências desta modularização é o menor
impacto sofrido pelos sistemas em caso de manutenção. A classe é um
tipo abstrato que engloba estado e operações. Dessa forma, as classes
reúnem dados e funções, sendo tratadas de forma conjunta.
(Rumbaugh, J.; Blaha, M.; Premerlani, W.; Eddy, F.; Lorensen, W., “Modelagem e
projetos baseados em objetos”, ed. Campus.)
Os objetos só existem em tempo de execução. Já as classes existem
fisicamente mesmo quando o programa não está executando.
4
Para entender o mundo real, o homem constantemente emprega três
métodos para organizar o seu pensamento:
• Diferenciação de experiências em objetos e seus atributos;
• Distinção entre objetos e suas partes;
• Formação e distinção entre diferentes classes de objetos.
Esses três métodos constam da teoria da classificação e têm grande
influência na reutilização de código.
(Coad, P.; Yourdon, E., “Object-oriented analisys”, Yourdon Press Computing
Series.)
Dessa maneira, a base para a classificação consiste em abstrair objetos
do mundo real, organizando-os em classes conforme a equivalência
entre eles.
Boa parte dessa classificação se dá na modelagem de sistemas. Os
sistemas orientados a objetos são coleções de classes. Assim, a classe
é o bloco básico para a construção de sistemas. Já o objeto é a
instância de uma classe, ou seja, os possíveis indivíduos que pertencem
à classe.
5
Para definir uma classe, precisamos definir as propriedades comuns aos
objetos da classe. De uma forma mais computacional, é preciso definir
os atributos e os métodos (mensagens) que condizem com a classe.
Como exemplo, podemos definir a classe pessoa. Atributos possíveis
poderiam ser: primeiro nome: string; último nome: string; ano de
nascimento: number; casado: boolean; veste: roupa. Uma pessoa
poderia ter serviços (métodos ou mensagens) como casar-se, vestir-se
ou dizer seu nome.
Podemos dividir as classes em dois tipos: classes abstratas e
concretas.
As classes abstratas implementam características de suas subclasses.
Já as classes concretas implementam métodos para suas subclasses.
Por exemplo, a classe “animal” pode ser considerada uma classe
abstrata, já que implementa características para suas subclasses, como
tipo de respiração, alimentação peculiar, modo de locomoção, dentre
outras.
Então todas as suas subclasses (mamíferos, peixes etc) respiram, se
alimentam, se locomovem etc. Já a classe “peixe” pode ser considerada
uma classe concreta pois implementa métodos para suas subclasses,
como por exemplo método “respirar” (água passa pelas brânquias, que
retém o oxigênio). Esse método é utilizado por todas as subclasses de
“peixe”, pois todos os peixes respiram dessa forma.
6
As classes podem se relacionar de duas maneiras: cliente/servidor e
ascendente/descendente.
Na primeira, uma classe depende de outra para executar alguma rotina.
Isto se dá através de envio de mensagem. Por exemplo, uma classe que
emite os contra-cheques dos funcionários de uma empresa precisa do
nome de cada um deles. Porém, para conseguir esses nomes, ela terá
que enviar uma mensagem a cada uma das instâncias de funcionário,
pedindo o nome do mesmo. Essa mensagem poderia se chamar
“getNome”.
Já o relacionamento ascendente/descendente se dá através do
conceito de herança, que será exposto posteriormente. Vamos
aproveitar o exemplo anterior, onde a classe “funcionário” recebia a
mensagem “getNome”. Imaginem que a classe “funcionário” seja
subclasse da classe “pessoa”. Toda pessoa tem um nome. Espera-se,
então, que o método “getNome” seja implementado na classe “pessoa” e
herdado pela subclasse “funcionário”. Desta forma, “funcionário” executa
o método “getNome”, herdando-o da superclasse “pessoa”, e não
enviando mensagem a ela.
1.1.1.3. Subclasse
As classes podem ser estruturadas hierarquicamente. Uma classe pode
possuir uma ou mais subclasses e essas podem possuir outras
subclasses e assim sucessivamente.
A classe “mãe” da subclasse é conhecida como sua superclasse. Só se
define uma subclasse quando existe uma característica particular
adicionada às da superclasse.
7
Todo objeto pertencente a uma subclasse também pertence à
superclasse. Porém o objeto na subclasse é caracterizado com mais
detalhes. Na superclasse ele é mais genérico. O número de objetos na
classe pode ser maior que a soma dos objetos das suas subclasses,
porque podem haver objetos que só pertençam à classe (não serem
“subclasseados”). Mas é possível também que a soma dos objetos das
subclasses seja maior que o número de objetos da superclasse. Isto
porque um objeto pode pertencer ao mesmo tempo a duas subclasses
de uma superclasse.
Vejamos o exemplo em que modelamos um sistema para uma empresa
distribuidora de energia. Temos uma superclasse “parceiroComercial”
que representa as empresas ou pessoas físicas que tem algum
relacionamento com a distribuidora de energia.
Esses parceiros comerciais podem ser consumidores de energia e
fornecedores. Isto está representado por duas subclasses: “fornecedor”
e “consumidor”.
O caso mais típico é o de uma pessoa comum. Ela é “instanciada” na
classe “consumidor”, e por conseqüência na sua superclasse. Porém,
existe um caso mais específico onde temos que “instanciar” uma
empresa que fornece energia, como uma usina hidroelétrica. Acontece
que ela é fornecedora, mas também é consumidora de energia, já que
seus prédios administrativos consomem energia. Logo podemos
“instanciar” o mesmo objeto “parceiroComercial” como “fornecedor” e
“consumidor”. Dessa forma, a distribuidora de energia tem controle
sobre duas informações de um mesmo parceiro comercial (se ele é um
bom fornecedor, mas um mau consumidor, por exemplo).
8
1.1.1.4. Hierarquia de classes
Um objeto em uma subclasse herda as propriedades definidas no nível
da classe a qual pertence a subclasse. A herança pode ser simples ou
múltipla. Simples quando uma classe só é subclasse de uma única
classe. Múltipla quando ela pode ser subclasse de mais de uma classe,
herdando as propriedades dessas. O “smalltalk” não suporta herança
múltipla.
Para podermos aproveitar melhor os recursos da herança, é importante
observar características semelhantes em coisas completamente
diferentes e não características diferentes em coisas muito parecidas.
9
1.1.1.5. Encapsulamento
Os métodos encapsulam o comportamento de um objeto. As mensagens
constituem a interface pública de um objeto. Somente através das
mensagens, é possível executar um método, isto é, acessar um objeto.
Porém, os métodos não são “visíveis” externamente ao objeto, ou seja,
não se pode ver o funcionamento interno dos métodos de um objeto.
A unidade de encapsulamento utilizada pela orientação a objetos é o
objeto. Isto significa que um objeto guarda dentro de si todas as
características de alguma coisa do mundo real.
(Martin, J., “Principios de análise e projeto baseado em objetos”, ed. Campus.)
1.1.1.6. Abstração
É uma representação concisa de uma idéia ou de um objeto. Uma
abstração nos ajuda a compreender algo complexo de forma simples,
aumentando a nossa capacidade de lidar com a idéia ou com o objeto
em questão.
10
1.1.2 Métodos para desenvolvimento orientado a
objetos
1.1.2.1. Visão Geral
Somente no final dos anos 80 os métodos de desenvolvimento
orientados a objetos começaram a ser divulgados. Uma defasagem de
10 anos para as técnicas estruturadas.
Vários autores passaram a desenvolver métodos como Booch, Shlaer,
Jacobison, Coad, Hood, Rumbaugh, dentre outros.
Várias dessas propostas ganharam aceitação. Algumas dessas,
surgidas meados da década de 90, eram adaptações dos métodos
estruturados, sendo bem percebidos pelas pessoas que já os
conheciam.
Entretanto, esses métodos transitórios tinham um grande problema. Ao
usar a análise estruturada junto com o projeto e a programação
orientados a objetos, existia a necessidade de se fazer o mapeamento
da análise para o projeto.
11
Outras porém, eram propostas completamente novas, sendo
interessantes para aqueles que não tinham experiência em nenhum
método.
Neles, não existe necessidade de mapeamento, já que todas as fases,
análise, projeto e programação são orientados a objetos.
Os métodos totalmente orientados a objetos nos ajudam nos seguintes
itens, em cada fase do desenvolvimento:
ANÁLISE:
•
•
•
•
identificar objetos;
identificar classes;
definir atributos;
definir métodos.
12
PROJETO:
• Especificar módulos para implementação das classes;
• Especificar métodos.
PROGRAMAÇÃO:
• Implementar classes;
• Implementar aplicação.
Ao longo dos anos 90, a tendência tem sido a de unificação dos
métodos existentes. Os pontos mais interessantes de cada um deles
têm sido identificados em estudos conjuntos dos principais autores.
Esta convergência originou em 96 a UML (Unified Modeling
Language).
A UML é uma linguagem de modelagem. Ainda não é um método para
desenvolvimento e, portanto, não permite as definições de um processo
para tanto.
13
1.1.2.2. Estratégia para desenvolvimento
A análise, o entendimento e a modelagem conceitual do negócio são
fundamentais para o sucesso dos sistemas.
Melhorias nos processos, adequação dos processos ao negócio, alto
nível de integração dos sistemas e de forma harmoniosa (sem
transferências de arquivos), simplicidade do todo, alto nível de
reutilização e facilidade para manutenção dependem fortemente de uma
arquitetura baseada em um modelo conceitual do negócio.
O modelo conceitual do negócio captura as principais abstrações e seus
relacionamentos para o domínio do problema, isto é, os tipos essenciais
de objetos do negócio.
Já no modelo da análise é feito a determinação e o detalhamento das
principais abstrações e seus relacionamentos a serem tratados pelo
sistema.
Pode ocorrer também a identificação de novas abstrações não
percebidas no modelo conceitual do negócio. Tanto o modelo conceitual
do negócio quanto o modelo da análise são modelos conceituais
voltados para os usuários.
O modelo do projeto decide a forma de implementação das principais
abstrações e seus relacionamentos. Além disso, adiciona artefatos para
implementação. É, na verdade, uma versão computacional do modelo
da análise, sendo voltado para os analistas.
14
1.1.2.3. Processo de negócio
Um processo é uma ordenação específica de atividades de trabalho,
através do tempo e do espaço, com um início e um fim e entradas e
saídas bem definidas. Pode ser considerado uma estrutura de ação.
(Davenport, T., “Process Innovation: Reengineering Business Processes through
Information Technology”, Harvard Business School Press)
Um processo de negócio é um conjunto de atividades internas
realizadas para servir a um cliente. O propósito de cada processo de
negócio é oferecer a cada cliente o produto ou serviço correto.
(Jacobson, I.; Ericsson, M.; Jacobson, A., “Business Process Re-engeneering with
Object Technology”, Addison-Wesley.)
A modelagem conceitual de objetos de um negócio considera um
processo de negócio isoladamente. Está voltada para as necessidades
do processo, e não para a integração. O produto final é o modelo
conceitual dos tipos de objetos tratados pelo processo.
A modelagem conceitual de objetos de um sistema considera um
sistema específico e está voltada para as necessidades do próprio e não
para a integração.
O seu produto é o modelo conceitual dos tipos de objetos tratados pelo
sistema. Todas as consultorias, que promovem a reengenharia em uma
empresa, deveriam gerar este modelo no final de seu trabalho.
A modelagem conceitual de objetos de uma área de negócio considera
uma área de negócio da empresa. Utiliza os modelos dos processos da
área e está voltada para a integração e reutilização no nível da área.
Produz o modelo conceitual dos tipos de objetos utilizados pela área.
Finalmente, a modelagem conceitual de objetos da empresa como um
todo considera o negócio global da empresa. Utiliza os modelos das
áreas de negócio e está voltada para a integração e reutilização no nível
da empresa. Produz o modelo conceiutal dos tipos de objetos utilizados
pela empresa.
15
1.1.3 Técnicas de análise para orientação a objetos
1.1.3.1. Questões básicas
Quando analisamos um problema, algumas perguntas básicas nos
ocorrem. As técnicas de análise orientada a objetos exposta agora
sugere um modelo como resposta a cada uma dessas perguntas. A
seguir, vemos cada uma delas relacionadas com as modelagem
propostas.
• Quem vai utilizar o sistema e para fazer o quê?
Resposta: Modelagem dos casos de uso.
• Que classes existem? O que define o estado dos objetos de
cada classe? Qual o comportamento esperado para os objetos de
cada classe? Quais os relacionamentos existentes entre cada
classe?
Resposta: Modelagem de classes.
• Como os estados dos objetos são afetados pelos eventos
externos e sua ordenação?
Resposta: Modelagem do ciclo de vida dos objetos.
• Que mecanismos de colaboração entre objetos são necessários
para dar a funcionalidade desejada para a aplicação?
Resposta: Modelagem de mecanismos.
1.1.3.2. Modelagem de casos de uso
Apesar de ter nascido no contexto da orientação a objetos, a
modelagem de casos de uso não tem nenhum vínculo com ela, podendo
ser utilizado em qualquer metodologia.
Um caso de uso modela uma perspectiva de utilização do sistema por
parte de um tipo de usuário. Só é considerado como caso de uso, os
que agregam um valor ao uso do sistema, isto é, tem que ser uma tarefa
completa e não parte de uma tarefa.
(Jacobson, I., “Object-oriented software engineering - A use case driven approach”,
Addison-Wesley.)
16
Podemos considerar, como exemplos de casos de uso de um sistema
de contas correntes de um banco, uma retirada de dinheiro de um caixa
eletrônico, uma transferência de fundos, um depósito e a manutenção
do administrador do caixa eletrônico.
A ação do usuário se identificar não é considerado um caso de uso, pois
não agrega valor (não é uma tarefa completa).
Os usuários acessam um sistema interagindo com seus casos de uso.
Desse modo, um caso de uso modela um modo de utilização do
sistema, expressa um requisito de uso a ser atendido, captura uma
perspectiva de uso e é externamente observável por um usuário.
O conjunto de casos de uso de um sistema especifica todos os seus
requisitos de utilização, facilitando a medição de tempo de
desenvolvimento do sistema. Também modela o sistema, o seu
ambiente e como esses estão relacionados e descreve como o sistema
é visto externamente por seus usuários.
A modelagem é feita através de interações entre analistas e usuários e
deve ser feita na fase inicial de especificação do sistema.
Um modelo de casos de uso é um gráfico que apresenta: atores, casos
de uso e a interação entre eles.
17
Primeiro, identifica-se os atores, isto é, todos os elementos externos que
se relacionam com o sistema. Depois os casos de uso e seus
relacionamentos.
A especificação deve prever as seqüências normais (o que o usuário faz
normalmente), as seqüências alternativas (coisas a mais que se pode
fazer) e as excepcionais (situações especiais, como a falta de dinheiro
no caixa eletrônico).
A formalização da especificação pode ser feita através de pseudocódigo
ou diagramas de interação (diagrama de seqüência ou diagrama de
colaboração).
18
A validação desse modelo passa pelos seguintes itens:
• Revisar e refinar, interativamente, o modelo;
• Conceber, descrever e prototipar as interfaces a serem
utilizadas pelos usuários;
• Quando aprovadas, liberar uma nova versão do modelo de
casos de uso.
As aplicações desse modelo estão no planejamento e organização do
desenvolvimento, planejamento e especificação dos testes e
estruturação e desenvolvimento dos manuais de usuário.
Em resumo, o modelo de casos de uso representa tudo que os usuários
podem fazer com o sistema. Dessa forma, esse modelo é válido como
um contrato entre os usuários e os analistas sobre o que poderá ser
feito através do sistema.
(Jacobson, I., “Object-oriented software engineering - A use case driven approach”,
Addison-Wesley.)
(Jacobson, I., “Basic use case modeling”, Addison-Wesley.)
1.1.3.3. Modelagem de classes
As técnicas de modelagem de classes persistem na identificação de
classes e na identificação dos relacionamentos entre classes.
Além destas, existem mais duas técnicas complementares que nos
auxiliam a fazer o modelo de classes:
• a percepção e modelagem do domínio do problema;
• a modelagem de cada caso de uso.
Primeiramente, é interessante abstrair o problema e fazer o modelo de
classes do todo.
19
Depois, fazer o modelo de classes para cada caso de uso e validar cada
um desses com o modelo do todo. A notação básica para classes é a
seguinte:
20
O que podemos observar para melhor identificação de objetos e classes:
• As estruturas.
• Outros sistemas com o qual o nosso irá interagir, recebendo e
enviando informações, controlando ou sendo controlado. Também
outros sistemas do mesmo domínio de problema, existentes na
empresa, em outras empresas ou em bibliotecas de classes.
• Com que equipamentos o sistema irá interagir, como sensores, por
exemplo.
• Eventos
para os quais se deseja fazer planejamento, fazer
programação, reter informações e analisar conseqüências.
• O papel exercido pelas pessoas que atuam no domínio do problema.
• Observar os locais, como posição geográfica e localização física.
• A organização da empresa (setores, departamentos, subsidiárias,
filiais etc) e do negócio (segmentos, áreas de atuação etc).
Outra fonte de inspiração são os padrões (patterns).
21
Padrões são soluções genéricas, em forma de modelo de classes, que
são catalogadas para serem utilizadas em problemas com domínios
parecidos.
Esta idéia vem da arquitetura, onde são utilizados vários padrões de
plantas, conforme a necessidade do projeto. Os padrões são a grande
sensação do momento, pois oferece grande chance de reutilização de
código.
1.1.3.4. Modelagem do ciclo de vida dos objetos das
classes
Existem três pontos básicos para este modelo: estados, eventos e
operações. Durante sua existência, um objeto passa por diferentes
estados, de acordo com os eventos que ocorrem com ele, provocando a
execução de operações.
(Shlaer, S.; Mellor, S. J., “Object lifecycles - Modeling the word in states”, Yourdon
Press Computing Series.)
Por exemplo, para uma conta corrente temos:
ESTADOS
Em abertura
Ok
Tratando transação
EVENTOS
Solicitação de abertura
Aceite do banco
Retirada
OPERAÇÕES
Abrir a conta
Colocar conta ativa
Tratar retirada
22
O comportamento de um sistema pode ser expresso em termos da
coordenação do comportamento de todos os objetos com os quais ele
lida.
O comportamento de um objeto pode ser estudado através da análise
do seu ciclo de vida. O modelo de estado representa o ciclo de vida de
um objeto através dos seguintes conceitos: estado, evento e ação.
Durante sua existência, um objeto passa por alguns estados, de acordo
com os eventos que ocorrem com ele. Por exemplo, um projeto passa
pelos seguintes estados: em estudo, aguardando aprovação,
aguardando alocação de recursos, em execução e concluído.
Durante a permanência de um objeto em um estado, ele segue um
conjunto de regras: as leis físicas e as políticas operacionais que
regulam o seu comportamento. O estado de um objeto representa o
resultado acumulado de seu comportamento. Para certos estados, é útil
apresentar as ações a ele associadas.
Um evento é um acontecimento que pode causar uma transição de
estado. A partir de um mesmo estado, um evento só pode causar uma
transição de estado. Por exemplo, um funcionário ao receber seu salário
não sofre alteração de seu estado. Já, ao entrar de férias, muda de
estado.
No mundo real, existem acontecimentos envolvendo uma instância de
um objeto. Um acontecimento possui dois aspectos relevantes: o fato
que aconteceu e com quem aconteceu.
Esses aspectos são formalizados através do conceito de evento, um
sinal de controle que leva consigo o identificador da instância do objeto
envolvida no acontecimento. Um evento causa uma transição de um
estado para outro ou do estado para ele mesmo.
A identificação de eventos nos ajuda a definir as fronteiras do sistema e
assinalar responsabilidades comportamentais para as classes, já que
um evento pode provocar a execução de alguma ação.
Ações são operações instantâneas. As atividades são operações não
instantâneas.
O ciclo de vida de um objeto pode ser analisado através de um gráfico
que apresente:
23
• Os estados pelos quais o objeto passa durante sua vida;
• Os eventos que provocam as mudanças de estado;
• As ações a serem executadas.
Por que analisar o ciclo vida de um objeto?
•
•
•
•
Para melhorar o entendimento do comportamento do objeto;
Para auxiliar a identificação dos métodos;
Para verificar se todos os atributos foram identificados;
Para refinar e complementar as restrições de integridade.
Para quais objetos devemos analisar o ciclo de vida?
• Para os que necessitamos melhorar a nossa compreensão;
• Para os que tem tratamento diferenciado em função dos seus
estados;
• Para os que são fundamentais para a compreensão do negócio
sendo modelado.
Para criar o modelo de estados, o analista deve:
1. Identificar os estados, identificar os eventos e produzir o
modelo:
• analisar a cronologia dos fatos, funções e eventos que afetam o
objeto;
• analisar os atributos, identificando os que caracterizam a
dinâmica de estados do objeto.
2. Identificar os eventos de interesse:
• os que provocam alteração de atributos que caracterizam a
dinâmica de estado do objeto;
• os que são pré-requisitos para a ocorrência de outros eventos;
• os que provocam e determinam a execução de métodos do
objeto.
24
3. Produzir o modelo:
• ordenar cronologicamente os estados;
• associar os eventos aos estados;
• desenhar o modelo.
25
1.1.3.5. Modelagem de mecanismos
Para cada caso de uso podem ocorrer diferentes cenários.
Cada cenário deve ser tratado por um mecanismo de colaboração entre
objetos. Existem duas técnicas equivalentes para modelar um
mecanismo:
• Diagrama de seqüência (fluxo de mensagens entre objetos);
• Diagrama de colaboração (integração entre objetos).
26
Modelados os mecanismos, podemos identificar:
• as responsabilidades de cada objeto em cada mecanismo;
• as responsabilidades totais de cada objeto.
27
1.2 Linguagens Orientadas a Objetos
1.2.1 Mensagens
Possibilitam interação com os objetos
São compostas por:
nome do objeto
nome do método
parâmetros
menu_principal . seleciona (segunda_opcao) ;
↑
↑
↑
objeto
mensagem
parâmetro
Normalmente são bidirecionais
1.2.2 Programação OO com C++
Na prática de programação orientada a objetos estaremos atentos em
nossos programas para pontos como:
•
•
•
•
Compatibilidade, portabilidade.
Segurança.
Reusabilidade.
Facilidade de integração.
28
• Facilidade de extensão.
• Eficiência.
1.2.2.1. Classes e Objetos
Uma classe é um tipo definido pelo usuário que contém o molde, a
especificação para os objetos, assim como o tipo inteiro contém o molde
para as variáveis declaradas como inteiros. A classe envolve, associa,
funções e dados, controlando o acesso a estes, definí-la implica em
especificar os seus atributos (dados) e suas funções membro (código).
Um programa que utiliza uma interface controladora de um motor
elétrico provavelmente definiria a classe motor. Os atributos desta
classe seriam: temperatura, velocidade, tensão aplicada. Estes
provavelmente seriam representados na classe por tipos como float ou
long. As funções membro desta classe seriam funções para alterar a
velocidade, ler a temperatura etc.
Um programa editor de textos definiria a classe parágrafo que teria
como um de seus atributos uma string ou um vetor de strings, e como
funções membro, funções que operam sobre estas strings. Quando um
novo parágrafo é digitado no texto, o editor cria a partir da classe
parágrafo um objeto contendo as informações particulares do novo
texto. Isto se chama instanciação ou criação do objeto.
Classes podem ser declaradas usando a palavra reservada struct ou a
palavra reservada class.
1.2.2.1.1. Especificando Uma Classe
Suponha um programa que controla um motor elétrico através de uma
saída serial. A velocidade do motor é proporcional à tensão aplicada, e
esta proporcional aos bits que vão para saída serial e passando por um
conversor digital analógico.
Vamos abstrair todos esses detalhes por enquanto e modelar somente a
interface do motor como uma classe, a pergunta é que funções e que
dados membro deve ter nossa classe, e que argumentos e valores de
retorno devem ter essas funções membro:
29
Representação da velocidade:
A velocidade do motor será representada por um atributo, ou dado
membro, inteiro (int). Usaremos a faixa de bits que precisarmos, caso o
valor de bits necessário não possa ser fornecido pelo tipo, usaremos
então o tipo long, isso depende do conversor digital analógico utilizado
e do compilador.
Representação da saída serial:
O motor precisa conhecer a sua saída serial, a sua ligação com o "motor
do mundo real". Suponha uma representação em hexadecimal do
atributo endereço de porta serial, um possível nome para o atributo:
enderecomotor.
Alteração do valor da velocidade:
Internamente o usuário da classe motor pode desejar alterar a
velocidade, cria-se então o método (em C++ função membro): void
altera_velocidade(int novav);.
O código anterior corresponde ao cabeçalho da função membro, ela é
definida junto com a classe motor, associada a ela. O valor de retorno
da função é void (valor vazio), poderia ser criado um valor de retorno
(int) que indicasse se o valor de velocidade era permitido e foi alterado
ou não era permitido e portanto não foi alterado.
Não faz sentido usar, chamar, esta função membro separada de uma
variável do tipo motor, mas então porque na lista de argumentos não se
encontra um motor?
Esse pensamento reflete a maneira de associar dados e código
(funções) das linguagens procedurais. Em linguagens orientadas a
objetos o código e os dados são ligados de forma diferente, a própria
declaração de um tipo definido pelo usuário já engloba as declarações
das funções inerentes a este tipo.
1.2.2.1.2. Struct em C++
Objetos são instâncias de uma classe. Quando um objeto é criado ele
precisa ser inicializado, ou seja, para uma única classe Estudante de
graduação, podemos ter vários objetos em um programa Estudante de
30
graduação Carlos, Identificação 941218, Curso Computação;
Estudante de graduação Luiza, Identificação 943249, Curso
Engenharia Civil...
A classe representa somente o molde para a criação dos objetos, esses
sim contém informação.
1.2.2.1.2.1. Atributos ou Dados Membro
Este exemplo declara uma struct e em seguida cria um objeto deste tipo
em main alterando o conteúdo dessa variável. Uma struct é parecida
com um record de Pascal, a nossa representa um círculo com os
atributos raio, posição x, posição y, que são coordenadas cartesianas.
Note que este objeto não possui funções membro ainda.
#include <iostream.h>
struct circulo
//struct que representa um circulo.
{
float raio;
float x;
//posicoes em coordenadas cartesianas
float y;
};
void main()
{
circulo ac;
//criacao de variavel , veja comentarios.
ac.raio=10.0;
//modificacao de conteudo (atributos) da struct
ac.x=1.0;
//colocando o circulo em uma posicao determinada
ac.y=1.0;
//colocando o circulo em uma posicao determinada
cout << "Raio:"<<ac.raio <<endl;
//verificacao dos atributos alterados.
cout << "X:"<<ac.x << "\n"; // "\n"==endl
cout << "Y:" <<ac.y<< endl;
}
31
Resultado do programa
Raio:10
X:1
Y:1
Comentários
struct circulo
//struct que representa um circulo.
{
float raio;
float x;
//posicoes em coordenadas cartesianas
float y;
};
Este código é a declaração da classe círculo, entre chaves vem os
dados membro e as funções membro que não foram apresentadas
ainda.
A sintaxe para criação de objetos da classe círculo (circulo ac;) , por
enquanto não difere da sintaxe para a criação de variáveis do tipo int.
O acesso aos dados membro deve ser feito usando o nome do objeto e
o nome do dado membro, separados por um ponto: ac.raio=10.0;. Note
que raio sozinho não faz sentido no programa, precisa-se especificar de
que objeto se deseja acessar o raio.
1.2.2.1.2.2. Métodos ou Funções Membro
A linguagem C++ permite que se acrescente funções de manipulação da
struct em sua declaração, juntando tudo numa só entidade que é uma
classe. Essas funções membro podem ter sua declaração (cabeçalho) e
implementação (código) dentro da struct ou só o cabeçalho (assinatura)
na struct e a implementação, código, fora.
Este exemplo apresenta a primeira versão, o próximo a segunda versão
(implementação fora da classe).
Essas funções compõem a interface da classe. A terminologia usada
para designá-las é bastante variada: funções membro, métodos etc.
32
Quando uma função membro é chamada, se diz que o objeto está
recebendo uma mensagem (para executar uma ação).
Um programa simples para testes sobre funções membro seria o
seguinte:
#include <iostream.h>
struct contador
//conta ocorrencias de algo
{
int num;
//numero do contador
void incrementa(void){num=num+1;};
//incrementa contador
void comeca(void){num=0;};
//comeca a contar
};
void main()
//teste do contador
{
contador umcontador;
umcontador.comeca();
//nao esqueca dos parenteses, e uma funcao membro e
//nao atributo!
cout << umcontador.num << endl;
umcontador.incrementa();
cout << umcontador.num << endl;
}
Resultado do programa
0
1
Comentários
O programa define um objeto que serve como contador, a
implementação representa a contagem no atributo num que é um
número inteiro. As funções membro são simples: incrementa adiciona
um ao contador em qualquer estado e comeca inicia a contagem em
zero.
A sintaxe para declaração de funções membro dentro de uma classe é a
33
mesma sintaxe de declaração de funções comuns: tipoderetorno
nomedafuncao(lista_de_argumentos) { /*codigo */ }.
A diferença é como a função membro está definida na classe, ela ganha
acesso direto aos dados membros, sem precisar usar o "ponto",
exemplo um_objeto.dadomembro;. Lembre-se que as chamadas de
funções membro já se referem a um objeto específico, embora elas
sejam definidas de uma forma geral para toda a classe.
A sintaxe de chamada ou acesso à funções membro é semelhante a
sintaxe de acesso aos dados membro com exceção dos parênteses que
contém a lista de argumentos da função, mesmo que a lista seja vazia
eles devem estar presentes: umcontador.incrementa();.
Primeiro insere-se o nome do objeto e depois a chamada da função,
estes são separados por um ponto. Não esquecer os parênteses nas
chamadas de funções membro.
#include <iostream.h> //para cout
struct circulo
{
float raio;
float x;
//atributo coordenada cartesiana x
float y;
//atributo coordenada cartesiana y
void move(float dx,float dy)
//função membro ou função membro move
{
x+=dx;
//equivale a x=x+dx;
y+=dy;
}
void mostra(void) //função membro mostra
{
cout << "Raio:"<<raio <<endl;
cout << "X:"<<x << endl;
cout << "Y:" <<y<< endl;
}
};
void main()
{
circulo ac;
34
// * instanciação de um objeto circulo (criacao)
ac.x=0.0;
ac.y=0.0;
ac.raio=10.0;
ac.mostra();
ac.move(1.0,1.0);
ac.mostra();
ac.x=100.0;
ac.mostra();
}
Resultado do programa
Raio:10
X:0
Y:0
Raio:10
X:1
Y:1
Raio:10
X:100
Y:1
Comentários
A função membro move altera as coordenadas do objeto. O objeto tem
suas coordenadas x e y somadas com os argumentos dessa função
membro. Note que esta função membro representa uma maneira mais
segura, clara, elegante de alterar as coordenadas do objeto do que
acessá-las diretamente da seguinte forma: ac.x+=dx;. ac.y+=dy;.
Lembre-se que ac.x+=dx é uma abreviação para ac.x=ac.x+dx;.
É possível imaginar que as definições de funções membro ocupam um
grande espaço na representação interna dos objetos, mas lembre-se
que elas são todas iguais para uma classe então basta manter para
cada classe uma tabela de funções membro que é consultada no
momento da chamada . Os objetos só precisam ter uma referência para
esta tabela.
1.2.2.1.2.3. Funções Membro que Retornam Valores
Uma função membro, assim como uma função comum, pode retornar
35
qualquer tipo, inclusive os definidos pelo usuário. Sendo assim, sua
chamada no programa se aplica a qualquer lugar onde se espera um
tipo igual ou equivalente ao tipo do seu valor de retorno, seja numa lista
de argumentos de outra função, em uma atribuição ou em um operador
como o cout << variavel;.
#include <iostream.h>
struct contador
//conta ocorrencias de algo
{
int num;
//numero, posicao do contador
void incrementa(void){num=num+1;};
//incrementa contador
void comeca(void){num=0;};
//comeca a contar, "reset"
int retorna_num(void) {return num;};
};
void main()
//teste do contador
{
contador umcontador;
umcontador.comeca();
//nao esqueca dos parenteses, e uma funcao membro nao
dado!
cout << umcontador.retorna_num() << endl;
umcontador.incrementa();
cout << umcontador.retorna_num() << endl;
}
Resultado do programa
0
1
1.2.2.1.2.4. Funções Declaradas Externas a Classe, Funções
Membro Chamando Funções Membro
Este exemplo apresenta a implementação, definição, das funções fora
da declaração da struct. Além disso introduz uma nova função chamada
inicializa e funções float retorna_raio (void); e void altera_raio
(float a).
36
Inicializa coloca o ponto nas coordenadas passadas como seus
argumentos.
Comentários
Em uma declaração de uma classe normalmente se coloca a declaração
das funções membro depois da declaração dos atributos, porém
podemos fazer intercalações ou adotar qualquer ordem que nos
convenha.
O programador não é obrigado a implementar as funções membro
dentro da declaração da classe, basta defini-las e apresentar a
implementação em separado segundo a sintaxe (compilável) descrita a
seguir:
#include <iostream.h>
struct teste
{
int x;
void altera_x(int v);
//somente definicao implementacao vem depois, fora da
//classe
};
void teste::altera_x(int v) { x=v;}
//esta ja e a implementacao codigo
void main()
{
teste a;
//instaciacao de um objeto
a.altera_x(10);
//chamada da funcao membro com valor 10 que sera
//impresso a seguir
cout << a.x;
//imprimindo o dado membro
}
Resultado do programa anterior
10
37
Programa exemplo círculo, mais complexo:
#include <iostream.h>
//para cout
struct circulo
{
float raio;
float x;
float y;
void inicializa(float ax,float by,float cr);
void altera_raio(float a);
float retorna_raio(void);
void move(float dx,float dy);
void mostra(void);
};
void circulo::inicializa(float ax,float by,float cr)
{
x=ax;
y=by;
raio=cr;
}
void circulo::altera_raio(float a)
{
raio=a;
}
float circulo::retorna_raio(void)
{
return raio;
}
void circulo::move(float dx,float dy)
{
x+=dx;
y+=dy;
}
void circulo::mostra(void)
{
cout << "Raio:"<< retorna_raio() <<endl;
cout << "X:"<<x << endl;
cout << "Y:" <<y<< endl;
}
void main()
{
circulo ac;
ac.inicializa(0.0,0.0,10.0);
38
ac.mostra();
ac.move(1.0,1.0);
ac.mostra();
ac.x=100.0;
ac.altera_raio(12.0);
ac.mostra();
}
Comentários
Observe que a função membro mostra chama a função membro float
retorna_raio(void) que é da mesma classe. Fica implícito da definição
de mostra que retorna_raio() se aplica ao mesmo objeto instanciado
que recebeu a chamada de mostra, ou seja, não é necessário usar o .
(ponto) na chamada de retorna_raio().
Em programas maiores, chamadas aninhadas de funções membro são
bastante comuns.
Programação orientada a objetos e interfaces gráficas com o
usuário
Existem libraries de classes que permitem o programador C++
desenvolver aplicações para ambientes como o Microsoft Windowsreg
de uma maneira bastante abstrata, este é um exemplo claro de reuso de
código, afinal o programador não precisa saber de detalhes da interface
para programar nela.
Resultado do programa
Raio:10
X:0
Y:0
Raio:10
X:1
Y:1
Raio:12.0
X:100.0
Y:1
39
1.2.2.1.2.5. Algo Parecido em Uma Linguagem Procedural
Este tópico apresenta uma comparação entre C++ e Pascal, para tal
implementou-se dois programas semelhantes. O programa C++ é o
programa círculo do tópico anterior: 1.2.2.1.2.4. O programa em Pascal
vem a seguir:
PROGRAM Comparacao;
{COMPARACAO COM UM PROGRAMA C++}
TYPE Circulo=RECORD
x:real;
{COORDENADAS X E Y}
y:real;
r:real;
{somente dados}
END;
var ac:circulo;
leitura:integer;
PROCEDURE Inicializa(var
altereme:Circulo;ax,by,cr:real);
{COLOCA O CIRCULO EM DETERMINADA POSICAO}
BEGIN
altereme.x:=ax;
altereme.y:=by;
altereme.r:=cr;
END;
PROCEDURE Altera_Raio(var altereme:Circulo;ar:real);
{ALTERA O RAIO DO CIRCULO}
BEGIN
altereme.r:=ar;
END;
FUNCTION Retorna_Raio(copieme:Circulo):real;
BEGIN
Retorna_Raio:=copieme.r;
END;
PROCEDURE Move(var altereme:Circulo;dx,dy:real);
{MODE AS COORDENADAS X E Y ACRESCENTANDO DX E DY}
BEGIN
altereme.x:=altereme.x+dx;
altereme.y:=altereme.y+dy;
END;
PROCEDURE Mostra(copieme:Circulo);
{MOSTRA O CIRCULO NA TELA}
40
BEGIN
writeln('X:',copieme.x,' Y:',copieme.y,'
R:',copieme.r);
END;
BEGIN
{TESTES}
Inicializa(ac,0.0,0.0,10.0);
Mostra(ac);
Move(ac,1.0,1.0);
Mostra(ac);
ac.x:=100.0;
Altera_Raio(ac,12.0);
Mostra(ac);
read(leitura);
END.
Resultado do programa
X: 0.0000000000E+00 Y: 0.0000000000E+00 R: 1.0000000000E+01
X: 1.0000000000E+00 Y: 1.0000000000E+00 R: 1.0000000000E+01
X: 1.0000000000E+02 Y: 1.0000000000E+00 R: 1.2000000000E+01
Comentários
C++
As classes em C++ englobam os dados membros e as funções
membros. Para executar uma ação sobre o objeto ou relativa a este
basta chamar uma função membro para este: ac.mostra();.
A função membro não precisa de muitos argumentos, porque é própria
da classe e portanto ganha acesso aos dados membro do objeto para
ao qual ela foi associada:
float circulo::retorna_raio(void)
{ return raio; //tenho acesso direto a raio. }
Pascal
Em Pascal, os procedimentos e os dados são criados de forma
separada, mesmo que só tenham sentido juntos.
A junção entre os dados e procedimentos se dá através de passagem
de parâmetros. No caso de uma linguagem procedural como Pascal, o
41
que normalmente é feito se assemelha ao código seguinte:
Move(ac,1.0,1.0);. Ac nesse caso é um record, mas sem funções
membro, algo semelhante ao struct de C (não C++). Move, acessa os
dados do record alterando os campos. O parâmetro é passado por
referência e o procedimento é definido a parte do registro, embora só
sirva para aceitar argumentos do tipo Circulo e mover suas
coordenadas.
Segurança
Em ambos os programas (Pascal, C++) o programador pode acessar
diretamente os dados do tipo definido pelo usuário: ac.x:=100.0;
(Pascal) ou ac.x=100.0; (C++).
Veremos em ENCAPSULAMENTO maneiras de proibir em C++ este tipo
de acesso direto ao dado membro, deixando este ser modificado
somente pelas funções membro. Isto nos garante maior segurança e
liberdade pois podemos permitir ou não o acesso para cada dado
membro de acordo com nossa vontade.
Eficiência
Alguém pode argumentar que programas que usam bastante chamadas
de funções podem se tornar pouco eficientes e que poderia ser melhor
acessar diretamente os dados de um tipo definido pelo usuário ao invés
de passar por todo o trabalho de cópia de argumentos, inserção da
função na pilha etc.
Em verdade não se perde muito em eficiência, e além disso muitas
vezes não se deseja permitir sempre o acesso direto aos dados de um
tipo definido pelo usuário por razões de segurança. Nesse sentido C++
oferece um recurso que permite ganhos em segurança sem perder
muito em eficiência.
1.2.2.1.2.6. Construtores
Construtores são funções membro especiais chamadas pelo sistema no
momento da criação de um objeto. Elas não possuem valor de retorno,
porque você não pode chamar um construtor para um objeto.
42
Contrutores representam uma oportunidade de iniciar de forma
organizada os objetos, imagine se você esquece de iniciar corretamente
ou o faz duas vezes etc.
Um construtor tem sempre o mesmo nome da classe e não pode ser
chamado pelo usuário desta. Para uma classe string, o construtor teria
a forma string(char* a); com o argumento char* especificado pelo
programador. Ele seria chamado automaticamente no momento da
criação, declaração de uma string:
string a("Texto");
//alocacao estatica implica na chamada do construtor
a.mostra();
//chamada de metodos estatica.
Existem variações sobre o tema que veremos mais tarde: Sobrecarga de
construtor, copy constructor, como conseguir construtores virtuais,
construtor de corpo vazio.
O exemplo a seguir é simples, semelhante aos anteriores, preste
atenção na função membro com o mesmo nome que a classe (struct),
este é o construtor:
#include <iostream.h>
struct ponto
{
float x;
float y;
public:
ponto(float a,float b);
//esse e o contrutor, note a ausencia do valor de
//retorno
void mostra(void);
void move(float dx,float dy);
};
ponto::ponto(float a,float b)
//construtor tem sempre o nome da classe.
{
x=a;
//incializando atributos da classe
y=b;
//colocando a casa em ordem
}
43
void ponto::mostra(void)
{cout << "X:" << x << " , Y:" << y << endl;}
void ponto::move(float dx,float dy)
{
x+=dx;
y+=dy;
}
void main()
{
ponto ap(0.0,0.0);
ap.mostra();
ap.move(1.0,1.0);
ap.mostra();
}
Resultado do programa
X:0 , Y:0
X:1 , Y:1
Comentários
Note que com a definição do construtor, você é obrigado a passar os
argumentos deste no momento da criação do objeto.
1.2.2.1.2.7. Construtores e Agregação
O programa exemplo deste tópico cria uma classe reta com dois dados
membro da classe ponto. C++ permite que no construtor da classe reta,
você chame os construtores dos atributos da classe ponto, se você não
o fizer o compilador acusará um erro, pois os atributos ponto possuem
construtores e eles precisam ser chamados para que a inicialização se
complete de modo correto para o conjunto.
Observe o código do construtor da classe reta usado no exemplo:
reta(float x1,float y1,float x2,float y2): p1(x1,y1),
p2(x2,y2)
{
//nada mais a fazer, os construtores de p1 e p2 ja
//foram chamados
}
44
p1(x1,y1) e p2(x2,y2) são as chamadas dos construtores da classe
ponto, elas devem ficar fora do corpo { } do construtor, nesta lista
separada por vírgulas você deve iniciar todos os atributos. Os tipos
básicos como int, float, etc podem ser iniciados nessa lista.
Por exemplo, se a classe reta tivesse um atributo inteiro de nome
identificação, a lista poderia ser da seguinte forma:
reta(float x1,float y1,float x2,float y2):
p1(x1,y1),p2(x2,y2), identificacao(10)
{
//nada mais a fazer, os construtores de p1 e p2 ja
//foram chamados
}
seria como se a identificação tivesse um construtor que tem como
argumento seu valor.
reta (float x1,float y1,float x2,float y2):
p1(x1,y1),p2(x2,y2)
{
identificacao=10;
//tambem pode, porque tipos básicos (int) em C++ não
//são objetos portanto nao tem construtores
}
Uma outra alternativa seria usar alocação dinâmica para os atributos
pontos que passariam a ser agora ponteiros para pontos. Nesse
caso, o construtor da classe reta não precisaria criar os pontos.
Vamos ao exemplo, que novamente é semelhante aos anteriores, para
que o leitor preste atenção somente nas mudanças, que são os
conceitos novos, sem ter que se esforçar muito para entender o
programa:
#include <iostream.h>
struct ponto
{
float x;
float y;
//coordenadas
ponto(float a,float b)
{
45
x=a;
y=b;
}
//construtor
void move(float dx,float dy)
{ x+=dx; y+=dy; }
//funcao membro comum
void inicializa(float a,float b)
{ x=a; y=b; }
void mostra(void)
{cout << "X:" << x << " , Y:" << y << endl;}
};
struct reta
{
ponto p1;
ponto p2;
reta(float x1,float y1,float x2,float
y2):p1(x1,y1),p2(x2,y2)
{
//nada mais a fazer, os contrutores de p1 e p2 ja
//foram chamados
}
void mostra(void);
};
void reta::mostra(void)
{
p1.mostra();
p2.mostra();
}
void main()
{
reta r1(1.0,1.0,10.0,10.0); //instanciacao da reta r1
r1.mostra();
}
Resultado do programa
X:1 , Y:1
X:10 , Y:10
1.2.2.1.2.8. Destrutores
Análogos aos construtores, os destrutores também são funções membro
46
chamadas pelo sistema, só que elas são chamadas quando o objeto sai
de escopo ou em alocação dinâmica, tem seu ponteiro desalocado,
ambas (construtor e destrutor) não possuem valor de retorno.
Você não pode chamar o destrutor, o que você faz é fornecer ao
compilador o código a ser executado quando o objeto é destruído,
apagado. Ao contrário dos construtores, os destrutores não tem
argumentos.
Os destrutores são muito úteis para limpar a casa quando um objeto
deixa de ser usado, no escopo de uma função em que foi criado, ou
mesmo num bloco de código. Quando usados em conjunto com
alocação dinâmica eles fornecem uma maneira muito prática e segura
de organizar o uso do heap.
A importância dos destrutores em C++ é aumentada pela ausência de
garbage collection ou coleta automática de lixo.
A sintaxe do destrutor é simples, ele também tem o mesmo nome da
classe só que precedido por ~. Ele não possui valor de retorno e seu
argumento é void sempre:
~nomedaclasse(void) { /* Codigo do destrutor */ }
A seguir, um exemplo simples:
//destrutor de uma classe
#include <iostream.h>
struct contador{
int num;
contador(int n) {num=n;}
//construtor
void incrementa(void) {num+=1;}
//funcao membro comum, pode ser chamada pelo usuario
~contador(void)
{cout << "Contador destruido, valor:" << num <<endl;}
//destrutor
};
void main()
{
contador minutos(0);
minutos.incrementa();
cout << minutos.num << endl;
47
{
//inicio de novo bloco de codigo
contador segundos(10);
segundos.incrementa();
cout << segundos.num <<endl;
//fim de novo bloco de codigo
}
minutos.incrementa();
}
Resultado do programa
1
11
Contador destruido, valor:11
Contador destruido, valor:2
Comentários
No escopo de main é criado o contador minutos com valor
inicial==0.
2. Minutos é incrementado, agora minutos.num==1.
3. O valor de num em minutos é impresso na tela.
4. Um novo bloco de código é criado.
5. Segundos é criado, instanciado como uma variável deste bloco de
código, o valor inicial de segundos é 10, para não confundir com o
objeto já criado.
6. Segundos é incrementado atingindo o valor 11.
7. O valor de segundos é impresso na tela.
8. Finalizamos o bloco de código em que foi criado segundos, agora
ele sai de escopo, é apagado, mas antes o sistema chama
automaticamente o destrutor.
9. Voltando ao bloco de código de main(), minutos é novamente
incrementado.
10. Finalizamos main(), agora, todas as variáveis declaradas em main()
saem de escopo, mas antes o sistema chama os destrutores
daquelas que os possuem.
1.
1.2.2.1.3. Encapsulamento com Class
Encapsulamento, data hiding. Neste tópico vamos falar das maneiras
de restringir o acesso às declarações de uma classe, isto é feito em C++
48
através do uso das palavras reservadas public, private e protected.
Friends também restringe o acesso a uma classe.
Tudo que foi feito até agora pode ser feito com a palavra class ao invés
de struct, incluindo pequenas modificações. Mas porque usar class?
A diferença é que os dados membro e funções membro de uma struct
são acessíveis por default fora da struct enquanto que os atributos e
métodos de uma classe não são acessíveis fora dela (main) por default.
Você nem deve ter se preocupado com isso porque usando struct da
forma como usávamos, tudo ficava acessível.
Então como controlar o acesso de atributos e métodos em uma classe?
Simples, através das palavras reservadas private, public e protected.
Public, private e protected podem ser vistos como qualificadores,
(specifiers). Para facilitar a explicação suponha a seguintes
declarações equivalentes de classes:
(Declaração 1)
class ponto {
float x;
//dados membro
float y;
public:
//qualificador
void inicializa(float a, float b) {x=a; y=b;};
//funcao membro
void move(float dx, float dy) {x+=dx; y+=dy; };
};
A declaração 1 equivale totalmente à:
(Declaração 2)
class ponto {
private:
float x;
float y;
49
public:
//qualificador
void inicializa(float a, float b) {x=a;
y=b;};
void move(float dx, float dy) {x+=dx;
y+=dy; };
};
que equivale totalmente à:
(Declaração 3)
struct ponto {
private:
//se eu nao colocar private eu perco o encapsulamento
//em struct.
float x;
float y;
public:
//qualificador
void inicializa(float a, float b) {x=a;
y=b;};
void move(float dx, float dy) {x+=dx; y+=dy;
};
};
Fica fácil entender essas declarações se você pensar no seguinte:
esses qualificadores se aplicam aos métodos e atributos que vem após
eles, se houver então um outro qualificador, teremos agora um novo tipo
de acesso para os métodos declarados posteriormente.
Mas então porque as declarações são equivalentes? É porque o
qualificador private é default para class, ou seja, se você não
especificar nada, até que se insira um qualificador, tudo o que for
declarado numa classe é private. Já em struct, o que é default é o
qualificador public.
Agora vamos entender o que é private e o que é public:
Vamos supor que você instanciou (criou) um objeto do tipo ponto em
50
seu programa:
ponto meu; //instanciacao
Segundo o uso de qualquer uma das definições da classe ponto dadas
acima você não pode escrever no seu programa:
meu.x=5.0; //erro !
Como fazíamos nos exemplos anteriores, a não ser que x fosse
declarado depois de public na definição da classe o que não ocorre
aqui. Mas você pode escrever x=5.0; na implementação (dentro) de um
método porque enquanto não for feito uso de herança, porque uma
função membro tem acesso a tudo que é de sua classe, veja o programa
seguinte.
Você pode escrever: meu.move(5.0,5.0);, porque sua declaração
(move) está na parte public da classe. Já é possível perceber que
podem existir funções membro private também e essas só são
acessíveis dentro do código da classe (outras funções membro).
1.2.2.1.3.1. Atributos Private, Funções Membro Public
Aplicando encapsulamento à classe ponto definida anteriormente.
#include <iostream.h>
class ponto
{
private:
//nao precisaria por private, em class e default
float x;
//sao ocultos por default
float y;
//sao ocultos por default
public:
//daqui em diante tudo e acessivel.
void inicializa(float a,float b)
{ x=a; y=b; }
//as funcoes de uma classe podem acessar os atributos
//private dela mesma.
void mostra(void)
{cout << "X:" << x << " , Y:" << y << endl;}
};
51
void main()
{
ponto ap; //instanciacao
ap.inicializa(0.0,0.0); //metodos public
ap.mostra(); //metodos public
}
Resultado do programa
X:0 , Y:0
Comentários
Este programa não deixa você tirar o ponto de (0,0) a não ser que seja
chamada inicializa novamente. Fica claro que agora, encapsulando x e
y precisamos de mais métodos para que a classe não tenha sua
funcionalidade limitada.
Novamente: escrever ap.x=10; em main é um erro! Pois x está
qualificada como private.
1.2.2.1.3.2. Um Dado Membro é Public
Este programa é uma variante do anterior, a única diferença é que Y é
colocado na parte public da definição da classe e é acessado
diretamente. Além disso, fornecemos aqui a função membro move, para
que você possa tirar o ponto do lugar.
#include <iostream.h>
class ponto
{
float x;
//sao ocultos por default
public:
//daqui em diante tudo e acessivel.
ponto(float a,float b);
//construtor tambem pode ser inline ou nao
void mostra(void);
void move(float dx,float dy);
float y;
//* Y nao e' mais ocultado
};
52
ponto::ponto(float a,float b)
{
x=a;
y=b;
}
void ponto::mostra(void)
{cout << "X:" << x << " , Y:" << y << endl;}
void ponto::move(float dx,float dy)
{
x+=dx;
y+=dy;
}
void main()
{
ponto ap(0.0,0.0);
ap.mostra();
ap.move(1.0,1.0);
ap.mostra();
ap.y=100.0;
ap.mostra();
}
Resultado do programa
X:0 , Y:0
X:1 , Y:1
X:1 , Y:100
Comentários
Observe que agora nada impede que você acesse diretamente y:
ap.y=100.0, porém ap.x=10.00 é um erro. Observe em que parte (área)
da classe cada um desses dados membro foi declarado.
1.2.2.1.3.3. Compilando um Programa com Vários Arquivos
Normalmente os programas C++ são divididos em arquivos para melhor
organização e encapsulamento, porém nada impede que o programador
faça seu programa em um só arquivo. O programa exemplo da classe
ponto poderia ser dividido da seguinte forma:
53
//Arquivo 1 ponto.h, definicao para a classe ponto.
class ponto
{
public:
//daqui em diante tudo e acessivel.
void inicializa(float a,float b);
void mostra(void);
private:
float x;
//sao ocultos por default
float y;
//sao ocultos por default
};
//Arquivo 2 , ponto.cpp , implementacao para a classe
//ponto.
#include <iostream.h>
#include "ponto.h"
void ponto::inicializa(float a,float b)
{ x=a; y=b; }
//as funcoes de uma classe podem acessar os atributos
//private dela mesma.
void ponto::mostra(void)
{cout << "X:" << x << " , Y:" << y << endl;}
//Arquivo 3 . Programa principal: princ.cpp
#include "ponto.h"
void main()
{
ponto ap;
//instanciacao
ap.inicializa(0.0,0.0);
//metodos public
ap.mostra();
//metodos public
}
Os arquivos com extensão .h indicam header files. É costume deixar
nesses arquivos somente a interface das classes e funções para que o
usuário possa olhá-lo, como numa library.
54
No nosso caso o único arquivo header (.h) que temos é o ponto.h que
define a classe ponto. Caso as dimensões de seu programa permitam,
opte por separar cada classe em um header file, ou cada grupo de
entidades (funções, classes) relacionadas.
O arquivo 2 ponto.cpp é um arquivo de implementação. Ele tem o
mesmo nome do arquivo ponto.h, embora isto não seja obrigatório. É
mais organizado ir formando pares de arquivos header /
implementation.
Este arquivo fornece o código para as operações da classe ponto, daí a
declaração: #include "ponto.h". As aspas indicam que se trata de um
arquivo criado pelo usuário e não uma library da linguagem como
<iostream.h>, portanto o diretório onde se deve encontrar o arquivo é
diferente do diretório onde se encontra <iostream.h>.
O arquivo 3 princ.cpp é o arquivo principal do programa, não é costume
relacionar seu nome com nomes de outros arquivos como no caso do
arquivo 2 e o arquivo 1.
Observe que o arquivo 3 também declara #include "ponto.h", isto
porque ele faz uso das dos tipos definidos em "ponto.h", porém
"princ.cpp" não precisa declarar #include <iostream.h> porque este
não usa diretamente as definições de iostream em nenhum momento,
caso "princ.cpp" o fizesse, o include <iostream> seria necessário.
Em alguns dos exemplos seguintes, serão encontradas as diretivas de
compilação abaixo. Quando da elaboração de uma library para ser
usada por outros programadores, não se deve esquecer de usá-las:
#ifndef MLISTH_H
#define MLISTH_H
//Codigo
#endif
#ifndef MLISTH_H
#define MLISTH_H
//defina aqui seu header file.
//perceba que "Nomearq.h" e escrito na diretiva como
//NOMEARQ_H
#endif
Essas diretivas servem para evitar que um header file seja incluído
mais de uma vez no mesmo projeto.
55
Saber compilar programas divididos em vários arquivos é muito
importante. Isto requer um certo esforço por parte do programador,
porque os métodos podem variar de plataforma para plataforma.
1.2.2.1.4. Tipo Abstrato de Dados
Tipo abstrato de dados, TAD, se preocupa em proporcionar uma
abstração sobre uma estrutura de dados em termos de uma interface
bem definida. São importantes os aspectos de encapsulamento, que
mantém a integridade do objeto evitando acessos inesperados, e o fato
de o código estar armazenado em um só lugar o que cria um programa
modificável, legível, coeso.
Uma classe implementa um tipo abstrato de dados.
São exemplos de tipos abstratos de dados:
1.
2.
3.
Uma árvore binária com as operações usuais de inserção, remoção,
busca.
Uma representação para números racionais (numerador,
denominador) que possua as operações aritméticas básicas e
outras de conversão de tipos.
Uma representação para ângulos na forma (Graus, Minutos,
Segundos). Também com as operações relacionadas, bem como as
operações para converter para radianos, entre outras.
1.2.2.1.4.1. TAD Fração
Tipo abstrato de dados fração. Baseado no conceito de número racional
do campo da matemática.
Resumo das operações matemáticas envolvidas:
• Simplificação de fração: (a/b)=( (a/mdc(a,b)) / (b/mdc(a,b)) ); onde
mdc(a,b) retorna o máximo divisor comum de ab.
• Soma de fração: (a/b)+(c/d)=( (a.d+c.b) / b.d ) simplificada.
• Multiplicação de fração: (a/b) * (c/d)= ( (a*c) / (b*d) ) simplificada.
• Igualdade: (a/b)== (c/d) se a*d == b*c.
56
• Não igualdade: (a/b) != (c/d) se a*d != b*c
• Maior ou igual que: (a/b) >= (c/d) se a*d >= b*c
//header file para o TAD fracao.
//File easyfra.h
long mdc(long n,long d);
//maximo divisor comum metodo de Euclides.
class fracao {
private:
long num;
//numerador
long den;
//denominador
public:
fracao(long t,long m);
//construtor comum
void simplifica(void);
//divisao pelo mdc
~fracao() { /* nao faz nada*/ }
//Nao e preciso fazer nada.
//operacoes matematicas basicas
fracao soma (fracao j);
fracao multiplicacao(fracao j);
//operacoes de comparacao
int igual(fracao t);
int diferente(fracao t);
int maiorouigual(fracao t);
//operacoes de input output
void mostra(void);
//exibe fracao no video
void cria(void);
//pergunta ao usuario o valor da fracao
//operacoes de conversao de tipos
double convertedbl(void);
//converte para double
long convertelng(void);
//converte para long
};
//implementacao para a classe fracao.
#include <iostream.h>
57
#include "easyfra.h"
long mdc(long n,long d)
//maximo divisor comum
//metodo de Euclides +- 300 anos AC.
{
if (n<0) n=-n;
if (d<0) d=-d;
while (d!=0) {
long r=n % d;
//%=MOD=Resto da divisao inteira.
n=d;
d=r;
}
return n;
}
void fracao::simplifica(void)
{
long commd;
commd=mdc(num,den);
//divisor comum
num=num/commd;
den=den/commd;
if (den<0) { den=-den; num=-num;};
//move sinal para cima
}
fracao::fracao(long t,long m)
{
num=(t);
den=(m);
simplifica(); //chamada para o mesmo objeto.
}
fracao fracao::soma(fracao j)
{
fracao g((num*j.den)+(j.num*den),den*j.den);
return g;
}
fracao fracao::multiplicacao(fracao j)
{
fracao g(num*j.num,den*j.den);
return g;
}
int fracao::igual(fracao t)
{
return ((num*t.den)==(den*t.num));
58
//funciona bem mesmo para nao simplificada
}
int fracao::diferente(fracao t)
{
return ((num*t.den)!=(den*t.num));
}
int fracao::maiorouigual(fracao t)
{
return ((num*t.den)>=(t.num*den));
}
void fracao::mostra(void)
{
cout << "(" << num << "/" << den << ")";
}
void fracao::cria(void)
{
cout << "Numerador:";
cin >> num;
cout << "Denominador:";
cin >> den;
simplifica();
}
double fracao::convertedbl(void)
{
double dbl;
dbl=(double(num)/double(den));
return dbl;
}
//conversao para long
long fracao::convertelng(void)
{
long lng;
lng=num/den;
return lng;
}
#include <iostream.h>
#include "easyfra.h"
//nossa definicao da classe
#include <stdio.h>
main()
{
fracao a(0,1),b(0,1);
59
cout << " Entre com fracao a: ";
a.cria();
a.mostra();
cout << " Entre com fracao b: ";
b.cria();
b.mostra();
fracao c(a.soma(b));
//c(a+b)
cout << endl << "c de a+b:";
c.mostra();
cout << endl << "a*b";
c=a.multiplicacao(b);
c.mostra();
cout << endl << "a+b";
c=a.soma(b);
c.mostra();
cout << endl << "a>=b";
cout << a.maiorouigual(b);
cout << endl << "a==b";
cout << a.igual(b);
cout << endl << "a!=b";
cout << a.diferente(b);
cout << endl << "long(a) ";
cout << a.convertelng();
cout << endl << "double(a) ";
cout << a.convertedbl();
return 0;
}
Comentários
Observe o seguinte código usado no programa: fracao c(a.soma(b));. O
resultado de a.soma(b) é uma fração, mas não criamos um construtor
que recebe uma fração como argumento, como isso foi possível no
programa?
Simples, a linguagem oferece para as classes que você cria a cópia bit a
bit. Cuidado com o uso dessa cópia em conjunto com alocação
dinâmica, objetos poderão ter cópias iguais de ponteiros, ou seja
compartilhar uso de posições na memória.
60
Resultado do programa
Entre com fracao a: Numerador:4
Denominador:2
(2/1) Entre com fracao b: Numerador:5
Denominador:3
(5/3)
c de a+b:(11/3)
a*b(10/3)
a+b(11/3)
a>=b1
a==b0
a!=b1
long(a) 2
double(a) 2
1.2.2.1.5. Considerações C++
1.2.2.1.5.1. Const
Este exemplo mostra o uso de funções const e sua importância para o
encapsulamento. Const pode qualificar um parâmetro de função
(assegurando que este não será modificado), uma função membro
(assegurando que esta não modifica os dados membro de sua classe),
ou uma instância de objeto/tipo (assegurando que este não será
modificado).
Os modos de qualificação descritos atuam em conjunto, para assegurar
que um objeto const não será modificado, C++ só permite que sejam
chamados para este objeto funções membro qualificadas como const.
Const é também um qualificador (specifier).
#include <iostream.h>
#include <math.h>
//double sqrt(double x); de math.h retorna raiz
//quadrada do numero
//double pow(double x, double y); de math.h calcula x
//a potencia de y
const float ZERO=0.0;
class ponto
61
{
private:
float x;
//sao ocultos por default nao precisaria private mas
//e' bom
float y;
//sao ocultos por default
public:
//daqui em diante tudo e acessivel em main.
ponto(float a,float b)
{ x=a; y=b; }
void mostra(void) const
{cout << "X:" << x << " , Y:" << y << endl;}
float distancia(const ponto hi) const
{
return
float(
sqrt(
(
pow(double(hi.x-x),2.0)
+
pow(double(hi.y-y),2.0)
) ) );
//teorema de Pitagoras
}
};
void main()
{
ponto ap(3.0,4.0);
//instanciacao
ap.mostra();
//funcoes membro public
const ponto origem(ZERO,ZERO);
//defino objeto constante
origem.mostra();
cout << "Distancia da origem:" <<
origem.distancia(ap);
}
Resultado do programa
X:3 , Y:4
X:0 , Y:0
Distancia da origem:5
62
Comentários
• Const qualificando instâncias de objetos/tipos:
const ponto origem(ZERO,ZERO);
const float ZERO=0.0;
• Const qualificando funções:
float distancia(const ponto hi) const;
• Const qualificando argumentos:
float distancia(const ponto hi) const;
1.2.2.1.5.2. Funções inline
O que são funções inline?
Imagine uma chamada de uma função membro void altera_raio(float a)
da classe círculo já apresentada, ac.altera_raio(a). Esta chamada
envolve a passagem de parâmetros, inserção da função na pilha (stack),
retorno de um valor (void), tudo isso representa uma diminuição da
velocidade do programa com relação a um simples: ac.raio=a.
Porque programar desta forma?
Porque é mais seguro, mais próximo dos princípios de orientação a
objetos. Em verdade POO se caracteriza por muitas chamadas de
métodos e uso do "heap", área de memória usada pela alocação
dinâmica.
O que as funções declaradas como inline fazem é traduzir a chamada
do método em tempo de compilação em um equivalente ac.raio=17.0,
evitando todo o contratempo descrito. Essa tradução do método é
colocada na seqüência de código do programa, pode-se ter vários
trechos que chamariam funções, desviariam o fluxo do programa até o
retorno desta, convertidos em instruções simples.
Como desvantagem temos o aumento do tamanho do programa, visto
que passarão a existir várias cópias diferentes da função no programa
(uma para cada argumento) ao invés de um só protótipo de função que
era colocado na pilha no momento da chamada e então tinha os
argumentos substituídos.
63
Nos programa anteriores sobre a classe círculo, se a função membro
mostra fosse inline haveria uma conversão da chamada interna (dentro
de mostra) de retorna_raio() em simplesmente ac.raio. Pode haver
conversão de várias funções inline aninhadas, estas conversões são
seguras, porque são feitas pelo compilador.
Normalmente é vantajoso usar inline para funções pequenas que não
aumentem muito o tamanho do programa ou funções onde velocidade é
crucial. Aqui vale a conhecida regra 80:20, oitenta porcento do tempo do
programa é gasto em vinte por cento dos métodos. Porém o
programador não precisa se preocupar muito com funções inline na
fase de desenvolvimento, este é um recurso C++ para aumento de
eficiência que pode muito bem ser deixado para o final do projeto. Saiba
porém que as diferenças de tempo decorrentes de seu uso são
sensíveis.
Este exemplo explora as possibilidades que temos para declarar
funções membro e como declará-las para que sejam do tipo inline:
#include <iostream.h>
struct ponto
{
float x;
float y;
void inicializa(float a,float b)
{ x=a; y=b; } //Apesar de nao especificado compilador
//tenta expandir chamada da funcao como inline porque
//esta dentro da definicao da classe.
void mostra(void); //com certeza nao e' inline,
//externa a classe e sem qualificador.
inline void move(float dx,float dy); //e' inline ,
//prototipo, definicao
};
void ponto::mostra(void)
{cout << "X:" << x << " , Y:" << y << endl;}
inline void ponto::move(float dx,float dy)
//implementacao, codigo
{
x+=dx;
y+=dy;
}
void main()
{
64
ponto ap;
ap.inicializa(0.0,0.0);
ap.mostra();
ap.move(1.0,1.0);
ap.mostra();
ap.x=100.0;
ap.mostra();
}
Comentários
O compilador tenta converter a função inicializa em inline, embora não
esteja especificado com a palavra reservada inline que isto é para ser
feito. Esta é uma regra, sempre que a função estiver definida na própria
classe (struct{}) o compilador tentará convertê-la em inline.
Foi dito que o compilador tenta porque isto pode variar de compilador
para compilador, se ele não consegue converter em inline, devido à
complexidade da função, você é normalmente avisado, tendo que trocar
o lugar da definição da função membro.
Note que se a função membro é implementada fora da classe, tanto o
protótipo da função membro, quanto a implementação devem vir
especificados com inline para que a conversão ocorra.
Resultado do programa
X:0 , Y:0
X:1 , Y:1
X:100 , Y:1
1.2.2.1.5.3. Alocação Dinâmica com new e delete
Ponteiros, ("pointers")
Este exemplo mostra como trabalhar com ponteiros para variáveis de
tipos pré-definidos (não definidos pelo usuário) usando new e delete.
O programa a seguir cria um ponteiro para uma variável inteira, aloca
memória para esta variável e imprime seu valor.
65
#include <iostream.h>
void main()
{
int* a;
//declara um ponteiro para endereco de variavel
//inteira
a=new int(3);
//aloca memoria para o apontado por a, gravando neste
//o valor 3
cout << (*a) << endl ;
//imprime o valor do apontado por a
delete a;
//desaloca memoria
}
Resultado do programa
3
Comentários
Se a fosse uma struct ou class e tivesse um dado membro chamado
dd, poderíamos obter o valor de dd através de (*a).dd que pode ser
todo abreviado em a->dd onde -> é uma seta, ou flecha que pode-se ler
como "o apontado" novamente. Os parênteses em (*a).dd são
necessários devido a precedência do operador . com relação ao *.
Observação
int* a;
a=new int(3);
pode ser abreviado por:
int* a=new int(3);
Vetores Criados Estaticamente
O exemplo a seguir aloca estaticamente, em tempo de compilação, um
vetor de inteiros com três posições. Em seguida, gravamos as três
posições e as mostramos na tela:
66
#include <iostream.h>
void main()
{
int a[3];
//aloca o vetor de tamanho 3, estaticamente
a[0]=1;
//atribui a posicao indice 0 do vetor
a[1]=2;
//atribui 2 a posicao indice 1 do vetor
a[2]=3;
//atribui 3 a posicao indice 2 do vetor
cout << a[0] << " " << a[1] << " " << a[2] << endl;
//mostra o vetor
}
Resultado do programa
1 2 3
Resumo da sintaxe de vetores
int a[3]; //cria um vetor de inteiros a com tres
//posicoes, indices uteis de 0 ate 2
float b[9]; //cria um vetor de float b com nove
//posicoes, indices uteis de 0 ate 8
b[8]=3.14156295; //grava 3.1415... na ultima posicao
//do vetor b
if (b[5]==2.17) { /*acao*/} ; //teste de igualdade
Diagrama do vetor
Perceba que a faixa útil do vetor vai de 0 até (n-1) onde n é o valor dado
como tamanho do vetor no momento de sua criação, no nosso caso 3.
Nada impede que se grave ou leia índices fora dessa área útil, isso é
muito perigoso, porque fora dessa área, o que se tem são outras
variáveis de memória e não o espaço reservado para seu vetor. É
perfeitamente aceitável, embora desastroso, escrever em nosso
programa a[4]=3;. O compilador calcula o endereço de memória da
posição 4 com base na posição inicial do vetor e o tamanho do tipo
alocado. Após calculado o endereço da posição 4 o valor 3 é copiado,
apagando o conteúdo anterior!
67
Comentários
Note que não estamos usando ponteiros neste exemplo e é por isso que
o vetor é alocado estaticamente, em tempo de compilação, é também
por esse motivo que o argumento que vai no lugar do 3 no código int
a[3]; deve ser uma expressão constante e não uma variável.
Cópia de Objetos com Vetores Alocados Estaticamente
No primeiro exemplo do TAD fração vimos que o compilador fornece
cópia bit a bit para objetos. O exemplo seguinte mostra um caso onde
esta cópia oferecida pelo compilador é segura.
#include <iostream.h>
class vetor_tres
{
public:
int vet[3];
//vetor alocado estaticamente numa classe.
vetor_tres(int a,int b,int c)
{ vet[0]=a; vet[1]=b; vet[2]=c; }
//construtor do vetor
void mostra(void)
{ cout << vet[0] << " " << vet[1] << " " << vet[2] <<
endl;}
//funcao membro para mostrar o conteudo do vetor
};
void main()
{
vetor_tres v1(1,2,3);
//criacao de um objeto vetor.
vetor_tres v2(15,16,17);
//criacao de um objeto vetor.
v1.mostra();
//mostrando o conteudo de v1.
v2.mostra();
//mostrando o conteudo de v2.
v2=v1;
//atribuindo objeto v1 ao objeto v2.
v2.mostra();
//mostrando v2 alterado.
v1.vet[0]=44;
v1.mostra();
68
//mostrando o conteudo de v1.
v2.mostra();
//mostrando o conteudo de v2.
}
Resultado do programa
1 2 3
15 16 17
1 2 3
44 2 3
1 2 3
Comentários
No caso de alocação estática, quando o tamanho do vetor é conhecido
em tempo de compilação, a cópia é segura. Por cópia segura entenda:
as posições do vetor são copiadas uma a uma e os objetos não ficam
fazendo referência a um mesmo vetor. Isso pode ser visto no resultado
do programa, quando alteramos a cópia de v1, v1 não se altera.
Vetores Criados Dinamicamente
No exemplo a seguir, os vetores são alocados dinamicamente, ou seja,
o programador determina em tempo de execução qual o tamanho do
vetor.
#include <iostream.h>
void main()
{
int tamanho;
//armazena o tamanho do vetor a criar.
int* vet;
//ponteiro para inteiro ou vetor de inteiro ainda nao
//criado
cout << "Entre com o tamanho do vetor a criar";
cin >> tamanho;
vet=new int[tamanho];
//alocando vetor de "tamanho" posicoes comecando em
//a[0]
for (int i=0;i<tamanho;i++)
{
69
cout << "Entre com o valor da posicao " << i << ":";
cin >> vet[i];
cout << endl;
}
//loop de leitura no vetor
for (int j=0;j<tamanho;j++)
{
cout << "Posicao " << j << ":" << vet[j]<<endl;
}
//loop de impressao do vetor
}
Resultado do programa
Entre com o
Entre com o
Entre com o
Entre com o
Posicao 0:1
Posicao 1:2
Posicao 2:3
tamanho do vetor
valor da posicao
valor da posicao
valor da posicao
a criar3
0:1
1:2
2:3
Comentários
• int* a; declara um ponteiro para inteiro.
• a=new int[10]; diferente de new int(10); os colchetes indicam que é
para ser criado um vetor de tamanho 10 e não uma variável de valor
10. Ao contrário da alocação estática, o parâmetro que vai no lugar
do valor 10 não precisa ser uma expressão constante.
• int* a; a=new int[10]; equivale à abreviação int* a=new int[10];
• A faixa de índices úteis do vetor novamente vai de 0 até (10-1) ou
(n-1).
Cópia de Objetos com Vetores Alocados Dinamicamente
Essa determinação do tamanho do vetor em tempo de execução vai
tornar a cópia de objetos feita pelo compilador diferente, ele vai copiar o
ponteiro para o vetor, ou seja os objetos passam a compartilhar a
estrutura na memória, o que nem sempre pode ser desejável!
70
#include <iostream.h>
class vetor_tres
{
public:
int* vet;
//vetor alocado estaticamente numa classe.
vetor_tres(int a,int b,int c)
{
vet=new int[3];
vet[0]=a;
vet[1]=b;
vet[2]=c;
}
//construtor do vetor
void mostra(void)
{ cout << vet[0] << " " << vet[1] << " " << vet[2] <<
endl;}
//funcao membro para mostrar o conteudo do vetor
};
void main()
{
vetor_tres v1(1,2,3);
//criacao de um objeto vetor.
vetor_tres v2(15,16,17);
//criacao de um objeto vetor.
v1.mostra();
//mostrando o conteudo de v1.
v2.mostra();
//mostrando o conteudo de v2.
v2=v1;
//atribuindo objeto v1 ao objeto v2.
v2.mostra();
//mostrando v2 alterado.
v1.vet[0]=44;
v1.mostra();
//mostrando o conteudo de v1.
v2.mostra();
//mostrando o conteudo de v2.
}
Resultado do programa
1 2 3
15 16 17
71
1 2 3
44 2 3
44 2 3
Comentários
Quando alteramos a cópia de v1, v1 se altera. Isso ocorre porque o
vetor não é copiado casa a casa, só se copia o ponteiro para a posição
inicial do vetor, a partir do qual se calcula os endereços das posições
seguintes v[3]==*(v+3).
#include <iostream.h>
void main()
{
int* v;
v=new int[3];
cout << *(v+1)<<endl;
//imprime o lixo contido na memoria de v[1]
cout << v[1] <<endl;
//imprime o lixo contido na memoria de v[1]
//*(v)==v[0] é uma expressao sempre verdadeira
}
Resultado do programa
152
152
Comentários
O que é importante deste exemplo é que só se armazena a posição
inicial do vetor, as outras posições são calculadas com base no tamanho
do tipo alocado e regras de aritmética de ponteiros. Vetores alocados
dinamicamente e ponteiros são a mesma coisa.
Tad e Alocação Dinâmica
Um dos grandes problemas de trabalhar com vetores comuns de C++
(int * a; a=new int[10]; a[22]=3;) é que freqüentemente lemos índices
inválidos, ou pior gravamos acima deles.
Gravar acima de um índice fora dos limites de um vetor é um erro
freqüente em programação C++. Saiba que fazendo isso você pode
72
estar apagando instruções de outros programas na memória e outras
informações importantes!
Não checar os índices de um vetor em programas grandes é como
instalar uma bomba relógio em seu código, é muito provável que em
algum instante você ou até mesmo outra pessoa usando seu programa
se distraia e acabe por escrever uma rotina que acessa um índice
inválido de um vetor, fazendo na maioria das vezes o programa falhar.
A proposta deste exemplo é criar um tipo abstrato de dados vetor com
uma interface flexível que sirva para várias aplicações e possa ser
facilmente estendida.
//file exvet1.h
//header file para classe vetor
const int inicio=0;
class vetor{
private:
int* v;
//este e' o vetor
int tamanho;
//tamanho maximo do vetor,
public:
vetor (int tam);
//construtor, aloca memória para o vetor.
void atribui(int index,int valor);
//altera uma posicao do vetor
int conteudo(int index);
//retorna conteudo de posicao do vetor
int maximo(void);
//retorna o maior elemento do vetor
int primeiro(void);
//primeiro indice do vetor
int ultimo(void);
//ultimo indice do vetor
~vetor() {delete v;}
//inline function ou use delete v[];
};
//codigo, implementacao, para o header file
#include <iostream.h>
#include <stdlib.h>
73
#include "exvet1.h"
vetor::vetor (int tam)
{v=new int[tam]; tamanho=tam;}
void vetor::atribui(int index,int valor)
{
if (index<tamanho && index>=inicio)
v[index]=valor;
}
int vetor::conteudo(int index)
{
if (index>=tamanho || index<inicio) {cerr << "Fora
dos limites"; exit(1);}
return v[index];
}
int vetor::primeiro(void)
{ return inicio;}
int vetor::ultimo(void)
{ return tamanho-1;}
int vetor:: maximo(void)
{int candidato=inicio; //candidato ao maximo
for (int i=inicio;i<tamanho;i++)
if (v[i]>v[candidato]) candidato=i;
return v[candidato];}
//programa pricipal
#include <iostream.h>
#include "exvet1.h"
main()
{
int aux;
//para ler valor a atribuir
vetor meu(5);
for (int i=meu.primeiro();i<=meu.ultimo();i++)
{
cout << "Entre com valor da posicao:" << i << "\n";
cin >> aux;
meu.atribui(i,aux);
}
for (int j=meu.primeiro();j<=meu.ultimo();j++) cout<<
meu.conteudo(j)<< " ";
cout <<endl << "Maximo:" << meu.maximo();
return 0;
}
74
Comentários
O método ~vetor() {delete v;} //use delete []v; depende do compilador.
É um destrutor. A única ação do destrutor é liberar a memória ocupada
pelo atributo vetor de inteiros (int * v) da classe vetor.
Note quando não dispúnhamos do destrutor o programador era obrigado
a apagar passo a passo todas as estruturas dinâmicas dos objetos que
saíam de escopo.
Resultado do programa
Entre com
4
Entre com
5
Entre com
9
Entre com
2
Entre com
1
4 5 9 2 1
Maximo:9
valor da posicao:0
valor da posicao:1
valor da posicao:2
valor da posicao:3
valor da posicao:4
1.2.2.1.5.4. Referência &
O operador &, também chamado de operador "endereço de..." é usado
para fazer passagem de parâmetros por referência. Este operador
fornece o endereço, a posição na memória de uma variável, de um
argumento etc. Sua utilização é muito simples, se a é uma variável
inteira, &a retorna um ponteiro para a. O programa a seguir ilustra a
sintaxe do operador:
#include <iostream.h>
void main()
{
int a;
a=10;
int* p;
p=& a;
(*p)=13;
cout << a; }
75
O programa a seguir usa o operador "endereço de" para modificar
argumentos, parâmetros de uma função, ou seja utiliza passagem por
referência, equivalente ao VAR de Pascal.
#include <iostream.h>
void incrementa(int& a)
{
a++;
}
//primeira funcao que usa passagem por referencia
void troca(int& a,int& b)
{
int aux=a;
a=b;
b=aux;
}
//segunda funcao que usa passagem por referencia
void main()
{
int i1=10;
int i2=20;
incrementa(i1);
cout << i1 << endl;
troca(i1,i2);
cout << i1 << endl;
}
Resultado do programa
11
20
Comentários
As funções criadas no programa são capazes de alterar seus
parâmetros. A função incrementa, incrementa seu único parâmetro e a
função troca, troca os dois parâmetros inteiros.
76
1.2.2.2. Herança
1.2.2.2.1. Hierarquias de Tipos
Neste tópico, será mostrado como construir hierarquias de tipo por
generalização/especialização.
1.2.2.2.1.1. Uma Hierarquia Simples
Herança Pública
Na herança pública as classes filhas passam a ter as mesmas funções
membro public da classe pai, as classes filhas podem acrescentar
funções membro, dados membro e até redefinir funções membro
herdadas.
Os atributos da classe pai não são acessíveis diretamente na classe
filha a não ser que sejam qualificados como protected. Por isso é que
se diz que as classes filhas garantem pelo menos o comportamento
"behaviour" da classe pai, podendo acrescentar mais características.
Construtores e herança
No construtor de uma classe filha o programador pode incluir a chamada
do construtor da classe pai.
Destrutores e herança
Quando um objeto da classe derivada é destruído, o destrutor da classe
pai também é chamado dando a oportunidade de liberar a memória
ocupada pelos atributos private da classe pai.
//header file
class ponto
{
private:
float x; //sao ocultos por default
float y; //sao ocultos por default
public: //daqui em diante tudo e acessivel.
ponto(float a,float b);
void inicializa(float a,float b);
77
float retorna_x(void);
float retorna_y(void);
void altera_x(float a);
void altera_y(float b);
void mostra(void);
};
class ponto_reflete:public ponto //classe filha
{
private: //se voce quer adicionar atributos...
public:
ponto_reflete(float a, float b);
void reflete(void);
};
class ponto_move:public ponto
{
public:
ponto_move(float a,float b);
void move(float dx,float dy);
};
//implementation file
#include <iostream.h>
#include "pontos.h"
ponto::ponto(float a,float b)
{
inicializa(a,b);
}
void ponto::inicializa(float a,float b)
{
x=a;
y=b;
}
float ponto::retorna_x(void)
{ return x; }
float ponto::retorna_y(void)
{ return y; }
void ponto::altera_x(float a)
{ x=a; }
void ponto::altera_y(float b)
{ y=b; }
void ponto::mostra(void)
{
cout << "(" << x << "," << y << ")" <<endl;
78
}
ponto_reflete::ponto_reflete(float a,float
b):ponto(a,b)
{ }
void ponto_reflete::reflete(void)
{
altera_x(-retorna_x());
altera_y(-retorna_y());
}
ponto_move::ponto_move(float a,float b):ponto(a,b)
{ }
void ponto_move::move(float dx,float dy)
{
altera_x(retorna_x()+dx);
altera_y(retorna_y()+dy);
}
#include <iostream.h>
#include "pontos.h"
void main()
{
ponto_reflete p1(3.14,2.17);
p1.reflete();
cout << "P1";
p1.mostra();
ponto_move p2(1.0,1.0);
p2.move(.5,.5);
cout << "P2";
p2.mostra();
}
Resultado do programa
P1(-3.14,-2.17)
P2(1.5,1.5)
Herança "Private"
Pode-se especificar uma classe herdeira por herança private como:
class herdeira: private nome_classe_base;. As funções membro
dessa classe base só são acessíveis dentro das declarações da classe
filha, ou seja, a classe filha não atende por essas funções membro, mas
79
pode usá-las em seu código.
Herança private é um recurso que se precisa tomar cuidado quando
usar. Normalmente, quando usamos herança dizemos que a classe filha
garante no mínimo o comportamento da classe pai (em termos de
funções membro) , a herança private pode invalidar esta premissa.
Muitos programadores usam herança private quando ficaria mais
elegante, acadêmico, trabalhar com agregação. Uma classe pilha pode
ser construída a partir de uma classe que implementa uma lista ligada
por agregação ou por herança private. Na agregação (a escolhida em
hierarquias de implementação) a classe pilha possui um dado
membro que é uma lista ligada.
1.2.2.2.1.2. Protected
O exemplo a seguir é Igual ao exemplo anterior, mas agora tornando os
atributos da classe pai acessíveis para as classes filhas através do uso
de protected. Protected deixa os atributos da classe pai visíveis,
acessíveis "hierarquia abaixo".
//header file
class ponto
{
protected:
//*****aqui esta a diferenca ******
float x;
//visiveis hierarquia abaixo
float y;
//visiveis hierarquia abaixo
public:
//daqui em diante tudo e acessivel.
ponto(float a,float b);
void inicializa(float a,float b);
float retorna_x(void);
float retorna_y(void);
void altera_x(float a);
void altera_y(float b);
void mostra(void);
};
class ponto_reflete:public ponto
{
80
private:
//se voce quer adicionar dados membro encapsulados...
public:
ponto_reflete(float a, float b);
void reflete(void);
};
class ponto_move:public ponto
{
public:
ponto_move(float a,float b);
void move(float dx,float dy);
};
//implementation file
#include <iostream.h>
#include "pontos.h"
ponto::ponto(float a,float b)
{
inicializa(a,b);
}
void ponto::inicializa(float a,float b)
{
x=a;
y=b;
}
float ponto::retorna_x(void)
{ return x; }
float ponto::retorna_y(void)
{ return y; }
void ponto::altera_x(float a)
{ x=a; }
void ponto::altera_y(float b)
{ y=b; }
void ponto::mostra(void)
{
cout << "(" << x << "," << y << ")" <<endl;
}
ponto_reflete::ponto_reflete(float a,float
b):ponto(a,b)
{ }
void ponto_reflete::reflete(void)
{
x=-x;
81
//*** protected da esse tipo de acesso aos atributos
da classe pai
y=-y;
}
ponto_move::ponto_move(float a,float b):ponto(a,b)
{ }
void ponto_move::move(float dx,float dy)
{
x=x+dx;
//acesso so na hierarquia, no resto do programa nao.
y=y+dy;
}
#include <iostream.h>
#include "pontos.h"
void main()
{
ponto_reflete p1(3.14,2.17);
p1.reflete();
cout << "P1";
p1.mostra();
ponto_move p2(1.0,1.0);
p2.move(.5,.5);
cout << "P2";
p2.mostra();
}
1.2.2.2.1.3. Redefinição de Funções Membro Herdadas
Neste exemplo será redefinido a função membro mostra para a classe
filha ponto_reflete.
Comentários
Uma classe filha pode fornecer uma outra implementação para uma
função membro herdada, caracterizando uma redefinição "overriding"
de função membro. Importante: a função membro deve ter a mesma
assinatura (nome, argumentos e valor de retorno), senão não se trata de
uma redefinição e sim sobrecarga "overloading".
No nosso exemplo, a classe ponto_reflete redefine a função membro
mostra da classe pai, enquanto que a classe herdeira ponto_move
82
aceita a definição da função membro mostra dada pela classe ponto
que é sua classe pai.
//header file
class ponto
{
private:
float x;
//sao ocultos por default
float y;
//sao ocultos por default
public:
//daqui em diante tudo e acessivel.
ponto(float a,float b);
void inicializa(float a,float b);
float retorna_x(void);
float retorna_y(void);
void altera_x(float a);
void altera_y(float b);
void mostra(void);
};
class ponto_reflete:public ponto
{
private:
//se voce quer adicionar dados membro
public:
ponto_reflete(float a, float b);
void reflete(void);
void mostra(void);
//redefinicao
};
class ponto_move:public ponto
{
public:
ponto_move(float a,float b);
void move(float dx,float dy);
//esta classe filha nao redefine mostra
};
//implementation file
#include <iostream.h>
#include "pontos.h"
ponto::ponto(float a,float b)
83
{
inicializa(a,b);
}
void ponto::inicializa(float a,float b)
{
x=a;
y=b;
}
float ponto::retorna_x(void)
{ return x; }
float ponto::retorna_y(void)
{ return y; }
void ponto::altera_x(float a)
{ x=a; }
void ponto::altera_y(float b)
{ y=b; }
void ponto::mostra(void)
{
cout << "(" << x << "," << y << ")" <<endl;
}
ponto_reflete::ponto_reflete(float a,float
b):ponto(a,b)
{ }
void ponto_reflete::reflete(void)
{
altera_x(-retorna_x());
altera_y(-retorna_y());
}
void ponto_reflete::mostra(void)
{
cout << "X:" << retorna_x() << " Y:";
cout << retorna_y() << endl;
}
//somente altera o formato de impressao
ponto_move::ponto_move(float a,float b):ponto(a,b)
{ }
void ponto_move::move(float dx,float dy)
{
altera_x(retorna_x()+dx);
altera_y(retorna_y()+dy);
}
#include <iostream.h>
84
#include "pontos.h"
void main()
{
ponto_reflete p1(3.14,2.17);
p1.reflete();
cout << "P1";
p1.mostra();
ponto_move p2(1.0,1.0);
p2.move(.5,.5);
cout << "P2";
p2.mostra();
}
Resultado do programa
P1X:-3.14 Y:-2.17
P2(1.5,1.5)
1.2.2.2.1.4. Uma Hierarquia de Listas Ligadas
Agora um dos objetivos do próximo é obter uma implementação de lista
que possa ser reutilizada para criação de pilhas e filas.
A associação entre as classes lista e nó é uma associação do tipo "has
many" enquanto que a associação entre a classe lista e as classes
listaultimo e listaordenada indica herança, é uma associação do tipo
"is a".
#ifndef MLISTH_H
#define MLISTH_H
85
#include <stdlib.h>
#include <iostream.h>
//Criacao de uma hierarquia de listas ligadas.
//O elemento da lista e' um inteiro
enum Boolean{FALSE,TRUE};
class no{ //este e' o no da lista ligada, so e' usado
//por ela
private:
int info; //informacao
no* prox; //ponteiro para o proximo
public:
no();
no(int i,no* p);
no* get_prox(void);
void set_prox(no* p);
int get_info(void);
void set_info(int i);
no* dobra(void);
~no(void);
} ;
class lista{ //esta e' a lista ligada comum.
protected: //"visivel hierarquia abaixo"
no* primeiro; //primeiro no da lista, aqui eu insiro
//e removo.
public:
lista(void);
Boolean vazia(void)const;
Boolean contem(int el)const;
void insere_primeiro(int elem);
int* remove_primeiro();
void mostra()const;
~lista(void);
}; //fim classe lista
class listaultimo:public lista { //essa e a lista
util para
//implementar pilhas e filas.
protected: //protected e uma opcao outra e'
get_ultimo() e set_...
no* ultimo;
public:
listaultimo(void);
void insere_ultimo(int elem); //nova
void insere_primeiro(int elem); //redefinicao
int* remove_primeiro();//redefinicao
86
~listaultimo(void);
//as operacoes nao redefinidas sao validas.
};
class listaordenada:public lista {
//essa e' a lista comum com
aprimoramentos/especializacoes
public:
listaordenada(void);
Boolean contem(int el)const;
void insere_primeiro(int elem);
//insere em ordem
int* remove_elemento(int el);
//remove elemento el se existir
~listaordenada(void);
};
#endif
#include "mlisth.h"
#include <iostream.h>
#include <stdlib.h>
no::no()
{prox=NULL;cout << "Hi";}
no::no(int i,no* p)
{info=i;prox=p;cout << "Hi";}
no* no::get_prox(void){return prox;}
void no::set_prox(no* p) {prox=p;}
int no::get_info(void) {return info;}
void no::set_info(int i) {info=i;}
no* no::dobra(void)
{
if (get_prox()==NULL) return new no(get_info(),NULL);
else return new no(get_info(),this->get_prox()
->dobra());
//recursividade para duplicacao da lista
}
no::~no(void) {cout << "bye";}
lista::lista(void):primeiro(NULL) {}
//bloco de codigo vazio
Boolean lista::vazia(void)const
{
return Boolean(primeiro==NULL);
}
87
Boolean lista::contem(int el) const//mais rapido que
//iterador
{
no* curr;
int Conti;
curr=primeiro;
Conti=TRUE;
while ((curr!=NULL) && Conti )
{
if (curr->get_info()!=el)
{if (curr->get_prox()==NULL) Conti=FALSE; else
curr=curr->get_prox();}
else
Conti=FALSE;
}; //while
return Boolean(curr->get_info()==el);
};
void lista::insere_primeiro(int elem)
{
no* insirame;
if (primeiro==NULL) //lista vazia
primeiro=new no(elem,NULL);
else {
insirame=new no(elem,primeiro);
primeiro=insirame;
};
};
int* lista::remove_primeiro()
{
int* devolvame=new int; //return
no* temp; //to delete
if (primeiro==NULL) return NULL; //lista vazia
else {
(*devolvame)=primeiro->get_info();
temp=primeiro;
primeiro=primeiro->get_prox();
delete temp;
return devolvame;
};
};
void lista::mostra() const
{
no* curr;
cout << "=";
88
curr=primeiro;
while (curr!=NULL)
{
cout <<"("<<curr->get_info()<<")"<<"-";
curr=curr->get_prox();
};
}
lista::~lista(void)
{
no* temp;
while (primeiro!=NULL)
{
temp=primeiro;
primeiro=primeiro->get_prox();
delete temp;
};
}
listaordenada::listaordenada(void):lista()
{};
Boolean listaordenada::contem(int el)const
{
no* curr;
Boolean conti=TRUE;
curr=primeiro;
while ((curr!=NULL) && conti)
{
if (curr->get_info()<el)
curr=curr->get_prox();
else conti=FALSE;
};
if (curr==NULL) return FALSE;
else return Boolean(curr->get_info()==el);
}
void listaordenada::insere_primeiro(int elem)
{
no* curr=primeiro;
no* prev=NULL;
no* insirame;
Boolean conti=TRUE;
while ((curr!=NULL) && conti)
{
if (curr->get_info()<elem)
{prev=curr; curr=curr->get_prox();}
else conti=FALSE;
89
};
insirame=new no(elem,curr);
if (prev==NULL) primeiro=insirame;
else prev->set_prox(insirame);
}
int* listaordenada::remove_elemento(int el)
{
int* devolvame=new int;
no* curr=primeiro;
no* prev=NULL;
no* deleteme;
Boolean conti=TRUE;
while ((curr!=NULL) && conti) //acha lugar onde pode
estar el
{
if (curr->get_info()<el)
{prev=curr; curr=curr->get_prox();} //anda
else conti=FALSE;
};
if (curr==NULL) return NULL; //fim de lista ou vazia
else //pode ser o elemento ou ele nao existe
{
if (curr->get_info()==el)
{
deleteme=curr;
if (prev==NULL) //lista so com um elemento ou
primeiro el
primeiro=curr->get_prox();
else
{
prev->set_prox(curr->get_prox());
}
(*devolvame)=deleteme->get_info(); //so para
verificar
delete deleteme;
return devolvame;
}
else return NULL;
}
}
listaordenada::~listaordenada(void)
{cout << "Lista destruida.";};
listaultimo::listaultimo(void):lista()
{
90
ultimo=NULL;
}
void listaultimo::insere_ultimo(int elem)
{
no* insirame;
insirame=new no(elem,NULL);
if (ultimo==NULL) ultimo=insirame; //lista vazia
else {
ultimo->set_prox(insirame);
ultimo=insirame;
};
if (primeiro==NULL) primeiro=ultimo; //lista vazia
}
void listaultimo::insere_primeiro(int elem)
//redefinicao
{
no* insirame;
if (primeiro==NULL) //lista vazia
{
primeiro=new no(elem,ultimo);
ultimo=primeiro;
}//lista vazia
else {
insirame=new no(elem,primeiro);
primeiro=insirame;
};
}
int* listaultimo::remove_primeiro()//redefinicao
{
int* devolvame=new int; //return
no* temp; //to delete
if (primeiro==NULL) return NULL; //lista vazia
else {
(*devolvame)=primeiro->get_info();
temp=primeiro;
primeiro=primeiro->get_prox();
delete temp;
if (primeiro==NULL) ultimo=NULL; //volta lista vazia
return devolvame;
};
}
listaultimo::~listaultimo(void)
{
no* temp;
91
while (primeiro!=NULL)
{
temp=primeiro;
primeiro=primeiro->get_prox();
delete temp;
};
delete ultimo;
}
#include "mlisth.h"
main()
{
listaordenada minha;
char option; //use in menu as option variable
int el; //elemento a inserir
int* receptor;
do {
cout <<"\n"; //menu options display
cout <<"P:Insere no primeiro.\n";
cout <<"R:Remove no primeiro.\n";
cout <<"D:Remove elemento.\n";
cout <<"E:Existe elemento?\n";
cout <<"V:Vazia?\n";
cout <<"M:Mostra lista.\n";
cout <<"Q:Quit teste lista.\n";
cout <<"Entre comando:";
cin >> option; //reads user option
switch(option) //executes user option
{
case 'D':
case 'd':
cout << "Entre elemento:";
cin >>el;
receptor=minha.remove_elemento(el);
if (receptor==NULL) cout << "NULL" << endl;
else cout << (*receptor) << endl;
break;
case 'P':
case 'p':
cout << "Entre elemento:";
cin >> el;
minha.insere_primeiro(el);
break;
92
case 'R':
case 'r':
if (!minha.vazia())
cout << (*minha.remove_primeiro()) <<endl;
else cout << "NULL, Lista vazia." <<endl;
break;
case 'M':
case 'm':
minha.mostra();
break;
case 'E':
case 'e':
cout << "Entre elemento:";
cin >>el;
cout << minha.contem(el);
break;
case 'V':
case 'v':
cout << minha.vazia();
break;
default: ;
} //switch-case code block
} while ((option!='Q') && (option!='q')); //menu loop
code block
return 0;
} //main code block
Comentários
Note que o programa principal só testa a listaordenada.
1.2.2.2.2. Hierarquias de Implementação
Nossas hierarquias de implementação em termos de código (herança)
não são hierarquias, usamos delegação para obter pilhas a partir de
listas. Agregamos uma lista em nossas classes e usamos esta lista de
acordo com a lógica envolvida.
1.2.2.2.2.1. Fila a Partir de Uma Lista
Reuso de código de uma lista ligada para a implementação de uma fila
através de agregação. Para podermos declarar e usar um objeto lista na
93
nossa classe fila precisamos conhecer sua interface. Sabemos que
nosso objeto lista permite inserir em ambas extremidades, início e fim
da lista, mas só permite remoções em um extremo, o início.
Como uma fila permite inserções somente num extremo e remoções nos
extremo oposto, precisaremos usar nossa lista da seguinte forma:
inserção no final da lista e remoções no começo.
//header file para a classe fila
#include "mlisth.h"
class fila { //agregacao de uma lista
private:
listaultimo al; //a lista
public:
fila();
Boolean vazia();
Boolean contem(int el);
void insere(int el);
int* remove();
void mostra();
};
//implementacao para a classe fila
#include "mqueue.h"
#include "mlisth.h"
fila::fila(){};
Boolean fila::vazia()
{return al.vazia();}
Boolean fila::contem(int el)
{return al.contem(el);}
void fila::insere(int el)
{al.insere_ultimo(el);}
int* fila::remove()
{return al.remove_primeiro();}
void fila::mostra()
{al.mostra();}
//programa principal, testes da classe fila
#include "mqueue.h"
main()
{
fila minha;
94
char option; //usada em menu como variavel de opcao
int el; //elemento a inserir
do {
cout <<"\n"; //opcoes do menu
cout <<"I:Insere.\n";
cout <<"R:Remove.\n";
cout <<"M:Mostra fila.\n";
cout <<"Q:Quit fila test.\n";
cout <<"V:Vazia?\n";
cout <<"C:Contem?\n";
cout <<"Entre comando:";
cin >> option; //le opcao do usuario
switch(option) //executa opcao do usuario
{
case 'I':
case 'i':
cout << "Entre elemento:";
cin >>el;
minha.insere(el);
break;
case 'R':
case 'r':
if (!minha.vazia())
cout << (*minha.remove()) <<endl;
else cout << "NULL, fila vazia." <<endl;
break;
case 'C':
case 'c':
cout << "Entre elemento:";
cin >>el;
cout << minha.contem(el);
break;
case 'M':
case 'm':
minha.mostra();
break;
case 'V':
case 'v':
cout << "Resultado:" << minha.vazia() <<endl;
break;
default: ;
} //switch-case bloco de codigo
} while ((option!='Q') && (option!='q')); //loop do
//menu fim
95
return 0;
} // bloco de codigo principal
1.2.2.3. Polimorfismo, Funções Virtuais
Existem vários tipos de polimorfismo.
1.2.2.3.1. O Que Significa Polimorfismo
Polimorfismo, do grego: muitas formas. Polimorfismo é a capacidade de
um operador executar a ação apropriada dependendo do tipo do
operando. Aqui operando e operador estão definidos num sentido mais
geral: operando pode significar argumentos atuais de um procedimento
e operador o procedimento, operando pode significar um objeto e
operador um método, operando pode significar um tipo e operador um
objeto deste tipo.
1.2.2.3.1.1. Copy Constructor
A função membro ponto (ponto& a); é um copy constructor, além
disso tem o mesmo nome que ponto (float dx, float dy);. Tal
duplicação de nomes pode parecer estranha, porém C++ permite que
eles coexistam para uma classe porque não tem a mesma assinatura
(nome + argumentos). Isto se chama sobrecarga de função membro, o
compilador sabe distinguir entre esses dois construtores.
Outras funções membro, não só construtores poderão ser redefinidas,
ou sobrecarregadas para vários argumentos diferentes.
O que é interessante para nós é o fato de o argumento do construtor
ponto(ponto& a); ser da mesma classe para qual o construtor foi
implementado, esse é o chamado "copy constructor". Ele usa um
objeto de seu tipo para se inicializar. Outros métodos semelhantes
seriam: circulo (circulo& a); mouse (mouse& d);.
#include <iostream.h>
struct ponto
{
float x;
float y;
96
ponto(float a,float b);
//construtor tambem pode ser inline ou nao
ponto(ponto& a); //copy constructor
void mostra(void);
void move(float dx,float dy);
};
ponto::ponto(float a,float b)
{
x=a;
y=b;
}
ponto::ponto(ponto& a)
{
x=a.x;
y=a.y;
}
void ponto::mostra(void)
{cout << "X:" << x << " , Y:" << y << endl;}
void ponto::move(float dx,float dy)
{
x+=dx;
y+=dy;
}
void main()
{
ponto ap(0.0,0.0);
ap.mostra();
ap.move(1.0,1.0);
ap.mostra();
ponto ap2(ap);
ap2.mostra();
}
Comentários
Observe o código:
ponto::ponto(ponto& a)
{
x=a.x;
y=a.y;
}
97
Essa função membro, esse método, pertence a outro objeto que não o
argumento a, então para distinguir o atributo x deste objeto, do atributo x
de a usamos a.x e simplesmente x para o objeto local (dono da função
membro).
1.2.2.3.1.2. Sobrecarga de Função em C++
Sobrecarga "Overloading" de função é um tipo de polimorfismo. C++
permite que funções de mesmo nome tenham parâmetros distintos. Este
exemplo mostra a sobrecarga da função abs que calcula o valor
absoluto de um número:
//header file funcover.h
float abs(float a);
int abs(int a);
//implementation file
#include "funcover.h"
float abs(float a)
{
if (a>0.0) return a;
else return -a;
}
int abs(int a)
{
if (a>0) return a;
else return -a;
}
#include <iostream.h>
#include "funcover.h"
void main()
{
int i1;
float f1;
cout << abs(int(-10))<<endl;
cout << abs(float(-10.1))<<endl;
f1=-9.1;
i1=8.0;
cout << abs(f1) << endl;
98
cout << abs(i1) << endl;
}
Resultado do programa
10
10.1
9.1
8
Comentários
cout << abs(float(-10.1))<<endl;
Quando chamamos a função abs para um valor (-10.1) e não uma
variável (possui um tipo), temos que fazer a conversão explícita para o
compilador, porque este não sabe decidir qual função chamar (para
float ou int), mesmo estando presente o ponto indicando a casa
decimal.
Observe que -10.1 pode ser double ou float. Enquanto que 10 pode ser
long ou int.
//header file funcover.h
float max(float a,float b);
float max(float a,float b,float c);
int max(int a,int b);
#include "funcover.h"
float max(float a,float b)
{
if (a>b) return a;
else return b;
}
float max(float a,float b,float c)
{
if (a>b) return max(a,c);
else return max(b,c);
}
int max(int a,int b)
{
if (a>b) return a;
99
else return b;
}
#include <iostream.h>
#include "funcover.h"
void main()
{
cout << max(float(1.2),float(3.4),float(2.1))<<endl;
cout << max(float(1.5),float(.65)) << endl;
cout << max(int(12),int(120));
}
Resultado do programa
3.4
1.5
120
1.2.2.3.1.3. "Default Arguments", Valores Sugestão
Valores sugestão, argumentos padrão ou "default arguments", são
nomes para um tipo de polimorfismo fornecido por C++.
Para demonstrar o uso de default values vamos relembrar o nosso tipo
abstrato de dados fração. Um de seus construtores tinha a seguinte
forma: fracao() {num=0; den=1;} //construtor vazio, default enquanto
que o construtor normal da fração tinha a seguinte forma: fracao (long t,
long m);.
"Default arguments" nos dá a oportunidade de fundir esses dois
construtores num só resultando no seguinte: fracao (long t=0, long
m=1) {num=t; den=m;} onde 1 e 0 são valores sugestão para os
argumentos.
A instanciação fracao a() segundo aquele único construtor cria: (0/1)
A instanciação fracao b(1) segundo aquele único construtor cria: (1/1)
A instanciação fracao c(1,2) segundo aquele único construtor cria: (1/2)
Regras para a criação de "Default arguments"
100
Não são permitidas declarações do tipo fracao (long t=0, long m); uma
vez que você inseriu um argumento padrão na lista de argumentos todos
a direita deste também deverão ter seus valores sugestão. Então, por
esta regra a única alternativa restante para o tipo fração seria
fracao (long t, long m=1);.
1.2.2.3.1.4. Sobrecarga de Operador
O tipo abstrato de dados fração (versão completa) possui vários
operadores sobrecarregados. Algumas sobrecargas deste exemplo
envolvem o uso da palavra chave friends.
O próximo exemplo é uma extensão da classe vetor para incluir um
iterador.
//header file para classe vetor: vet.h
#include <iostream.h>
#include <stdlib.h> //exit(1)
const int inicio=0; //inicio do vetor
class vetor{
private:
float* v; //pode ser qualquer tipo que atenda as
//operacoes < > =
int tamanho;
public:
vetor (int tamanho) ;
float& operator[] (int i);
float maximo(); //acha o valor maximo do vetor
int primeiro(void);
int ultimo(void);
};
vetor::vetor (int tam)
{v=new float[tam]; tamanho=tam;}
int vetor::primeiro (void)
{return inicio;}
int vetor::ultimo (void)
{return tamanho-1;}
float& vetor::operator[](int i)
{
if (i<0 || i>=tamanho)
{cout << "Fora dos limites! Exit program"; exit(1);}
return v[i];
}
101
float vetor:: maximo(void)
{int candidato=inicio;
for (int i=inicio;i<tamanho;i++)
if (v[i]>v[candidato]) candidato=i;
return v[candidato];}
Explicação das operações, das função membros do iterador vetor
• Iteradorvetor (vetor & v);
Construtor, já cria o iterador de vetor inicializando-o para apontar
para o começo do vetor.
• virtual int comeca();
Inicializa o iterador para o começo do vetor.
• virtual int operator!();
Verifica se a iteração não chegou no fim do vetor: 1 indica que não
chegou, 0 indica que chegou no fim do vetor.
• virtual int operator ++ ();
Faz o iterador mover adiante uma posição.
• virtual float operator() ();
Retorna o elemento daquela posição do vetor.
• virtual void operator= (float entra);
Atribui a posição atual do vetor.
• int pos();
Retorna a posição (índice) do vetor em que o iterador se encontra,
não é virtual porque não faz sentido para um iterador de árvore por
exemplo.
//it.h , arquivo com definicoes do iterador.
class iteradorvetor
{
private:
vetor vetorref;
int posicao;
public:
iteradorvetor(vetor & v);
int comeca();
102
int operator!();
int operator ++ ();
float operator() ();
void operator= (float entra);
int pos(); //retorna posicao, n~ virtual pq n~ faz
//sentido para arvore por ex.
};
int iteradorvetor::pos()
{
return posicao;
}
int iteradorvetor::operator!()
{
return posicao<=vetorref.ultimo();
}
iteradorvetor::iteradorvetor(vetor &
vet):vetorref(vet)
{
comeca();
}
int iteradorvetor::comeca()
{
posicao=vetorref.primeiro();
return operator!();
}
int iteradorvetor::operator ++()
{
posicao++;
return operator!();
}
void iteradorvetor::operator=(float entra)
{
vetorref[posicao]=entra;
}
float iteradorvetor::operator() ()
{
float copia;
copia=vetorref[posicao];
return copia;
}
#include <iostream.h>
#include "vet.h"
103
#include "it.h"
main()
{
int repete=0;
int ind;
float item;
vetor meu(5);
iteradorvetor itmeu(meu);
for (itmeu.comeca();!itmeu;++itmeu)
{
cout << "Entre com valor da posicao:" << itmeu.pos()
<< "\n";
cin >> item;
itmeu=item;
}
for (itmeu.comeca();!itmeu;++itmeu) cout<< itmeu()<<
" ";
cout << "\nEntre com o indice da posicao a
atualizar:\n";
cin >> ind;
cout << "Entre com o valor a incluir:";
cin >> item;
meu[ind]=item;
for (int k=meu.primeiro();k<=meu.ultimo();k++) cout<<
meu[k]<< " ";
cout <<endl << "Maximo:" << meu.maximo();
return 0;
}
Comentários
O significado do operador é você que define, mas é recomendável dar
ao operador um significado próximo ao já definido na linguagem. Por
exemplo: o operador + seria ótimo para representar a concatenação de
dois objetos do tipo string. A sintaxe de cada operador é fixa: número de
operandos, precedência etc.
104
3. Especificação e Documentação de
Software
3.1 UML (Unified Modeling Language)
3.1.1 Introdução
O desenvolvimento de sistemas de software de grande porte são
suportados por métodos de análise e projeto que modelam esse sistema
de modo a fornecer para toda a equipe envolvida (cliente, analista,
programador etc.) uma compreensão única do projeto.
A UML (Unified Modeling Language) é o sucessor de um conjunto de
métodos de análise e projeto orientados a objeto.
A UML é um modelo de linguagem, não um método. Um método
pressupõe um modelo de linguagem e um processo. O modelo de
linguagem é a notação que o método usa para descrever o projeto. O
processo são os passos que devem ser seguidos para se construir o
projeto.
O modelo de linguagem é uma parte muito importante do método.
Corresponde ao ponto principal da comunicação. Se uma pessoa quer
conversar sobre o projeto, com outra pessoa, é através do modelo de
linguagem que elas se entendem. Nessa hora, o processo não é
utilizado.
A UML define uma notação e um meta-modelo. A notação são todos os
elementos de representação gráfica vistos no modelo (retângulo, setas,
o texto etc.), é a sintaxe do modelo de linguagem. A notação do
diagrama de classe define a representação de itens e conceitos tais
como: classe, associação e multiplicidade. Um meta-modelo é um
diagrama de classe que define de maneira mais rigorosa a notação.
105
Comportamento do Sistema
O comportamento do sistema é capturado através de análise de casos
de uso do sistema.
Descrição informal do sistema automatizado de Matrícula num
Curso
No início de cada semestre os alunos devem requisitar um catálogo de
cursos contendo os cursos oferecidos no semestre. Este catálogo deve
conter informações a respeito de cada curso tais como: professor,
departamento e pré-requisitos. Desse modo, os alunos podem tomar
suas decisões mais apropriadamente.
O novo sistema permitirá que os alunos selecionem quatro cursos
oferecidos para o próximo semestre. Além disso, o aluno indicará dois
cursos alternativos, caso o aluno não possa ser matriculado na primeira
opção. Cada curso terá o máximo de 10 e o mínimo de 3 alunos.
Um curso com número de alunos inferior a 3 será cancelado. Para cada
matrícula feita por um aluno, o sistema envia informação ao sistema de
cobrança para que cada aluno possa ser cobrado durante o semestre.
Os Professores devem acessar o sistema " on line", indicando quais
cursos irão lecionar. Eles também podem acessar o sistema para saber
quais alunos estão matriculados em cada curso.
Em cada semestre, há um prazo para alteração de matrícula. Os alunos
devem poder acessar o sistema durante esse período para adicionar ou
cancelar cursos.
Definição do Diagrama de Casos de Uso
É um diagrama usado para se identificar como o sistema se comporta
em várias situações que podem ocorrer durante sua operação. Descreve
o sistema, seu ambiente e a relação entre os dois. Os componentes
deste diagrama são os atores e os "Use Case".
Ator
Representa qualquer entidade que interage com o sistema. Pode ser
uma pessoa, outro sistema etc. Algumas de suas características são
descritas abaixo:
106
• Ator não é parte do sistema. Representa os papéis que o usuário
do sistema pode desempenhar.
• Ator pode interagir ativamente com o sistema.
• Ator pode ser um receptor passivo de informação.
• Ator pode representar um ser humano, uma máquina ou outro
sistema.
"Use Case"
Como foi exemplificado acima, é uma seqüência de ações que o sistema
executa e produz um resultado de valor para o ator. Algumas de suas
características são descritas abaixo:
• Um "Use Case" modela o diálogo entre atores e o sistema.
• Um "Use Case" é iniciado por um ator para invocar uma certa
funcionalidade do sistema.
• Um "Use Case" é fluxo de eventos completo e consistente.
• O conjunto de todos os "Use Case" representa todos as situações
possíveis de utilização do sistema.
Descrição textual resumida do "Use Case" Matrícula nos Cursos
Este "Use Case" é iniciado pelo aluno. Fornece os meios para o aluno
criar, anular, modificar e consultar o formulário de matrícula de um dado
semestre.
Descrição do Fluxo principal de eventos associados a esse "Use
Case"
Este "Use Case" inicia-se quando o aluno fornece a chave de acesso. O
sistema verifica se a chave de acesso do aluno é valida (E1) e
apresenta ao aluno a opção de selecionar o semestre atual ou o próximo
semestre (E2).
O aluno seleciona o semestre desejado. O sistema pede ao aluno para
selecionar a atividade desejada: Criar, Consultar, Modificar, Imprimir,
Anular ou Sair do Sistema.
107
Se atividade selecionada é:
• Criar, o subfluxo A1 (Criar uma Matrícula Nova) é executado.
• Consultar, o subfluxo A2 (Consulta da Matrícula) é executado.
• Modificar, o subfluxo A3 (Modificação da Matrícula) é
executado.
• Imprimir, o subfluxo A4 (Imprimir a Matrícula) é executado.
• Anular, o subfluxo A5 (Anular Matrícula) é executado.
• Sair, o "Use Case" é encerrado.
Descrição dos Subfluxos Alternativos associados a esse "Use
Case"
Criar uma Matrícula Nova
O sistema apresenta numa tela um formulário de matrícula em branco.
O aluno preenche-o com 4 cursos oferecidos, como primeira escolha e
preenche 2 cursos oferecidos como segunda escolha (E3).
A seguir, o aluno submete o formulário preenchido ao sistema. Para
cada curso de primeira escolha, o sistema irá verificar se os prérequisitos são satisfeito (E4) e matricula o aluno no curso, se esse
estiver sendo oferecido, e se houver vaga (E5).
O sistema imprime o formulário de matrícula (E6) e envia a informação
para ser processado pelo sistema de cobrança (E7). O sistema fica
disponível.
Consulta da Matrícua
O sistema recupera (E8) e apresenta numa tela as seguintes
informações para todos os cursos nos quais o aluno está matriculado:
nome do curso, número do curso, dias da semana, horário,
localização e número de créditos .
Quando o aluno indica que terminou a consulta, o sistema fica
disponível.
Modificação da Matrícula
O sistema verifica se a data limite para mudanças não expirou (E9). O
sistema recupera (E8) e apresenta as seguintes informações para todos
os cursos nos quais o aluno está matriculado: nome do curso, número
108
do curso, dias da semana, horário, localização e número de
créditos.
O sistema oferece um menu com as seguintes opções: anule um curso
oferecido, adicione um curso oferecido ou sair do sistema.
Se a atividade selecionada for:
• Anular um curso matriculado, o procedimento de anulação de
um curso (A6), é executado.
• Adicionar um curso oferecido, o procedimento de adição de
curso (A7) é executado.
• Sair do sistema, o sistema imprime formulário de matrícula
(E6), envia a informação para ser processado pelo sistema de
cobrança e fica disponível.
Imprimir a Matrícula
O sistema imprime a matrícula do aluno (E6) e fica disponível.
Anular Matrícula
O sistema recupera (E8) e apresenta as informações atuais da
matrícula. O sistema pede ao usuário para confirmar a anulação da
matrícula. Se efetuada, a matrícula é removida do sistema. Se a
anulação não for confirmada, a operação é cancelada e o sistema fica
disponível.
Anular um curso escolhido
O aluno entra com o número do curso a ser anulado. O sistema pede ao
usuário para confirmar a anulação do curso. Se efetuada, o curso é
removido da matrícula do aluno. Se a anulação não for confirmada, a
operação é cancelada e o sistema fica disponível.
Adicionar um curso
O aluno entra com o número do curso a ser adicionado. O sistema
verifica se os pré-requisitos são satisfeitos (E4) e adiciona o aluno ao
curso, se o curso estiver sendo oferecido (E5) e o sistema fica
disponível.
109
Descrição dos Subfluxos de Exceção
O aluno fornece chave de acesso inválida
O aluno pode entrar com a chave de acesso novamente ou sair do
sistema.
O aluno fornece um semestre inválido
O aluno pode fornecer novamente um semestre ou sair do sistema.
O aluno fornece número de curso inválido (formato)
O aluno pode fornecer outro número ou sair do sistema.
O aluno não satisfaz todos os requisitos necessários
O aluno é informado que não pode se matricular nesse curso e a razão
para tal. Se possível, um curso alternativo é apresentado. O sistema
segue adiante.
O aluno é informado de que a matrícula para curso selecionado
está encerrada
Se possível, um curso alternativo é apresentado. O sistema segue
adiante.
A matrícula não pode ser imprimida
A informação é armazenada e o aluno é informado de que o pedido de
impressão deve ser repetido. O sistema segue a diante.
O sistema armazenará todas as informações necessárias ao
sistema de cobrança e a fornecerá assim que possível
O fluxo segue adiante.
O sistema não pode recuperar as informações de matrícula
O aluno deve reiniciar o fluxo desde o início.
110
O sistema informa ao aluno que a matrícula não pode ser alterada
O aluno deve reiniciar o fluxo desde o início.
Documentação de um "Use Case"
Como apresentado acima, a documentação de um "Use Case" é
composta de uma Descrição textual resumida e dos Fluxos de eventos
(Fluxo principal, Subfluxos Alternativos e Subfluxos de Exceção).
Cenários
Um cenário primário
Rubens fornece sua chave de acesso. O sistema valida a chave e pede
para Rubens escolher o semestre. Ele escolhe o semestre atual e pede
para criar uma matrícula nova.
Rubens escolhe os cursos primários: Inglês, Geologia, História Geral e
Álgebra. Também seleciona dois cursos alternativos: Teoria Musical e
Introdução à Programação Java.
O sistema constata que Rubens tem todos os pré-requisitos necessários
e adiciona-o às listas de cada curso.
O sistema avisa que a matrícula foi realizada. Imprime o formulário de
matrícula de Rubens. Envia informação de cobrança referente aos
quatro cursos para ser processada no sistema de cobrança.
Cenários secundários
Walker não seleciona os quatro cursos primários.
Um dos cursos primários selecionados não possui mais vagas.
Um curso primário ou secundário não está sendo oferecido.
Definição Cenário
É uma instância de um "Use Case". O "Use Case" deve ser descrito
através de vários cenários. Devem ser construídos tantos cenários
quantos forem necessários para se entender completamente todo o
sistema. Podem ser considerados como testes informais para validação
dos requisitos do sistema.
111
Tipos de cenários
Primários
São os cenários nos quais o fluxo segue normalmente. Não há quebra
no fluxo por alguma espécie de erro.
Secundários
São os casos que compõem exceção. O fluxo normal de operação é
interrompido.
Definição de "Use case"
É um modelo das funções a serem executadas pelo sistema e a
interação com suas fronteiras. Sua principal aplicação é confirmar aos
usuários e clientes as suas funcionalidades e comportamento.
Objetos e Classes de Objetos
Definição de Objeto
Representa um entidade que pode ser física, conceitual ou de software.
É uma abstração de algo que possui fronteira definida e significado para
a aplicação.
Componentes de um objeto
• Estado
• Comportamento
• Identidade
Identidade: É o que identifica unicamente um objeto, mesmo que ele
possua estados ou comportamento comuns a outros objetos.
Estado de um objeto: É cada condição na qual o objeto pode existir. É
mutável ao longo do tempo. É implementado por um conjunto de
atributos, os valores desses atributos e ligações que o objeto pode ter
com outros objetos.
Comportamento de um objeto: Determina como um objeto age e
reage a estímulos de outros objetos. É modelado através de um
112
conjunto de mensagens que representam resultados visíveis das
operações executadas internamente pelo objeto.
Definição de Classe É uma descrição de um grupo de objetos com
atributos, comportamentos, relacionamentos com outros objetos e
semântica comuns. Uma classe é uma abstração que enfatiza
características relevantes dos objetos, suprimindo outras características.
Exemplo da Classe Curso
Nome:
Curso
Estado:
Nome
Número do Curso
Localização
Dias do Curso
Número de créditos
Hora de Início
Hora de Término
Comportamento: Comportamento:
Adicionar um aluno
Cancelar um aluno
Obter alunos Matriculados
Determinar se a turma está completa
Exemplo da Classe Professor
Nome:
Professor
Estado:
Nome
Número do Empregado
Data de Admissão
Curso ministrado
Tipo de contratação
Salário
113
Comportamento:
Consultar Lista de Alunos
Indicar suas Disciplinas
A notação usada pela UML para representar uma Classe de Objetos é:
Nome
Atributo
Operações
A classe de objeto é representada por um retângulo, subdividido em três
áreas. A primeira contém o nome da Classe. A segunda contém seus
atributos. A terceira contém suas operações. A seguir, tem-se os
exemplos que esclarecem a representação descrita acima.
Curso
Número do Curso
Nome
Localização
Dias do Curso
Número de Créditos
Hora de Início
Hora de Término
Adicionar um Aluno( )
Cancelar um Aluno( )
Obter Alunos Matriculados( )
Determinar se a turma está completa( )
Professor
Número do Empregado
Nome
Data de Admissão
Curso ministrado
Tipo de contratação
Salário
Consultar Lista de Alunos( )
Indicar suas Disciplinas( )
Estereótipos
Estereótipo é um elemento de modelagem que rotula tipos de Classes
de Objeto. Uma Classe de Objetos pode ter um ou mais tipos de
estereótipos. Os estereótipos mais comuns são:
•
•
•
•
•
•
Classe Fronteiriça
Classe de Entidade
Classe de Controle
Classe de Exceção
Metaclasse
Classe Utilitária
114
<<entidade>>
Curso
Número do Curso
Nome
Localização
Dias do Curso
Número de Créditos
Hora de Início
Hora de Término
Adicionar um Aluno( )
Cancelar um Aluno( )
Obter Alunos Matriculados( )
Determinar se a turma está completa( )
<<entidade>>
Professor
Número do Empregado
Nome
Data de Admissão
Curso ministrado
Tipo de contratação
Salário
Consultar Lista de Alunos( )
Indicar suas Disciplinas( )
A notação usada pela UML para Estereótipos, dentro da representação
gráfica da Classe de Objeto, é colocá-lo entre << >> na área reservada
para o nome da classe e acima deste.
Classe fronteiriça. É uma classe que modela a comunicação entre a
vizinhança do sistema e suas operações internas.
Exemplos: Interface tipo Janela, Protocolo de Comunicação, Interface de
Impressão, Sensores etc.
No presente estudo de caso, sistema automatizado de Matrícula num
Curso, as classes de objeto Formulário em Branco e Sistema de
Cobrança são exemplos de estereótipos desta classe.
Classe de Entidade. É uma classe que modela objetos cuja informação
e o comportamento associados são, de maneira geral, persistentes.
No presente estudo de caso, sistema automatizado de Matrícula num
Curso, as classes de objeto: Lista de Cursos, Curso, Catálogo e
Matrícula, são exemplos de estereótipos desta classe.
Classe de Controle. É uma classe que modela o comportamento de
controle específico para uma ou mais "Use Case". Suas principais
características são:
• Cria, ativa e anula objetos controlados.
• Controla a operação de objetos controlados
115
• Controla a concorrência de pedidos de objetos controlados.
• Em muitos casos corresponde a implementação de um objeto
intangível.
No presente estudo de caso, sistema automatizado de Matrícula num
Curso, a classe de objeto Gerente de Registro é um exemplo de
estereótipo desta classe.
Interação entre objetos
A UML se utiliza de dois diagramas para representar a interação entre
os objetos: Diagrama de Seqüência e Diagrama de Colaboração.
Diagrama de Seqüência
Mostra a interação entre os Objetos ao longo do tempo. Apresentando
os objetos que participam da interação e a seqüência de mensagens
trocadas.
A notação usada pela UML para representar o Diagrama de Seqüência,
é a seguinte:
• Objetos são representados por retângulos com seus nomes
sublinhados.
• As linhas de vida dos Objetos são representadas por linhas
verticais tracejadas.
• As interações entre Objetos são indicadas por flechas
horizontais que são direcionadas da linha vertical que
representa o Objeto cliente para a linha que representa o Objeto
fornecedor.
• As flechas horizontais são rotuladas com as mensagens.
• A ordem das mensagens no tempo é indicada pela posição
vertical, com a primeira mensagem aparecendo no topo.
• A numeração é opcional e baseada na posição vertical.
Foco de Controle
Representa o tempo relativo que o fluxo de controle esta focalizado num
dado Objeto. Ele representa o tempo que um Objeto dedica a uma dada
mensagem.
116
Diagrama de Colaboração
É um modo alternativo para representar a troca de mensagens entre um
conjunto de Objetos. O Diagrama de Colaboração mostra a interação
organizada em torno dos Objetos e suas ligações uns com os outros.
A notação usada pela UML para representar o Diagrama de
Colaboração, é a seguinte:
• Objetos são representados por retângulo com seus nomes
sublinhados.
• As interações entre Objetos são indicadas por uma linha
conectando-os.
• As ligações indicam a existência de um caminho para
comunicação entre os Objetos conectados.
• As ligações no Diagrama de Colaboração podem ser
apresentada por:
¾ flechas apontando do Objeto cliente para o Objeto
fornecedor;
¾ nome da mensagem;
¾ numeração seqüencial, mostrando a ordem relativa de
envio das mensagens.
Como Descobrir as Classes de Objetos
Análise do "Use Case"
É o processo de examinar o conjunto de "Use Cases" para extrair os
Objetos e Classes do sistema sob desenvolvimento. Os Objetos e
Classes não podem ser obtidos do detalhamento dos Cenários
(instâncias de "Use Case").
Cenário para criar matrícula
Cleber entra com o número de identificação do aluno 369 53 3449 e
o sistema valida o número. O sistema pergunta a qual semestre
refere-se a matrícula. Cleber indica que é para o semestre atual e
escolhe a opção "nova matrícula".
Da lista de cursos disponíveis, Cleber seleciona como cursos de
primeira
escolha:
Engenharia
de
Softwarwe,
Sistemas
Computacionais Cliente Servidor, Tópicos em Análise Orientada a
Objetos 200 e Gerência de Mainframe 110. Ele seleciona como
117
cursos de segunda escolha: Introdução à Programação Java 200 e
Teoria da Música 300.
O sistema verifica que Cleber tem todos os pré-requisitos
necessários, examinando os registro do aluno e adiciona-o à lista de
alunos do curso.
O sistema indica que a atividade está completa. O sistema imprime a
matrícula e envia a informação de cobrança, referente aos quatro
cursos, para processamento no sistema de cobrança.
Objetos pertencente à Classe de Entidades
São identificados examinando-se os substantivos e frases
substantivadas no cenário. No cenário acima estão destacados os
substantivos candidatos a Objetos da Classe de Entidade. Os
substantivos podem ser: Objetos, descrição do estado de um Objeto,
entidade externa e/ou ator ou ainda nenhuma das anteriores.
Lista de substantivos e sua classificação
Cleber –– ator
número de identificação do aluno 369 53 3449 –– propriedade de
aluno
sistema –– o que está sendo definido
número –– propriedade do aluno
semestre –– estado (que é selecionado quando aplicável)
semestre atual –– mesmo que semestre
nova matrícula –– Objeto candidato
lista de cursos disponíveis –– Objeto candidato
cursos de primeira escolha –– estado
Engenharia de Softwarwe –– Objeto candidato
Sistemas Computacionais Cliente Servidor –– Objeto candidato
Tópicos em Análise Orientada a Objetos 200 –– Objeto candidato
Gerência de Mainframe 110 –– Objeto candidato
cursos de segunda escolha –– estado
Introdução à Programação Java 200 –– Objeto candidato
Teoria da Música 300 –– Objeto candidato
pré-requisitos necessários –– cursos com outra identificação
registro do aluno –– Objeto candidato
lista de alunos do curso –– Objeto candidato
atividade –– expressão
matrícula –– mesmo que nova matrícula
118
informação de cobrança –– Objeto candidato
quatro cursos –– informação necessária ao sistema de cobrança
sistema de cobrança –– ator
Lista de Objetos da Classe Entidade
nova matrícula –– Lista de cursos para um dado semestre de um dado
aluno
lista de cursos disponíveis –– Lista de todos os cursos que estão
sendo oferecidos no semestre
Engenharia de Softwarwe –– Um curso oferecido no semestre
Sistemas Computacionais Cliente Servidor –– Um curso oferecido no
semestre
Tópicos em Análise Orientada a Objetos –– Um curso oferecido no
semestre
Gerência de Mainframe –– Um curso oferecido no semestre
Introdução à Programação Java –– Um curso oferecido no semestre
Teoria da Música –– Um curso oferecido no semestre
registro do aluno –– Lista dos cursos feitos pelo aluno nos semestres
anteriores
lista de alunos do curso –– Lista com os alunos matriculados num
curso específico.
informação de cobrança –– Informações necessárias para o ator
sistema de cobrança.
Criando as Classes de Entidades
Baseando-se na similaridade de estrutura e de comportamento dos
objetos.
Lista de Classes de Entidades presentes no Cenário "Criar
Matrícula"
• Matrícula – Lista dos cursos para um dado semestre para um
dado aluno.
• Catálogo – Lista de todos os cursos oferecidos em um
semestre.
• Curso – Curso oferecido para um semestre.
• RegistroDoAluno – Lista dos cursos feitos anteriormente.
• ListaDosAlunosNumCurso – Lista dos alunos matriculados em
um curso específico.
• InformaçõesDeCobrança – Informações necessárias para o ator
sistema de cobrança.
119
Objetos pertencentes à Classe Fronteiriça
São identificados examinando-se cada par ator/cenário e criando-se as
classes fronteiriças óbvias. Classes Fronteiriças também são criadas
para comunicação sistema/sistema e para descrever a escolha de
protocolos de comunicação.
Exemplos de classe fronteiriça:
• Deve ser apresentado ao aluno, mais de uma opção do "Use
Case" "Matrícula nos Cursos". Para tanto, é criada a Classe
"FormulárioDeRegistro" para permitir ao estudante selecionar
a opção desejada.
• O aluno deve fornecer ao sistema a informação dos cursos
escolhidos.
Para
tanto
é
criada
a
Classe
"FormulárioDeMatrícula" para permitir que o aluno entre com a
informação.
• A matrícula do aluno é impressa. Para tanto é criada a Classe
"Impressora".
• A informação de cobrança é enviada para o sistema de
cobrança. Para tanto, é criada a Classe "SistemaDeCobrança".
Objetos pertencente à Classe de Control
Tipicamente, contêm a informação de seqüenciamento. Cada "Use
Case" deve ter uma Classe de Controle, responsável pelo fluxo de
eventos. Para tanto, é criada a Classe "GerenteDeRegistro". Essa
classe, para cada curso selecionado na Classe Fronteiriça
"FormulárioDeMatrícula" deve executar as seguintes atividades:
• Busca na Classe Curso seus pré-requisistos.
• Verifica, através da Classe RegistoDoAluno se todos os prérequsisitos do curso selecionado foram satisfeitos.
• Sabe o que fazer se um pré-requisito não foi satisfeito.
• Interroga se há vaga no curso.
• Se houver vaga, pede a classe ListaDeAlunosNoCurso para
adicionar o aluno.
• Sabe o que fazer se um dos quatro cursos não está disponível.
• Cria os objetos: MatrículaDoAluno e InformaçãoDeCobrança.
• Verifica se o sistema de cobrança está habilitado a receber a
informação de cobrança.
120
Cartão Class-Responsibility-Collaboration (CRC)
Novas Classe podem ser descoberta através do uso do cartão CRC. Um
CRC é um cartão que contém:
• Nome e descrição da Classe.
• As responsabilidades da Classe:
¾ Conhecimento interno da Classe;
¾ Serviços fornecidos pela Classe.
• Os colaboradores com essas responsabilidades:
¾ Um colaborador é uma classe cujos serviços são
necessários para execução de dada responsabilidade.
Nome da Classe: Curso
Responsabilidade
Adicionar Aluno (incrementar o no. vagas
preenchidas)
Conhecer pré-requisitos
Conhecer quando o curso é dado
Conhecer onde o curso é dado
Colaboradores
Aluno
Uma sessão com o uso do cartão CRC compreende
• Um grupo de pessoas é escolhido para representar um cenário.
• É criado um cartão para cada Classe de Objeto já identificado
dentro desse cenário.
• A cada participante é associada uma Classe, de modo que cada
pessoa torna-se aquela Classe.
• Um cenário é encenado pelos participantes.
• Os cartões são preenchidos com as responsabilidades e os
colaboradores.
• Novos cartões são criados para classes de Objetos descobertos
na sessão.
Benefícios do uso do cartão CRC
• À medida que os cenários são encenados, padrões de
colaboração emergem.
• Os cartões que colaboram entre si podem ser arranjados
fisicamente próximos.
121
• Esse arranjo ajuda a identificar hierarquias de generalização/
especialização ou agregação entre as classes.
• O uso do cartão CRC é mais efetivo para grupo iniciantes no
uso de técnicas OO.
O uso do cartão CRC permite
• Confirmar ou não as classes de objetos candidatas, além de
permitir a descoberta de novas Classes.
• Determinar o relacionamento entre as Classes.
• Identificar atributos (conhecimento interno) e operações
(serviços fornecidos).
À medida que Classes de Objetos são descobertas, elas são
documentadas nos diagramas de interação (diagramas de seqüência e
de colaboração) anteriormente confeccionados.
Pacote
As classes pertencentes ao Sistema de Matrícula podem ser agrupadas
em três pacotes:
•
•
•
•
•
•
•
•
•
•
•
•
•
•
ElementosDaUniversidade
Matrícula
Curso
RegistroDeAluno
Catálogo
ListaDosAlunosNumCurso
InformaçõesDeCobrança
RegrasDeNegócio
GerenteDeRegistro
Interfaces
FormulárioDeRegistro
FormulárioDeMatrícula
Impressora
SistemaDeCobrança
Definição de Pacote
É uma generalização com o propósito de organizar as Classes de
Objetos em grupos. Esta abordagem facilita a análise à medida que o
122
número de Classes de Objetos cresce em um do cenário.
A notação usada pela UML para representar pacotes é:
Relacionamentos
Definição de relacionamento
É a maneira como as Classes de Objetos interagem entre si para formar
o comportamento do sistema. Esse relacionamento é apresentado
através de Diagrama de Classes. Os dois principais tipos de
relacionamento são associação e agregação.
Associação
• É uma conexão bidirecional entre Classes que indica a
existência de um relacionamento entre os objetos dessas
Classes.
• É representada, nos Diagramas de Classe, por uma linha
conectando as Classes associadas.
• O fluxo de dados pode ser unidirecional ou bidirecional, através
da conexão.
• Para esclarecer o significado de uma associação, ela é
nomeada. No Diagrama de Classes, o nome é apresentado ao
longo da linha de associação. Usualmente, esse nome é um
verbo ou um frase verbalizada.
• Entre duas Classes, podem existir mais de uma associação.
Multiplicidade de Associação
• É o número de instâncias de uma classe relacionada com uma
instância de outra classe.
• Para cada associação, há uma multiplicidade em cada direção.
Associação Reflexiva
É quando os Objetos da própria Classe estão se relacionando.
Agregação
• É uma forma especializada de associação na qual um todo é
relacionado com suas partes. Também conhecida como relação
de conteúdo.
123
• É representada como uma linha de associação com um
diamante junto à Classe agregadora.
• A multiplicidade é representada da mesma maneira que nas
associações.
Um objeto da Classe FormulárioDeRegistro contém um único objeto
FormulárioDeMatrícula. Um objeto FormulárioDeMatrícula está
contido num único objeto FormulárioDeRegistro.
Agregação Reflexiva
É quando Objetos de uma Classe é composto de Objetos da própria
Classe.
Classe de uma Associação de Classe
Permite adicionar atributos, operações e outras características a uma
dada associação.
A classe de uma Associação de Classe normalmente é gerada a partir
de uma associação de muitos para muitos.
Relacionamento entre Pacotes
• Pacotes são relacionados uns com os outros usando um
relacionamento de dependência.
• Se uma Classe de um pacote interage com uma Classe de
outro pacote, a relação de dependência é adicionada a nível de
pacote.
• Relacionamento entre pacotes são obtidos a partir dos
diagramas de Classe e de Cenário.
Operações e Atributos
Definição de Operações
São procedimentos que executam as responsabilidades de uma Classe
de Objetos e portanto definem o comportamento dos objetos da Classe.
Uma operação é um serviço que pode ser requisitado por um Objeto
para realizar um comportamento. Operações devem ser nomeadas em
124
função de suas saídas e não em função de seus passos internos.
Definição de Atributos
São dados que caracterizam uma instância da Classe de Objetos.
Atributos não tem comportamento. Atributos são sempre valorados.
Cada valor de um atributo é particular para um dado objeto. Atributos
são nomeados por substantivo simples ou por verbo substantivado.
Cada atributo tem uma definição concisa e clara.
A notação usada pela UML é apresentar Atributos no segundo
compartimento da Caixa de representação de Classe de Objetos,
conforme mostra a figura acima.
Cada atributo tem tipo do dado e valor inicial, por exemplo, o tipo de
dado para o atributo horário é: hh:mm:ss e o valor inicial é 00:00:00.
A determinação dos atributos de uma Classe de Objetos pode ser
conseqüência de:
• Análise dos fluxos de evento nos "Use Case".
• Definição de uma Classe de Objetos.
• Conhecimento do sistema.
Comportamento
O comportamento de uma Classe de Objetos é representado através de
um Diagrama de Transição de Estado, que descreve o ciclo de vida de
uma dada classe, os eventos que causam a transição de um estado
para outro e as ações resultantes da mudança de estado.
O espaço amostral dos estados de uma dada Classe corresponde à
enumeração de todos os estados possíveis de um objeto.
O estado de um Objeto é uma das possíveis condições na qual o objeto
pode existir. O estado compreende todas as propriedades dos objetos
(estáticas) associadas aos valores correntes (dinâmico) de cada uma
dessas propriedades.
Estados e Atributos
Estados podem ser distinguidos pelos valores assumidos por certos
atributos.
125
Por exemplo, o número máximo de estudantes por curso, no "Use Case"
Matrícula do Aluno, é igual a 10.
Estados e Ligações
Estados também podem ser distinguidos pela existência de certas
ligações.
Exemplo
A instância da Classe Professor pode ter dois estados:
• Ensinando –– quando o Professor está ministrando um Curso.
• Licenciado –– quando não está ministrando nenhum Curso.
Estados Especiais
Estado Inicial
É o estado atribuído a um objeto quando é criado. O estado Inicial tem
as seguintes características:
• É mandatório.
• Somente um estado Inicial é permitido.
• O estado Inicial é representado por um círculo preenchido.
Estado Final
É o estado que indica o fim do ciclo de vida de um objeto. O estado Final
tem as seguintes características:
• É opcional.
• Pode existir mais de um estado final.
• estado Final é representado por um "olho de boi".
Eventos
Um evento é uma ocorrência que acontece em algum ponto no tempo e
que pode modificar o estado de um objeto, podendo gerar uma resposta.
126
Exemplo
• Adicionar um aluno a um curso.
• Criar um novo curso.
Transição
É a mudança do estado atual para o estado subseqüente como
resultado de algum estímulo. O estado subseqüente pode ser igual ao
estado original. Uma transição pode ocorrer em resposta a um evento.
As transições rotuladas com o nome dos eventos.
Condição de Guarda
A condição de guarda é uma expressão booleana de valores de atributo
que permitem que a transição ocorra somente se a condição assumida
pela expressão é verdadeira.
Ações
É uma operação que está associada a uma transição, ocorrendo
instantaneamente e que não pode ser interrompida. Nome de uma ação
é mostrado, na seta indicativa da transição, precedida por um barra
inclinada (/).
Envio de eventos a partir de outro evento
Um evento pode provocar o envio de outro evento. O nome do evento
enviado é mostrado, na seta indicativa de transição, precedido por um
circunflexo (^) seguido pelo nome da Classe para onde o evento será
enviado, separados por um ponto.
Exemplo
Evento1^Classe.Evento2, onde Evento1 é o evento que causou a
transição e Evento2 é o evento gerado a partir da transição.
Atividade
É uma operação que está associada a um estado, leva um tempo para
ser executada e que pode ser interrompida.
127
Envio de eventos a partir de atividade
Uma atividade também pode provocar o envio de um evento para um
outro Objeto.
Transição Automática
Algumas vezes, o único propósito da existência de um estado é
desenvolver uma atividade. Uma transição automática ocorre quando a
atividade é completada. Se múltiplas transições automáticas existem,
uma condição de guarda é necessária para cada transição e as
condições de guarda devem ser mutuamente exclusivas.
Download

Engenharia De Sistemas C