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.