Baixe o app para aproveitar ainda mais
Prévia do material em texto
Paradigmas de linguagens de programação Aula 6: Tipos compostos e abstratos de dados em linguagens de programação Apresentação Armazenar dados temporariamente em memória é crucial para as linguagens de programação (LPs) executarem com presteza sua tarefa: permitir aos programadores escrever programas que recebam dados e os transformem em informação. Ao estudar uma linguagem de programação (LP), precisamos conhecer os tipos de dados que podem ser manipulados. A capacidade de uma LP lidar com uma variedade de dados baliza suas possibilidades de armazenar diferentes tipos de dados, as operações disponíveis sobre eles e a forma como podem ser estruturados e usados. Os tipos de dados estruturados nos permitem armazenar dados complexos, combinando aqueles simples e estruturados. Nesta aula, apontaremos os diferentes tipos de dados estruturados que uma LP pode implementar, exempli�cando com linguagens bem conhecidas, e quais são os tipos abstratos de dados (TAD) permitidos por LPs. Objetivos Anunciar os tipos de dados compostos ou estruturados em LPs; Registrar os conceitos e as aplicações de TAD. Tipos de dados estruturados ou compostos Os tipos estruturados ou compostos são de�nidos a partir de tipos de dados simples e/ou estruturados. Figuram nesse rol: 1 Registros (produto cartesiano). 2 Vetores. 3 Matrizes (mapeamentos). 4 Arquivo. 5 Listas encadeadas (tipos recursivos). 6 Cadeias de caracteres (string). Pessoas interagindo em processo de analise de dados (Fonte: Shutterstock). Atenção! Aqui existe uma videoaula, acesso pelo conteúdo online Figura 1: Tipos compostos ou estrutura. Observe a seguir o detalhamento de cada um desses itens abordados: Considerações sobre tipos de dados estruturados compostos 1.1. Produto cartesiano Combina valores de tipos de dados diversos em duplas, tendo como exemplos os registros em Modula-2, Pascal, Ada, Cobol e Delphi, bem como as estruturas de C e C++. Exemplo Um produto cartesiano pelas structs do C: Struct Pessoa = { Struct Funcionario { Char primeiro[20]; char nome nomefunc; char meio[20]; float salario; char Ultimo[20] } func; }; Foram de�nidas acima duas estruturas (struct), que, implementando conceitos de produto cartesiano em C, encapsulam os dados em uma estrutura. Isso irá facilitar a vida do programador, que vai fazer referência à estrutura como um todo: A variável func é do tipo Funcionário. O acesso ao primeiro nome seria: func.nomefunc.primeiro; O acesso ao nome do meio seria: func.nomefunc.meio. Exemplo Em Pascal e C, as estruturas equivalentes às duas acima seriam: Type TPessoa = record TFuncionario = record primeiro:string [20]; Nomefunc:TPessoa; meio:string[20]; Salario:Real; ultimo: string[20]; end; end; Var Func, Funcant: Tfuncionario; Algumas LPs – como Pascal – possuem facilidades para referenciar os registros. Exemplo Cláusula With: With func do with func.nomefunc do Begin ;begin Nomefunc.primeiro:=`Carlos`; primeiro:=`Carlos`; Nomefunc.meio:=`Jose`; meio := `Jose`; Nomefunc.ultimo:=`Tavares`; ultimo := `Tavares`’ End; ;end; Quanto aos registros (produto cartesiano), a maioria das LPs permite atribuição direta entre eles. Exemplo Em Pascal, há duas variáveis do tipo TFuncionario, que é um registro. Ada e C também permitem isso. Já a atribuição de valores com um agregado é permitida em C. Em Pascal, por sua vez, a atribuição de dados deverá ser feita elemento a elemento: Exemplo Struct data {int dia, mes, ano;}. Struct data = {14,3,2019}. Em LPs orientadas a objeto, produtos cartesianos são de�nidos a partir das classes, como, por exemplo, Java. C++, contudo, mantém o conceito de classe e registro (struct) para não perder a compatibilidade com a linguagem C. Os registros são armazenados em posições consecutivas de memória usando o acesso por endereço base e deslocamento. 1.2. Uniões Junção de diferentes tipos de dados para formar um novo tipo. Pode ser implementado pelas LPs de duas formas: a) Uniões livres Formato de dados que pode armazenar tipos diferentes, porém apenas um tipo de cada vez. Uma estrutura que implemente o conceito de produto cartesiano, como o struct em C e C++, pode armazenar diferentes tipos de dados: um int, um double e um char. Uma união (union) somente pode armazenar um int ou um double ou um char. Exemplo Equivalence do Fortran e o union de C e C++. Mas aí surge um problema: isso pode violar o sistema de tipos da LP, pois, se usarmos o elemento da união que não foi inicializado, teremos valores inde�nidos para essa posição de memória. Por isso, Java não possui união. Exemplo Union em C, C++: union exemplo; {int val_int; long: val_long; double: Val_double; } O modelo de implementação, em geral, reserva o espaço para abrigar o maior componente da união, que é compartilhado com os demais elementos. Pelo exemplo acima, double, que ocupa mais espaço em memória, irá determinar o tamanho do espaço reservado para a união. b) Uniões disjuntas Neste tipo de união, não há interseção entre o conjunto de valores dos tipos que se formam. Em geral, usa-se um dos campos para ser uma tag (marcação) a identi�car essa união. O modelo de implementação é igual ao das uniões livres. Exemplo Aparece em registros variáveis de Pascal, Modula-2 e Ada, na cláusula rede�nes do Cobol e na union do Algol 68. Em Pascal, o acesso ao TAG e ao conteúdo variável será feito da mesma forma que nos records (que são os registros desta LP). Decimal e Fracionária são as tags dos tipos da união. Considere que a variável Num tem valor decimal igual a 1.5. Se Num.Tag=decimal, Num.val será, portanto, igual 1.5. Se o programa tentar acessar num.numerador, pode haver um erro de execução caso a veri�cação (de tipos) seja dinâmica, pois não teremos conteúdo valido (e sim lixo de memória) no elemento em questão: Type TRepresentacao = (decimal, fracionaria) TNumero = RECORD case Tag:Representacao Of Decimal: (Val:Real) Fracionaria: (numerador, denominador: integer) END; Var Num: TNumero; 1.3. Conjunto No tipo de dado Conjunto (ou Conjunto potência), o conjunto de valores corresponde a todos os possíveis subconjuntos formados a partir de um tipo base Conj. Exemplo Conj= {a,b,c}. Os subconjuntos são: {}, {a},{b},{c},{a,b},{a,c},{b,c},{a,b,c}. Em geral, as operações básicas permitidas pelas linguagens a esse tipo de dado são as mesmas da teoria de conjunto: Pertence; Contém; Está contido; União; Diferença; Diferença simétrica; Interseção; Igualdade; Desigualdade; Atribuição. Atenção Nem todas essas operações, obviamente, serão implementadas nas LPs por questões diversas. Usando conjunto em um trecho de código em Pascal, exempli�caremos as possibilidades de operações sobre conjuntos permitidos por esta LP. Exemplo Type Carros = (Honda Civic, Renault Fluence, Toyota Corola) ConjCarros = SET of Carros; Var Carro: Carros; CarrosSeda : ConjCarros; Begin Carro:=`Honda Civic`; CarrosSeda := [Renault Fluence, Toyota Corola]; //Atribuição de valores CarrosSeda := CarrosSeda + :=`Honda Civic`; // união CarrosSeda := CarrosSeda * ` Toyota Corola`; // interseção If Carro in CarrosSeda Then // Pertinência If CarrosSeda >= [Honda Civic, Toyota Corola] Then // Contém End. Ainda em Pascal, declararemos a criação de uma variável Car, que é um conjunto de caracteres, com o código a seguir. Car pode assumir qualquer subconjunto dos caracteres da LP. Exemplo Type Caracter = set of char; Var Car : Caracter; 1.4. Mapeamentos A implementação mais comum do conceito de mapeamento é o �nito, que é representado pelos vetores (ou arrays). O acesso ao elemento de um vetor se dá pelo índice; do contrário, haverá um mapeamento (�nito) do conjunto índice para conjunto dos elementos.1.4.1. Mapeamentos �nitos de uma dimensão Considere a seguinte declaração de um vetor em Pascal: Vet: Array [1..50] of integer; Foi declarado um vetor de 50 posições, cujo conteúdo de cada elemento tem valores inteiros, conforme ilustra a �gura a seguir. A declaração simboliza Array: Figura 2: Representação de um vetor (mapeamento finito). Isso signi�ca que qualquer valor possível do vetor corresponde a um mapeamento do conjunto índice (1..50) para o conjunto dos inteiros. Não existirão valores possíveis para índices não mapeados, como o 51. Os elementos dos vetores são acessados pelo seu índice. Por isso, eles são chamados de indexados: Vet[2]:= 15. A posição de índice 2 do vetor Vet recebe o valor 15 (vide a �gura acima). Dica As LPs, em geral, usam parênteses ( ) ou colchetes [ ] para as indexações. O conjunto índice deve ser �nito e discreto, sendo, portanto, enumerável. A maior parte das LPs restringe o índice a valores inteiros. Algumas linguagens, como Java, C e C++, de�nem como zero o limite inferior do índice (índice inicial). Em outras LPs, como Pascal e Ada, tais limites são de�nidos pelo programador. Exemplo Em Pascal, o índice pode ser qualquer valor enumerável, sendo válida a declaração abaixo: Vet: Array [`a`..`z`] of integer; Declarar um vetor de 26 posições será igual às 26 letras do alfabeto. Cada elemento do vetor pode armazenar valores inteiros. Os índices são letras (a..z), como aponta esta �gura: Figura 3: Representação de um vetor em Pascal com índices como caracteres. Linguagens como Ada, Modula-2, Pascal e Java implementam a veri�cação de tipos de forma dinâmica (tempo de execução), o que aumenta a con�abilidade, embora reduza a e�ciência da LP. Isso ocorre porque qualquer acesso ao vetor requer veri�cação do índice, o que é agravado quando, dentro de uma estrutura de repetição, se deverá inicializar com zero cada elemento do vetor Vet. Por outro lado, linguagens como C, C++ e Fortran não fazem a veri�cação de índices, comprometendo, com isso, a con�abilidade do programa. Tenha cuidado, pois alguns erros em C não são detectados. Exemplo O código int vet[7]; declara um vetor com sete posições (índices de 0 a 6) de valores inteiros. Já vet[13]:=200; faz uma atribuição válida, mas, neste caso, errada, pois não há índice 13. A tabela a seguir ilustra um resumo dos possíveis tipos de vetores que podem ser implementados em LPs segundo dois critérios: Tamanho e tempo de de�nição do vetor; Momento e local de sua alocação em memória. Categoria de vetores Tamanho Tempo de definição Alocação Local de alocação Exemplos de LPs Estáticos Fixo Compilação Estática Base Fortran 77 Semiestáticos Fixo Compilação Dinâmica Pilha Pascal, C, Modula-2 Semidinâmicos Fixo Execução Dinâmica Pilha Algol 68, Ada Dinâmicos Variável Execução Dinâmica Monte Apl, Pearl Tabela 1: Tipos de vetores que podem ser implementados. As linguagens citadas na coluna “Exemplos de LPs” mostram a categoria de vetor que melhor caracteriza a LP, mas isso não signi�ca que essa seja a única forma. Exemplo C e C++ também permitem a implementação de vetores estáticos. Iremos diferenciar agora nesta tabela as categorias de vetores apresentadas na coluna 1 da tabela anterior: Categoria de vetor Alocação Vantagem Desvantagem Estáticos Exemplo em C: Static int vet[10]; No início da execução, ficam em uma posição fixa chamada Base. Permanecem ali por toda a execução. Eficiência. Consomem eventualmente mais memória que o necessário se a maximização de elementos não for simples. Semiestáticos Exemplo em C: Int vet[10]’ Alocados em pilhas sempre que o bloco em que estiverem definidos for executado. São mais econômicos com memória por alocarem o vetor na região de sua execução. Redução da eficiência. Semidinâmicos Exemplo em Ada: Get (Tam); Declare Lista: array[1..tam] of integer Alocados em pilhas sempre que o bloco em que estiverem definidos for executado. Flexibilidade, pois não precisam definir tamanho máximo para o vetor. Tamanho do vetor só é conhecido na execução. Dinâmicos Exemplo em APL: A ← (2,3,4) A ← (2,3,4,15,20) Alocados em qualquer ponto da execução. Tamanho do vetor pode ser alterado em execução. Tabela 2: Categorias de vetores possíveis nas LPs. Vetores semidinâmicos podem ser implementados em C, C++ e Java se considerarmos que tais vetores podem ser alocados em pilha. Exemplo Em C, usamos ponteiros, bem como as funções malloc e free, para gerenciar a alocação de espaço em memória. Já em C++, serão utilizados ponteiros e operadores new e delete. Na linguagem Java, por sua vez, basta criar um objeto do tipo vetor. Na tabela a seguir, podemos conferir as implementações em C, C++ e Java de vetores semidinâmicos. Esse raciocínio pode ser usado para alocar vetores dinâmicos nas respectivas LPs: C C++ Java Void f (int a) { int *p; p=(int *); malloc (a * sizeof(int)); p[0]=10; free (p); } Void f (int a) { int * P; p=new int[a] p[0]=p[10]; delete [ ] p; } Void f (int a) { int p [ ]; p = new int [a]; p[0]=10; Tabela 3: Implementação de vetores semidinâmicos em LPs. 1.4.2. Mapeamentos �nitos multidimensionais A maioria das LPs implementa vetores multidimensionais. O vetor com duas dimensões (linha e coluna) chama-se matriz. Exemplo A matriz pode ser declarada da seguinte forma em Pascal. Note que ela passa a ser acessada por dois índices de [1..5] na declaração: Type TMatriz = array [1..5,1..5] of integer; Var Matriz : TMatriz; Nos comandos escritos em Pascal da imagem a seguir, o primeiro índice acessa a linha; o segundo, a coluna: Begin Matriz[1,1] := 11; // linha 1, coluna 1 Matriz[1,2] := 12; // linha 1, coluna 2 Matriz[3,3] := 33; // linha 3, coluna 3 Matriz[2,2] := 22; // linha 2, coluna 2 End. Agora con�ra, nesta �gura, como �ca o preenchimento da matriz com os três comandos acima. Eles são comandos de atribuição que atribuem valores a posições especí�cas da matriz: Figura 4: Matriz preenchida após os comandos de atribuição. Na maioria das LPs, um vetor multidimensional é regular, ou seja, todas as suas linhas têm as mesmas quantidades de colunas: o número de elementos de cada dimensão é �xo. Exemplo Em Java, além desse número �xo, pode-se lançar mão de vetores em que uma ou mais dimensões variem a quantidade de elementos. Na prática, eles são vetores unidimensionais cujos elementos também o são. Veri�que outras peculiaridades relativas a vetores em geral nesta tabela: LP Característica C Oferece inicialização de vetor, além da indexação int lista [ ] = {4,5,7,90,90}. Fortran 70 Permite manipulação de parte do vetor (subconjunto). APL Fornece o mais amplo conjunto de operações sobre vetores, incluindo soma, subtração, multiplicação, transposição e inversão de matrizes. Tabela 4: LPs implementando vetores em geral. 1.5. Cadeia de caracteres (string) String é uma sequência �nita de caracteres. Eles são úteis para armazenar dados não numéricos, como nome de pessoas, de disciplinas, de professores etc. Na prática, não há consenso entre as LPs sobre a classi�cação desse tipo de dado. Exemplo Pearl trata string como tipo simples ou primitivo de dados. Outras linguagens, como Pascal e C, o veem como mapeamento �nito: as operações sobre strings são as mesmas usadas para manipular vetores. Tais LPs, em geral, implementam um conjunto de procedimentos e funções prede�nidas para manipular strings. Já Java prefere tratar string como uma classe da biblioteca padrão com as operações pertinentes, enquanto Pearl oferece poderosos recursos de operações sobre strings. A tabela 5 ilustra as possíveis formas de implementação de string pelas LPs: Forma de implementar Característica Exemplo de LP Estática O tamanho da string é definido em tempo de compilação e não pode ser alterado. Cobol. Dinâmica limitada O tamanho da string é definido em tempo de compilação e pode ser alterado até seu limite máximo. C. O final da string é indicado pelo caractere nulo. Em dialetos doPascal, o índice 0 armazena o tamanho da string. Dinâmica O tamanho da string pode ser alterado livremente em tempo de execução. Pearl, Snobol e APL. Tabela 5: LPs implementando string. 1.6. Tipos recursivos Ser recursivo é de�nir algo em termos de si mesmo. No caso dos tipos de dados recursivos, os valores são compostos por valores do mesmo tipo. A implementação do princípio da recursividade para dados é feita em muitas LPs pelo tipo de dado ponteiro. A partir dos ponteiros (representado por * nos trechos de código abaixo), de�nem-se os tipos recursivos: Na linguagem C Na linguagem C++ struct no { int elem; struct no* prox; } class no { int elem; no* prox; } Tabela 6: Uso de ponteiro em C e C++. Já em LPs orientadas a objetos, como Java, os tipos recursivos são de�nidos sem a necessidade de ponteiros. Exemplo Trecho de declaração: class no { int elem; no prox; } 1.6.1. Tipo ponteiro Ponteiro é um conceito de baixo nível relacionado à arquitetura de computadores (endereçamento indireto de memória) no que se refere à necessidade de as LPs implementarem alocação dinâmica (em tempo de execução) de dados. Com o uso de mapeamentos �nitos (vetores em geral), a alocação de memória sempre será, na prática, desperdiçada, pois maximiza-se o tamanho do vetor, embora possa haver posições que nunca serão usadas. A alocação dinâmica resolve esse problema, pois cada elemento será alocado em tempo de execução à medida que a necessidade surgir. O tipo ponteiro tem como conteúdo um endereço de memória. Ele, portanto, aponta para um endereço de memória. Já o conteúdo de um ponteiro com valor nil ou null não aponta para nenhuma posição de memória. Exemplo Linguagens como Pascal, Modula-2, C, Ada e C++ usam ponteiros para permitir a criação de estruturas de dados complexas (listas, �las e pilhas encadeadas, grafos etc.). A linguagem C possui poderosos recursos para manipular estruturas usando ponteiros, porém, como sempre ocorre com esta linguagem, quanto mais recursos, haverá menos legibilidade no código. A �gura adiante exibe o conceito de ponteiro. Em Pascal, é criada a variável Pont, que é um ponteiro para um inteiro: Figura 5: Ponteiro para um inteiro. A variável Pont, do tipo ponteiro, tem como conteúdo o endereço da posição de memória onde está o dado inteiro, ou seja, o endereço de memória para o qual ele aponta. O dado inteiro não está na variável Pont, e sim na posição de memória que ele tem como conteúdo. Na próxima �gura, a linha Memo mostra a posição de memória em decimal. Na variável Pont, um ponteiro para inteiro está na posição 12 de memória. Já o conteúdo da posição 12 de memória é 20, que indica a posição de memória onde está o dado inteiro, enquanto o conteúdo da posição 20 é 33, representando o número inteiro para o qual Pont aponta. Figura 6: Exemplificando ponteiro. Dentre os possíveis problemas no uso das variáveis do tipo ponteiro, destacam-se: Baixa legibilidade, pois o entendimento do código não é simples, a menos que se use com muito cuidado e boa técnica; Possibilidade de prejudicar o endereçamento de memória, deixando posições inacessíveis e sem limpeza de memória, o que limita o espaço para endereçar novas variáveis. Atividade 1. Sobre os tipos de dados compostos ou estruturados, avalie: I. Os registros encapsulam diferentes tipos de dados, di�cultando o programador para referenciar os dados contidos nos registros. II. Os vetores podem ter uma ou mais dimensões e são estruturas de dados homogêneas, ou seja, armazenam dados do mesmo tipo. III. Os ponteiros são tipos de dados recursivos cujos conteúdos são endereços de memória. IV. Não é possível combinar estruturas de dados, como, por exemplo, termos um vetor cujo conteúdo é formado por registros. Com base em sua análise, marque a opção que apresenta apenas as assertivas corretas. a) II e III b) II c) I d) II e IV e) I, II, III e IV Tipos abstratos de dados (TAD) De uma forma genérica, um TAD pode ser de�nido como um modelo que encapsula dados e os procedimentos a eles necessários para determinada �nalidade. Isso implica dizer que todo e qualquer processamento sobre os dados encapsulados (protegidos) ocorrerá pelos procedimentos que devem estar de�nidos no modelo TAD. A implementação de um TAD em uma LP será realizada em duas partes: Atenção! Aqui existe uma videoaula, acesso pelo conteúdo online Qualquer alteração na implementação da estrutura de dados TAD não afetará as partes do programa que o usarem. Conceito elementar para a teoria da orientação a objeto, TAD foi a estrutura percursora do conceito de classe. Trata-se da base para a implementação de objetos em memória. Reaproveitou-se o TAD em vários programas, módulos ou sistemas para que seu utilizador conheça apenas a sua interface (operações disponibilizadas sobre os dados), mas não saiba detalhes de como ele está implementado. Isso é tarefa do programador. Exemplo Em linguagens estruturadas como C, a implementação de TAD é realizada com a de�nição de tipos e a implementação de funções. Na tabela a seguir, o lado esquerdo exibe um programa em C usando o TAD; o lado direito, as diferentes possibilidades de implementação desse modelo. Para o programa que o usar, será indiferente a forma de implementação do TAD, pois ele utilizará a interface com as operações desse modelo. Programa usuário do TAD Implementação do TAD Int main () { Lista *L; int num=10 L = cria_lista (); Insere (L, num); ....... ...... } 1) Com vetor void insere (Lista *l, int dado) { l -> arranjo [ ] = dado; } 2) Com lista simplesmente encadeada (usando ponteiro) void insere (Lista *l; int dado); { Celula *c – cria_celula (dado); L -> ultimo = c; } Tabela 7: Uso e implementação do TAD. Conforme ilustra a imagem ao lado, o uso de TAD permite o reuso de um código: a mesma implementação pode ser usada por diferentes programas. Figura 7: Uso x implementação do TAD. Vejamos as bases conceituas sobre as quais está sedimentado o conceito do TAD: Abstração Capacidade de se concentrar nos aspectos de interesse em um determinado contexto, ignorando o que não tem relevância. Ao de�nirmos um TAD, nos concentramos nas operações e abstraímos a sua implementação. Encapsulamento Separar a especi�cação da implementação, protegendo os dados. Apenas as operações do TAD alteram os dados. Reutilização Uma vez implementado e testado, o TAD pode ser usado por diferentes programas. Pode-se também alterar a lógica do programa sem reconstruir as estruturas de dados. Independência e portabilidade de código Poder de alterar a implementação do TAD sem afetar o seu uso por um ou mais módulos, programas ou sistema. A implementação muda, mas sua funcionalidade (realizar operações) segue sem alteração.. Atenção! Aqui existe uma videoaula, acesso pelo conteúdo online Atividade 2. Como se chama o tipo de dado que está sendo implementado pelo trecho de código abaixo em Pascal? Está correta a seguinte a�rmativa: Program Exemplo; Type TCaracteres = set of char; Var Caractere: Char; Caracteres: TCaracteres; a) Tipo enumerado b) Vetores c) Listas encadeadas d) Conjunto e) Registros 3. (Enade 2011) O conceito de TAD é popular em LPs. Nesse contexto, analise as a�rmativas a seguir: I. A Dois mecanismos utilizáveis na implementação de TAD em programas orientados a objetos são a composição e a herança. II. Especi�cação de um TDA é composta das operações aplicáveis a ele, da sua representação interna e das implementações das operações. III. Se S é um subtipo de T, então entidades do tipo S em um programa podem ser substituídas por entidades do tipo T sem alterar a corretude do programa. IV. O encapsulamento em LPs orientadas a objetos é uma consequência do uso do TAD. Está correto o que se a�rma em: a) I b) II c) I e III d) II e IV e) III e IV NotasReferências GHEZZI, C.; JAZEYERI, M. Conceitos de linguagens de programação. 2. ed. Rio de Janeiro: Campus, 1987. SEBESTA, R. W. Conceitos de linguagemde programação. 11. ed. Porto Alegre: Bookman, 2018. TUCKER, A. B. Programação: princípios e paradigmas. 2. ed. Porto Alegre: AMGH, 2010. VAREJÃO, F. M. Linguagem de programação: conceitos e técnicas. Rio de Janeiro: Elsevier, 2004. Próxima aula Conceitos considerados pelas LPs na construção de expressões e comandos; Principais tipos de expressões e comandos das LPs. Explore mais Saiba mais, acesse: Introdução à programação estruturada.; javascript:void(0);
Compartilhar