Estrutura de Dados – Básica Professor: Osvaldo Kotaro Takai. Aula 6: Tipos Abstratos de Dados O objetivo desta aula é introduzir os conceitos envolvidos em Tipos Abstratos de Dados e explorar esses conceitos implementando-os com uma linguagem de programação orientada a objetos. Tratando os Problemas A primeira coisa com a qual nos confrontamos quando escrevemos programas é o problema. Normalmente, você é desafiado com problemas reais e você quer facilitar sua vida fornecendo um programa para o problema. No entanto, problemas reais são nebulosos e a primeira coisa que você faz é tentar entender o problema separando os detalhes necessários e descartando os desnecessários: Você tenta obter sua própria visão abstrata, ou modelo, do problema. Este processo de modelagem é chamado abstração e é ilustrado na figura abaixo. O modelo define uma visão abstrata do problema. Isso implica que o modelo focaliza apenas nos assuntos associados ao problema e para o qual você tenta definir as propriedades do problema. Dentre essas propriedades estão: a) os dados que são afetados no problema, e b) as operações que são identificadas no problema. Como exemplo, considere a administração de empregados em uma instituição. O chefe da administração pede que você crie um programa que permita administrar os empregados. Bem, isso não é muito específico. Por exemplo, quais informações de empregados são necessárias para a administração? Quais tarefas devem ser permitidas? Os empregados são pessoas reais que podem ser caracterizados por várias propriedades, tais como: • • • • • • • • nome, altura, data de nascimento, foto, número do rg, número da sala, cor do cabelo, passatempo. Certamente, nem todas estas propriedades não necessárias para resolver o problema da administração. Apenas algumas são relevantes ao problema. Consequentemente, você cria um 1 modelo de um empregado para o problema. Este modelo apenas indica propriedades que são necessárias para atender aos requisitos da administração, por exemplo, o nome, a data de nascimento e o número do rg. Estas propriedades são chamadas “dados do modelo (empregado)”. Agora você poderá que descrever as pessoas reais com a ajuda de um empregado abstrato. Naturalmente, uma mera descrição não será suficiente. Devem existir algumas operações para que a administração esteja apta a manipular os empregados abstratos. Por exemplo, deve existir uma operação que permita criar um novo empregado quando uma pessoa é contratada pela instituição. Consequentemente, você terá que identificar as operações que permitam manipular o empregado abstrato. Você também decide que os dados de empregados devem ser acessados apenas pelas operações associadas. Isso irá garantir que os elementos de dados sempre estejam num estado apropriado. Por exemplo, você terá condições de verificar se uma data fornecida é válida. Para resumir, a abstração é a estruturação de um problema nebuloso em entidades bemdefinidas através de seus dados e operações. Consequentemente, tais entidades combinam dados e operações. Eles não estão desassociados. Propriedades dos Tipos Abstratos de Dados O exemplo anterior mostra que com a abstração você cria entidades bem-definidas e que podem ser manipuladas apropriadamente. Essas entidades definem a estrutura de dados de um conjunto de itens. Por exemplo, cada empregado administrado tem um nome, data de nascimento e número do rg. A estrutura de dados só pode ser acessada pelas operações definidas. Esse conjunto de operações é chamado de interface e é exportado pela entidade. Uma entidade que tenha essas propriedades descritas é chamada de tipo abstrato de dados (TAD). A figura abaixo ilustra um TAD que consiste de uma estrutura de dados e operações. As operações que são visíveis pelo lado de fora definem a interface. Quando um novo empregado é “criado”, a estrutura de dados é preenchida com os valores reais: Você agora tem uma instância de um empregado abstrato. Você pode criar quantas instâncias de um empregado abstrato quanto forem necessárias para descrever todas as pessoas realmente empregadas. Vamos tentar colocar as características de um TAD de maneira mais formal: Definição de Tipo Abstrato de Dados: Um tipo abstrato de dados (TAD) é caracterizado pelas seguintes propriedades: 1. Ele exposta um tipo. 2. Ele exporta um conjunto de operações. Esse conjunto é chamado de interface. 3. As operações da interface são os únicos mecanismos de acesso à estrutura de dados do tipo. 4. Axiomas e pré-condições definem o domínio da aplicação do tipo. Com a primeira propriedade é possível criar mais do que uma instância de um TAD com exemplificado no exemplo de empregado. 2 A importância de encapsular a estrutura de dados O princípio de esconder a estrutura de dados utilizada e fornecer apenas uma interface bemdefinida é conhecido como encapsulamento. Por que é tão importante encapsular a estrutura de dados? Para responder a esta pergunta, considere o seguinte exemplo matemático onde queremos definir um TAD para os números complexos. Para isso, é necessário apenas saber que os números complexos consistem de duas partes: a parte real e a parte imaginária; ambas representadas por números reais. Os números complexos definem várias operações: adição, subtração, multiplicação e divisão, entre outras. Axiomas e pré-condições válidos são os definidos pela matemática dos números complexos. Por exemplo, existe um elemento neutro para a adição. Para representar um número complexo é necessário definir a estrutura de dados utilizada pelo TAD. Podemos pensar no mínimo em duas possibilidades para fazer isso: • As partes são armazenadas num vetor de duas posições, onde a primeira posição guarda a parte real e a segunda a parte imaginária do número complexo. Se você deseja obter a parte real em x e a parte imaginária em y, você acessar diretamente as posições do vetor: x= c[0] e y = c[1]. • As partes são armazenadas num registro com dois elementos. Se o nome do elemento da parte real é r e a parte imaginária i, x e y podem ser obtidos fazendo: x = c.r e y = c.i. O terceiro item da definição de TAD diz que cada acesso à estrutura de dados deve possuir uma operação definida. O exemplo de acesso exemplificado anteriormente parece ir contra a este requisito. Isso é verdade? Permita-nos verificar novamente as duas possibilidades de representar números complexos. Vamos nos ater à parte real. Na primeira versão, x recebe c[0]. Na segunda versão, x recebe c.r. Nos dois casos, x recebe “alguma coisa”. Essa “alguma coisa” varia de acordo com a estrutura de dados utilizada. Mas em ambos os casos a operação realizada “recebe” possui o mesmo significado: atribuir à variável x a parte real de um número complexo c; e nos dois casos, esta semântica é alcançada. Se você pensar em operações mais complexas o impacto de separar a estrutura de dados de suas operações ficará mais evidente. Por exemplo, a adição de dois números complexos exige que você realize uma adição para cada parte. Consequentemente, você deve acessar o valor de cada parte que é diferente para cada versão. Ao fornecer uma operação “add” você pode encapsular esses detalhes de quem for utilizá-la. Num contexto de uma aplicação, você simplesmente “adiciona dois números complexos” sem pensar em como essa funcionalidade é realmente executada. Uma vez que você tenha criado um TAD para os números complexos, digamos complex, você pode usá-lo da mesma forma que os tipos de dados bem-conhecidos, tais como o int, float, entre outros. Resumindo: A separação da estrutura de dados e operações, e a restrição de somente acessar a estrutura de dados via um interface bem-definida, permitem que você escolha a estrutura de dados mais apropriada para o problema que você está resolvendo. Criando TAD complex em linguagem C Imagine que a sua equipe precise desenvolver um sistema, em linguagem C, que faz uso intenso de números complexos como, por exemplo, um sistema para controle do consumo de energia elétrica. Após algumas discussões, a equipe decidiu que seria necessário criar uma única biblioteca de funções que manipule números complexos, para facilitar o desenvolvimento do sistema. A pessoa que ficou responsável para desenvolver tal biblioteca foi você. A melhor forma de criar essa biblioteca é fazer uso dos conceitos de TAD. Infelizmente, a linguagem C não possui mecanismos explícitos para criar um TAD. No entanto, você pode implementar um TAD nesta linguagem criando 3 arquivos separados. O primeiro arquivo (complexo.h) conterá as definições das interfaces (protótipos de funções) e a definição da 3 estrutura de dados. O segundo arquivo (complexo.c) conterá as implementações das interfaces e das funções auxiliares (funções que não fazem parte da interface). O terceiro arquivo (main.c) faz uso do TAD complex. Para implementar este exemplo em Dev-C++, crie um novo projeto com o nome TAD complex e associe mais dois arquivos ao projeto além do main.c. Veja a figura abaixo: Em cada arquivo, inclua os códigos correspondentes descritos abaixo. Analise atentamente o código contido nesses três arquivos: Arquivo 1: complexo.h Lembre-se que os arquivos com extensão “h” (h de header) são arquivos que devem conter apenas as definições de tipos, constantes e cabeçalhos de funções. Este arquivo foi construído tendo em mente que as funções set, add, sub, mult e notation são as únicas operações de números complexos que a sua equipe irá precisar para construir o sistema; ou seja, todas as interfaces do seu TAD complex. Este arquivo deve ser distribuído à equipe juntamente com a biblioteca que ainda não foi construída, mas a sua implementação encontra-se no arquivo complexo.c. 4 Arquivo 2: complexo.c Note que o arquivo complexo.c acima é um programa normal em linguagem C. A única diferença é que ele não possui a função principal (main). Nesse arquivo são implementadas todas as operações, sejam de interface ou auxiliares (funções que auxiliam na implementação das interfaces, no caso: getR() e getI()). 5 Para verificar se a sua biblioteca funciona, crie um programa que use a sua biblioteca, como por exemplo, o programa main.c abaixo. Arquivo 3: main.c Este programa pode servir de exemplo para que a sua equipe saiba como utilizar a sua biblioteca. A seguinte saída será apresentada quando esse programa for executado: Observe que após a compilação desse programa, o arquivo complexo.o foi criado no diretório do projeto. Esse arquivo é o programa-objeto, versão em código binário do programa complexo.c. Agora, para concluir, distribua para a sua equipe os arquivos complexo.h e complexo.o como sendo a biblioteca e o arquivo main.c como um programa que exemplifica a utilização dessa biblioteca. Note que você não distribuiu o arquivo complexo.c. Isso normalmente é feito para evitar que alguém altere indevidamente a sua biblioteca. Utilizando o TAD complex Os membros de sua equipe podem fazer uso de sua biblioteca incluindo em seu programafonte o arquivo complexo.h (da mesma forma que foi feito no programa main.c), e indicando ao compilador para linkeditar o arquvio.o fornecido. Isso é feito em Dev-C++ seguindo os seguintes passos: 6 a) Crie um novo projeto, por exemplo: UsandoTADComplex e digite o programa que use o TAD Complex. b) Selecione a opção “Opções do Projeto” do menu Projeto. c) Na aleta Parâmetros, pressione o botão “Adicionar”. 7 d) Navegue até o local onde o arquivo complexo.o foi copiado e selecione na caixa de lista Arquivos do tipo: a extensão Object (*.o, *.obj). e) Selecione o arquivo complexo.o e pressione o botão “Abrir”. 8 f) Agora, o arquivo complexo.o será ligado pelo Dev-C++ quando o programa quando for compilado. Implementando o TAD complex com classes da linguagem C++ A linguagem C++ fornece melhores mecanismos para implementar um TAD. A intenção desta seção não é apresentar todos os conceitos envolvidos na programação orientada a objetos. A idéia aqui é simplesmente apresentar uma outra forma de implementar um TAD utilizando alguns mecanismos da linguagem C++. Para maiores detalhes sobre a linguagem C++, consulte http://www.cplusplus.com/doc/tutorial/index.html ou http://oopweb.com/CPP/Files/CPP.html. Arquivo 1: complexoOO.h 9 Arquivo 2: complexoOO.cpp Arquivo 3: main.c 10 Tipos de dados abstratos genéricos Este assunto será discutido na aula 8, quando for discutido a estrutura de dados Pilha. Pré-condições e Pós-condições As pré-condições e pós-condições permitem que o programador especifique as rotinas que ele deve implementar. Frequentemente o programador necessita comunicar precisamente o que uma rotina deve realizar, sem qualquer indicação de como ela deve ser implementada. Por exemplo, suponha que você seja o chefe de um grupo de programadores e você quer que um dos programadores escreva uma função como parte de um projeto. As pré-condições e pós-condições são declarações de requisitos de uma rotina. A declaração da pré-condição indica o que deve ser verdadeiro antes que a rotina seja chamada. A declaração da pós-condição indica o que deve ser verdade quando a rotina terminar o seu trabalho. Por exemplo: As pré-condições e pós-condições devem aparecer como comentários no seu programa e são colocadas normalmente depois do cabeçalho de uma rotina. A pós-condição sempre indica qual trabalho deve ser realizado pela rotina. Neste caso, quando a função finalizar, a raiz quadrada de n deve ser retornada. 11 Note que ehVogal(‘A’) deve retornar 1 (verdadeiro); ehVogal(‘Z’) deve retornar 0 (falso); e ehVogal(‘?’)? Ninguém sabe, pois a pré-condição está sendo violada. O programador que chama a função é responsável por certificar que a pré-condição seja válida quando o procedimento é chamado. Por outro lado, programadores cuidadosos também seguem as seguintes regras: • Quando você escreve uma função, você deve detectar em quais condições uma précondição será violada. • Se você detectar que uma pré-condição foi violada, então imprima uma mensagem de erro e aborte o programa. 12 Vantagens: • Descreve resumidamente o comportamento de uma função, sem entrar em detalhes de implementação. • Posteriormente, você pode reimplementar a função de uma nova forma, entretanto, os programas, os quais dependem exclusivamente da pré-condição e pós-condição, continuarão a funcionar sem alterações. Exercícios Estenda as duas implementações do TAD complex com as seguintes operações: a) Módulo de um número complexo. b) Divisão de dois números complexos. c) Verificar se dois números complexos são iguais. Retornar 1 se for igual e 0 caso contrário. 13