Preview only show first 10 pages with watermark. For full document please download

Apontadores E Estruturas De Dados Dinâmicas Em C

A apostila ensina um dos conceitos mais importantes da Linguagem C, que são os Apontadores e Alocação Dinâmica de forma brilhante!

   EMBED


Share

Transcript

Apontadores e Estruturas de Dados Dinˆamicas em C Fernando Mira da Silva Departamento de Engenharia Electrot´ecnica e de Computadores Instituto Superior T´ecnico Novembro de 2002 Resumo O C e´ provavelmente a mais flex´ıvel das linguagens de programac¸a˜ o de alto-n´ıvel, mas apresenta uma relativa complexidade sint´actica. Uma das maiores dificuldades na abordagem do C numa disciplina de introdut´oria de programac¸a˜ o e´ a necessidade de introduzir os conceitos de enderec¸o de mem´oria, apontador e mem´oria dinˆamica. Este texto foi preparado para apoio a` disciplina de Introduc¸a˜ o a` Programac¸a˜ o da Licenciatura em Engenharia Electrot´ecnica e Computadores do Instituto Superior T´ecnico. Este texto tenta focar de modo sistem´atico alguns dos t´opicos que maiores d´uvidas suscita nas abordagens iniciais da linguagem: apontadores e estruturas de dados dinˆamicas. Assim, embora se pressuponha o conhecimentos dos elementos b´asicas da linguagem C por parte do leitor – nomeadamente, os tipos de dados elementares e as estruturas de controlo – o texto e´ mantido ao n´ıvel elementar de uma disciplina introdut´oria de inform´atica. Na apresentac¸a˜ o das estruturas de dados consideradas, que incluem pilhas, filas, listas e an´eis, introduz-se de forma natural a noc¸a˜ o de abstracc¸a˜ o de dados, e os princ´ıpios essenciais de estruturac¸a˜ o e modularidade baseados neste paradigma de programac¸a˜ o. Para o programador experiente em C, alguns dos exemplos de c´odigo poder˜ao parecer pouco optimizados. Trata-se de uma opc¸a˜ o premeditada que tenta beneficiar a clareza e a simplicidade algor´ıtmica, ainda que em alguns casos esta opc¸a˜ o possa sacrificar ligeiramente a eficiˆencia do c´odigo apresentado. Pensamos, no entanto, que esta e´ a opc¸a˜ o correcta numa abordagem introdut´oria da programac¸a˜ o. ´Indice 1 Introduc¸a˜ o 1 2 Apontadores 5 2.1 Motivac¸a˜ o para os apontadores em C . . . . . . . . . . . . . . . . . . . . . . . . 5 2.2 Modelos de mem´oria em C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 2.3 Apontadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 2.4 Func¸o˜ es e passagem por referˆencia . . . . . . . . . . . . . . . . . . . . . . . . . 11 2.4.1 Passagem por referˆencia . . . . . . . . . . . . . . . . . . . . . . . . . . 11 2.4.2 Erros frequentes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 Vectores e apontadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 2.5.1 Declarac¸a˜ o de vectores . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 2.5.2 Aritm´etica de apontadores . . . . . . . . . . . . . . . . . . . . . . . . . 22 2.5.3 ´Indices e apontadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 2.5.4 Vectores como argumentos de func¸o˜ es . . . . . . . . . . . . . . . . . . . 26 Matrizes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28 Declarac¸a˜ o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28 2.5 2.6 2.6.1 IV ´INDICE 2.7 3 Matrizes como argumento de func¸o˜ es . . . . . . . . . . . . . . . . . . . 29 2.6.3 Matrizes e vectores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 2.6.4 Matrizes e apontadores . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 Generalizac¸a˜ o para mais do que duas dimens˜oes . . . . . . . . . . . . . . . . . . 38 Vectores e mem´oria dinˆamica 41 3.1 Introduc¸a˜ o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 3.2 Vectores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43 3.3 “Vectores” dinˆamicos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46 3.4 Gest˜ao da mem´oria dinˆamica . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48 3.4.1 Verificac¸a˜ o da reserva de mem´oria . . . . . . . . . . . . . . . . . . . . . 48 3.4.2 Outras func¸o˜ es de gest˜ao de mem´oria dinˆamica . . . . . . . . . . . . . . 50 3.4.3 Garbbage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52 Criac¸a˜ o dinˆamica de matrizes . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55 3.5.1 Introduc¸a˜ o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55 3.5.2 Matrizes est´aticas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55 3.5.3 Matrizes dinˆamicas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56 3.5.4 Vectores de apontadores e matrizes . . . . . . . . . . . . . . . . . . . . 58 3.5 4 2.6.2 Listas dinˆamicas 61 4.1 Introduc¸a˜ o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61 4.2 Abstracc¸a˜ o de dados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62 ´INDICE 4.3 Listas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 4.4 Listas dinˆamicas: listar elementos . . . . . . . . . . . . . . . . . . . . . . . . . 66 4.5 Listas: pilhas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68 4.5.1 Introduc¸a˜ o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68 4.5.2 Declarac¸a˜ o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70 4.5.3 Inicializac¸a˜ o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70 4.5.4 Sobreposic¸a˜ o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71 4.5.5 Remoc¸a˜ o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72 4.5.6 Teste . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72 4.5.7 Exemplo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73 Listas: filas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80 4.6.1 Introduc¸a˜ o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80 4.6.2 Declarac¸a˜ o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80 4.6.3 Inicializac¸a˜ o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81 4.6.4 Inserc¸a˜ o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82 4.6.5 Remoc¸a˜ o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83 4.6.6 Teste . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83 4.6.7 Exemplo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84 Listas ordenadas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87 4.7.1 Introduc¸a˜ o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87 4.7.2 Declarac¸a˜ o e inicializac¸a˜ o . . . . . . . . . . . . . . . . . . . . . . . . . 88 4.6 4.7 V VI ´INDICE 4.8 4.9 4.7.3 Listagem ordenada . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89 4.7.4 Procura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89 4.7.5 Abstracc¸a˜ o de dados e m´etodos de teste . . . . . . . . . . . . . . . . . . 91 4.7.6 Inserc¸a˜ o ordenada . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92 4.7.7 Remoc¸a˜ o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95 4.7.8 Exemplo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96 Variantes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107 4.8.1 Introduc¸a˜ o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107 4.8.2 Listas com registo separado para a base . . . . . . . . . . . . . . . . . . 107 4.8.3 Listas duplamente ligadas . . . . . . . . . . . . . . . . . . . . . . . . . 108 4.8.4 Aneis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109 Anel duplo com registo separado para a base . . . . . . . . . . . . . . . . . . . . 109 4.9.1 Introduc¸a˜ o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109 4.9.2 Declarac¸a˜ o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110 4.9.3 Inicializac¸a˜ o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110 4.9.4 Listagem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111 4.9.5 Procura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111 4.9.6 Inserc¸a˜ o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112 4.9.7 Remoc¸a˜ o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113 4.9.8 Exemplo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114 4.10 Listas de listas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121 ´INDICE 5 Conclus˜oes Bibliografia 125 127 VII Cap´ıtulo 1 ˜ Introduc¸ao At´e ao aparecimento da linguagem C, as linguagens de alto-n´ıvel tinham por objectivo distanciar o programador do hardware espec´ıfico de trabalho. Pretendia-se, deste modo, que o programador focasse a sua actividade na soluc¸a˜ o conceptual e algor´ıtmica do problema e, simultaneamente, que o c´odigo final fosse independente do hardware e, como tal, facilmente port´avel entre diferentes plataformas. Este princ´ıpio te´orico fundamental, ainda hoje correcto em muitas a´ reas de aplicac¸a˜ o de software, conduzia no entanto a problemas diversos sempre que o programador, por qualquer motivo, necessitava de explorar determinadas regularidades das estruturas de dados de modo a optimizar zonas cr´ıticas do c´odigo ou, em outros casos, por ser conveniente ou desej´avel explorar determinadas facilidades oferecidas pelas instruc¸o˜ es do processador que n˜ao estavam directamente dispon´ıveis na linguagem de alto-n´ıvel. Nestas situac¸o˜ es, a u´ nica alternativa era a programac¸a˜ o directa em linguagem m´aquina destes blocos de c´odigo, opc¸a˜ o que implicava a revis˜ao do software em cada alterac¸a˜ o ou evoluc¸a˜ o de hardware. Por raz˜oes semelhantes, quer o sistema operativo quer ferramentas de sistema como compiladores, gestores de ficheiros ou monitores de sistema eram consideradas aplicac¸o˜ es que, por requisitos de eficiˆencia do c´odigo, eram incompat´ıveis com linguagens de alto-n´ıvel. O mesmo sucedia com todos os programas que necessitavam, de alguma forma, de controlar directamente dispositivos de hardware. Como corol´ario, estas aplicac¸o˜ es eram tradicionalmente escritas totalmente em linguagem m´aquina, implicando um enorme esforc¸o de desenvolvimento e manutenc¸a˜ o em cada evoluc¸a˜ o do hardware. Quando Ken Thompson iniciou a escrita do sistema operativo Unix (Ritchie e Thmompson, 1974), desenvolveu uma linguagem, designada B, que funcionava no hardware de um computador ˜ 2 I NTRODUC¸ AO Digital PDP-10. O B era uma linguagem pr´oxima da linguagem m´aquina, mas que facilitava extraordinariamente a programac¸a˜ o de baixo-n´ıvel. O B adoptava algumas estruturas decisionais e ciclos comuns a linguagens de alto-n´ıvel e, simultaneamente, disponibilizava um conjunto de facilidades simples, geralmente s´o acess´ıveis em linguagem m´aquina, como o acesso a enderec¸os de mem´oria e a m´etodos de enderec¸amento indirecto. De facto, os mecanismos de acesso a vari´aveis e estruturas de dados previstos no B cobriam a esmagadora maioria das necessidades dos programadores quando anteriormente eram obrigados a recorrer a` linguagem m´aquina. Ora estes mecanismos, embora obviamente dependentes do hardware, obedeciam na sua generalidade ao modelo de mem´oria previsto na arquitectura de Von Neuman, o qual, nos seus princ´ıpios essenciais, est´a na base da maioria das plataformas computacionais desde os anos 50 at´e aos nossos dias. Foi assim surgiu a ideia da possibilidade de desenvolvimento de uma linguagem de alto n´ıvel, independente do hardware, que permitisse simultaneamente um acesso flex´ıvel aos modos de enderec¸amento e facilidades dispon´ıveis ao n´ıvel da linguagem m´aquina da maioria dos processadores. E´ assim que, em 1983, e´ inventada a linguagem C (Kernighan e Ritchie, 1978), a qual permitiu a re-escrita de 90% do n´ucleo do sistema operativo Unix em alto-n´ıvel. Para al´em da manipulac¸a˜ o directa de enderec¸os de mem´oria, uma das facilidades introduzida pela linguagem C foi a incorporac¸a˜ o de mecanismos para gest˜ao de mem´oria dinˆamica. O conceito de mem´oria dinˆamica permite que um programa ajuste de modo flex´ıvel a dimens˜ao da mem´oria que utiliza de acordo com as suas necessidades efectivas. Por exemplo, um mesmo programa de processamento de texto pode ocupar pouca mem´oria se estiver a tratar um documento de pequena dimens˜ao, ou ocupar um volume mais significativo no caso de um documento de maior n´umero de p´aginas. A invenc¸a˜ o da linguagem C (Kernighan e Ritchie, 1978) permitiu a re-escrita de 90% do n´ucleo do sistema operativo Unix em linguagem de alto-n´ıvel. A possibilidade de manipular enderec¸os de mem´oria permitiu ainda a implementac¸a˜ o eficiente em linguagem de alto n´ıvel de muitos algoritmos e estruturas de dados at´e ent˜ao geralmente escritos em linguagem Assembler (Knuth, 1973). Neste texto apresenta-se uma introduc¸a˜ o aos apontadores e mem´oria dinˆamica na linguagem de programac¸a˜ o C. Para exemplificar estes conceitos, s˜ao introduzidas algumas estruturas de dados dinˆamicas simples, como pilhas, filas, listas e aneis. Durante a exposic¸a˜ o, introduz-se de forma natural a noc¸a˜ o de abstracc¸a˜ o de dados, e os princ´ıpios essenciais de estruturac¸a˜ o e modularidade baseados neste paradigma de programac¸a˜ o. Deste modo, a noc¸a˜ o de abstracc¸a˜ o de dados n˜ao e´ introduzida formalmente num cap´ıtulo aut´onomo, mas sim quando se instroduzem listas dinˆamicas, altura em que o conceito e´ fundamental para justificar a estrutura adoptada. Esta abordagem, embora pouco convencional, deriva da nossa experiˆencia na docˆencia da disciplina de Introduc¸a˜ o a` Programac¸a˜ o durante v´arios anos no IST, tendo beneficiado das cr´ıticas e sugest˜oes de diversas 3 gerac¸o˜ es de alunos. Tenta-se neste texto dar-se primazia a` clareza algor´ıtmica e legibilidade do c´odigo, ainda que em alguns casos esta opc¸a˜ o possa sacrificar pontualmente a eficiˆencia do c´odigo produzido. Considera-se, no entanto, que e´ esta a abordagem mais adequada numa disciplina de Introduc¸a˜ o a` Programac¸a˜ o. Por outro lado, a maioria dos compiladores recentes incluem processos de optimizac¸a˜ o que dispensam a utilizac¸a˜ o expl´ıcita dos mecanismos de optimizac¸a˜ o previstos originalmente na linguagem C. Cap´ıtulo 2 Apontadores 2.1 ˜ para os apontadores em C Motivac¸ao 2.2 ´ Modelos de memoria em C Embora os mecanismos de enderec¸amento e acesso a` mem´oria tenham sofrido v´arias evoluc¸o˜ es nas u´ ltimas d´ecadas e sejam evidentemente dependentes do hardware, o C requer apenas hip´oteses muito simples relativamente ao modelo de mem´oria do processador. A mem´oria de um computador encontra-se organizada em c´elulas ou palavras de mem´oria individuais. Cada palavra de mem´oria e´ referenciada por um enderec¸o e armazena um dado conte´udo. Designa-se por “n´umero de bits da arquitectura” o n´umero m´aximo de bits que um processador e´ capaz de ler num u´ nico acesso a` mem´oria. Cada palavra de mem´oria de referˆencia de um processador tem em geral um n´umero de bits idˆentico ao n´umero de bits da arquitectura1 . Admita-se que num dado programa em C declara, entre outras as vari´aveis i,j,k com o tipo int, a vari´avel f com o tipo float e uma vari´avel d do tipo double, n˜ao necessariamente por esta ordem. Admita-se que ap´os a declarac¸a˜ o destas vari´aveis s˜ao realizadas as seguintes atribuic¸o˜ es: i = 2450; j = 11331; k = 113; f = 225.345; d = 22.5E+145; 1 A complexidade dos modos de enderec¸amento dos processadores actuais conduz a que a definic¸a˜ o e os modelos aqui apresentados pequem por uma simplicidade excessiva, mas s˜ao suficientes para os objectivos propostos. 6 A PONTADORES Endereço .. . Conteúdo Variável .. . .. . 1001 ??? 1002 2450 1003 ??? 1004 225.345 f 1005 11331 j 1006 113 k 1007 22.5E145 1008 (double, 64bits) 1009 ??? .. . i d .. . Figura 2.1: Modelo de mem´oria em C (exemplo). Um exemplo esquem´atico de um modelo mem´oria correspondente a esta configurac¸a˜ o encontra-se representado na figura 2.1. Admite-se aqui que a palavra de mem´oria e o tipo inteiro s˜ao representados por 32 bits. De acordo com a figura, durante a compilac¸a˜ o foram atribu´ıdos a` s vari´aveis i,j,k,f os enderec¸os 1002,1005, 1006 e 1004, respectivamente, enquanto que a` vari´avel d foi atribu´ıdo o enderec¸o 10072 . Note-se que, com excepc¸a˜ o do tipo double, cada vari´avel tem uma representac¸a˜ o interna de 32 bits, ocupando por isso apenas um enderec¸o de mem´oria. A vari´avel d, de tipo double, tem uma representac¸a˜ o interna de 64 bits, exigindo por isso duas palavras de mem´oria: os enderec¸os 1007 e 1008. Na figura 2.1 encontram-se representados outras posic¸o˜ es de mem´oria, provavelmente ocupadas por outras vari´aveis, e cujo conte´udo e´ desconhecido do programador. 2 Estritamente falando, a cada vari´avel local, e´ apenas atribu´ıdo um enderec¸o relativo durante a compilac¸a˜ o, sendo o enderec¸o final fixado durante a execuc¸a˜ o da func¸a˜ o ou activac¸a˜ o do bloco de instruc¸o˜ es em que a declarac¸a˜ o e´ realizada. Este tema voltar´a a ser abordado na secc¸a˜ o 3.2. A PONTADORES 7 2.3 Apontadores Um apontador em C e´ uma vari´avel cujo conte´udo e´ um enderec¸o de outra posic¸a˜ o de mem´oria. A declarac¸a˜ o de vari´aveis do tipo apontador pode ser constru´ıda a partir de qualquer tipo definido anteriormente, e deve especificar o tipo de vari´avel referenciada pelo apontador. A declarac¸a˜ o de uma vari´avel do tipo apontador realiza-se colocando um “*” antes do nome da vari´avel. Assim, na declarac¸a˜ o double *pd; int i; int *pi; float f; int j,k; double d; int m,*pi2; double *pd2,d2; as vari´aveis i,j,k,m s˜ao do tipo int, f e´ do tipo float, e d,d2 s˜ao do tipo double. Al´em destas vari´aveis de tipos elementares, s˜ao declaradas as vari´aveis pi, pi2 do tipo apontador para inteiro, enquanto que pd e pd2 s˜ao do tipo apontador para double. Admita-se agora se realiza a seguinte sequˆencia de atribuic¸o˜ es: i = 2450; f = 225.345; k = 113; d = 22.5E145; m = 9800; Ap´os estas instruc¸o˜ es, o mapa de mem´oria poderia ser o representado na figura 2.2, situac¸a˜ o A. Sublinhe-se que as vari´aveis de tipo apontador apenas contˆem um enderec¸o de mem´oria, independentemente do tipo referenciado. Desta forma, todas as vari´aveis de tipo apontador tˆem uma representac¸a˜ o igual em mem´oria, normalmente de dimens˜ao idˆentica ao do tipo inteiro (uma palavra de mem´oria). Note-se que o conte´udo dos apontadores, tal como o de algumas vari´aveis elementares, ainda n˜ao foi inicializado, pelo que surgem representados com ???. E´ importante compreender que cada uma destas vari´aveis tem de facto um conte´udo arbitr´ario, resultante da operac¸a˜ o anterior do computador. Claro que estes valores, n˜ao tido sido ainda inicializados pelo programa, s˜ao desconhecidos do programador, mas esta situac¸a˜ o n˜ao deve ser confundida com a “ausˆencia de conte´udo”, erro de racioc´ınio frequentemente cometido por programadores principiantes. As vari´aveis de tipo elementar podem ser inicializadas pela atribuic¸a˜ o directa de valores constantes. A mesma t´ecnica pode ser utilizada para a inicializac¸a˜ o de apontadores, embora este 8 A PONTADORES Endereço .. . Conteúdo Variável .. . .. . Endereço .. . Conteúdo Variável .. . .. . Endereço .. . Conteúdo Variável .. . .. . pd 1001 1007 pd 1001 1007 pd i 1002 2450 i 1002 2450 i pi 1003 1006 pi 1003 1006 pi 225.345 f 1004 225.345 f 1004 225.345 f 1005 ??? j 1005 ??? j 1005 ??? j 1006 113 k 1006 113 k 1006 113 k d 1007 d 1007 1001 ??? 1002 2450 1003 ??? 1004 1007 22.5E145 1008 (double, 64bits) 1009 9800 1010 1011 1012 m 1009 9800 m ??? pi2 1010 1006 pi2 ??? pd2 1011 1007 pd2 d2 1012 ??? d2 m 1009 9800 ??? pi2 1010 ??? pd2 1011 d2 1012 (double, 64bits) 1013 .. . .. . .. . A .. . 1008 1008 ??? Conteúdo ??? (double, 64bits) 1013 .. . .. . Variável Endereço .. . Conteúdo Variável .. . .. . (double, 64bits) .. . C B .. . .. . d 22.5E145 (double, 64bits) (double, 64bits) 1013 Endereço 22.5E145 Endereço .. . Conteúdo Variável .. . .. . 1001 1007 pd 1001 1007 pd 5001 3 i1 1002 2450 i 1002 2450 i 5002 4 i2 1003 1006 pi 1003 1006 pi 5003 5001 pi1 5004 5003 pi2 1004 225.345 f 1005 113 j 1005 113 j 5005 5004 pi3 1006 113 k 1006 113 k 5006 10 k1 d 1007 d 5007 46 k2 5008 5006 pk1 5003 pk2 1007 22.5E145 1008 (double, 64bits) 1009 9800 1010 1011 1012 1013 .. . 1004 225.345 f 22.5E145 1008 (double, 64bits) m 1009 24500 m 5009 1006 pi2 1010 1002 pi2 5010 1007 pd2 1011 1012 pd2 5011 d2 1012 d2 5012 22.5E145 (double, 64bits) .. . D 1013 .. . 22.5E144 (double, 64bits) .. . E 5013 .. . .. . F Figura 2.2: Mapa de mem´oria ap´os diferentes sequˆencias de atribuic¸a˜ o (explicac¸a˜ o no texto). A PONTADORES 9 m´etodo raramente seja utilizado: em geral, o programador n˜ao sabe quais os enderec¸os de mem´oria dispon´ıveis no sistema, e a manipulac¸a˜ o directa de enderec¸os absolutos tem reduzidas aplicac¸o˜ es pr´aticas.3 Assim, A inicializac¸a˜ o de apontadores e´ geralmente realizada por outros m´etodos, entre os quais a utilizac¸a˜ o do enderec¸o de outras vari´aveis j´a declaradas. Este enderec¸o pode ser obtido em C aplicando o operador & a uma vari´avel. Na continuac¸a˜ o do exemplo anterior, admita-se que era realizada agora a seguinte sequˆencia de atribuic¸o˜ es: pd = &d; pi = &k; Ap´os esta fase, o mapa de mem´oria seria o representado na figura 2.2, situac¸a˜ o B, onde as vari´aveis pd e pi surgem agora preenchidas com os enderec¸os de d e k. Como seria de esperar, a consistˆencia da atribuic¸a˜ o exige que a vari´avel referenciada e o tipo do apontador sejam compat´ıveis. Por exemplo, a atribuic¸a˜ o pd=&k e´ incorrecta, atendendo a que k e´ do tipo int e pd est´a declarada como um apontador para double. A partir do momento em que os apontadores s˜ao inicializados, o seu conte´udo pode ser copiado e atribu´ıdo a outras vari´aveis, desde que os tipos ainda sejam compat´ıveis. Assim, as atribuic¸o˜ es pd2 = pd; pi2 = pi; conduziriam apenas a` c´opia dos enderec¸os guardados em pd e pi para pd2 e pi2. Ap´os esta sequˆencia, a situac¸a˜ o seria a representada na figura 2.2, caso C. Uma vez estabelecido um mecanismo para preecher o conte´udo de um apontador, coloca-se a quest˜ao de como utilizar este apontador de modo a aceder aos dados apontados. Este mecanismo e´ suportado no chamado sistema de enderec¸amento indirecto, o qual e´ realizado em C pelo operador *. Assim, a atribuic¸a˜ o j = *pi; tem o significado “consultar o enderec¸o guardado em pi (1006) e, seguidamente, ler o inteiro cujo enderec¸o e´ 1006 e colocar o resultado (113) na vari´avel j. Deste modo, ap´os as atribuic¸o˜ es j = *pi; d2 = *pd; a situac¸a˜ o seria a representada na figura 2.2, caso D. Referiu-se anteriomente que um apontador e´ apenas um enderec¸o de mem´oria. Assim, poder´a questionar-se a necessidade de distinguir um apontador para um inteiro de um apontador para um 3 Embora possa ser pontualmente utilizada em programas que lidam directamente com o hardware. 10 A PONTADORES real, por exemplo. De facto, a dependˆencia do apontador do tipo apontado n˜ao tem a ver com a estrutura do apontador em si, mas sim com o facto de esta informac¸a˜ o ser indispens´avel para desreferenciar (aceder) correctamente o valor enderec¸ado. Por exemplo, na express˜ao d2 = *pd, e´ o facto de pd ser um apontador para double que permite ao compilador saber que o valor referenciado ocupa n˜ao apenas uma, mas duas palavras de mem´oria e qual a sua representac¸a˜ o. S´o na posse desta informac¸a˜ o e´ poss´ıvel efectuar a atribuic¸a˜ o correcta a d2. Outras sequˆencias de atribuic¸a˜ o seriam poss´ıveis. Por exemplo, ap´os a sequˆencia pd2 = &d2; *pd2 = *pd1 / 10.0; pi2 = &i; m= *pi2 * 10; a situac¸a˜ o seria a representada no caso E. Note-se que aqui o operador * foi utilizado no lado esquerdo da atribuic¸a˜ o. Assim, por exemplo, a atribuic¸a˜ o *pd2 = *pd1 e´ interpretada como ler o real cujo enderec¸o e´ especificado pelo conte´udo de pd1 e colocar o resultado no enderec¸o especificado por pd2. Embora at´e aqui tenham sido considerados apontadores para tipos elementares, um apontador pode tamb´em enderec¸ar um outro apontador. Assim, na declarac¸a˜ o int i1,i2,*pi1,**pi2,***pi3; int k1,k2,*pk1,**pk2; enquanto que p1 e´ um apontador para um inteiro, p2 e´ um apontador para um apontador para um inteiro e p3 e´ um apontador para um apontador para um apontador para um inteiro. Como seria de esperar, a inicializac¸a˜ o destes apontadores realiza-se segundo as mesmas regras seguidas para os apontadores simples, sendo apenas necess´ario ter em atenc¸a˜ o, o n´ıvel correcto de enderec¸amento indirecto m´ultiplo. Assim, se ap´os a declarac¸a˜ o anterior, fosse executada a sequˆencia de instruc¸o˜ es i1 = 3; i2 = 4; pi1 = &i1; pi2 = &pi1; pi3 = &pi2; k1 = 10; pk1 = &k1; pk2 = pi2; k2 = i1 * ***pi3 + *pi1 + i2 + **pk2 * *pk1; a situac¸a˜ o final poderia ser a representada na figura 2.2, caso F. Em apontadores m´utilpos a possibilidade de desreferenciar um apontador continua a ser dependente do tipo apontado. Deste modo, um apontador para um inteiro e um apontador para um apontador para um inteiro s˜ao tipos claramente distintos e cujos conte´udos n˜ao podem ser mutuamente trocados ou atribu´ıdos. Por outro lado, os n´ıveis de indirecc¸a˜ o devem ser claramente ˜ E PASSAGEM POR REFER Eˆ NCIA 11 F UNC¸ OES respeitados. Assim, a atribuic¸a˜ o i1 = *pk2; no exemplo precedente seria incorrecta, j´a que i1 e´ do tipo inteiro e, *pk2 e´ um apontador para inteiro (recorde-se que pk2 e´ um apontador para um apontador para um inteiro). Como e´ sabido, o valor de uma vari´avel nunca deve ser utilizado antes de esta ser inicializada explicitamente pelo programa. Com efeito, no in´ıcio de um programa, as vari´aveis tˆem um valor arbitr´ario desconhecido. Por maioria de raz˜ao, o mesmo princ´ıpio deve ser escrupulosamente seguido na manipulac¸a˜ o de apontadores. Suponha-se, por exemplo que, no in´ıcio de um programa, s˜ao inclu´ıdas a declarac¸a˜ o e as instruc¸o˜ es seguintes: int *p,k; k = 4; *p = k*2; Aqui, a segunda atribuic¸a˜ o especifica que o dobro de k (valor 8) deve ser colocado no enderec¸o especificado pelo conte´udo da vari´avel p. No entanto, dado que p n˜ao foi inicializada, o seu conte´udo e´ arbitr´ario. Com elevada probabilidade, o seu valor corresponde a um enderec¸o de mem´oria inexistente ou inv´alido. No entanto, o C n˜ao realiza qualquer ju´ızo de valor sobre esta situac¸a˜ o e, tendo sido instru´ıdo para “colocar 8 no enderec¸o indicado por p” tentar´a executar esta operac¸a˜ o. A tentativa de escrita numa posic¸a˜ o de mem´oria inv´alida ou protegida conduzir´a ou ao compromisso da integridade do sistema operativo se o espac¸o de mem´oria n˜ao for convenientemente protegido, como e´ o caso do DOS. Se o sistema tiver um modo protegido, como o Unix ou o Windows NT, esta situac¸a˜ o pode originar um erro de execuc¸a˜ o, devido a uma violac¸a˜ o de mem´oria detectada pelo sistema operativo. Os erros de execuc¸a˜ o conduzem a` interrupc¸a˜ o imediata, em erro, do programa. Sublinhe-se que nas figuras desta secc¸a˜ o foram utilizados enderec¸os espec´ıficos nos apontadores de modo a melhor demonstrar e explicar o mecanismo de funcionamento dos mecanismos de apontadores e indirecc¸a˜ o em C. No entanto, na pr´atica, o programador n˜ao necessita de conhecer o valor absoluto dos apontadores, sendo suficiente a manipulac¸a˜ o indirecta do seu conte´udo atrav´es dos mecanismos de referenciac¸a˜ o e desreferenciac¸a˜ o descritos. 2.4 2.4.1 ˜ ˆ Func¸oes e passagem por referencia ˆ Passagem por referencia Em C, na chamada de uma func¸a˜ o, os parˆametros formais da func¸a˜ o recebem sempre uma c´opia dos valores dos parˆametros actuais. Este mecanismo de passagem de argumentos e´ desig- 12 A PONTADORES nado passagem por valor (Martins, 1989) e est´a subjacente a todas as chamadas de func¸o˜ es em C. Esta situac¸a˜ o e´ adequada se se pretender apenas que os argumentos transmitam informac¸a˜ o do m´odulo que chama para dentro da func¸a˜ o. Dado que uma func¸a˜ o pode tamb´em retornar um valor, este mecanismo b´asico e´ tamb´em adequado quando a func¸a˜ o recebe v´arios valores de entrada e tem apenas um valor de sa´ıda. Por exemplo, uma func¸a˜ o que determina o maior de trˆes inteiros pode ser escrita como int maior_3(int i1,int i2, int i3){ /* * Func ¸˜ ao que determina o maior de trˆ es inteiros * */ if((i1 > i2) && (i1 > i3)) return i1; else return (i2 > i3 ? i2 : i3); } Existem no entanto situac¸o˜ es em que se pretende que uma func¸a˜ o devolva mais do que um valor. Uma situac¸a˜ o poss´ıvel seria uma variante da func¸a˜ o maior_3 em que se pretendesse determinar os dois maiores valores, e n˜ao apenas o maior. Outro caso t´ıpico e´ o de uma func¸a˜ o para trocar o valor de duas vari´aveis entre si. Numa primeira tentativa, poderia haver a tentac¸a˜ o de escrever esta func¸a˜ o como #include #include void trocaMal(int x,int y){ /* * ERRADO */ int aux; aux = x; x = y; y = aux; } int main(){ int a,b; printf("Indique dois n´ umeros: "); scanf("%d %d",&a,&b); trocaMal(&a,&b); printf("a = %d, b= %d\n",a,b); ˜ E PASSAGEM POR REFER Eˆ NCIA 13 F UNC¸ OES exit(0); } No entanto, este programa n˜ao funciona: o mecanismo de passagem por valor implica que a func¸a˜ o troca opera correctamente sobre as vari´aveis locais x e y, trocando o seu valor, mas estas vari´aveis n˜ao s˜ao mais do que c´opias das vari´aveis a e b que, como tal, se mantˆem inalteradas. Na figura 2.3 representa-se a evoluc¸a˜ o do mapa de mem´oria e do conte´udo das vari´aveis durante a chamada a` func¸a˜ o trocaMal. Este aparente problema pode ser resolvido pela utilizac¸a˜ o criteriosa de apontadores. A func¸a˜ o troca especificada anteriormente pode ser correctamente escrita como se segue: #include #include void troca(int *x,int *y){ /* * Func ¸˜ ao que troca dois argumentos */ int aux; aux = *x; *x = *y; *y = aux; } int main(){ int a,b; printf("Indique dois n´ umeros: "); scanf("%d %d",&a,&b); troca(&a,&b); printf("a = %d, b= %d\n",a,b); exit(0); } Tal como pode ser observado, a soluc¸a˜ o adoptada consiste em passar a` func¸a˜ o n˜ao o valor das vari´aveis a e b, mas sim os seus enderec¸os. Embora estes enderec¸os sejam passados por valor (ou seja, a func¸a˜ o recebe uma c´opia destes valores), o enderec¸o permite a` func¸a˜ o o conhecimento da posic¸a˜ o das vari´aveis a b em mem´oria e, deste modo, permite a manipulac¸a˜ o do seu conte´udo por meio de um enderec¸amento indirecto. Um hipot´etico exemplo de um mapa mem´oria associado ao funcionamento do programa troca encontra-se representado na figura 2.4. Admita-se que a declarac¸a˜ o de vari´aveis no pro- 14 A PONTADORES Endereço Conteúdo .. . .. . .. . .. . .. . .. . .. . .. . .. . Variáveis 2135 do bloco main() 2136 .. . Variável 8 a 2 b .. . .. . Endereço .. . .. . 2131 Variáveis da funçao 2132 troca() 2133 Variável .. . 2 8 x 8 2 y 8 aux .. . .. . .. . .. . .. . .. . Variáveis 2135 do bloco main() 2136 .. . .. . C Variável .. . 8 2 x y aux .. . .. . .. . .. . .. . .. . Variáveis 2135 do bloco main() 2136 .. . 8 a 2 b .. . .. . B Conteúdo .. . .. . 2131 Variáveis da funçao 2132 troca() 2133 A Endereço Conteúdo 8 a 2 b .. . Endereço Conteúdo Variável .. . .. . .. . .. . .. . .. . .. . .. . .. . Variáveis 2135 do bloco main() 2136 .. . .. . 8 a 2 b .. . D Figura 2.3: Mapa de mem´oria durante as diferentes fase de execuc¸a˜ o de um programa que utiliza a func¸a˜ o trocaMal. A - antes da chamada a` func¸a˜ o, B - no in´ıcio de troca, C - no final de troca, D - ap´os o regresso ao programa principal. O mecanismo de passagem por valor conduz a que os valores do programa principal n˜ao sejam alterados. ˜ E PASSAGEM POR REFER Eˆ NCIA 15 F UNC¸ OES Endereço Conteúdo .. . .. . .. . .. . .. . .. . .. . .. . .. . Variáveis 2135 do bloco main() 2136 .. . Variável 8 a 2 b .. . .. . Endereço .. . .. . 2131 Variáveis da funçao 2132 troca() 2133 Variável .. . 2135 2136 8 y aux .. . .. . .. . .. . .. . .. . 2 8 a 8 2 b .. . C .. . 2135 2136 x y aux .. . .. . .. . .. . .. . .. . Variáveis 2135 do bloco main() 2136 .. . 8 a 2 b .. . .. . Endereço Conteúdo Variável .. . .. . .. . .. . .. . .. . .. . .. . .. . x .. . Variáveis 2135 do bloco main() 2136 Variável B Conteúdo .. . .. . 2131 Variáveis da funçao 2132 troca() 2133 A Endereço Conteúdo .. . Variáveis 2135 do bloco main() 2136 .. . .. . 2 a 3 b .. . D Figura 2.4: Mapa de mem´oria durante as diferentes fase de execuc¸a˜ o do programa que utiliza a func¸a˜ o troca. A - antes da chamada a` func¸a˜ o, B - no in´ıcio de troca C - no final de troca, D ap´os o regresso ao programa principal. A passagem de apontadores para as vari´aveis do programa principal (passagem por referˆencia) permite que a func¸a˜ o altere as vari´aveis do programa principal. 16 A PONTADORES grama principal atribuiu a` s vari´aveis A e B os enderec¸os 2135 e 2136, e que estas foram inicializadas pelo utilizador com os valores 8 e 2, respectivamente. A situac¸a˜ o imediatamente antes da chamada da func¸a˜ o troca encontra-se representada em A. Durante a chamada da func¸a˜ o, realizase a activac¸a˜ o das vari´aveis x, y e aux, locais a` func¸a˜ o, eventualmente numa zona de mem´oria afastada daquela onde se encontram as vari´aveis a e b, sendo as duas primeiras destas vari´aveis inicializadas com os enderec¸os de a e b (situac¸a˜ o B). Atrav´es do enderec¸amento indirecto atrav´es das vari´aveis x e y, s˜ao alterados os valores das vari´aveis a e b do programa principal, atingido-se a situac¸a˜ o C. Ap´os o regresso ao programa principal, as vari´aveis da func¸a˜ o troca s˜ao libertadas, atingindo-se a situac¸a˜ o representada em D, com as Note-se que, estritamente falando, a passagem de argumento se deu por valor, atendendo a que x e y s˜ao vari´aveis locais a` func¸a˜ o, tendo recebido apenas valores correspondentes ao enderec¸o de vari´aveis declaradas no programa principal. No entanto, neste tipo de mecanismo, diz-se tamb´em que as vari´aveis a e b foram passadas por referˆencia(Martins, 1989), atendendo a que o seu enderec¸o (e n˜ao o seu conte´udo) que foi passadas a` func¸a˜ o. Mais genericamente, sempre que e´ necess´ario que uma func¸a˜ o altere o valor de um ou mais dos seus argumentos, este ou estes dever˜ao ser passados por referˆencia, de forma a ser poss´ıvel a` func¸a˜ o modificar o valor das vari´aveis por um mecanismo de indirecc¸a˜ o. E´ por este motivo que, na chamada da func¸a˜ o scanf(), todas vari´aveis a ler s˜ao passados por referˆencia, de modo a ser poss´ıvel a esta func¸a˜ o poder ler e alterar os valores das vari´aveis do programa principal. 2.4.2 Erros frequentes A tentativa de desdobramento do mecanismo de referenciac¸a˜ o de uma vari´avel quando uma dado programa envolve v´arios n´ıveis de func¸o˜ es e´ um erro frequente de programadores principiantes ou com reduzida experiˆencia de C. Considere-se o seguinte troc¸o de programa, em que se pretende que a vari´avel x do programa principal seja modificada na func¸a˜ o func2. Neste exemplo, o programador adoptou desnecessariamente uma referenciac¸a˜ o (c´alculo do seu enderec¸o) da vari´avel a modificar em cada um dos n´ıveis de chamada da func¸a˜ o: /* Utilizac ¸˜ ao incorrecta (desnecess´ aria) de referˆ encias m´ ultiplas. */ void func2(int **p2,int b2){ **p2 = -b2 * b2; } void func1(int *p1,int b1){ b1 = b1 + 1; ˜ E PASSAGEM POR REFER Eˆ NCIA 17 F UNC¸ OES func2(&p1,b1); } int main(){ int x; func1(&x,5); return 0; } Como pode ser facilmente entendido, a referenciac¸a˜ o de uma vari´avel uma u´ nica vez e´ suficiente para que este mesmo enderec¸o possa ser sucessivamente passado entre os v´arios n´ıveis de func¸o˜ es e ainda permitir a alterac¸a˜ o da vari´avel seja sempre poss´ıvel. Assim, embora o programa anterior funcione, a referenciac¸a˜ o de p1 na passagem de func1 para func2 e´ in´util: o mecanismo ali adoptado s´o faria sentido caso se pretendesse que func2 alterasse o conte´udo da vari´avel p1. Como e´ evidente n˜ao e´ esse o caso, e o programa anterior correctamente escrito tomaria a seguinte forma: /* ˜o correcta de uma passagem Utilizac ¸a por referˆ encia entre v´ arios n´ ıveis de func ¸˜ oes. */ void func2(int *p2,int b2){ *p2 = -b2 * b2; } void func1(int *p1,int b1){ b1 = b1 + 1; func2(p1,b1); } int main(){ int x; func1(&x,5); return 0; } Um outro erro frequente em programadores principiantes e´ passagem ao programa principal ou func¸a˜ o que chama a referˆencia de uma vari´avel local a` func¸a˜ o evocada. Este tipo de erro pode ser esquematizado pelo seguinte programa: int *func(int a){ int b,*c; b = 2 * a; c = &b; return c; 18 A PONTADORES } int main(){ int x,*y; y = func1(1); x = 2 * *y; return 0; } Aqui, a vari´avel b e´ local a` func¸a˜ o func e, como tal, e´ criada quando func e´ activada e a sua zona de mem´oria libertada quando a func¸a˜ o termina. Ora o resultado da func¸a˜ o func e´ passada ao programa principal sob a forma do enderec¸o de b. Quando o valor desta vari´avel e´ lido no programa principal por meio de um enderec¸amento indirecto na express˜ao x = 2 * *y, a vari´avel b j´a n˜ao est´a activa, realizando-se por isso um acesso inv´alido a` posic¸a˜ o de mem´oria especificada pelo enderec¸o em y. Com elevada probabilidade, o resultado final daquela express˜ao ser´a incorrecto. 2.5 2.5.1 Vectores e apontadores ˜ de vectores Declarac¸ao Um vector em C permite a criac¸a˜ o de uma estrutura com ocorrˆencias m´ultiplas de uma vari´avel de um mesmo tipo. Assim, a declarac¸a˜ o int x[5] = {123,234,345,456,567}; double y[3] = {200.0,200.1,200.2}; declara um vector x de 5 inteiros, indexado entre 0 e 4, e um vector y de 3 reais de dupla precis˜ao, indexado entre 0 e 2, inicializados na pr´opria declarac¸a˜ o. Um poss´ıvel mapa de mem´oria correspondente a esta declarac¸a˜ o encontra-se representado na figura 2.5.1. Cada elemento individual de um vector pode ser referenciado acrescentando a` frente do nome da vari´avel o ´ındice, ou posic¸a˜ o que se pretende aceder, representado por um inteiro entre []. Para um vector com N posic¸o˜ es, o ´ındice de acesso varia entre 0 (primeira posic¸a˜ o) e N − 1 (´ultima posic¸a˜ o). Cada elemento de x e y corresponde a uma vari´avel de tipo inteiro ou double, respectivamente. Deste modo, se se escrever, int x[5] = {123,234,345,456,567}; V ECTORES E APONTADORES 19 Endereço .. . Conteúdo Variável .. . .. . 1001 123 x[0] 1002 234 x[1] 1003 345 x[2] 1004 456 x[3] 1005 789 x[4] 1006 200.0 y[0] 1007 1008 y[1] 200.1 1009 1010 1011 .. . y[2] 200.2 .. . Figura 2.5: Mapa de mem´oria correspondente a` declarac¸a˜ o de dois vectores 20 A PONTADORES double y[3] = {200.0,200.1,200.2}; double a; a = y[1] + x[3]; o conte´udo final de a ser´a o resultado da soma da segunda posic¸a˜ o de y com a quarta posic¸a˜ o de x, ou seja 656.1. Dado que cada elemento de um vector e´ uma vari´avel simples, e´ poss´ıvel determinar o seu enderec¸o. Assim, e´ poss´ıvel fazer int x[5] = {123,234,345,456,567}; double y[3] = {200.0,200.1,200.2}; int *pi; double *pd; pi = &(x[2]); pd = &(y[1]); conduzindo-se assim a` situac¸a˜ o representada na figura 2.5.1. Note-se que nas atribuic¸o˜ es pi = &(x[2]) e pd = &(y[1]) os parenteses poderiam ser omitidos, dado que o operador [] (´ındice) tem precedˆencia sobre o operador & (enderec¸o de). Assim, aquelas atribuic¸o˜ es poderiam ser escritas como pi = &x[2] e pd = &y[1]. Uma das vantagens da utilizac¸a˜ o de vectores e´ o ´ındice de acesso poder ser uma vari´avel. Deste modo, inicializac¸a˜ o de um vector de 10 inteiros a 0 pode ser feita pela sequˆencia #define NMAX 10 int main(){ int x[NMAX]; int k; for(k = 0; k < NMAX ; k++) x[k] = 0; /* ...*/ Uma utilizac¸a˜ o comum dos vectores e´ a utilizac¸a˜ o de vectores de caracteres para guardar texto. Por exemplo a declarac¸a˜ o char texto[18]="Duas palavras"; V ECTORES E APONTADORES 21 Endereço .. . Conteúdo Variável .. . .. . 1001 123 x[0] 1002 234 x[1] 1003 345 x[2] 1004 456 x[3] 1005 789 x[4] 1006 200.0 y[0] 1007 1008 y[1] 200.1 1009 1010 y[2] 200.2 1011 1012 1003 pi 1013 1008 pd .. . .. . Figura 2.6: Apontadores e vectores (explicac¸a˜ o no texto). 22 A PONTADORES 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 texto[18] ’D’ ’u’ ’a’ ’s’ ’ ’ ’p’ ’a’ ’l’ ’a’ ’v’ ’r’ ’a’ ’s’’\0’ Figura 2.7: Utilizac¸a˜ o de vectores de caracteres para armazenar texto. cria um vector de 18 caracteres, e inicia as suas primeiras posic¸o˜ es com o texto Duas palavras. Uma representac¸a˜ o gr´afica deste vector depois de inicializado e´ apresentada na figura 2.7. Dado que o texto pode ocupar menos espac¸o que a totalidade do vector, como sucede neste caso, a u´ ltima posic¸a˜ o ocupada deste vector e´ assinalada pela colocac¸a˜ o na posic¸a˜ o seguinte a` u´ ltima do caracter com o c´odigo ASCII 0, geralmente representado pela constante inteira 0 ou pelo caracter ’\0’. Note-se que esta u´ ltima representac¸a˜ o n˜ao deve ser confundida com a representac¸a˜ o do algarismo zero ’0’, internamente representado por 48, o c´odigo ASCII de 0. 2.5.2 ´ Aritmetica de apontadores E´ poss´ıvel adicionar ou subtrair uma constante inteira de uma vari´avel de tipo apontador. Este tipo de operac¸a˜ o s´o faz sentido em vectores ou estruturas de dados regulares, em que o avanc¸o ou recuo de um apontador conduz a um outro elemento do vector (ou estrutura de dados regulares). E´ da u´ nica e exclusiva responsabilidade do programador garantir que tais operac¸o˜ es aritm´eticas se mantˆem dentro dos enderec¸os v´alidos da estrutura de dados apontada. Assim, por exemplo, se na sequˆencia do exemplo e da situac¸a˜ o representada na figura 2.5.1 (repetida por conveniˆencia na figura 2.5.2, A) fossem executadas as instruc¸o˜ es pi = pi + 2; pd = pd - 1; a situac¸a˜ o resultante seria a representada na figura 2.5.2, B. No caso do modelo de mem´oria adoptado como exemplo, a operac¸a˜ o aritm´etica sobre o apontador inteiro tem uma correspondˆencia directa com a operac¸a˜ o realizada sobre o enderec¸o. No entanto, o mesmo n˜ao sucede com o apontador para double, onde subtracc¸a˜ o de uma unidade ao apontador corresponde na realidade a uma reduc¸a˜ o de duas unidades no enderec¸o f´ısico da mem´oria. De facto, a aritm´etica de apontadores em C e´ realizada de forma a que o incremento ou decremento unit´ario corresponda a um avanc¸o ou recuo de uma unidade num vector, independentemente do tipo dos elementos do vector. O compilador de C garante o escalamento da constante adicionada V ECTORES E APONTADORES 23 Endereço .. . Conteúdo Variável .. . .. . Endereço .. . Conteúdo Variável .. . .. . 1001 123 x[0] 1001 123 x[0] 1002 234 x[1] 1002 234 x[1] 1003 345 x[2] 1003 345 x[2] 1004 456 x[3] 1004 456 x[3] 1005 789 x[4] 1005 789 x[4] y[0] 1006 1006 200.0 1007 1008 y[0] 1007 y[1] 200.1 1009 1010 200.0 1008 y[1] 200.1 1009 y[2] 200.2 1011 1010 y[2] 200.2 1011 1012 1003 pi 1012 1005 pi 1013 1008 pd 1013 1006 pd .. . .. . A .. . .. . B Figura 2.8: Aritm´etica de apontadores (explicac¸a˜ o no texto). 24 A PONTADORES ou subtra´ıda de acordo com a dimens˜ao do tipo apontado. Este modo de operac¸a˜ o garante que, na pr´atica, o programador possa realizar operac¸o˜ es sobre apontadores abstraindo-se do n´umero efectivo de bytes do elemento apontado. J´a anteriormente se mencionou que em C o tipo do apontador depende do objecto apontado, de modo a ser poss´ıvel determinar, num enderec¸amento indirecto, qual o tipo da vari´avel referenciada. A aritm´etica de apontadores reforc¸a esta necessidade, dado que o seu incremento ou decremento exige o conhecimento da dimens˜ao do objecto apontado. A aritm´etica de apontadores define apenas operac¸o˜ es relativas de enderec¸os. Assim, embora seja poss´ıvel adicionar ou subtrair constantes de apontadores, n˜ao faz sentido somar apontadores. Numa analogia simples, considere-se que os enderec¸os s˜ao os n´umeros da porta dos edif´ıcios de uma dada rua: faz sentido referir “dois n´umeros depois do pr´edio 174” ou “trˆes n´umeros antes do 180”, mas n˜ao existe nenuma aplicac¸a˜ o em que fac¸a sentido adicionar dois n´umeros de porta (174 e 180, por exemplo). No entanto, faz sentido referir “o n´umero de pr´edios entre o 174 e o 180”: de modo equivalente, tamb´em em C a subtracc¸a˜ o de apontadores e´ poss´ıvel, desde que sejam do mesmo tipo. Como e´ evidente, tal como na adic¸a˜ o, a subtracc¸a˜ o de operadores e´ convenientemenet escalada pela dimens˜ao do objecto apontado: int x[5] = {123,234,345,456,567}; double y[3] = {200.0,200.1,200.2}; int *pi1,*pi2; double *pd1,*pd2; int di,dd; pi1 = &x[2]; pi2 = &x[0]; di = pi1 - pi2; /* di <- 2 */ pd1 = &y[1]; pd2 = &y[3]; dd = pd1 - pd2; /* dd <- -1 */ . Uma u´ ltima nota relativamente aos apontadores de tipo void. O C-ANSI permite a definic¸a˜ o de apontadores gen´ericos, cujo tipo e´ independente do objecto apontado. Um apontador pv deste tipo manipula um enderec¸o gen´erico e pode ser simplesmente declarado por void *pv; e encontra utilizac¸a˜ o em situac¸o˜ es em que se pretende que uma mesma estrutura possa enderec¸ar entidades ou objectos de tipos diferentes. Sempre que poss´ıvel, este tipo de situac¸o˜ es deve ser preferencialmente abordado pela utilizac¸a˜ o de estruturas do tipo union. No entanto, existem ´ por exemplo, casos em que tal n˜ao e´ poss´ıvel, obrigando a` utilizac¸a˜ o deste tipo de apontadores. E, o caso das func¸o˜ es de gest˜ao de mem´oria, que trabalham com apontadores gen´ericos (V. secc¸a˜ o 3.3). Note-se, no entanto, que o C n˜ao consegue desreferenciar automaticamente um apontador V ECTORES E APONTADORES 25 para void (ou seja, aceder ao conte´udo apontado por). De igual modo, o desconhecimento da dimens˜ao do objecto apontado impede que a aritm´etica de apontadores seja aplic´avel a apontadores deste tipo. 2.5.3 ´Indices e apontadores Dos princ´ıpios gerais referidos anteriormente, resulta que atribuic¸a˜ o do 3o elemento de um vector x a uma vari´avel a pode ser realizada directamente por a = x[2]; ou, de forma equivalente, por a = *(&x[0] + 2); sendo o resultado idˆentico. Enquanto que no primeiro caso se adopta um mecanismo de indexac¸a˜ o directa, no segundo caso determina-se um apontador para o primeiro elemento do vector, incrementa-se o apontador de duas unidades para enderec¸ar o 3o elemento e, finalmente, aplica-se o operador “*” para realizar o enderec¸amento indirecto. De facto, a segunda express˜ao pode ser simplificada. O C define que o enderec¸o do primeiro elemento de um vector pode ser obtido usando simplesmente o nome do vector, sem o operador de indexac¸a˜ o (“[0]”) a` frente. Ou seja, no contexto de uma express˜ao em C, “ &x[0]” e´ equivalente a usar simplesmente “ x”. Por outras palavras, se x[] e´ um vector do tipo xpto, x e´ um apontador para o tipo xpto. Assim, a express˜ao “ x[k]” e´ sempre equivalente a “ *(xk)+”. Este facto conduz ao que designamos por regra geral de indexac¸a˜ o em C, que pode ser enunciada pela equivalˆencia x[k] <-> *(x + k) Registe-se, como curiosidade, que a regra geral de indexac¸a˜ o conduz a que, por exemplo, x[3] seja equivalente a 3[x]. Com efeito, x[3] <-> *(x+3) <-> *(3+x) <-> 3[x] Claro que a utilizac¸a˜ o desta propriedade e´ geralmente proibido, n˜ao pelo C, mas pelas normas de boa programac¸a˜ o! 26 A PONTADORES 2.5.4 ˜ Vectores como argumentos de func¸oes Para usar um vector como argumento de uma func¸a˜ o func() basta, no bloco que chama func(), especifiar o nome da vari´avel que se pretende como argumento. Assim, pode fazer-se, por exemplo, int x[10]; /* ... */ func(x); /* ... */ No entanto, e´ necess´ario ter em conta que x, quando usado sem ´ındice, representa apenas o enderec¸o da primeira posic¸a˜ o do vector. Como deve ent˜ao ser declarado o argumento formal correspondente no cabec¸alho de func? Diversas opc¸o˜ es existem para realizar esta declarac¸a˜ o. No exemplo seguinte, a func¸a˜ o set e´ utilizada para inicializar os NMAX elementos do vector a do programa principal com os inteiros entre 1 e NMAX. Numa primeira variante, o argumento formal da func¸a˜ o e´ apenas a repetic¸a˜ o da declarac¸a˜ o efectuada no programa principal. Assim, #define NMAX 100 void set(int x[NMAX],int n){ int k; for(k = 0; k < n; k++) x[k] = k+1; } int main(){ int a[NMAX]; set(a,NMAX); /* ... */ } Note-se que para uma func¸a˜ o manipular um vector e´ suficiente conhecer o enderec¸o do primeiro elemento. Dentro da func¸a˜ o, o modo de aceder ao k-´esimo elemento do vector e´ sempre adicionar k ao enderec¸o da base, independentemente da dimens˜ao do vector. Ou seja, o parˆametro formal int x[NMAX] apenas indica que x e´ um apontador para o primeiro elemento de um vector de inteiros. Deste modo, o C n˜ao usa a informac¸a˜ o “[NMAX]” no parˆametro formal para aceder a` func¸a˜ o. Assim, a indicac¸a˜ o expl´ıcita da dimens˜ao pode ser omitida, sendo v´alido escrever void set(int x[],int n){ V ECTORES E APONTADORES 27 int k; for(k = 0; k < n; k++) x[k] = k+1; } Note-se que a possibilidade de omitir a dimens˜ao resulta tamb´em da func¸a˜ o n˜ao necessitar de reservar o espac¸o para o vector: a func¸a˜ o limita-se a referenciar as c´elulas de mem´oria reservadas no programa principal. Na passagem de vectores como argumento o C utiliza sempre o enderec¸o da primeira posic¸a˜ o, n˜ao existindo nenhum mecanismo previsto que permita passar a totalidade dos elementos do vector por valor. Esta limitac¸a˜ o, inerente a` pr´opria origem do C, tinha por base a justificac¸a˜ o de que a passagem por valor de estruturas de dados de dimens˜ao elevada e´ pouco eficiente, dada a necessidade de copiar todo o seu conte´udo4 . Atendendo a que a, sem inclus˜ao do ´ındice, especifica um apontador para primeiro elemento do vector, uma terceira forma de declarar o argumento formal e´ void set(int *x,int n){ int k; for(k = 0; k < n; k++) x[k] = k+1; } Esta u´ ltima forma sugere uma forma alternativa de escrever o corpo da func¸a˜ o set. Com efeito, para percorrer um vector basta criar um apontador, inicializ´a-lo com o enderec¸o da primeira posic¸a˜ o do vector e seguidamente increment´a-lo sucessivamente para aceder a` s posic¸o˜ es seguintes. Esta t´ecnica pode ser ilustrada escrevendo a func¸a˜ o void set(int *x,int n){ int k; for(k = 0; k < n; k++) *x++ = k+1; } Note-se que sendo x um ponteiro cujo valor resulta de uma passagem c´opia do enderec¸o de a no programa principal, e´ poss´ıvel proceder ao seu incremento na func¸a˜ o de modo a percorrer todos os elementos do vector. Aqui, a express˜ao *x++ merece um coment´ario adicional. Em primeiro lugar, o operador ++ tem precedˆencia sobre o operador * e, deste modo, o incremento opera sobre o enderec¸o e n˜ao sobre a posic¸a˜ o de mem´oria em si. E´ este apenas o significado da precedˆencia, 4 Na vers˜ao original do C, esta limitac¸a˜ o estendia-se ao tratamento de estruturas de dados criadas com a directiva struct, mas esta limitac¸a˜ o foi levantada pelo C-Ansi 28 A PONTADORES o qual n˜ao deve ser confundido com a forma de funcionamento do operador incremento enquanto sufixo: o sufixo estabelece apenas que o conte´udo de x e´ utilizado antes da operac¸a˜ o de incremento se realizar. Ou seja, *x+=k;+ e´ equivalente a *x=k;x+;+. Um outro exemplo de utilizac¸a˜ o desta t´ecnica pode ser dado escrevendo uma func¸a˜ o para contabilizar o n´umero de caracteres usados de uma string. Atendendo a que se sabe que o u´ ltimo caracter est´a seguido do c´odigo ASCII 0, esta func¸a˜ o pode escrever-se int conta(char *s){ int n = 0; while(*s++ != ’\0’) n++; return n; } De facto, esta func¸a˜ o e´ equivalente a´ func¸a˜ o strlen, dispon´ıvel na biblioteca string.h. 2.6 2.6.1 Matrizes ˜ Declarac¸ao Na sua forma mais simples, a utilizac¸a˜ o de estruturas multidimensionais em C apenas envolve a declarac¸a˜ o de uma vari´avel com v´arios ´ındices, especificando cada um da dimens˜ao pretendida da estrutura. Por exemplo float x[3][2]; declara uma estrutura bidimensional de trˆes por dois reais, ocupando um total de seis palavras de mem´oria no modelo de mem´oria que temos vindo a usar como referˆencia. E´ frequente uma estrutura bidimensional ser interpretada como uma matriz, neste exemplo de trˆes linhas por duas colunas. Nos modos de utilizac¸a˜ o mais simples deste tipo de estruturas, o programadador pode abstrairse dos detalhes de implementac¸a˜ o e usar a vari´avel bidimensional como uma matriz. Assim, a inicializac¸a˜ o a zeros da estrutura x pode ser efectuada por float x[3][2]; int k,j; M ATRIZES 29 Endereço .. . Conteúdo Variável .. . .. . 1001 1.0 x[0][0] 1002 2.0 x[0][1] 1003 3.0 x[1][0] 1004 4.0 x[1][1] 1005 5.0 x[2][0] 1006 6.0 x[2][1] .. . .. . Figura 2.9: Mapa de mem´oria correspondente a` declarac¸a˜ o de uma estrutura de trˆes por dois reais for(k = 0 ; k < 3 ; k++) for(j = 0 ; j < 2 ; j++) x[k][j] = 0.0; Alternativamente, a inicializac¸a˜ o pode ser feita listando os valores iniciais, sendo apenas necess´ario agrupar hierarquicamente as constantes de inicializac¸a˜ o de acordo com as dimens˜oes da estrutura: float x[3][2] = {{1.0,2.0},{3.0,4.0}, {5.0,6.0}}; A disposic¸a˜ o em mem´oria desta estrutura e´ representada esquematicamente na figura 2.9. 2.6.2 ˜ Matrizes como argumento de func¸oes O facto de ser poss´ıvel omitir a dimens˜ao de um vector na declarac¸a˜ o do parˆametro formal de uma func¸a˜ o leva por vezes a pensar que o mesmo pode ser feito no caso de uma matriz. De facto, a situac¸a˜ o n˜ao e´ totalmente equivalente. Comece-se por regressar a` declarac¸a˜ o int x[3][2] e observar a forma como e´ determinado o enderec¸o de x[k][j]. Analisando a figura 2.9, e´ evidente que o enderec¸o deste elemento e´ obtido adicionando ao enderec¸o do primeiro elemento k*2+j. Ou seja, 30 A PONTADORES x[k][j] <-> *(&(x[0][0]) + k * 2 +j) No caso mais gen´erico da declarac¸a˜ o ter a forma x[N][M] ter-se-´a ainda x[k][j] <-> *(&(x[0][0]) + k * M +j) Ou seja, para aceder a um elemento gen´erico de uma matriz n˜ao basta conhecer o enderec¸o da primeira posic¸a˜ o e o os dois ´ındices de acesso: e´ necess´ario saber tamb´em o n´umero de colunas da matriz. Deste modo, quando uma matriz e´ passada como argumento de uma func¸a˜ o, e´ necess´ario que esta saiba a dimens˜ao das colunas da matriz. Por este motivo, dada a chamada a` func¸a˜ o /* ... */ #define NLIN 3 #define NCOL 2 /* ... */ int m[NLIN][NCOL]; int a; /* ... */ a = soma(m); /* ... */ o parˆametro formal da func¸a˜ o pode repetir na totalidade a declarac¸a˜ o da matriz, como em float norma(float x[NLIN][NCOL]){ int s = 0,k,j; for(k = 0; k < NLIN ; k++) for(j = 0; j < NCOL ; j++) s += x[k][j]; return s; } ou pode omitir o n´umero de linhas (dado que, como se mostrou, este n´umero n˜ao e´ indispens´avel para localizar o enderec¸o de um elemento gen´erico da matriz), como em float norma(float x[][NCOL]){ int s = 0,k,j; for(k = 0; k < NLIN ; k++) for(j = 0; j < NCOL ; j++) s += x[k][j]; return s; } M ATRIZES 31 No entanto, a omiss˜ao simultˆanea de ambos os ´ındices como em float norma(float x[][]){ /* ERRADO */ int s = 0,k,j; for(k = 0; k < NLIN ; k++) for(j = 0; j < NCOL ; j++) s += x[k][j]; return s; } n˜ao e´ poss´ıvel, gerando um erro de compilac¸a˜ o. Viu-se anteriormente que se o vector int x[NMAX] for utilizado na chamada a uma func¸a˜ o, a declarac¸a˜ o a adoptar nos parˆametros formais da func¸a˜ o podia ser ou int a[]) ou int *a. E´ frequente surgir a d´uvida se e´ poss´ıvel adoptar uma notac¸a˜ o de apontador equivalente no caso de uma matriz. De facto sim, embora esta notac¸a˜ o raramente seja utilizada na pr´atica. No caso do exemplo que tem vindo a ser utilizado a declarac¸a˜ o poss´ıvel seria float norma(float (*x)[NCOL]){ int s = 0,k,j; for(k = 0; k < NLIN ; k++) for(j = 0; j < NCOL ; j++) s += x[k][j]; return s; } Nesta declarac¸a˜ o, x e´ um apontador para um vector de NCOL floats. Uma explicac¸a˜ o mais detalhada do significado desta invulgar declarac¸a˜ o pode ser encontrado na secc¸a˜ o 2.6.4. 2.6.3 Matrizes e vectores Em C, uma matriz n˜ao e´ mais do que um vector de vectores. Ou seja, a declarac¸a˜ o int x[3][2] pode ser interpretada como “ x e´ um vector de 3 posic¸o˜ es, em que cada posic¸a˜ o e´ um vector de 2 posic¸o˜ es”. Esta interpretac¸a˜ o e´ tamb´em sugerida pela representac¸a˜ o que se adoptou na figura 2.9. Por outras palavras, x[0], x[1] e x[2] representam vectores de dois elementos constitu´ıdos respectivamente por {1.0 , 2.0}, {2.0 , 3.0}, e {4.0 , 5.0}. Na pr´atica, este facto significa que cada uma das linhas de uma matriz pode ser tratada individualmente como um vector. 32 A PONTADORES Suponha-se, por exemplo, que dada uma matriz a de dimens˜ao NLIN × NCOL, se pretende escrever uma func¸a˜ o que determine o m´aximo de cada uma das linhas da matriz e coloque o resultado num vector y. Esta func¸a˜ o pode ser escrita como void maxMat(float y[],float a[][NCOL]){ int k; for(k = 0; k < NLIN ; k++) y[k] = maxVec(a[k],NCOL); } onde cada linha foi tratada individualmente como um vector, cujo m´aximo e´ determinado pela func¸a˜ o vectorial float maxVec(float v[],int n){ float m; int k; m = v[0]; for(k = 1; k < n; k++) m = (v[k] > m ? v[k] : m); return m; } De igual forma, considere-se que se pretende calcular o produto matricial y = Ax onde A e´ uma matriz de dimens˜ao N × M e x e y s˜ao vectores de dimens˜ao M e N , respectivamente. Admita-se que o programa principal era escrito como #define N 3 #define M 2 int main(){ float A[N][M] = {{1,2},{3,4},{5,6}}; float x[M] = {10,20}; float y[N]; int k; matVecProd(y,A,x); for(k = 0; k < N ; k++) printf("%f\n",y[k]); exit(0); } M ATRIZES 33 Atendendo a que o produto de uma matriz por um vector e´ um vector em que cada elemento n˜ao e´ mais do que o produto interno de cada linha da matriz com o vector operando, a func¸a˜ o prodMatVec poderia ser escrita como void matVecProd(float y[],float A[][M],float x[]){ int k; for(k = 0; k < N ; k++) y[k] = prodInt(A[k],x,M); } com o produto interno definido por float prodInt(float a[],float b[],int n){ float s = 0.0; int k; for(k = 0; k < n; k++) s += a[k] * b[k]; return s; } Uma u´ ltima situac¸a˜ o em que e´ poss´ıvel exemplificar a utilizac¸a˜ o de linhas como vectores e´ o do armazenamento de v´arias linhas de texto. Admita-se, por exemplo, que se pretende ler uma sequˆencia de linhas de texto e imprimir as mesmas linhas por ordem inversas. Tal e´ poss´ıvel atrav´es da utilizac¸a˜ o de uma matriz de caracteres, utilizando cada linha como uma string convencional: #include #include #include #define NUM_LINHAS 5 #define DIM_LINHA 40 int main(){ char texto[NUM_LINHAS][DIM_LINHA]; int k; /* Leitura */ printf("Escreva uma sequˆ encia de %d linhas:\n",NUM_LINHAS); for(k = 0;k < NUM_LINHAS; k++){ fgets(texto[k],DIM_LINHA,stdin); if(texto[k][strlen(texto[k])-1] != ’\n’){ printf("Erro: linha demasiado comprida.\n"); exit(1); 34 A PONTADORES } } /* Escrita */ printf("\nLinhas por ordem inversa:\n"); for(k = NUM_LINHAS-1;k >= 0; k--) printf("%s",texto[k]); exit(0); } 2.6.4 Matrizes e apontadores Se a utilizac¸a˜ o de estruturas multidimensionais e´ relativamente simples e intuitiva, j´a o mesmo nem sempre sucede quando e´ necess´ario manipular apontadores relacionados com este tipo. 5 O tratamento de matrizes no C e´ frequentemente fonte de alvo de d´uvidas. Se se perguntar a qualquer programador de C “dada a declarac¸a˜ o int x[3], qual e´ o tipo de x quando usado isoladamente”, nenhum ter´a d´uvidas em afirmar que a resposta e´ “apontador para int”. Experimente-se, no entanto, a colocar a pergunta semelhante “dada a declarac¸a˜ o int x[3][2], qual e´ o tipo de x quando usado isoladamente” e, com elevada probabilidade, n˜ao ser´a obtida a mesma unanimidade nas respostas. Dado que, por consistˆencia sint´actica, se tem sempre x[k] <-> *(x + k) ent˜ao, no caso de uma estrutura bidimensional, ter´a que ser x[k][j] <-> *(x[k] + j) e, portanto, se x[k][j] e´ por exemplo do tipo float, x[k] e´ um apontador para float. Consultando novamente o exemplo da figura 2.9, significa isto que x[0] corresponde a um apontador para float com o valor 1001 (e portanto o enderec¸o do primeiro elemento do vector de floats formado pelos reais 1.0 e 2.0, enquanto que x[2] corresponde tamb´em a um apontador, cujo valor e´ 1003 (primeiro elemento do vector de floats formado pelos reais 3.0 e 4.0). Considere-se agora, novamente, a quest˜ao de qual o tipo de x, quando considerado isoladamente. Como j´a se disse anteriormente em C, uma estrutura multidimensional representa sempre 5 A leitura desta secc¸a˜ o pode ser omitida numa abordagem introdut´oria da linguagem C. M ATRIZES 35 uma hierarquia de vectores simples. Ou seja, x[3][2] representa um vector de trˆes elementos, em que cada um e´ por sua vez um vector de dois elementos. Assim, x[k] representa sempre um vector de dois floats. Com esta formulac¸a˜ o, resulta claro que x representa um apontador para um vector de dois floats. Isto n˜ao e´ mais do que a generalizac¸a˜ o da situac¸a˜ o dos vectores, em que dada a declarac¸a˜ o int a[N], se sabe que a isoladamente e´ um apontador para inteiro. Assim, e´ natural que x sem ´ındices especifique o enderec¸o do primeiro elemento de um vector de trˆes elementos, em que cada um e´ um vector de dois floats. Ou seja, no nosso exemplo, x corresponde ao valor 1001. Mas, afinal, qual a diferenc¸a entre um apontador para float e um apontador para um vector de floats? Por um lado, a forma de usar este tipo vari´avel num enderec¸amento indirecto e´ claramente diferente. Por exemplo, x e &x[0][0] correspondem ao mesmo valor (1001 no nosso exemplo), mas s˜ao tipos diferentes: o primeiro e´ um apontador para um vector, pelo que *x e´ um vector (a primeira linha de x, enquanto que *(&x[0][0]) e´ um float (o conte´udo de x[0][0]. No entanto, e´ provavelmente mais importante reter que a diferenc¸a fundamental reside na dimens˜ao do objecto apontado. Considere-se novamente o exemplo que temos vindo a considerar. Uma vari´avel do tipo apontador para float, cujo valor seja 1001, quando incrementada passa a ter o valor 1002. Mas uma vari´avel do tipo apontador para vector de dois float, cujo valor seja 1001, quando incrementada passa a ter o valor 1003 (j´a que o incremento e´ escalado pela dimens˜ao do objecto apontado). E´ interessante verificar que este modelo e´ o u´ nico que permite manter a consistˆencia sint´actica do C na equivalˆencia entre vectores e apontadores. J´a se disse que sendo sempre x[k] <-> *(x + k) ent˜ao, x[k][j] <-> *(x[k] + j) O que n˜ao se disse antes, mas que tamb´em se verifica, e´ que a primeira destas regras tamb´em deve ser aplic´avel a` entidade x[k] que surge na u´ ltima express˜ao. Ou seja, tem-se x[k][j] <-> *(*(x+k) + j) o que exige por outro lado estabelecer que 36 A PONTADORES Express˜ao Tipo Valor x Apontador para vector de dois floats Apontador para vector de dois floats Apontador para float Apontador para float float Apontador para float float float Apontador para float 1001 Elemento da estrutura - 1003 - 1003 1006 6.0 1001 1.0 2.0 1003 x[2][1] x[0][0] x[0][1] - x+1 *(x+1) *(x+2)+1 *(*(x+2)+1) *x **x *(*x+1) x[1] Tabela 2.1: Exemplos de tipos e valores derivados do exemplo da figura 2.9. • Se a e´ um apontador para um escalar, *a e´ desse tipo escalar, e o valor de *a e´ o conte´udo do enderec¸o especificado por a. • Se a e´ um apontador para um vector de elementos de um dado tipo, *a e´ um apontador para um elemento do tipo constituinte, e o seu valor e´ idˆentico ao de a. Um factor que contribui frequentemente para alguma confus˜ao deriva do facto de que, ainda que x n˜ao seja sintacticamente um duplo apontador para float, sendo x[0][0] <-> *(*(x+0) + 0) <-> **x verifica-se que **x e´ um float. A consistˆencia destas equivalˆencias podem ser verificada considerando casos particulares do exemplo que tem vindo a ser utilizado como referˆencia, tal como listados na tabela 2.1. O c´odigo de um pequeno programa que permite validar esta tabela est´a listado no apˆendice A. Como e´ natural, e´ poss´ıvel declarar um apontador para um vector de dois floats, sem ser da forma impl´ıcita que resulta da declarac¸a˜ o da matriz. A declarac¸a˜ o de uma vari´avel y deste tipo pode ser feita por float (*y)[2]; Por este motivo, que quando a matriz x e´ passada por argumento para uma func¸a˜ o func, a M ATRIZES 37 declarac¸a˜ o do parˆametro formal poder ser feita repetindo a declarac¸a˜ o total, omitindo a dimens˜ao do ´ındice interior, ou ent˜ao por void func(float (*y)[2]); tal como se referiu na secc¸a˜ o 2.6.2. Dado que este tipo de declarac¸o˜ es e´ alvo frequente de confus˜ao, e´ conveniente saber que existe uma regra de leitura que ajuda a clarificar a semˆantica da declarac¸a˜ o. Com efeito, e´ suficiente “seguir” as regras de precedˆencia, procedendo a` leitura na seguinte sequˆencia: float (*y)[3] yé um apontador para um vector de três floats Sublinhe-se que, face a tudo o que ficou dito anteriormente, n˜ao e´ poss´ıvel declarar um apontador para um vector sem especificar a dimens˜ao do vector: como j´a foi dito por diversas vezes, um apontador tem que conhecer a dimens˜ao do objecto apontado. Isto n˜ao e´ poss´ıvel sem especificar a dimens˜ao do vector. Como corol´ario, um apontador para um vector de trˆes inteiros e´ de tipo distinto de um apontador para um vector para seis inteiros, n˜ao podendo os seus conte´udos ser mutuamente atribu´ıdos. Note-se que a declarac¸a˜ o que se acabou de referir e´ claramente distinta de float *y[2]; onde, devido a` ausˆencia de parenteses, e´ necess´ario ter em atenc¸a˜ o a precedˆencia de “[]” sobre o “*”. Neste caso, y e´ um vector de dois apontadores para float, podendo a leitura da declarac¸a˜ o ser realizada pela sequˆencia: float *y[3] yé um vector de três apontadores para float A utilizac¸a˜ o de vectores de apontadores e´ abordada em maior detalhe na secc¸a˜ o 3.5.4. Finalmente, refira-se que os apontadores para vectores podem ainda surgir noutros contextos: dada a declarac¸a˜ o int x[10], x e´ um apontador para inteiro, mas a express˜ao &x e´ do tipo 38 A PONTADORES apontador para um vector de 10 inteiros. 2.7 ˜ para mais do que duas dimensoes ˜ Generalizac¸ao A generalizac¸a˜ o do que ficou dito para mais do que duas dimens˜oes e´ directa. Considere-se, como referˆencia, a declarac¸a˜ o da estrutura int x[M][N][L]; 1. No c´alculo do enderec¸o de qualquer elemento da estrutura tem-se a igualdade: &(x[m][n][l]) == (&x[0][0][0]) + m * (N*L) + n * L + l 2. x[k][j] e´ um apontador para inteiro. 3. x[k] e´ um apontador para um vector de L inteiros. 4. x e´ um apontador para uma matriz de N×L inteiros. 5. Em geral, x[m][n][l] == *(*(*(x+m)+n)+l) A passagem de uma estrutura multidimensional como argumento pode ser feita pela repetic¸a˜ o da declarac¸a˜ o completa do tipo. Assim, uma declarac¸a˜ o poss´ıvel e´ #define M ... #define N ... #define L ... void func(int x[M][N][L]){ /* ... */ } int main(){ int x[M][N][L]; /*...*/ func(x); /*...*/ return 0; } ˜ PARA MAIS DO QUE DUAS DIMENS OES ˜ G ENERALIZAC¸ AO 39 Como se mostrou anteriormente, o c´alculo do enderec¸o de um elemento gen´erico de uma estrutura tridimensional exige o conhecimento das duas dimens˜oes exteriores da estrutura (N e L no exemplo). A generalizac¸a˜ o desta regra mostra que para calcular o enderec¸o de um elemento de uma estrutura n-dimensional e´ necess´ario conhecer com rigor os n − 1 ´ındices exteriores da estrutura. Deste modo, nos argumentos formais de uma func¸a˜ o e´ sempre poss´ıvel omitir a dimens˜ao do primeiro ´ındice de uma estrutura multidimensional, mas n˜ao mais do que esse. No exemplo anterior, pode ent˜ao escrever-se void func(int x[][N][L]){ /* ... */ } Claro que todas as outras variantes em que exista consistˆencia sint´actica entre os argumentos formais e actuais do procedimento s˜ao v´alidas. Assim, pelas mesmas raz˜oes j´a detalhadas na secc¸a˜ o 2.6.4, void func(int (*x)[N][L]){ /* ... */ } e´ uma alternativa sintacticamente correcta neste caso. Cap´ıtulo 3 ´ ˆ Vectores e memoria dinamica 3.1 ˜ Introduc¸ao At´e ao aparecimento da linguagem C, a maioria das linguagens de alto n´ıvel exigia um dimensionamento r´ıgido das vari´aveis envolvidas. Por outras palavras, a quantidade m´axima de mem´oria necess´aria durante a execuc¸a˜ o do programa deveria ser definida na altura de compilac¸a˜ o do programa. Sempre que os requisitos de mem´oria ultrapassavam o limite fixado durante a execuc¸a˜ o do programa, este deveria gerar um erro. A soluc¸a˜ o nestes casos era recompilar o programa aumentando a dimens˜ao das vari´aveis, soluc¸a˜ o s´o poss´ıvel se o utilizador tivesse acesso e dominasse os detalhes do c´odigo fonte. Por exemplo, um programa destinado a` simulac¸a˜ o de um circuito electr´onico poderia ser obrigado a definir no c´odigo fonte o n´umero m´aximo de componentes do circuito. Caso este n´umero fosse ultrapassado, o programa deveria gerar um erro, porque a mem´oria reservada durante a compilac¸a˜ o tinha sido ultrapassada. A alternativa nestas situac¸o˜ es era a reserva a` partida de uma dimens˜ao de mem´oria suficiente para acomodar circuitos de dimens˜ao elevada, mas tal traduzia-se obviamente num consumo excessivo de mem´oria sempre que o programa fosse utilizado para simular circuitos de dimens˜ao reduzida. Em alguns casos, os programas recorriam a` utilizac¸a˜ o de ficheiros para guardar informac¸a˜ o tempor´aria, mas esta soluc¸a˜ o implicava geralmente uma complexidade algor´ıtmica acrescida. Tal como o nome indica, os sistemas de mem´oria dinˆamica permitem gerir de forma dinˆamica os requisitos de mem´oria de um dado programa durante a sua execuc¸a˜ o. Por exemplo, no caso do sistema de simulac¸a˜ o referido anteriormente, o programa pode, no in´ıcio da execuc¸a˜ o, determinar a dimens˜ao do circuito a simular e s´o nessa altura reservar a mem´oria necess´aria. Com esta metodologia, o programa pode minimizar a quantidade de mem´oria reservada e, deste modo, permitir que o sistema operativo optimize a distribuic¸a˜ o de mem´oria pelos v´arios programas que ´ ˆ 42 V ECTORES E MEM ORIA DIN AMICA se encontram simultaneamente em execuc¸a˜ o. No entanto, at´e ao aparecimento do C este tipo de mecanismos, caso estivessem dispon´ıveis no sistema operativo, eram apenas acess´ıveis em linguagem m´aquina ou Assembler, mais uma vez pela necessidade de manipular directamente enderec¸os de mem´oria. Apesar da flexibilidade oferecida, a gest˜ao directa da mem´oria dinˆamica exige algumas precauc¸o˜ es suplementares durante o desenvolvimento do c´odigo. Por este motivo, muitas linguagens de programac¸a˜ o de alto-n´ıvel mais recentes (Lisp, Java, Scheme, Python, Perl, Mathematica, entre outras) efectuam a gest˜ao autom´atica da mem´oria dinˆamica, permitindo assim que o programador se concentre na implementac¸a˜ o algor´ıtmica e nos modelos de dados associados, sem se preocupar explicitamente com os problemas de dimensionamento de vari´aveis ou os volumes de mem´oria necess´arios para armazenamento de dados. Neste contexto, poder´a perguntar-se quais as vantagens de programar em C ou porque e´ que o C ainda mant´em a popularidade em tantas a´ reas de aplicac¸a˜ o. H´a diversas respostas para esta quest˜ao: • A gest˜ao directa da mem´oria dinˆamica permite normalmente a construc¸a˜ o de programas com maior eficiˆencia e com menores recursos computacionais. • Dada a evidente complexidade dos compiladores de linguagens mais elaboradas, e´ frequente microprocessadores e controladores especializados (processadores digitais de sinal, microcontroladores, etc) apenas disporem de compiladores para a linguagem C, a qual se encontra mais pr´oximo da linguagem m´aquina do que linguagens conceptualmente mais elaboradas. Por este motivo, o C ainda se reveste hoje de uma importˆancia crucial em diversas a´ reas da Engenharia Electrot´ecnica, nomeadamente em aplicac¸o˜ es que implicam o recurso a microcontroladores especializados. • Dada a sua proximidade com o hardware1 , a maioria dos sistemas operativos actualmente existentes s˜ao ainda hoje programados na linguagem C. • A maioria dos compiladores de linguagens de alto n´ıvel, incluindo o pr´oprio C, s˜ao actualmente escritos e desenvolvidos em C. Ou seja, nesta perspectiva, o C e´ hoje uma linguagem indispens´avel a` gerac¸a˜ o da maioria das outras linguagens, constituindo, neste sentido, a “linguagem das linguagens”. 1 O C e´ frequentemente designado na g´ıria como um Assembler de alto n´ıvel, apesar do evidente paradoxo contido nesta designac¸a˜ o. V ECTORES 43 3.2 Vectores Em C, um vector e´ uma colecc¸a˜ o com um n´umero bem definido de elementos do mesmo tipo. Ao encontrar a declarac¸a˜ o de um vector num programa, o compilador reserva automaticamente espac¸o em mem´oria para todos os seus elementos. Por raz˜oes de clareza e de boa pr´atica de programac¸a˜ o, estas constantes s˜ao normalmente declaradas de forma simb´olica atrav´es de uma directiva define, mas a dimens˜ao continua obviamente a ser uma constante: #define DIM_MAX 100 #define DIM_MAX_STRING 200 int char x[DIM_MAX]; s[DIM_MAX_STRING]; Por outras palavras, a necessidade de saber quanta mem´oria e´ necess´aria para o vector que se pretende utilizar implica que a dimens˜ao deste vector seja uma constante, cujo valor j´a conhecido durante a compilac¸a˜ o do programa. Como e´ sabido, as vari´aveis locais a uma func¸a˜ o tˆem um tempo de vida limitado a` execuc¸a˜ o da func¸a˜ o2 . O espac¸o para estas vari´aveis e´ reservado na chamada zona da pilha (stack), uma regi˜ao de mem´oria dedicada pelo programa para armazenar vari´aveis locais e que normalmente e´ ocupada por ordem inversa. Por outras palavras, s˜ao usados primeiros os enderec¸os mais elevados e v˜ao sendo sucessivamente ocupados os enderec¸os inferiores a` medida que s˜ao reservadas mais vari´aveis locais. Dada a forma como se realiza esta ocupac¸a˜ o de mem´oria, o limite inferior da regi˜ao da pilha e´ geralmente designada por topo da pilha. Neste sentido, quando uma func¸a˜ o e´ chamada, o espac¸o para as vari´aveis locais e´ reservado no topo da pilha, que assim decresce. Quando uma func¸a˜ o termina, todas as vari´aveis locais s˜ao libertadas e o topo da pilha aumenta novamente. Assim, por exemplo, dada a declarac¸a˜ o int function(int z){ int a; int x[5]; int b; /* ... */ a chamada da func¸a˜ o func poderia dar origem a uma evoluc¸a˜ o do preenchimento da mem´oria da forma como se representa na figura 3.1. 2 Excepto se a sua declarac¸a˜ o for precedido do atributo static, mas este s´o e´ usado em circunstˆancias excepcionais. ´ ˆ 44 V ECTORES E MEM ORIA DIN AMICA Endereço .. . Conteúdo Variável .. . .. . Endereço .. . Conteúdo Variável .. . .. . 1001 1001 topo da pilha 1002 1002 b 1003 1003 x[0] 1004 1004 x[1] 1005 1005 x[2] 1006 1006 x[3] 1007 1007 a 1008 1008 z 1009 1009 ... .. . .. . topo da pilha A .. . .. . B Figura 3.1: Mapa de mem´oria antes e ap´os a chamada a` func¸a˜ o func (A) e mapa de mem´oria durante a execuc¸a˜ o da func¸a˜ o (B). A necessidade da dimens˜ao dos vectores ser conhecida na altura da compilac¸a˜ o conduz a` impossibilidade de declarac¸o˜ es do tipo int function(int n){ int x[n]; /* MAL: n ´ e uma vari´ avel, e o seu valor ´ e desconhecido na altura da compilac ¸˜ ao */ /* ... */ Como e´ evidente, os elementos de um vector podem n˜ao ser escalares simples como no exemplo anterior. Os elementos de um vector podem ser estruturas de dados, como por exemplo em #define MAX_NOME 200 #define MAX_ALUNOS 500 typedef struct _tipoAluno { int numero; char nome[MAX_NOME]; } tipoAluno; V ECTORES 45 int main(){ tipoAluno alunos[MAX_ALUNOS]; /* ... */ ou mesmo outro vector. Com efeito, na declarac¸a˜ o int mat[10][5] a matriz mat, do ponto de vista interno do C, n˜ao e´ mais do que um vector de 10 elementos, em que cada elemento e´ por sua vez um vector de 5 inteiro (V. secc¸a˜ o 2.6.3). A necessidade de definir de forma r´ıgida a dimens˜ao dos vectores na altura da compilac¸a˜ o obriga frequentemente ao estabelecimento de um compromisso entre a dimens˜ao m´axima e a eficiˆencia do programa em termos da mem´oria utilizada. Uma soluc¸a˜ o poss´ıvel e´ utilizar um majorante dos valores “t´ıpicos”. Considere-se novamente um sistema para simulac¸a˜ o de circuitos electr´onicos. Se os sistemas que se pretende simular tˆem em m´edia 1000 componentes, poderse-ia utilizar um valor de 2000 ou 5000 para dimensionar o vector de componentes. No entanto, a utilizac¸a˜ o de um valor m´aximo elevado pode conduzir a situac¸o˜ es frequentes de desperd´ıcio de mem´oria, com o programa a reservar a` partida volumes de mem´oria muito superiores aos necess´arios, enquanto que um valor reduzido deste parˆametro pode limitar seriamente a dimens˜ao dos problemas a abordar. Por outro lado, e´ por vezes dif´ıcil encontrar parˆametros razo´aveis para definir “valor m´edio”. Em determinadas aplicac¸o˜ es, 50 componentes pode ser um valor razo´avel, noutras 10,000 pode ser um valor reduzido. Para agravar a situac¸a˜ o, a dimens˜ao de mem´oria que e´ razo´avel reservar a` partida depende da mem´oria f´ısica dispon´ıvel no sistema em que se est´a a trabalhar: sistemas com 8MB ou 8GB de mem´oria conduzem obviamente a situac¸o˜ es distintas. N˜ao se pretende com esta an´alise colocar de parte todas as utilizac¸o˜ es de vectores convencionais com dimens˜ao fixa. Frequentemente, esta e´ uma soluc¸a˜ o mais que razo´avel. Por exemplo, uma linha de texto numa consola de texto tem em geral cerca de 80 caracteres, pelo que a definic¸a˜ o de um valor m´aximo para o comprimento de uma linha de 160 ou 200 caracteres e´ um valor que pode ser razo´avel e que n˜ao e´ excessivo na maioria das aplicac¸o˜ es. No entanto, em situac¸o˜ es em que o n´umero de elementos pode variar significativamente, e´ desej´avel que a mem´oria seja reservada a` medida das necessidades. ´ ˆ 46 V ECTORES E MEM ORIA DIN AMICA 3.3 ˆ “Vectores” dinamicos Uma forma de ultrapassar a dificuldade criada pelo dimensionamento de vectores e´ a reserva de blocos de mem´oria s´o ser realizada durante a execuc¸a˜ o do programa. Considere-se novamente o simulador de circuitos electr´onicos. A ideia essencial e´ iniciar o programa com um volume de mem´oria m´ınimo, determinar qual o n´umero de componentes do sistema a simular e s´o depois deste passo reservar espac¸o para manipular o n´umero de componentes pretendido. Recorde-se que ao declarar um vector int x[MAX_DIM] a utilizac¸a˜ o do nome do vector sem especificar o ´ındice e´ equivalente ao enderec¸o do ´ındice 0 do vector. Por outras palavras, a utilizac¸a˜ o no programa de x e´ equivalente a &x[0].Decorre tamb´em daqui a regra geral de indexac¸a˜ o em C: x[k] <-> *(x + k) equivalˆencia que e´ universal em C. A utilizac¸a˜ o de um bloco de mem´oria criado dinamicamente e que pode ser utilizado com mecanismos de acesso idˆenticos aos de um vector pode ser realizado declarando uma vari´avel de tipo de apontador e solicitando ao sistema operativo a reserva de um bloco de mem´oria da dimens˜ao pretendida. O pedido de reserva de mem´oria ao sistema operativo e´ efectuado atrav´es de um conjunto de func¸o˜ es declaradas no ficheiro stdlib.h. Uma das func¸o˜ es utiliz´aveis para este efeito tem como prot´otipo void *calloc(size_t nmemb, size_t size); A func¸a˜ o calloc reserva um bloco de mem´oria cont´ıgua com espac¸o suficiente para armazenar nmemb elementos de dimens˜ao size cada, devolvendo o enderec¸o (apontador) para a primeira posic¸a˜ o do bloco. size_t e´ o tipo utilizado para especificac¸a˜ o de dimens˜oes num´ericas em v´arias func¸o˜ es do C e a sua implementac¸a˜ o pode ser dependente do sistema operativo, mas corresponde geralmente a um inteiro sem sinal. O tipo de retorno, void*, corresponde a um enderec¸o gen´erico de mem´oria. o que permite que a func¸a˜ o seja utilizada independentemente do tipo espec´ıfico do apontador que se pretende inicializar. A func¸a˜ o retorna um apontador para o primeiro enderec¸o de ˆ “V ECTORES ” DIN AMICOS 47 uma regi˜ao de mem´oria livre da dimens˜ao solicitada. Caso esta reserva n˜ao seja poss´ıvel, a func¸a˜ o retorna NULL, para indicar que a reserva n˜ao foi efectuada. Considere-se, como exemplo, um programa para c´alculo da m´edia e variˆancia de N valores reais indicados pelo programador. Em vez de utilizar um vector de dimens˜ao fixa, e´ poss´ıvel utilizar apenas um apontador para um real e um programa com a seguinte estrutura: /* Ficheiro: media.c Conte´ udo: Programa para c´ alculo da m´ edia e variˆ anicia Autor: Fernando M. Silva, IST ([email protected]) Hist´ oria : 2001/06/01 - Criac ¸˜ ao */ #include #include int main(){ int n; /* N´ umero de valores */ float *f; /* Apontador para o primeiro valor */ float soma,media,variancia; int k; printf("Indique quantos valores pretende utilizar: "); scanf("%d",&n); f = (float*) calloc(n,sizeof(float)); /* reserva de mem´ oria para um bloco de n reais */ if(f == NULL){ /* Teste da reserva de mem´ oria */ fprintf(stderr,"Erro na reserva de mem´ oria\n"); exit(1); } /* A partir deste ponto, f pode ser tratado como um vector de n posic ¸˜ oes */ for( k = 0 ; k < n ; k++){ printf("Indique o valor ´ ındice %d : ",k); scanf("%f",&f[k]); } soma = 0.0; for( k = 0 ; k < n ; k++) soma += f[k]; media = soma / n; /* c´ alculo da m´ edia */ soma = 0.0; ´ ˆ 48 V ECTORES E MEM ORIA DIN AMICA for( k = 0 ; k < n ; k++) soma += (f[k] - media) * (f[k] - media); variancia = soma / n; /* c´ alculo da var. */ printf("M´ edia = %5.2f, var = %5.2f\n", media, variancia); free(f); exit(0); } Na chamada a` func¸a˜ o calloc(), dois aspectos devem ser considerados. Em primeiro lugar o operador sizeof() e´ um operador intr´ınseco do C que devolve a dimens˜ao (geralmente em bytes) do tipo ou vari´avel indicado no argumento. Por outro lado, a` esquerda da func¸a˜ o calloc() foi acrescentada e express˜ao (float*). Esta express˜ao funciona como um operador de cast, obrigando a` convers˜ao do apontador gen´erico devolvido pela func¸a˜ o calloc para um apontador para real. Em geral, na operac¸a˜ o de cast e´ indicado o tipo do apontador que se encontra do lado esquerdo da atribuic¸a˜ o. Embora este cast n˜ao seja obrigat´orio, e´ geralmente utilizado na linguagem C como uma garantia adicional da consistˆencia das atribuic¸o˜ es. Ap´os a reserva dinˆamica de mem´oria, o apontador f pode ser tratado como um vector convencional. Como e´ evidente, o ´ındice n˜ao deve ultrapassar a posic¸a˜ o n − 1, dado que para valores superiores se estaria a aceder a posic¸o˜ es de mem´oria inv´alidas. Enquanto que as vari´aveis locais s˜ao criadas na zona da pilha, toda a mem´oria reservada dinamicamente e´ geralmente criada numa regi˜ao independente de mem´oria designada por heap (molhe). Admita-se, no caso do programa para c´alculo das m´edias e variˆancias, que o valor de n especificado pelo utilizador era 4. Uma poss´ıvel representac¸a˜ o do mapa de mem´oria antes e ap´os a chamada da func¸a˜ o calloc encontra-se representado na figura 3.2. 3.4 ˜ da memoria ´ ˆ Gestao dinamica 3.4.1 ˜ da reserva de memoria ´ Verificac¸ao Como j´a se referiu, ao efectuar uma reserva dinˆamica de mem´oria e´ essencial testar se os recursos solicitados foram ou n˜ao disponibilizados pelo sistema. Com efeito, a reserva de mem´oria pode ser mal sucedida por falta de recursos dispon´ıveis no sistema. Este teste teste e deve ser sempre efectuado testando o apontador devolvido pela func¸a˜ o calloc(). Quando existe um erro, a func¸a˜ o devolve o enderec¸o 0 de mem´oria, o qual e´ representado simbolicamente pela constante ˜ DA MEM ORIA ´ ˆ G EST AO DIN AMICA 49 Zona da pilha (var. locais) Endereço .. . Conteúdo Zona do heap (var. dinamicas) Variável Endereço .. . .. . .. . ... 1001 k 2102 1003 variancia 2103 1004 media 2104 1005 soma 2105 1006 f 2106 n 2107 ... 2108 4 1008 1009 .. . Variável .. . .. . ... 2101 1002 1007 Conteúdo topo ... 2109 .. . .. . .. . A Zona da pilha (var. locais) Endereço .. . Conteúdo .. . Zona do heap (var. dinamicas) Variável Endereço .. . .. . ... 1001 Conteúdo Variável .. . .. . ... 2101 1002 k 2102 *f 1003 variancia 2103 *(f+1) <−> f[1] 1004 media 2104 *(f+2) <−> f[2] 1005 soma 2105 *(f+3) <−> f[3] f 2106 n 2107 ... 2108 1006 1007 2102 4 1008 1009 .. . <−>f[0] topo ... 2109 .. . .. . .. . B Figura 3.2: Mapa de mem´oria antes da reserva de mem´oria (A) e ap´os a reserva de mem´oria. (B). ´ ˆ 50 V ECTORES E MEM ORIA DIN AMICA NULL (definida em stdio.h). Se o enderec¸o devolvido tiver qualquer outro valor, a reserva de mem´oria foi bem sucedida, e o programa pode continuar a sua execuc¸a˜ o normal. 3.4.2 ˜ ˜ de memoria ´ ˆ Outras func¸oes de gestao dinamica ˜ free() Func¸ao Enquanto que a func¸a˜ o calloc() efectua uma reserva dinˆamica de mem´oria, a func¸a˜ o free(p) liberta o bloco de mem´oria apontado por p. Como seria de esperar, esta func¸a˜ o s´o tem significado se o apontador p foi obtido por uma func¸a˜ o pr´evia de reserva de mem´oria, como a func¸a˜ o calloc() descrita anteriormente. De um modo geral, e´ boa pr´atica de programac¸a˜ o libertar toda a mem´oria reservada pelo programa sempre que esta deixa de ser necess´aria. Isto sucede frequentemente pela necessidade de libertar mem´oria que s´o foi necess´aria temporariamente pelo programa. Note-se que sempre que o programa termina toda a mem´oria reservada e´ automaticamente libertada. Deste modo, a libertac¸a˜ o expl´ıcita da mem´oria reservada durante a execuc¸a˜ o do programa n˜ao e´ estritamente obrigat´oria antes de sair do programa atrav´es da func¸a˜ o exit() ou da directiva return no bloco main(). Apesar deste facto, alguns autores consideram que, por raz˜oes de consistˆencia e arrumac¸a˜ o do c´odigo, o programa deve proceder a` libertac¸a˜ o de toda a mem´oria reservada antes de ser conclu´ıdo. E´ este o procedimento adoptado no programa para c´alculo da variˆancia, onde a func¸a˜ o free() e´ chamada antes do programa terminar. ˜ malloc() Func¸ao A func¸a˜ o malloc() tem por prot´otipo void *malloc(size_t total_size); onde total_size representa a dimens˜ao total da mem´oria a reservar, expressa em bytes. A func¸a˜ o malloc() e´ muito semelhante a` func¸a˜ o calloc() A forma calloc(n,d) pode ser simplesmente substitu´ıda por malloc(n*d). A u´ nica diferenc¸a formal entre as duas func¸o˜ es e´ que enquanto a func¸a˜ o calloc() devolve um bloco de mem´oria inicializado com zero em todas as posic¸o˜ es, a func¸a˜ o malloc() n˜ao efectua explicitamente esta inicializac¸a˜ o. ˜ DA MEM ORIA ´ ˆ G EST AO DIN AMICA 51 ˜ realloc() Func¸ao A func¸a˜ o realloc() pode ser utilizada sempre que e´ necess´ario modificar a dimens˜ao de um bloco de mem´oria dinˆamica reservado anteriormente. O prot´otipo da func¸a˜ o e´ void *realloc(void *old_ptr,size_t total_new_size); onde old_ptr e´ o apontador para o bloco de mem´oria reservado anteriormente, enquanto que total_new_size e´ a dimens˜ao total que se pretende agora para o mesmo bloco. A func¸a˜ o retorna um apontador para o bloco de mem´oria redimensionado. Note-se que o segundo argumento tem um significado semelhante ao da func¸a˜ o malloc. Suponha-se, por exemplo, que no in´ıcio de um programa tinha sido reservado um bloco de mem´oria para a n inteiros: int main(){ int *x,n; /* Obtenc ¸˜ ao do valor de n ... */ x = (int*) calloc(n,sizeof(int)); mas que, mais tarde, se verificou a necessidade de acrescentar 1000 posic¸o˜ es a este bloco de mem´oria. Este resultado pode ser obtido fazendo x = (int*) realloc(x,(n+1000)*sizeof(int)); O funcionamento exacto da func¸a˜ o realloc() depende das disponibilidades de mem´oria existentes. Suponha-se, para assentar ideias, que inicialmente se tinha n=2000, e que o valor de x resultante da func¸a˜ o calloc era 10000 (figura 3.3, A). Deste modo o bloco de mem´oria reservado inicialmente estendia-se do enderec¸o 10000 ao enderec¸o 11999. Quando mais tarde e´ chamada a func¸a˜ o realloc duas situac¸o˜ es podem ocorrer. Se os enderec¸os de mem´oria entre 12000 e 12999 estiverem livres, o bloco de mem´oria e´ prolongado, sem mudanc¸a de s´ıtio, pelo que o valor de x permanece inalterado (figura 3.3, B). No entanto, se algum enderec¸o entre 12000 e 12999 estiver ocupado (figura 3.3, C), e´ necess´ario deslocar todo o bloco de mem´oria para uma nova regi˜ao. Neste caso, o novo bloco de mem´oria pode ser mapeado, por exemplo, entre os enderec¸os 12500 e 15499 (figura 3.3, D), retornando a func¸a˜ o realloc o novo enderec¸o da primeira posic¸a˜ o de mem´oria (12500). Como complemento, a func¸a˜ o realloc trata de copiar automaticamente todo ´ ˆ 52 V ECTORES E MEM ORIA DIN AMICA o conte´udo guardado nos enderec¸os 10000 a 11999 para os enderec¸os 12500 a 14499 e liberta as posic¸o˜ es de mem´oria iniciais. 3.4.3 Garbbage Como referido anteriormente, o espac¸o de mem´oria para as vari´aveis locais e´ reservado na zona de mem´oria da pilha quando uma func¸a˜ o e´ chamada, sendo automaticamente libertado (e com ele as vari´aveis locais) quando a func¸a˜ o termina. Ao contr´ario das vari´aveis locais, a mem´oria dinˆamica e´ criada e libertada sob controlo do programa, atrav´es das chamadas a` s func¸o˜ es calloc(), malloc() e free(). Em programas complexos, e´ frequente um programa reservar blocos de mem´oria que, por erro do programador ou pela pr´opria estrutura do programa, n˜ao s˜ao libertados mas para os quais se perdem todos os apontadores dispon´ıveis. Neste caso, o bloco de mem´oria dinˆamica e´ reservado, mas deixa de ser acess´ıvel porque se perderam todos os apontadores que indicavam a sua localizac¸a˜ o em mem´oria. Estes blocos de mem´oria reservados mas cuja localizac¸a˜ o se perdeu s˜ao geralmente designados por “garbbage” (lixo). Considere-se, por exemplo, o seguinte, programa: void func(){ int *x,y; y = 3; x = (int*) calloc(y,sizeof(int)); /* ... Utilizac ¸˜ ao de x, free() n˜ ao chamado... */ } void main() int a,b; func(); /* O Bloco reservado em func deixou de ser usado, mas deixou de estar inacess´ ıvel */ Um mapa de mem´oria ilustrativo desta situac¸a˜ o est´a representado na figura 3.4. ˜ DA MEM ORIA ´ ˆ G EST AO DIN AMICA 53 Zona da pilha (var. locais) Zona da pilha (var. locais) Endereço Conteúdo .. . Variável Endereço .. . .. . 2000 1003 10000 Conteúdo .. . ... 1001 1002 10000 n 10001 11999 1006 12000 1007 12001 ... .. . ... Endereço .. . Conteúdo Variável Endereço .. . .. . 1001 2000 .. . 1003 10000 1765 1005 n 1008 12003 1009 .. . .. . ... 12999 .. . Endereço .. . ... 1001 1002 Conteúdo .. . 2000 1003 10000 Conteúdo .. . 10000 666 10001 −300 x 1004 2000 1003 12500 .. . .. . n .. . ... 10001 −300 .. . 11999 1005 1009 1006 12000 1007 12001 11111111 12002 −555555 ... 1009 .. . C 12001 11111111 12002 −555555 ... Memoria libertada .. . .. . Memoria ocupada por outras var. dinamicas 12500 666 12501 −300 .. . 14599 .. . 12003 .. . 1765 ... 12000 1008 .. . Variável .. . .. . 666 x 1004 1765 Conteúdo 10000 Variável 1007 11999 .. . Endereço ... 1002 .. . 1005 1008 Zona do heap (var. dinamicas) Variável 1006 n .. . B Zona do heap (var. dinamicas) .. . 1765 .. . 1001 .. . ... 12000 .. . .. . Variável .. . .. . Zona da pilha (var. locais) .. . −300 1007 Endereço Conteúdo 666 10001 11999 A Endereço 10000 1006 12002 Variável .. . x 1004 Memoria livre .. . Zona da pilha (var. locais) Conteúdo .. . ... 1002 −300 1005 1009 .. . 666 x 1008 Variável .. . 1004 .. . Zona do heap (var. dinamicas) Zona do heap (var. dinamicas) .. . Memoria "realocada" .. . .. . D Figura 3.3: Mapa de mem´oria ap´os a chamada a` func¸a˜ o calloc() (A) e ap´os a func¸a˜ o realloc() (B), se existir espac¸o dispon´ıvel nos enderec¸os cont´ıguos. Em (C) e (D) representase a situac¸a˜ o correspondente no caso em que a mem´oria dinˆamica imediatamente a seguir ao bloco reservado est´a ocupada, sendo necess´ario deslocar todo o bloco para outra zona de mem´oria. Neste caso, o apontador retornado pela func¸a˜ o e´ diferente do inicial. ´ ˆ 54 V ECTORES E MEM ORIA DIN AMICA Zona da pilha (var. locais) Endereço .. . Conteúdo Zona do heap (var. dinamicas) Variável Endereço .. . .. . .. . ... 1001 Conteúdo Zona da pilha (var. locais) Variável Endereço .. . .. . .. . ... 2101 Conteúdo Zona do heap (var. dinamicas) Variável .. . .. . .. . ... 1001 2102 1003 2103 1003 1004 2104 1004 2105 1005 1006 b 2106 1007 a 2107 topo 1006 b 2106 1007 a 2107 1008 2108 1009 2109 .. . .. . .. . 1002 ... 2102 .. . ... 2102 x[0] 2103 x[1] y 2104 x[2] 2105 2108 1009 2109 .. . Variável .. . x 1008 .. . Conteúdo 2101 topo 1002 1005 topo Endereço .. . .. . A topo ... .. . B Zona da pilha (var. locais) Endereço .. . Conteúdo Zona do heap (var. dinamicas) Variável Endereço .. . .. . .. . ... 1001 Conteúdo Variável .. . .. . ... 2101 1002 2102 x[0] 1003 2103 x[1] 1004 2104 x[2] 1005 topo 2105 1006 b 2106 1007 a 2107 1008 2108 1009 2109 .. . .. . .. . topo ... .. . C Figura 3.4: Criac¸a˜ o de “garbbage” (lixo). Mapa de mem´oria (exemplo do texto): (A) antes da chamada a` func¸a˜ o func, (B) no final da func¸a˜ o func e (C) ap´os retorno ao programa principal. De notar que em (C) a mem´oria dinˆamica ficou reservada, mas que se perderam todas as referˆencias que permitiam o acesso a esta zona de mem´oria. ˜ DIN AMICA ˆ C RIAC¸ AO DE MATRIZES 55 3.5 3.5.1 ˜ dinamica ˆ Criac¸ao de matrizes ˜ Introduc¸ao A criac¸a˜ o e utilizac¸a˜ o de estruturas dinˆamicas em C que possam ter um acesso equivalente a uma matriz e´ simples. No entanto, a compreens˜ao detalhada de como funcionam este tipo de estruturas nem sempre e´ claro. De modo a simplificar a exposic¸a˜ o, comec¸ar-se-´a por apresentar na secc¸a˜ o 3.5.2 um resumo do modo de declarac¸a˜ o e de acesso de matrizes est´aticas. Seguidamente, na secc¸a˜ o 3.5.3, descreve-se uma metodologia simples para criar dinamicamente vectores de apontadores com um comportamento equivalente ao de uma matriz. Seguidamente, na secc¸a˜ o 3.5.4 ser˜ao discutidas as diferenc¸as e semelhanc¸as entre matrizes e vectores de apontadores, de modo a evidenciar as diferenc¸as entre as estruturas dinˆamicas criadas e as matrizes nativas do C. 3.5.2 ´ Matrizes estaticas Conforme j´a se viu na secc¸a˜ o 2.6.1, a utilizac¸a˜ o de estruturas multidimensionais em C apenas exige a declarac¸a˜ o de uma vari´avel com v´arios ´ındices, especificando cada um da dimens˜ao pretendida da estrutura. Por exemplo float x[3][2]; declara uma estrutura bidimensional de dois por trˆes reais, ocupando um total de seis palavras de mem´oria no modelo de mem´oria que temos vindo a assumir como referˆencia. E´ frequente uma estrutura bidimensional ser interpretada como uma matriz, neste exemplo de duas linhas por trˆes colunas. A inicializac¸a˜ o de uma matriz pode ser efectuada durante a execuc¸a˜ o do programa ou listando os valores iniciais, sendo apenas necess´ario fazer um agrupamento hier´arquico das constantes de inicializac¸a˜ o de acordo com as dimens˜oes da estrutura: float x[3][2] = {{1.0,2.0},{3.0,4.0}, {5.0,6.0}}; Como se mostrou anteriormente, a arrumac¸a˜ o em mem´oria desta estrutura encontra-se representada esquematicamente na figura 2.9. ´ ˆ 56 V ECTORES E MEM ORIA DIN AMICA 3.5.3 ˆ Matrizes dinamicas Admita-se que num dado programa se pretende substituir a sequˆencia #define N ... #define M ... ... int x[N][M]; por uma estrutura dinˆamica com um mecanismo de acesso equivalente, mas em que os limites n e m s˜ao vari´aveis cujo valor s´o e´ conhecido durante a execuc¸a˜ o do programa. A soluc¸a˜ o para este problema e´ substituir a estrutura x por um vector dinˆamico de apontadores para inteiros, em que cada posic¸a˜ o e´ por sua vez inicializada com um vector dinˆamico. Considerese, por exemplo, que se pretende criar uma matriz de n por m. O c´odigo para este efeito e´ dado por int main{ int k; int n,m; int **x; /* Inicializac ¸˜ ao de n e m */ x = (int**) calloc(n,sizeof(int*)); if(x == NULL){ printf("Erro na reserva de mem´ oria\n"); exit(1); } for(k = 0; k < n ; k++){ x[k] = (int*) calloc(m,sizeof(int)); if(x == NULL){ printf("Erro na reserva de mem´ oria\n"); exit(1); } } Por exemplo, se se pretender criar uma matriz de 4 por 2 e inicializ´a-la com um valores inteiros em que a classe das dezenas corresponde a` linha e a classe dos algarismos a` s unidades, poder-se-ia fazer ˜ DIN AMICA ˆ C RIAC¸ AO DE MATRIZES 57 int main{ int k,j; int n,m; int **x; n = 4; m = 3; x = (int**) calloc(n,sizeof(int*)); if(x == NULL){ printf("Erro na reserva de mem´ oria\n"); exit(1); } for(k = 0; k < n ; k++){ x[k] = (int*) calloc(m,sizeof(int)); if(x == NULL){ printf("Erro na reserva de mem´ oria\n"); exit(1); } } for(k = 0; k < n ; k++) for(j = 0; j < m ; j++) x[k][j] = 10 * k + j; O resultado da execuc¸a˜ o deste bloco de c´odigo seria o que se mostra na figura 3.5. Este exemplo indica que a soluc¸a˜ o geral para simular a criac¸a˜ o dinˆamica de matrizes e´ declarar um duplo apontador para o tipo pretendido dos elementos da matriz e, seguidamente, reservar dinˆamicamente um vector de apontadores e criar dinamicamente os vectores correspondentes a cada linha. Como e´ evidente, neste exemplo x e´ um duplo apontador para inteiro. Deste modo, se pretender usar esta “matriz dinˆamica” como argumento de uma func¸a˜ o fazer-se, no bloco que chama, func(x,n,m,/* Outros argumentos */); enquanto que o cabec¸alho de func dever´a ser void func(int **x,int n,int m,/* Outros argumentos */); ´ ˆ 58 V ECTORES E MEM ORIA DIN AMICA Zona da pilha Zona do heap Endereço Conteúdo Variável .. . .. . .. . 1543 2001 .. . .. . x .. . Endereço .. . Conteúdo Variável .. . .. . 2001 2006 x[0] 2002 2009 x[1] 2003 2012 x[2] 2004 2015 x[3] .. . .. . .. . Endereço Conteúdo Variável 2006 0 x[0][0] 2007 1 x[0][1] 2009 10 x[1][0] 2010 11 x[1][1] 2012 20 x[2][0] 2013 21 x[2][1] 2015 30 x[3][0] 2016 31 x[3][1] Figura 3.5: Criac¸a˜ o dinˆamica de matrizes por meio de um vector de apontadores. Os enderec¸os indicados s˜ao apenas ilustrativos. Esta soluc¸a˜ o e´ t˜ao frequente na pr´atica que, por analogia, muitos programadores (mesmo experientes) de C pensam que, dada a declarac¸a˜ o tipo x[N][M], x sem qualquer ´ındice corresponde a um duplo apontador para tipo. Como se mostrou na secc¸a˜ o 2.6.4, esta suposic¸a˜ o n˜ao corresponde a` realidade: nesta situac¸a˜ o, x e´ um apontador para um vector de elementos de tipo. 3.5.4 Vectores de apontadores e matrizes Considere-se a declarac¸a˜ o int *x[4]; Recordando que os [] tˆem precedˆencia sobre o operador *, e seguindo a regra de interpretac¸a˜ o semˆantica da declarac¸a˜ o apresentada na secc¸a˜ o 2.6.4, resulta que x e´ um vector de 4 apontadores para inteiros. Ou seja, em termos de modelo de mem´oria, encontramos uma representac¸a˜ o como a indicada na figura 3.6, situac¸a˜ o A. Um ponto importante a sublinhar e´ que esta declarac¸a˜ o apenas reserva mem´oria para quatro apontadores. Esta situac¸a˜ o e´ frequentemente mal interpretada pelo facto de, sendo x[k] uma apontador para um inteiro, ent˜ao *x[k] e *(x[k]+j) tamb´em s˜ao do tipo inteiro. Mas, devido a` s equivalˆencias sint´acticas de enderec¸amento em C, tem-se ˜ DIN AMICA ˆ C RIAC¸ AO DE MATRIZES 59 Endereço Conteúdo Variável .. . .. . .. . 1001 ??? x[0] 1002 ??? x[1] 1003 ??? x[2] 1004 ??? x[3] .. . .. . .. . A Endereço .. . Conteúdo Variável Endereço .. . 2001 1 x[0][0] 2002 2 x[0][1] 2005 3 x[1][0] 2006 4 x[1][1] 2010 5 x[2][0] 2011 6 x[2][1] 2013 7 x[3][0] 2014 8 x[3][1] .. . 1001 2001 x[0] 1002 2005 x[1] 1003 2010 x[2] 1004 2013 x[3] .. . .. . .. . Conteúdo Variável B Figura 3.6: Vectores de apontadores: A - Antes de inicializado, B- ap´os a inicializac¸a˜ o. ´ ˆ 60 V ECTORES E MEM ORIA DIN AMICA *x[k] <-> x[k][0] *(x[k]+j) <-> x[k][j] e, portanto, x[k][j] e´ um inteiro. O que conduz, por vezes, ao racioc´ınio errado de que um vector de apontadores pode ser utilizado indiscriminadamente como se de uma matriz se tratasse. Ora, de facto, tal s´o e´ poss´ıvel se os elementos de x tiverem sido convenientemente inicializados de modo a enderec¸arem a base de um vector de inteiros. Isto, s´o por si, n˜ao e´ realizado pela declarac¸a˜ o int *x[4], que se limita a declarar um vector de apontadores e a reservar 4 palavras de mem´oria para este efeito. Nesta declarac¸a˜ o, n˜ao e´ reservada mem´oria para qualquer inteiro. Claro que, se o utilizador o pretender, e´ f´acil inicializar as posic¸o˜ es de mem´oria do vector de apontadores reservando mem´oria dinˆamica de modo a armazenar os inteiros pretendido. Admitindo que se pretende que o vector de apontadores anteriores sirva de base a uma estrutura de enderec¸amento equivalente a uma matriz de 4 por 2, esta inicializac¸a˜ o pode ser feita pela sequˆencia int k,j; int *x[4]; for(k = 0; k < 4; k++) x[k] = (int*) calloc(2,sizeof(int)); for(k = 0; k < 4; k++) for(j = 0; j < 2; j++) x[k][j] = k*2 + j + 1; onde, al´em da reserva das posic¸o˜ es de mem´oria para os vectores inteiros, se procedeu a` sua inicializac¸a˜ o. A situac¸a˜ o final pode ser representada pelo modelo da figura 3.6, situac¸a˜ o B. Em s´ıntese, e´ conveniente reter que, apesar da manipulac¸a˜ o de matrizes e de vectores de apontadores seja sintacticamente semelhante, os mecanismos de reserva de mem´oria s˜ao claramente distintos. No caso da matriz int x[4][2], a pr´opria declarac¸a˜ o reserva automaticamente espac¸o para oito inteiros, os quais podem ser acedidos pelas regras habituais. No caso da declarac¸a˜ o de um vector de apontadores, como por exemplo int *x[4], apenas e´ efectuada a reserva de quatro apontadores. A sua utilizac¸a˜ o requer a pr´evia inicializac¸a˜ o de cada um dos seus elementos com um enderec¸o v´alido de um bloco de mem´oria, o qual pode ser obtido a partir de um vector j´a declarado ou, como no exemplo aqui usado, pela reserva de um bloco de mem´oria dinˆamica atrav´es da func¸a˜ o calloc(). Cap´ıtulo 4 ˆ Listas dinamicas 4.1 ˜ Introduc¸ao Como se viu anteriormente, a utilizac¸a˜ o de vectores em C apresenta a desvantagem de exigir o conhecimento a` partida da dimens˜ao m´axima do vector. Em muitas situac¸o˜ es, a utilizac¸a˜ o de vectores dinˆamicos, como descrito na secc¸a˜ o 3.3, e´ uma soluc¸a˜ o eficiente. No entanto, em situac¸o˜ es em que a quantidade de mem´oria dinˆamica necess´aria varia frequentemente durante a execuc¸a˜ o do programa, a utilizac¸a˜ o de vectores dinˆamicos revela-se frequentemente pouco eficaz. Considere-se, por exemplo, um gestor de uma central telef´onica que necessita de reservar dinamicamente mem´oria para cada chamada estabelecida (onde e´ armazenada toda a informac¸a˜ o sobre a ligac¸a˜ o, como por exemplo os n´umeros de origem e destino, tempo de ligac¸a˜ o, etc,) e libertar essa mesma mem´oria quando a chamada e´ terminada. Dado que o n´umero de ligac¸o˜ es activas varia frequentemente ao longo do dia, uma soluc¸a˜ o baseada num vector dinˆamico exigiria o seu redimensionamento frequente. Em particular, sempre que n˜ao fosse poss´ıvel encontrar mem´oria livre imediatamente a seguir ao fim do vector, seria necess´ario deslocar todo o vector (v. func¸a˜ o realloc, secc¸a˜ o 3.4.2) para uma nova zona de mem´oria, e a c´opia exaustiva de todo o seu conte´udo para a nova posic¸a˜ o de mem´oria. Este mecanismo, al´em de pouco eficiente, poderia inviabilizar o funcionamento em tempo real do sistema, dado que uma percentagem significativa do tempo dispon´ıvel seria dispendido na gest˜ao da mem´oria usada pelo vector. Quando se pretende armazenar entidades individuais de mem´oria que possam ser criadas e libertadas individualmente com uma certa frequˆencia, e´ normalmente mais adequado utilizar estruturas de dados criadas dinˆamicas e organizadas em listas. ˆ 62 L ISTAS DIN AMICAS 4.2 ˜ de dados Abstracc¸ao Neste cap´ıtulo descrevem-se listas dinˆamicas como uma forma de armazenar ou organizar informac¸a˜ o. Esta organizac¸a˜ o e´ , obviamente, independente do tipo de informac¸a˜ o que se pretende armazenar. Esta situac¸a˜ o e´ semelhante a` da construc¸a˜ o de um arm´ario com gavetas: a estrutura do m´ovel e´ independente do conte´udo que se ir´a arrumar nas gavetas. Esta independˆencia entre m´ovel e conte´udo garante a generalidade do m´ovel, no sentido em que este ser´a adapt´avel a v´arias situac¸o˜ es. De modo semelhante, ao desenhar um programa e´ importante que as suas componentes sejam o mais independentes poss´ıveis, de forma a manterem a generalidade. Tanto quanto poss´ıvel, e´ desej´avel que um bloco de c´odigo desenvolvido para o programa A seja reutiliz´avel no programa B ou, pelo menos, que tal seja poss´ıvel sem alterac¸o˜ es ou com alterac¸o˜ es m´ınimas. Uma forma de atingir este objectivo e´ , ao desenhar o programa, adoptar uma metodologia designada por abstracc¸a˜ o de dados(Martins, 1989). Esta metodologia baseia-se na definic¸a˜ o e distinc¸a˜ o clara dos v´arios tipos de dados utilizados pelo programa (ou, seguindo o exemplo anterior, distinguir tanto quanto poss´ıvel o m´ovel do conte´udo das gavetas). Segundo esta metodologia, cada tipo de dados deve ter manipulado apenas por um conjunto de func¸o˜ es espec´ıficas, designadas m´etodos, conhecedoras dos detalhes internos do tipo de dados associado. Todos os outros blocos do programa tˆem apenas que conhecer as propriedades abstractas ou gen´ericas deste tipo. A manipulac¸a˜ o do tipo s´o s˜ao acess´ıveis de outros blocos de programa atrav´es dos m´etodos disponibilizados pelo tipo de dados. Esta metodologia de programac¸a˜ o garante que, caso seja necess´ario alterar os detalhes internos de um determinado tipo, apenas e´ necess´ario alterar os m´etodos correspondentes a esse tipo. Dado que todos os blocos de programas onde este tipo de dados e´ utilizado apenas acedem a ele atrav´es dos m´etodos dispon´ıveis, requerendo apenas o conhecimento das suas propriedades gerais (ou abstractas), e´ poss´ıvel minimizar ou eliminar totalmente as alterac¸o˜ es necess´arias aos outros blocos de programa. Deste modo, a metodologia de abstracc¸a˜ o de dados contribui para uma relativa estanquecidade e independˆencia dos diversos m´odulos constituintes. Neste cap´ıtulo, utilizar-se-´a nos diversos exemplos uma metodologia estrita de abstracc¸a˜ o de dados, chamando-se a atenc¸a˜ o caso a caso para aspectos espec´ıficos da implementac¸a˜ o. L ISTAS 63 Zona da pilha (var. locais) Variável Conteúdo .. . base Zona do heap (var. dinamicas) Conteúdo .. . dados (pos 1) dados (pos 2) dados (pos 3) dados (pos 4) NULL Figura 4.1: Lista dinˆamica. Todos os elementos s˜ao criados dinamicamente na zona de heap, sendo suficiente uma vari´avel local de tipo apontador (vari´avel base, nas figura) para poder aceder a todos os elementos da lista. 4.3 Listas Uma lista dinˆamica n˜ao e´ mais do que uma colecc¸a˜ o de estruturas de dados, criadas dinamicamente, em que cada elemento disp˜oe de um apontador para o elemento seguinte (figura 4.1). Cada elemento da lista e´ constitu´ıdo por uma zona de armazenamento de dados e de um apontador para o pr´oximo elemento. Para ser poss´ıvel identificar o fim da lista, no apontador do u´ ltimo elemento e´ colocado o valor NULL. Para criar uma lista e´ necess´ario definir um tipo de dados e criar uma vari´avel local (normalmente designada base ou raiz. Admita-se, por exemplo, que se pretendia que cada elemento da lista guardasse uma string com um nome e um n´umero inteiro. A criac¸a˜ o de uma lista de elementos deste tipo exigiria uma declarac¸a˜ o de tipos e de vari´aveis como se segue: #define MAX_NOME 200 ˆ 64 L ISTAS DIN AMICAS typedef struct _tipoDados { char nome[MAX_NOME]; int num; } _tipoDados; typedef struct _tipoLista{ tipoDados dados; struct _tipoLista *seg; } tipoLista; int main(){ tipoLista *base; /* ... */ Note-se que na definic¸a˜ o do apontador seguinte, e´ necess´ario utilizar a construc¸a˜ o struct _tipoLista *seg e n˜ao tipoLista *seg. De facto, apesar de serem duas formas aparentemente semelhantes, a forma typedef struct _tipoLista{ tipoDados dados; tipoLista *seg; /* DECLARAC ¸ˆ AO INV´ ALIDA */ } tipoLista; e´ inv´alida porque tipoLista s´o e´ conhecido no final da declarac¸a˜ o, e n˜ao pode ser utilizado antes de completamente especificado. Uma das vantagens deste tipo de estruturas e´ que cada um dos seus elementos pode ser reservado e libertado individualmente, sem afectar os restantes elementos (exigindo apenas ajustar um ou dois apontadores, de forma a garantir a consistˆencia do conjunto). Adicionalmente, a ordem dos elementos da lista e´ apenas definida pela organizac¸a˜ o dos apontadores. Deste modo, se for necess´ario introduzir um elemento entre dois j´a existentes, n˜ao e´ necess´ario deslocar todos os elementos de uma posic¸a˜ o, como sucederia num vector. Basta de facto reservar espac¸o para um elemento adicional e deslocar todos os outros elementos de uma posic¸a˜ o. Na figura 4.2 apresentase um exemplo em que se ilustra esta independˆencia entre enderec¸os de mem´oria e sequˆencia da lista, e confere a esta uma flexibilidade superior a` de um vector. Dada esta independˆencia entre enderec¸os de mem´oria e sequˆencia, e´ frequente adoptar-se representac¸o˜ es gr´aficas simplificadas para as listas, como a se mostra na figura 4.3. A cruz no u´ ltimo elemento corresponde a uma representac¸a˜ o simb´olica abreviada para o valor NULL. L ISTAS 65 Zona da pilha (var. locais) Variável Conteúdo .. . base Zona do heap (var. dinamicas) Conteúdo .. . dados (pos 2) dados (pos 1) dados (pos 4) NULL dados (pos 3) Figura 4.2: Lista dinˆamica. A sequˆencia dos elementos da lista e´ apenas definida pelos apontadores de cada elemento, independentemente do enderec¸o de mem´oria ocupado. base Figura 4.3: Lista dinˆamica. Representac¸a˜ o simplificada ˆ 66 L ISTAS DIN AMICAS Apesar das vantagens j´a referidas de uma lista, e´ necess´ario ter em atenc¸a˜ o que o programa s´o disp˜oe de uma vari´avel para aceder a toda a lista, e que esta tem que ser percorrida elemento a elemento at´e se atingir a posic¸a˜ o pretendida. Num vector, dado que todos as posic¸o˜ es s˜ao adjacentes, para aceder a qualquer posic¸a˜ o basta saber o enderec¸o do primeiro elemento e o n´umero de ordem (´ındice) do elemento que se pretende aceder para ser poss´ıvel calcular o enderec¸o do elemento que se pretende. Ou seja, a flexibilidade acrescida da lista em termos de organizac¸a˜ o de mem´oria e´ conseguida a` custa do tempo de acesso aos seus elementos. 4.4 ˆ Listas dinamicas: listar elementos Uma operac¸a˜ o particularmente simples sobre uma lista, mas ilustrativa do mecanismos de acesso geralmente adoptados, e´ a listagem de todos os seus elementos. Considere-se, por exemplo, que se pretende listar no terminal o conte´udo de todos os n´umeros e nomes da lista do exemplo da secc¸a˜ o 4.3. Uma soluc¸a˜ o para este efeito seria a sequˆencia de c´odigo seguinte: #define MAX_NOME 200 typedef struct _tipoDados { char nome[MAX_NOME]; int num; } tipoDados; typedef struct _tipoLista{ tipoDados dados; struct _tipoLista *seg; } tipoLista; void escreveDados(tipoDados dados){ printf("Num = %5d Nome = %s\n",dados.num, dados.nome); } void lista(tipoLista *base){ tipoLista *aux; /* Apontador que percorre a lista */ aux = base; /* Incializa com o primeiro elemento while(aux != NULL){ /* Enquanto n˜ ao se atingir o final... escreveDados(aux -> dados); /* Escreve o conte´ udo aux = aux -> seg; /* Avanc ¸a para a pr´ oxima posic ¸˜ ao } } int main(){ */ */ */ */ ˆ L ISTAS DIN AMICAS : LISTAR ELEMENTOS tipoLista *base; /* Neste ponto, o apontador base e os restantes elementos da lista s˜ ao, de alguma forma, inicializados */ /* Listagem do conte´ Udo */ lista(base); O princ´ıpio essencial da operac¸a˜ o de listagem est´a inclu´ıda na func¸a˜ o lista e e´ muito simples. Inicializa-se um apontador aux para o in´ıcio da lista. Seguidamente, enquanto este apontador for diferente de NULL (ou seja, n˜ao atingir o fim da lista), o apontador e´ sucessivamente avanc¸ado para o elemento seguinte. Note-se como se adoptou neste bloco de c´odigo a metodologia de abstracc¸a˜ o de dados. Existe neste exemplo uma separac¸a˜ o funcional de tarefas entre as diversas entidades que participam o programa. A func¸a˜ o lista manipula unicamente a estrutura de dados que suporta a lista, percorrendo todos os seus elementos at´e ao fim da lista. No entanto, quando e´ necess´ario escrever o conte´udo de cada n´o no terminal, esta tarefa e´ delegada a uma func¸a˜ o espec´ıfica ( escreveDados), respons´avel pela manipulac¸a˜ o e processamento dos detalhes espec´ıficos de vari´aveis do tipo tipoDados. Neste sentido, tipoDados e´ , para a lista, um tipo abstracto gen´erico, cuja representac¸a˜ o interna ela desconhece, e da qual s´o tem que conhecer uma propriedade muito gen´erica: uma vari´avel deste tipo pode de alguma forma ser escrita no terminal, havendo um m´etodo competente para o fazer. Uma forma alternativa da func¸a˜ o listar frequentemente utilizada passa pela utilizac¸a˜ o de um ciclo for em vez do ciclo while: void lista(tipoLista *base){ tipoLista *aux; /* Apont. que percorre */ /* a lista. */ for(aux = base ; aux != NULL; aux = aux -> seg) escreveDados(aux -> dados); } 67 ˆ 68 L ISTAS DIN AMICAS 4.5 4.5.1 Listas: pilhas ˜ Introduc¸ao As listas tˆem habitualmente uma estrutura e organizac¸a˜ o em mem´oria semelhante a` apresentada na figura 4.1. No entanto, os mecanismos de acesso a` lista s˜ao vari´aveis e, no seu conjunto, podem permitir que a lista realize apenas um subconjunto de tarefas bem determinadas. Em teoria da computac¸a˜ o, uma pilha (ou stack, na lietratura anglo-sax´onica) e´ um sistema que armazena dados de forma sequencial e que permite a sua leitura por ordem inversa da escrita. A designac¸a˜ o pilha vem da semelhanc¸a funcional com uma pilha comum. Admita-se que se disp˜oe de um conjunto de palavras que se pretende armazenar. Considere-se ainda que o sistema de armazenamento dispon´ıvel e´ um conjunto de pratos, sendo cada palavra manuscrita no fundo ` medida que as palavras v˜ao sendo comunicadas ao sistema de armazenamento, s˜ao do prato. A escritas no fundo de um prato e este e´ colocado numa pilha. A leitura posterior das palavras registadas e´ feita desempilhando pratos sucessivamente e lendo o valor registado em cada um. Como e´ o´ bvio, a leitura ocorre por ordem inversa da escrita. Uma pilha e´ frequentemente designada por uma estrutura LIFO, acr´onimo derivado da express˜ao Last In First Out. A operac¸a˜ o de armazenamento na pilha e´ geralmente designada de operac¸a˜ o de push, enquanto que a leitura e´ designada de pop. As pilhas tˆem uma utilizac¸a˜ o frequente em inform´atica sempre que se pretende inverter uma sequˆencia de dados. Embora a relac¸a˜ o n˜ao seja o´ bvia, pode indicar-se, por exemplo, que o c´alculo de uma express˜ao aritm´etica em que v´arios operadores tˆem n´ıveis de precedˆencia diferentes e´ realizada acumulando numa pilha resultados interm´edios. Considere-se, por exemplo, que se pretende inverter os caracteres da palavra Lu´ıs. A forma como uma pilha pode contribuir para este resultado est´a graficamente sugerido na figura 4.4. Uma pilha pode ser realizada em C atrav´es de uma lista dinˆamica ligada. A colocac¸a˜ o de um novo elemento no topo da pilha corresponde a criar um novo elemento para a lista e inseri-lo junto a` base. De forma correspondente, a operac¸a˜ o de remoc¸a˜ o e leitura corresponde a retirar o elemento da base, ficando esta a apontar para o elemento seguinte (v. figura 4.5) L ISTAS : L L u í s s s s í í í í u u u u u u L L L L L L Entrada (push) (sobreposição) í u PILHAS L L Saida (pop) (remoção) Figura 4.4: Invers˜ao de uma sequˆencia de caracteres por meio de uma pilha. base ’u’ ’L’ ’í’ Figura 4.5: Realizac¸a˜ o de uma pilha por uma lista ligada. No exemplo, considera-se uma pilha de caracteres e a inserc¸a˜ o do caracter ’´ı’ no topo da pilha. A tracejado indica-se a ligac¸a˜ o existente antes da inserc¸a˜ o, a cheio ap´os a inserc¸a˜ o. A operac¸a˜ o de remoc¸a˜ o corresponde a` realizac¸a˜ o do mecanismo inverso (reposic¸a˜ o da ligac¸a˜ o a tracejado e libertac¸a˜ o do elemento de mem´oria correspondente ao ’´ı’). 69 ˆ 70 L ISTAS DIN AMICAS 4.5.2 ˜ Declarac¸ao Tal como qualquer lista, a realizac¸a˜ o de uma pilha implica a declarac¸a˜ o de um tipo de suporte da lista e a utilizac¸a˜ o de uma vari´avel para a base da pilha. A declarac¸a˜ o da pilha pode ser feita, por exemplo, pela declarac¸a˜ o typedef struct _tipoPilha { tipoDados dados; struct _tipoPilha *seg; } tipoPilha; onde se admite que tipoDados j´a foi definido e caracteriza o tipo de informac¸a˜ o registada em cada posic¸a˜ o da pilha. A utilizac¸a˜ o da pilha implica que no programa principal, ou no bloco de c´odigo onde se pretende utilizar a pilha, seja declarada uma vari´avel de tipo apontador para tipoPilha que registe a base da pilha. Ou seja, ser´a necess´ario dispor de uma vari´avel declarada, por exemplo, por tipoPilha *pilha; 4.5.3 ˜ Inicializac¸ao A primeira operac¸a˜ o necess´aria para utilizar a pilha e´ inicializ´a-la ou cri´a-la. Uma pilha vazia deve ter a sua base apontada para NULL. Embora seja poss´ıvel realizar directamente uma atribuic¸a˜ o a` vari´avel pilha, tratando-se de um detalhe interno de manipulac¸a˜ o do tipo tipoPilha, esta inicializac¸a˜ o dever´a ser delegada numa func¸a˜ o espec´ıfica. Deste modo, para respeitar a metodologia de abstracc¸a˜ o de dados, deve declarar-se uma pequena func¸a˜ o tipoPilha *inicializa(){ return NULL; } e utilizar-se esta func¸a˜ o sempre que seja necess´ario inicializar a pilha: pilha = inicializa(); L ISTAS : 4.5.4 PILHAS ˜ Sobreposic¸ao A operac¸a˜ o de sobreposic¸a˜ o exige a seguinte sequˆencia de operac¸o˜ es: • Reserva de mem´oria dinˆamica para armazenar um novo elemento. • C´opia dos dados para o elemento criado. • Inserc¸a˜ o do novo elemento na base da pilha. Para a criac¸a˜ o da mem´oria dinˆamica, e´ conveniente criar uma func¸a˜ o novoNo() que cria espac¸o para um novo n´o, verificando a simultaneamente a existˆencia de erros na reserva de mem´oria. Esta func¸a˜ o pode ser definida como tipoPilha *novoNo(tipoDados x){ tipoPilha *novo = calloc(1,sizeof(tipoPilha)); if(novo == NULL) Erro("Erro na reserva de mem´ oria"); novo -> dados = x; novo -> seg = NULL; return novo; } onde Erro() e´ uma func¸a˜ o gen´erica simples que imprime uma mensagem de erro e termina o programa (ver secc¸a˜ o 4.5.7). A operac¸a˜ o de inserc¸a˜ o no topo da pilha exige a realizac¸a˜ o das operac¸o˜ es representadas na figura 4.5. Estas podem ser realizada pela seguinte func¸a˜ o: tipoPilha *sobrepoe(tipoPilha *pilha,tipoDados x){ tipoPilha *novo; novo = novoNo(x); novo -> seg = pilha; return novo; } Esta func¸a˜ o, ap´os a criac¸a˜ o do novo elemento, limita-se a colocar o apontador seg a apontar para o elemento que anteriormente se encontrava no topo, e indicar a` base que o novo topo corresponde ao novo elemento inserido. 71 ˆ 72 L ISTAS DIN AMICAS Note-se que esta func¸a˜ o aceita como argumento o apontador original para o in´ıcio da lista (topo da pilha) e devolve a nova base alterada. 4.5.5 ˜ Remoc¸ao A operac¸a˜ o de remoc¸a˜ o da pilha e´ acompanhada da leitura. Tal como anteriormente a func¸a˜ o recebe o apontador para a base da lista e devolve a base alterada. Dado que a remoc¸a˜ o e´ acompanhada de leitura, a func¸a˜ o recebe um segundo apontador, passado por referˆencia, que deve ser usado para guardar o valor do elemento removido. Uma func¸a˜ o poss´ıvel para a realizac¸a˜ o desta operac¸a˜ o e´ tipoPilha *retira(tipoPilha *pilha,tipoDados *x){ tipoPilha *aux; if(pilhaVazia(pilha)) Erro("remoc ¸˜ ao de uma pilha vazia"); *x = pilha -> dados; aux = pilha -> seg; free(pilha); return aux; } A operac¸a˜ o de remoc¸a˜ o e´ precedida de um teste para verificac¸a˜ o de pilha vazia, para garantia de integridade da mem´oria (v. secc¸a˜ o 4.5.6). Note-se que ap´os a leitura da pilha, o bloco de mem´oria utilizado e´ libertado pela func¸a˜ o free(). 4.5.6 Teste Antes de ser tentada a remoc¸a˜ o de um elemento da pilha, e´ conveniente testar se a pilha se encontra vazia. Numa primeira abordagem, poder-se-ia ser tentado a testar, sempre que necess´ario, se o apontador da base se encontra com o valor NULL. Esta realizac¸a˜ o, ainda que correcta, violaria o princ´ıpio essencial de abstracc¸a˜ o de dados que temos vindo a respeitar: a caracterizac¸a˜ o como vazia de uma pilha e´ um detalhe interno do tipoPilha, e como tal deve ser delegado numa func¸a˜ o espec´ıfica, de tipo booleano. Deste modo, poderia fazer-se int pilhaVazia(tipoPilha *pilha){ return (pilha == NULL); } L ISTAS : PILHAS Esta func¸a˜ o retorna 1 se a pilha estiver vazia, e 0 em caso contr´ario. 4.5.7 Exemplo Suponha-se que se pretende implementar um sistema de invers˜ao de caracteres baseado na pilha definida anteriormente. Uma das vantagens de utilizar uma pilha para este efeito consiste no facto de n˜ao ser necess´ario definir a` partida a dimens˜ao m´axima da cadeia de caracteres: o programa efectua a reserva e libertac¸a˜ o de mem´oria a` medida das necessidades do programa. Neste caso, tipoDados e´ realizado por um simples caracter. Para respeitar e exemplificar o princ´ıpio de abstracc¸a˜ o, iremos escrever os m´etodos espec´ıficos de leitura e escrita deste tipo. De modo a explorar de forma eficaz a independˆencia dos diversos tipos de dados, os m´etodos correspondentes a cada tipo devem ser declarados em ficheiros separados. Deste modo, os m´etodos de acesso a tipoDados s˜ao agrupados num ficheiro dados.c. Os prot´otipos correspondentes s˜ao declarados no ficheiro dados.h. Ficheiro dados.h /* * Ficheiro: dados.h * Autor: Fernando M. Silva * Data: 7/11/2002 * Conte´ udo: * Ficheiro com declarac ¸˜ ao de tipos e * prot´ otipos dos m´ etodos para manipulac ¸˜ ao * de um tipoDados, concretizados aqui * por um tipo caracter simples. */ #ifndef _DADOS_H #define _DADOS_H #include #include typedef char tipoDados; /* M´ etodos de acesso a tipo dados */ int leDados(tipoDados *x); void escreveDados(tipoDados x); #endif /* _DADOS_H */ 73 ˆ 74 L ISTAS DIN AMICAS Ficheiro dados.c /* * * * * * * * */ Ficheiro: dados.c Autor: Fernando M. Silva Data: 12/11/2002 Conte´ udo: M´ etodos para manipulac ¸˜ ao de um tipoDados, concretizado por um caracter simples. #include "dados.h" int leDados(tipoDados *x){ /* Lˆ e um caracter. Devolve 1 se ler um ’\n’ */ *x = getchar(); if(*x == ’\n’) return 1; else return 0; } void escreveDados(tipoDados x){ putchar(x); } De igual modo, a metodologia de abstracc¸a˜ o de dados implementada sugere que todos os m´etodos de acesso a tipoPilha sejam agrupados num ficheiro pilha.c, ficando as declarac¸o˜ es e prot´otipos correspondentes acess´ıveis num ficheiro pilha.h. Deste modo, ter-se-ia Ficheiro pilha.h /* * * * * * * Ficheiro: pilha.h Autor: Fernando M. Silva Data: 7/11/2000 Conte´ udo: Ficheiro com declarac ¸˜ ao de tipos e prot´ otipos dos m´ etodos para manipulac ¸˜ ao L ISTAS : * de uma pilha simples de elementos gen´ ericos * de "tipoDados". */ #ifndef _PILHA_H #define _PILHA_H #include "dados.h" typedef struct _tipoPilha { tipoDados dados; struct _tipoPilha *seg; } tipoPilha; /* Prot´ otipos das func ¸˜ oes */ tipoPilha *inicializa(void); tipoPilha *sobrepoe(tipoPilha *pilha,tipoDados x); tipoPilha *retira(tipoPilha *pilha,tipoDados *x); int pilhaVazia(tipoPilha *pilha); #endif /* _PILHA_H */ Ficheiro pilha.c /* * Ficheiro: pilha.c * Autor: Fernando M. Silva * Data: 1/12/2000 * Conte´ udo: * M´ etodos para manipulac ¸˜ ao de uma pilha suportada * numa estrutura dinˆ amica ligada */ #include "pilha.h" #include "util.h" tipoPilha *novoNo(tipoDados x){ /* * Cria um novo n´ o para a pilha */ tipoPilha *novo = calloc(1,sizeof(tipoPilha)); if(novo == NULL) Erro("Erro na reserva de mem´ oria"); novo -> dados = x; novo -> seg = NULL; return novo; PILHAS 75 ˆ 76 L ISTAS DIN AMICAS } tipoPilha *inicializa(){ /* * Cria uma nova pilha * Retorna: * Pilha inicializada */ return NULL; } int pilhaVazia(tipoPilha *pilha){ /* * Verifica o estado da pilha * Argumentos: * pilha - Apontador para a base da pilha * Retorna: * 1 se a pilha estiver vazia * 0 em caso contr´ ario */ return (pilha == NULL); } tipoPilha *sobrepoe(tipoPilha *pilha,tipoDados x){ /* * Adiciona um elemento ` a pilha * Argumentos: * pilha - Apontador para a base da pilha * x - dados a inserir * Retorna: * Pilha modificada */ tipoPilha *novo; novo = novoNo(x); novo -> seg = pilha; return novo; } tipoPilha *retira(tipoPilha *pilha,tipoDados *x){ /* * Retira um elemento da pilha * Argumentos: * pilha - Apontador para a base da pilha * x - Apontador para vari´ avel que * retorna o valor lido da pilha * Retorna: * Pilha modificada L ISTAS : PILHAS */ tipoPilha *aux; if(pilhaVazia(pilha)) Erro("remoc ¸˜ ao de uma pilha vazia"); *x = pilha -> dados; aux = pilha -> seg; free(pilha); return aux; } A func¸a˜ o gen´erica de erro Erro() pode ser declarada num m´odulo gen´erico util.c, onde s˜ao agrupadas func¸o˜ es utilit´arias gen´ericas (neste exemplo, Erro() e´ u´ nica). O prot´otipos correspondente deve ser declarado num m´odulo util.h. Ficheiro util.h /* * Ficheiro: util.h * Autor: Fernando M. Silva * Data: 7/11/2002 * Conte´ udo: * Ficheiro com declarac ¸˜ ao de func ¸˜ oes e * prot´ otipos gen´ ericos */ #ifndef _UTIL_H #define _UTIL_H void Erro(char *msgErro); #endif /* _UTIL_H */ Ficheiro util.c /* * * * * * */ Ficheiro: Autor: Data: Conte´ udo: Func ¸˜ oes util.c Fernando M. Silva 1/11/2002 gen´ ericas 77 ˆ 78 L ISTAS DIN AMICAS #include #include #include "util.h" void Erro(char *msgErro){ /* * Termina o programa, escrevendo uma mensagem * de erro */ fprintf(stderr, "Erro: %s\n",msgErro); exit(1); } Considere-se, por u´ ltimo, o programa principal. Este limita-se a inicializar a pilha e a efectuar a leitura de um sequˆencia de dados, acumulando-os sucessivamente na pilha. A leitura e´ efectuada at´e que a func¸a˜ o leDados() (m´etodo de tipoDados) identifique uma mudanc¸a de linha na entrada. Seguidamente, os dados s˜ao sucessivamente removidos (desempilhados) e escritos no dispositivo de sa´ıda, at´e que a pilha esteja vazia. Deste modo, ter-se-ia: Ficheiro main.c /* * Ficheiro: main.c * Autor: Fernando M. Silva * Data: 7/11/2000 * Conte´ udo: * Programa principal simples para teste * de uma pilha, usada para invers˜ ao de * uma cadeia de caracteres */ #include #include #include "pilha.h" int main(){ tipoPilha *pilha; tipoDados x; pilha = inicializa(); printf("Introduza uma sequˆ encia de caracteres:\n"); while(!leDados(&x)){ pilha = sobrepoe(pilha,x); } L ISTAS : PILHAS printf("Sequˆ encia invertida:\n"); while(!pilhaVazia(pilha)){ pilha = retira(pilha,&x); escreveDados(x); } printf("\n"); exit(0); } A compilac¸a˜ o autom´atica de todos os m´odulos pode ser realizada por meio do utilit´ario make. A Makefile correspondente poderia ser escrita como # # Ficheiro: Makefile # Autor: Fernando M. Silva # Data: 7/11/2000 # Conte´ udo: # Makefile para teste de estruturas dinˆ amicas # # A vari´ avel CFLAGS especifica as flags usadas # por omiss˜ ao nas regras de compilac ¸˜ ao # # A vari´ avel SOURCES, que define os ficheiro # fonte em C, s´ o e usada para permitir a # evocac ¸˜ ao do utilit´ ario "makedepend" (pelo # comando ’make depend’), de modo a actualizar # automaticamente as dependˆ encias dos ficheiros # .o nos ficheiros .h # # A vari´ avel OBJECTS define o conjunto dos # ficheiros objectos # CFLAGS=-g -Wall -ansi -pedantic SOURCES=main.c pilha.c dados.c util.c OBJECTS=main.o pilha.o dados.o util.o # # Comando de linkagem dos execut´ aveis # teste: $(OBJECTS) gcc -o $@ $(OBJECTS) # # A regra ’make depend’ efectua uma actualizac ¸˜ ao da # makefile, actualizando as listas de dependˆ encias dos 79 ˆ 80 L ISTAS DIN AMICAS # ficheiros .o em .c # # depend:: makedepend $(SOURCES) # # Regra clean: ’make clean’ apaga todos os ficheiros # reconstru´ ıveis do disco # clean:: rm -f *.o a.out *˜ core teste *.bak # DO NOT DELETE 4.6 4.6.1 Listas: filas ˜ Introduc¸ao Referiu-se anteriormente que uma pilha correspondia a uma estrutura LIFO (Last In, First Out). De forma semelhante, podemos descrever uma fila como um sistema FIFO (First In, First Out). O exemplo corrente de funcionamento de uma fila e´ a vulgar fila de espera. Cada novo elemento armazenado e´ colocado no final da fila, enquanto que cada elemento retirado e´ obtido do in´ıcio da fila (figura 4.6, A). Uma fila pode ser realizada por meio de uma lista ligada, relativamente a` qual s˜ao mantidos n˜ao um, mas dois apontadores: um para a base ou in´ıcio, por onde s˜ao retirados os elementos, e outro para o fim ou u´ ltimo elemento da lista, que facilita a inserc¸a˜ o de novos elementos na lista.1 4.6.2 ˜ Declarac¸ao De forma a que seja poss´ıvel manipular uma u´ nica vari´avel do tipoFila, esta e´ realizada por uma estrutura de dados que agrupa os dois apontadores, in´ıcio e fim. Deste modo, a declarac¸a˜ o da fila pode ser realizada por 1 De facto, o apontador para a base seria suficiente para realizar a fila; no entanto, cada operac¸a˜ o de inserc¸a˜ o exigiria percorrer a totalidade da fila para realizar a inserc¸a˜ o no final, m´etodo que seria pouco eficiente. L ISTAS : fim ’L’ ’u’ FILAS ’í’ inicio A fim ’L’ ’u’ ’í’ ’s’ ’í’ ’s’ inicio B fim ’L’ ’u’ inicio C Figura 4.6: Realizac¸a˜ o de uma fila de caracteres com uma lista ligada. A - Estrutura da fila, B Fila em A ap´os a inserc¸a˜ o do caracter ’s’ (a tracejado, as ligac¸o˜ es eliminadas), C - Fila em B ap´os a leitura de um caracter (’L’). typedef struct _tipoLista { tipoDados dados; struct _tipoLista *seg; } tipoLista; typedef struct _tipoFila { tipoLista *fim; tipoLista *inicio; } tipoFila; 4.6.3 ˜ Inicializac¸ao A inicializac¸a˜ o da fila vazia corresponde simplesmente a colocar a NULL os dois apontadores da fila. Deste modo, a inicializac¸a˜ o da fila pode ser realizada simplesmente por void inicializa(tipoFila *fila){ fila -> inicio = NULL; fila -> fim = NULL; } Note-se que na chamada a` func¸a˜ o o argumento deve ser efectuada por referˆencia (passando o enderec¸o da vari´avel tipoFila) de modo a que a vari´avel seja alter´avel pela func¸a˜ o. 81 ˆ 82 L ISTAS DIN AMICAS 4.6.4 ˜ Inserc¸ao A inserc¸a˜ o de novos elementos corresponde a` colocac¸a˜ o de elementos no final da fila, tal como representado graficamente na figura 4.6, caso B. Tal como no caso da pilha, para a criac¸a˜ o da mem´oria dinˆamica, e´ conveniente criar uma func¸a˜ o auxiliar novoNo(). tipoLista *novoNo(tipoDados x){ tipoLista *novo = calloc(1,sizeof(tipoFila)); if(novo == NULL) Erro("Erro na reserva de mem´ oria"); novo -> dados = x; novo -> seg = NULL; return novo; } A func¸a˜ o de inserc¸a˜ o propriamente dita pode ser realizada por void adiciona(tipoFila *fila,tipoDados x){ tipoLista *novo; novo = novoNo(x); novo -> dados = x; if(fila -> fim != NULL){ fila -> fim -> seg = novo; } else{ fila -> inicio = novo; } fila -> fim = novo; } Nesta func¸a˜ o e´ necess´ario considerar o caso particular em que a fila est´a vazia e em que, como tal, o apontador fim est´a a NULL. Neste caso particular, ambos os apontadores ( inicio e fim) devem ser colocados ser inicializados com o enderec¸o do novo elemento inserido. No caso habitual (lista n˜ao vazia), basta adicionar o novo elemento ao u´ ltimo da lista e modificar o apontador fim. L ISTAS : 4.6.5 FILAS ˜ Remoc¸ao A inserc¸a˜ o de novos elementos corresponde a` eliminac¸a˜ o de elementos presentes no final da fila, tal como representado graficamente na figura 4.6, caso C. Esta func¸a˜ o pode ser realizada por void retira(tipoFila *fila,tipoDados *x){ tipoLista *aux; if(filaVazia(fila)) Erro("Remoc ¸˜ ao de uma fila vazia"); *x = fila -> inicio -> dados; aux = fila -> inicio; fila -> inicio = fila -> inicio -> seg; if(fila -> inicio == NULL) fila -> fim = NULL; free(aux); } A func¸a˜ o de retira tem tamb´em uma estrutura simples. Tal como na func¸a˜ o de inserc¸a˜ o, e´ necess´ario considerar o caso particular em que a lista tem apenas um elemento, situac¸a˜ o em que o apontador fim deve ser colocado a NULL depois da remoc¸a˜ o. Tal como no caso da pilha, e´ testada a condic¸a˜ o de pilha vazia antes de efectuar a remoc¸a˜ o (ver secc¸a˜ o 4.6.6). 4.6.6 Teste Para manipulac¸a˜ o da lista e´ indispens´avel dispor de uma func¸a˜ o para testar se a fila se encontra vazia. Para este teste basta verificar qualquer um dos apontadores se encontra a NULL. Este teste pode ser efectuado pelo c´odigo int filaVazia(tipoFila *fila){ return (fila -> inicio == NULL); } 83 ˆ 84 L ISTAS DIN AMICAS 4.6.7 Exemplo Para uma maior semelhanc¸a com o exemplo da pilha, utiliza-se tamb´em neste exemplo o caso de uma fila de caracteres. Como seria de esperar, a fila n˜ao realiza neste caso uma invers˜ao, mas apenas um armazenamento tempor´ario da informac¸a˜ o, permitindo a sua reproduc¸a˜ o pela mesma ordem de entrada. Tal como no exemplo da pilha, o c´odigo foi distribu´ıdo por trˆes ficheiros: main.c, fila.c e dados.c, tendo para os dois u´ ltimos sido desenvolvido um ficheiro de prot´otipos associado. Dada a semelhanc¸a com o exemplo da pilha, apresentam-se aqui apenas o conte´udo dos v´arios ficheiros, sem outros coment´arios. Omite-se aqui o conte´udo dos ficheiros dados.h, dados.c, util.h e util.c por serem obviamente idˆenticos aos anteriores. Ficheiro fila.h /* * Ficheiro: fila.h * Autor: Fernando M. Silva * Data: 7/11/2002 * Conte´ udo: * Ficheiro com declarac ¸˜ ao de tipos e * prot´ otipos dos m´ etodos para manipulac ¸˜ ao * de uma fila simples de elementos gen´ ericos * de "tipoDados". */ #ifndef _FILA_H #define _FILA_H #include "dados.h" typedef struct _tipoLista { tipoDados dados; struct _tipoLista *seg; } tipoLista; typedef struct _tipoFila { tipoLista *fim; tipoLista *inicio; } tipoFila; /* Prot´ otipos das func ¸˜ oes */ L ISTAS : void void void int inicializa(tipoFila *fila); adiciona(tipoFila *fila,tipoDados x); retira(tipoFila *fila,tipoDados *x); filaVazia(tipoFila *fila); #endif /* _FILA_H */ Ficheiro fila.c /* * Ficheiro: fila.c * Autor: Fernando M. Silva * Data: 1/11/2002 * Conte´ udo: * M´ etodos para manipulac ¸˜ ao de uma fila suportada * numa estrutura dinˆ amica ligada */ #include "fila.h" #include "util.h" tipoLista *novoNo(tipoDados x){ /* * Cria um novo n´ o para a fila */ tipoLista *novo = calloc(1,sizeof(tipoFila)); if(novo == NULL) Erro("Erro na reserva de mem´ oria"); novo -> dados = x; novo -> seg = NULL; return novo; } void inicializa(tipoFila *fila){ /* * Inicializa uma nova fila */ fila -> inicio = NULL; fila -> fim = NULL; } int filaVazia(tipoFila *fila){ /* * Verifica o estado da fila * Argumentos: FILAS 85 ˆ 86 L ISTAS DIN AMICAS * fila - Apontador para a base da fila * Retorna: * 1 se a fila estiver vazia * 0 em caso contr´ ario */ return (fila -> inicio == NULL); } void adiciona(tipoFila *fila,tipoDados x){ /* * Adiciona um elemento ` a fila * Argumentos: * fila - Apontador para a fila * x - dados a inserir */ tipoLista *novo; novo = novoNo(x); if(fila -> fim != NULL){ fila -> fim -> seg = novo; } else{ fila -> inicio = novo; } fila -> fim = novo; } void retira(tipoFila *fila,tipoDados *x){ /* * Retira um elemento fila * Argumentos: * fila - Apontador para a fila * x - Apontador para vari´ avel que * retorna o valor lido da fila */ tipoLista *aux; if(filaVazia(fila)) Erro("Remoc ¸˜ ao de uma fila vazia"); *x = fila -> inicio -> dados; aux = fila -> inicio; fila -> inicio = fila -> inicio -> seg; if(fila -> inicio == NULL) fila -> fim = NULL; free(aux); } L ISTAS ORDENADAS 87 Ficheiro main.c /* * Ficheiro: main.c * Autor: Fernando M. Silva * Data: 7/11/2000 * Conte´ udo: * Programa principal simples para teste * de uma fila. */ #include #include #include "fila.h" int main(){ tipoFila fila; tipoDados x; inicializa(&fila); printf("Introduza uma sequˆ encia de caracteres:\n"); while(!leDados(&x)){ adiciona(&fila,x); } printf("Sequˆ encia lida:\n"); while(!filaVazia(&fila)){ retira(&fila,&x); escreveDados(x); } printf("\n"); exit(0); } 4.7 4.7.1 Listas ordenadas ˜ Introduc¸ao Um vector pode ser ordenado segundo uma qualquer relac¸a˜ o de ordem utilizando um algoritmo apropriado(Knuth, 1973) (selection sort, bubble-sort, quicksort ou qualquer outro). Embora estes algoritmos estejam bem estudados e sejam frequentemente necess´arios, todos eles requerem um esforc¸o computacional significativo. E´ poss´ıvel provar que a quantidade de trabalho necess´aria para esta tarefa n˜ao e´ apenas proporcional ao n´umero de elementos a ordenar: a complexidade da ˆ 88 L ISTAS DIN AMICAS tarefa aumenta significativamente mais que a dimens˜ao do vector. Para compreender melhor a quantidade de trabalho exigido, podemos comparar esta situac¸a˜ o ao de arrumar alfabeticamente uma biblioteca a partir de uma situac¸a˜ o desorganizada. Se arrumar uma biblioteca com 5,000 livros demorar um dia, arrumar uma biblioteca com 10,000 livros n˜ao demorar´a apenas dois dias, mas sim trˆes ou quatro, j´a que cada t´ıtulo individual ter´a que ser alfabeticamente comparado com um n´umero muito maior de outros livros. No caso de uma biblioteca, se se pretender evitar a tarefa herc´ulea de ordenar todos os livros, o melhor e´ mantˆe-la sempre ordenada. Desta forma, cada novo livro adicionado ter´a apenas que ser colocado no local certo, tarefa obviamente muito mais r´apida. Em C, quando os elementos a ordenar se encontram armazenados num vector, o m´etodo anterior corresponde basicamente a encontrar o local de inserc¸a˜ o, deslocar todos os elementos posteriores de uma posic¸a˜ o, e inserir o elemento na posic¸a˜ o assim aberta. Embora o algoritmo seja simples, a tarefa de deslocar todos os elementos de uma posic¸a˜ o e´ computacionalmente pesada, sobretudo quando a inserc¸a˜ o se efectua nas posic¸o˜ es iniciais. O paralelo que podemos encontrar no exemplo da biblioteca e´ o de uma estante repleta de livros, excepto na u´ ltima prateleira. A introduc¸a˜ o ordenada de um novo t´ıtulo na primeira prateleira exige deslocar todos os t´ıtulos de uma posic¸a˜ o. Dado que s´o existe espac¸o dispon´ıvel na u´ ltima prateleira, este processo pode exigir de facto movimentar livros em todas as prateleiras da estante. As listas dinˆamicas oferecem uma forma simples de criar e manter eficientemente uma colecc¸a˜ o de objectos ordenados. Ao contr´ario do vector, em que todas as posic¸o˜ es se encontram em enderec¸os de mem´oria cont´ıguos, a ordem dos elementos da lista n˜ao depende do seu enderec¸o de mem´oria, mas apenas da ordem definida pela sequˆencia de apontadores, tal como j´a se mostrou 4.2. As operac¸o˜ es necess´arias para a manipulac¸a˜ o e manutenc¸a˜ o de uma lista ordenada s˜ao generalizac¸o˜ es relativamente simples dos casos da pilha e da fila. Algumas destas operac¸o˜ es, como criar a lista ou listar os seus elementos, j´a foram apresentadas anteriormente. Apenas algumas das operac¸o˜ es descritas seguidamente, como a inserc¸a˜ o, procura e remoc¸a˜ o no meio da lista s˜ao de facto totalmente novas. 4.7.2 ˜ e inicializac¸ao ˜ Declarac¸ao A declarac¸a˜ o e inicializac¸a˜ o de uma lista segue os passos j´a vistos para o caso da pilha. A declarac¸a˜ o da lista pode ser realizada por typedef struct _tipoLista { L ISTAS ORDENADAS 89 tipoDados dados; struct _tipoLista *seg; } tipoLista; onde, tal como nos exemplos anteriores, tipoDados e´ um tipo abstracto que define a informac¸a˜ o armazenada em cada elemento. De igual modo, a inicializac¸a˜ o da lista corresponde apenas a` inicializac¸a˜ o do apontador a NULL, por meio da func¸a˜ o de inicializac¸a˜ o tipoLista *inicializa(){ return NULL; } 4.7.3 Listagem ordenada A listagem ordenada corresponde apenas a uma listagem convencional do conte´udo. Deste modo, a listagem pode ser feita por void listar(tipoLista *base ){ tipoLista *aux = base; while(aux){ printf(" -> "); escreveDados(aux -> dados); printf("\n"); aux = aux -> seg; } } 4.7.4 Procura A procura e´ um procedimento relativamente simples. Basta percorrer a lista at´e encontrar o elemento que se procura, e retornar um apontador para o respectivo elemento. Convenciona-se que a func¸a˜ o de procura deve retornar NULL caso o elemento a procurar n˜ao exista. Para assentar ideias, admita-se temporariamente que se est´a a lidar com uma lista de inteiros. Deste modo, presume-se que se definiu previamente typedef int tipoDados; ˆ 90 L ISTAS DIN AMICAS Neste caso, a func¸a˜ o de procura pode ser realizada por tipoLista *procura(tipoLista *base,tipoDados x){ tipoLista *aux; aux = base; while((aux!=NULL) && aux = aux -> seg; return aux; aux -> dados != x) } A func¸a˜ o anterior, embora funcione, apresenta o inconveniente de n˜ao explorar o facto da lista estar ordenada. Por exemplo, no caso de uma lista ordenada de inteiros onde estejam todos os valores pares entre 10 e 10000, se se procurar o n´umero 15 ser´a necess´ario ir at´e ao fim da lista para concluir que o n´umero n˜ao est´a presente. Como e´ o´ bvio, esta conclus˜ao poderia ter sido tirada muito mais cedo, logo que fosse atingido o n´umero 16 na lista. Deste modo, uma vers˜ao mais optimizada da func¸a˜ o de procura pode ser escrita como tipoLista *procuraOrdenado(tipoLista *base,tipoDados x){ tipoLista *aux = base; while((aux!=NULL) && (aux -> dados seg; if((aux != NULL) && (aux -> dados == x)) return aux; else return NULL; } Neste caso, a lista e´ percorrida enquanto o elemento a procurar for inferior a` posic¸a˜ o actual da lista. Quando o ciclo e´ interrompido, e´ testado se se se atingiu o fim da lista ou se se encontrou o elemento procurado. Nesta func¸a˜ o, vale a pena considerar a estrutura do teste if((aux != NULL) && ... } (aux -> dados == x)){ e verificar a forma como se toma partido do modo como o C avalia express˜oes l´ogicas. Repare-se que o segundo operando da disjunc¸a˜ o ( aux -> dados == x) s´o pode ser avaliado se aux L ISTAS ORDENADAS 91 for diferente de NULL, j´a que de outra forma se poderia estar a gerar uma violac¸a˜ o de mem´oria (tentativa de acesso atrav´es do enderec¸o 0, o que se encontra fora do controlo do programador). No entanto, o C garante que realiza a avaliac¸a˜ o de express˜oes l´ogicas da esquerda para a direita e que interrompe a sua avaliac¸a˜ o assim que for poss´ıvel determinar univocamente o resultado final. Neste exemplo, se aux for NULL, o primeiro operando da conjunc¸a˜ o l´ogica ’&&’ e´ falso, o que implica que o resultado global da express˜ao tamb´em o e´ . Assim, n˜ao sendo necess´ario o c´alculo do segundo operando, n˜ao h´a o risco de se produzir a violac¸a˜ o de mem´oria decorrente do acesso atrav´es de um apontador NULL. Refira-se, por u´ ltimo, que num problema pr´atico podem coexistir v´arias func¸o˜ es de procura, consoante o que se pretende encontrar. Por exemplo, se os elementos de uma lista s˜ao estruturas que incluem, por exemplo, um n´umero e um nome, podem ser escritas duas func¸o˜ es de procura, uma para o n´umero e outra para o nome. Claro que se a lista estiver ordenada por n´umeros, a busca pelo n´umero poder´a ser optimizada (procura s´o at´e encontrar um elemento de n´umero igual ou superior ao procurado), mas a busca pelo nome ter´a obviamente que ser exaustiva se o nome n˜ao existir, j´a que s´o no final da lista ser´a poss´ıvel ter a certeza de que determinado nome n˜ao faz parte da lista. 4.7.5 ˜ de dados e metodos ´ Abstracc¸ao de teste Nas duas func¸o˜ es de procura anteriores admitiu-se que o tipoDados era um inteiro. Esta hip´otese permitiu utilizar os operadores relacionais == e < de uma forma intuitiva. No caso mais geral em que tipoDados e´ um tipo abstracto gen´erico esta comparac¸a˜ o n˜ao pode ser realizada directamente pelos operadores relacionais. Para demonstrar este facto, considere-se que tipoDados corresponde a uma estrutura com um n´umero e um nome de um aluno. Neste caso, n˜ao faz sentido usar o operador relacional < para comparar dois alunos a e b. De facto, o C desconhece o que se pretende comparar: e´ o nome dos alunos a e b ou os seus n´umeros? Para contornar esta dificuldade, seria poss´ıvel utilizar o operador ’.’ (membro de estrutura) e aceder directamente ao campo num´erico, utilizando o operador < para a comparac¸a˜ o, ou aceder ao campo com o nome e utilizar a func¸a˜ o strcmp() de forma adequada. No entanto, qualquer destas soluc¸o˜ es estaria a violar o princ´ıpio de abstracc¸a˜ o de dados, j´a que os m´etodos de procura da lista, para quem tipoDados deveria ser um tipo abstracto gen´erico, estariam a aceder directamente a detalhes internos do tipo. A forma de resolver esta quest˜ao e´ as func¸o˜ es procura delegarem o processo de comparac¸a˜ o ˆ 92 L ISTAS DIN AMICAS em m´etodos (func¸o˜ es) espec´ıficos internos de tipoDados, respons´aveis pela implementac¸a˜ o dos detalhes pr´aticos da comparac¸a˜ o. Deste modo, admitindo que foram associados ao tipo abstracto tipoDados um m´etodo com prot´otipo int menor(tipoDados a,tipoDados b); que devolve 1 se a for de algum modo anterior a b e 0 em caso contr´ario (as func¸o˜ es da lista n˜ao precisam de conhecer os detalhes desta comparac¸a˜ o) e um outro int igual(tipoDados a,tipoDados b); que devolve 1 se a for igual b segundo um dado crit´erio e 0 em caso contr´ario. Deste modo, uma soluc¸a˜ o mais geral da func¸a˜ o de procura ordenada deveria ser escrita como tipoLista *procuraOrdenado(tipoLista *base,tipoDados x){ tipoLista *aux = base; while((aux!=NULL) && menor(aux -> dados,x)) aux = aux -> seg; if((aux != NULL) && igual(aux -> dados ,x)) return aux; else return NULL; } Alternativamente, e´ frequente unificar todos os m´etodos de comparac¸a˜ o numa u´ nica func¸a˜ o que devolve -1, 0 ou 1 consoante o primeiro elemento e´ anterior, igual ou posterior ao segundo. E´ esta soluc¸a˜ o que e´ adoptada na func¸a˜ o strcmp() para comparac¸a˜ o de strings. 4.7.6 ˜ ordenada Inserc¸ao Para realizar uma inserc¸a˜ o ordenada e´ suficiente percorrer a lista at´e encontrar um elemento que seja superior ao que se pretende inserir. O novo elemento dever´a ser colocado antes deste. Apesar desta metodologia simples, a inserc¸a˜ o ordenada requer algumas considerac¸o˜ es suplementares. Um dos pontos a ter em conta e´ que para posicionar o novo elemento antes do que foi identificado e´ necess´ario alterar o apontador do elemento que se encontra antes deste. Ou seja, ao L ISTAS ORDENADAS 93 antes depois 5 novo 10 7 Figura 4.7: Inserc¸a˜ o entre dois elementos da lista. O apontador antes n˜ao aponta para o elemento anterior da lista, mas sim para o campo apontador seg desse elemento. percorrer a lista, e´ necess´ario manter uma referˆencia n˜ao apenas para o elemento da lista que est´a a ser comparado (elemento actual) mas tamb´em para o seu predecessor (elemento anterior). De modo a melhor compreender esta operac¸a˜ o, e´ conveniente comec¸ar por desenvolver uma func¸a˜ o auxiliar de inserc¸a˜ o em que se admite que a posic¸a˜ o de inserc¸a˜ o j´a foi determinada e, como tal, que j´a existem referˆencias para o apontador do elemento anterior e para o elemento actual (v. figura 4.7). Designaremos estas vari´aveis de referˆencia por antes e depois. Neste caso o c´odigo desta func¸a˜ o pode ser escrito void insere(tipoLista **antes, tipoDados x, tipoLista *depois){ tipoLista *novo = novoNo(x); *antes = novo; novo -> seg = depois; } onde a func¸a˜ o novoNo() e´ semelhantes a` s anteriores tipoLista *novoNo(tipoDados x){ tipoLista *novo = calloc(1,sizeof(tipoLista)); if(novo == NULL) Erro("Erro na reserva de mem´ oria"); novo -> dados = x; novo -> seg = NULL; return novo; } Uma vez definida esta func¸a˜ o, a inserc¸a˜ o ordenada apenas tem que ter em conta alguns casos particulares, nomeadamente a inserc¸a˜ o numa fila vazia ou a inserc¸a˜ o antes do in´ıcio. Nos outros ˆ 94 L ISTAS DIN AMICAS casos, basta percorrer a lista com um apontador act, e manter presente que se deve manter um apontador ant para o elemento anterior. A func¸a˜ o insereOrdenado() pode ent˜ao ser escrita tipoLista *insereOrdenado(tipoLista *base,tipoDados x){ tipoLista *act,*ant; if(base == NULL){ insere(&base,x,NULL); /* Lista vazia */ } else if(menor(x,base -> dados)){ insere(&base,x,base); /* Insere *antes* da base */ } else{ ant = base; act = base -> seg; while((act != NULL) && (menor(act -> dados,x))){ ant = act; act = act -> seg; } insere(&(ant -> seg),x,act); } return base; } Esta func¸a˜ o recebe como um dos argumentos o valor da base e devolve o valor desta depois da inserc¸a˜ o. A base retornada e´ idˆentica a` base inicial, excepto quando a inserc¸a˜ o e´ feita no in´ıcio da lista. O par de func¸o˜ es insere() e insereOrdenado() foi escrita de modo a sublinhar os diversos aspectos necess´arios a` inserc¸a˜ o numa lista. No entanto, o C permite escrever uma func¸a˜ o de inserc¸a˜ o ordenada de modo muito mais compacto: tipoLista *insereOrdenado(tipoLista *base,tipoDados x){ tipoLista *aux = base, *novo = novoNo(x); if(aux == NULL || menor(x,aux -> dados)) { novo -> seg = base; return novo; } aux = base; while( aux -> seg!= NULL && menor(aux -> seg -> dados,x)) aux = aux -> seg; novo -> seg = aux -> seg; aux -> seg = novo; return base; L ISTAS ORDENADAS 95 reg 1 4 9 Figura 4.8: Remoc¸a˜ o. O apontador reg referencia o campo seg do registo anterior da lista. } Aqui, o primeiro if abrange simultaneamente a inserc¸a˜ o numa lista vazia e antes do primeiro elemento. Seguidamente, o ciclo while percorre a lista, mantendo apenas um apontador (equivalente ao apontador ant do exemploanterior). 4.7.7 ˜ Remoc¸ao Tal como a inserc¸a˜ o, a remoc¸a˜ o pode ser simplificada se for escrita uma func¸a˜ o auxiliar libertaReg que e´ executada quando j´a e´ conhecido o local de remoc¸a˜ o. Esta func¸a˜ o (v. figura 4.8) pode ser escrita tipoLista *libertaReg(tipoLista *reg){ tipoLista *aux; aux = reg; reg = reg -> seg; free(aux); return reg; } Seguidamente, e´ necess´ario escrever uma func¸a˜ o que procure o o elemento a apagar e chame a func¸a˜ o anterior. Esta func¸a˜ o e´ dada por tipoLista *apaga(tipoLista *base,tipoDados x){ tipoLista *aux; aux = base; if(base != NULL){ if(igual(base -> dados,x)){ base = libertaReg(base); } else{ ˆ 96 L ISTAS DIN AMICAS aux = base; while((aux -> seg != NULL) && (menor(aux -> seg -> dados,x))) aux = aux -> seg; if((aux -> seg != NULL) && igual(aux -> seg -> dados,x)) aux -> seg = libertaReg(aux -> seg); } } return base; } 4.7.8 Exemplo Como exemplo, apresenta-se aqui o c´odigo completo de um programa que manipula uma lista ordenada de racionais. O tipo racional e´ representado por dois inteiros, que descrevem o seu numerador e o denominador. O programa aceita uma sequˆencia de racionais. Um racional positivo e´ inserido na lista, enquanto que um racional negativo provoca uma tentativa de remoc¸a˜ o do seu sim´etrico, caso este exista (ou seja, a indicac¸a˜ o de − 35 provoca a remoc¸a˜ o do racional 53 ). Omite-se aqui a listagem dos ficheiros util.h e util.c, idˆenticos ao introduzido no exemplo da pilha (v. secc¸a˜ o 4.5.7). Ficheiro dados.h /* * * * * * * * * * * * # */ Ficheiro: lista.h Autor: Fernando M. Silva Data: 7/11/2000 Conte´ udo: Definic ¸˜ ao do tipo gen´ erico "tipoDados" usado nos exemplos de estruturas de dados dinˆ amicas. Para fins de exemplo, "tipoDados" ´ e realizado por uma fracc ¸˜ ao inteira, a qual e’ especificada por um numerador e um denominador #ifndef _DADOS_H L ISTAS ORDENADAS 97 #define _DADOS_H #include typedef struct{ int numerador; int denominador; } tipoDados; void tipoDados float int int int int tipoDados #endif escreveDados(tipoDados x); leDados(char mensagem[]); valor(tipoDados x); igual(tipoDados x,tipoDados y); menor(tipoDados x,tipoDados y); numerador(tipoDados x); denominador(tipoDados x); simetrico(tipoDados x); Ficheiro dados.c /* * * * * * * * * * * */ Ficheiro: dados .c Autor: Fernando M. Silva Data: 1/12/2000 Conte´ udo: M´ etodos de acesso exemplificativos da definic ¸˜ ao de um tipo abstracto "tipoDados" Neste exemplo, "tipoDados" implementa um numero racional, especificado por um numerador e um denominador #include "dados.h" #define DIM_LINE 100 tipoDados leDados(char mensagem[]){ /* * Escreve a mensagem passada por argumento, lˆ e um racional e * valida a entrada * Argumento: * mensagem - mensagem a escrever * Retorna: * racional lido ˆ 98 L ISTAS DIN AMICAS */ char line[DIM_LINE]; tipoDados x; int ni; do{ printf("%s\n",mensagem); fgets(line,DIM_LINE,stdin); ni=sscanf(line,"%d %d",&x.numerador,&x.denominador); if(ni !=2){ printf("Erro na leitura\n"); } else{ if(x.denominador == 0) printf("Racional inv´ alido\n"); } }while((ni != 2) || (x.denominador == 0)); return x; } void escreveDados(tipoDados x){ /* * Escreve x em stdout, no formato * numerador / denominador * Argumento: * x - valor a escrever */ printf("%d/%d",x.numerador,x.denominador); } float valor(tipoDados x){ /* * Retorna um real com o valor (aproximado) do racional x * Argumento: * x - racional * Retorna * valor aproximado de x */ return (float) x.numerador / (float) x.denominador; } int menor(tipoDados x,tipoDados y){ /* * Retorna 1 caso o racional x seja menor que y * Argumentos: * x,y - racionais a comparar * Retorna L ISTAS ORDENADAS 99 * 1 se x < y * 0 em caso contr´ ario */ if(x.denominador * y.denominador > 0) return x.numerador * y.denominador < y.numerador * x.denominador; else return x.numerador * y.denominador > y.numerador * x.denominador; } int igual(tipoDados x,tipoDados y){ /* * Retorna 1 caso o racional x seja igual a y * Argumentos: * x,y - racionais a comparar * Retorna * 1 se x = y * 0 em caso contr´ ario */ return x.numerador * y.denominador == x.denominador * y.numerador; } int numerador(tipoDados x){ /* * Retorna o numerador do racional x * Argumento: * x - racional * Retorna * numerador de x */ return x.numerador; } int denominador(tipoDados x){ /* * Retorna o denominador do racional x * Argumento: * x - racional * Retorna * denominador de x */ return x.denominador; } tipoDados simetrico(tipoDados x){ /* * Retorna o sim´ etrico do racional x * Argumento: ˆ 100 L ISTAS DIN AMICAS * x - racional * Retorna * sim´ etrico de x */ tipoDados aux; aux.numerador = -x.numerador; aux.denominador = x.denominador; return aux; } Ficheiro lista.h /* * Ficheiro: lista.h * Autor: Fernando M. Silva * Data: 7/11/2000 * Conte´ udo: * Ficheiro com declarac ¸˜ ao de tipos e * prot´ otipos dos m´ etodos para manipulac ¸˜ ao * de uma lista dinˆ amica simples * */ #ifndef _LISTA_H #define _LISTA_H #include #include /* * Tipo dos dados da lista */ #include "dados.h" /* * Definic ¸˜ ao de tipoLista */ typedef struct _tipoLista { tipoDados dados; struct _tipoLista *seg; } tipoLista; /* * Prot´ otipos dos m´ etodos de acesso */ L ISTAS ORDENADAS 101 /* Inicializac ¸˜ ao */ tipoLista *inicializa(void); /* * Procura x na lista iniciada por base, retornando um apontador * para o registo que contem este valor (ou NULL se n˜ ao existe) */ tipoLista *procura(tipoLista *base,tipoDados x); tipoLista *procuraOrdenado(tipoLista *base,tipoDados x); /* * Insere x antes do registo "depois" e modificando o apontador "antes". */ void insere(tipoLista **antes, tipoDados x, tipoLista *depois); /* * Insere x na lista ordenada iniciada por base * Devolve a base, eventualmente alterada */ tipoLista *insereOrdenado(tipoLista *base,tipoDados x); /* * Lista todos os elementos da estrutura. */ void listar(tipoLista *base); /* * Liberta da lista o elemento especificado por reg */ tipoLista *libertaReg(tipoLista *reg); /* * Apaga da lista o registo que cont´ em x * Retorna a base, eventualmente alterada */ tipoLista *apaga(tipoLista *base,tipoDados x); #endif Ficheiro lista.c /* * * * Ficheiro: lista.c Autor: Fernando M. Silva Data: 7/11/2000 ˆ 102 L ISTAS DIN AMICAS * Conte´ udo: * M´ etodos para manipulac ¸˜ ao * de uma lista dinˆ amica * simples (ordenada) * */ /* * Inclui ficheiro com tipo e prot´ otipos */ #include "lista.h" #include "util.h" tipoLista *novoNo(tipoDados x){ /* * Cria um novo n´ o da lista */ tipoLista *novo = calloc(1,sizeof(tipoLista)); if(novo == NULL) Erro("Erro na reserva de mem´ oria"); novo -> dados = x; novo -> seg = NULL; return novo; } tipoLista *inicializa(){ /* * Cria uma nova lista * Retorna: * lista inicializada */ return NULL; } tipoLista *procura(tipoLista *base,tipoDados x){ /* * Procura um elemento na lista iniciada por base * Argumentos: * base - apontador para a base da lista * x - elemento a procurar * Retorna * apontador para x (ou NULL caso nao encontre) */ tipoLista *aux = base; while((aux!=NULL) && !igual(aux -> dados,x)) aux = aux -> seg; return aux; L ISTAS ORDENADAS 103 } tipoLista *procuraOrdenado(tipoLista *base,tipoDados x){ /* * Idˆ entico ao anterior, mas optimizado para listas * ordenadas * Argumentos: * base - apontador para a base da lista * x - elemento a procurar * Retorna * apontador para x (ou NULL caso nao encontre) */ tipoLista *aux = base; while((aux!=NULL) && menor(aux -> dados,x)) aux = aux -> seg; if((aux != NULL) && igual(aux -> dados ,x)) return aux; else return NULL; } void insere(tipoLista **antes, tipoDados x, tipoLista *depois){ /* * Insere x entre dois registos * Argumentos: * antes - predecessor na lista * x - elemento a inserir * depois - elemento seguinte a x */ tipoLista *novo = novoNo(x); *antes = novo; novo -> seg = depois; } tipoLista *insereOrdenado(tipoLista *base,tipoDados x){ /* * Insere x na lista ordenada * Argumentos: * base - apontador para a base da lista * x - elemento a procurar * Retorna * base, eventualmente alterada */ tipoLista *act,*ant; if(base == NULL){ insere(&base,x,NULL); /* Lista vazia */ ˆ 104 L ISTAS DIN AMICAS } else if(menor(x,base -> dados)){ insere(&base,x,base); /* Insere *antes* da base */ } else{ ant = base; act = base -> seg; while((act != NULL) && (menor(act -> dados,x))){ /* * Procura local de inserc ¸˜ ao */ ant = act; act = act -> seg; } insere(&(ant -> seg),x,act); } return base; } void listar(tipoLista *base ){ /* * Lista todos os elementos * Argumentos: * base - apontador para a base da lista */ while(base){ printf(" -> "); escreveDados(base -> dados); printf("\n"); base = base -> seg; } } tipoLista *libertaReg(tipoLista *reg){ /* * Remove da lista o elemento apontado por reg e * liberta a mem´ oria associada. * Argumentos: * reg - apontador para a vari´ avel da lista que * aponta para o registo a libertar * Retorna - novo valor do apontandor */ tipoLista *aux; aux = reg; reg = reg -> seg; free(aux); return reg; } L ISTAS ORDENADAS 105 tipoLista *apaga(tipoLista *base,tipoDados x){ /* * Apaga da lista o registo que cont´ em x * Argumentos: * base - apontador para a base da lista * x - elemento a eliminar * Retorna * base, eventualmente alterada */ tipoLista *aux; aux = base; if(base != NULL){ if(igual(base -> dados,x)){ base = libertaReg(base); } else{ aux = base; while((aux -> seg != NULL) && (menor(aux -> seg -> dados,x))) aux = aux -> seg; if((aux -> seg != NULL) && igual(aux -> seg -> dados,x)) aux -> seg = libertaReg(aux -> seg); } } return base; } Ficheiro main.c /* * * * * * * * * * * * * Ficheiro: main.c Autor: Fernando M. Silva Data: 7/11/2000 Conte´ udo: Programa principal simples para teste de estruturas dinˆ amicas ligadas. Neste teste, os dados armazenados na lista s˜ ao fracc ¸˜ oes inteiras, implementadas por um tipo abstracto "tipoDados". Assim: 1. A especificac ¸˜ ao de um racional positivo, ˆ 106 L ISTAS DIN AMICAS * acrescenta o racional ` a lista * 2. A introduc ¸˜ ao de um racional negativo, * especifica que deve ser apagada o seu * sim´ etrico da lista * 3. Com a entrada de um racional com valor 0, * termina o programa * */ #include #include #include "lista.h" int main(){ tipoLista *base; tipoDados x; base = inicializa(); printf(" Programa para teste de uma lista " "ordenada de racionais:\n"); printf(" - A introduc ¸˜ ao de um racional positivo " ` "conduz a sua\n" " inserc ¸˜ ao ordenada na lista.\n"); printf(" - A introduc ¸˜ ao de um racional " "negativo procura o seu\n" " sim´ etrico na lista e, " "caso exista, remove-o.\n"); x = leDados("\nIndique o numerador e denominador " "de um racional:\n" "(dois inteiros na mesma linha)"); while(numerador(x) != 0) { if(valor(x) > 0){ base = insereOrdenado(base,x); } else{ x = simetrico(x); if(procuraOrdenado(base,x) == NULL){ printf(" ************ Erro: elemento de " "valor idˆ entico a "); escreveDados(x); printf(" n˜ ao encontrado na lista\n"); } else{ base = apaga(base,x); } } printf("\nConte´ udo actual da lista:\n"); VARIANTES 107 base        1 4 9 Figura 4.9: Lista de inteiros com registo separado para a base. Nesta figura, a lista tem apenas trˆes elementos efectivos (1, 4 e 9), sendo o primeiro registo utilizado apenas para o suporte da base da lista. listar(base); x = leDados("\nIndique o numerador " "e denominador de um racional:\n" "(dois inteiros na mesma linha)"); } printf("\n Racional com valor 0: fim do programa\n"); exit(0); } 4.8 Variantes 4.8.1 ˜ Introduc¸ao Embora a estrutura fundamental das listas dinˆamicas seja no essencial a que se viu anteriormente, existem diversas variantes que tˆem como objectivo simplificar os mecanismos de acesso ou ajustar a lista a objectivos espec´ıficos. 4.8.2 Listas com registo separado para a base Conforme se viu anteriormente, a manipulac¸a˜ o de listas ordenadas exige que a base da lista seja tratada como um caso particular. Uma forma de evitar estes testes adicionais e´ manter permanentemente um registo mudo no in´ıcio da lista (registo separado para a base) que, embora n˜ao seja utilizado de facto, permite simplificar o acesso aos restantes elementos. Adicionalmente, este tipo de estrutura (v. figura 4.9 tem a vantagem do apontador para a base n˜ao ser modificado depois da criac¸a˜ o da lista. As func¸o˜ es de manipulac¸a˜ o da lista s˜ao no essencial semelhantes a` s da lista ordenada simples. Embora sem listar aqui todas as func¸o˜ es de acesso, referem-se algumas das que s˜ao modificadas pela existˆencia de um registo permanente na base. A inicializac¸a˜ o da lista corresponde neste caso a` criac¸a˜ o do registo da base. Deste modo, a ˆ 108 L ISTAS DIN AMICAS fim 1 5 8 9 inicio Figura 4.10: Lista dupla de inteiros. func¸a˜ o de inicializac¸a˜ o e´ dada por Por outro lado, a func¸a˜ o de inserc¸a˜ o ordenada pode ser simplificada. Admitindo a utilizac¸a˜ o da mesma func¸a˜ o insere() j´a apresentado para a lista simples, a inserc¸a˜ o pode ser feita por onde se pode constatar a ausˆencia do teste particular a` base que se realiza na lista ordenada simples. 4.8.3 Listas duplamente ligadas Um dos inconvenientes das listas simplesmente ligadas e´ serem unidireccionais. Por outras palavras, ao aceder a um dado elemento da lista e´ f´acil aceder ao elemento seguinte, mas o acesso ao elemento anterior n˜ao e´ poss´ıvel. Para evitar este inconveniente e´ poss´ıvel desenvolver uma lista duplamente ligada, em que cada elemento disp˜oe de dois apontadores, um para o elemento seguinte e outro para o anterior. Adicionalmente, tal como no caso da fila, o acesso a` lista e´ geralmente mantido por dois apontadores, um para o in´ıcio e outro para o fim da lista (v. figura 4.10). A declarac¸a˜ o de uma lista duplamente ligada pode ser realizada simplesmente por typedef struct _tipoRegDupla{ tipoDados dados; struct _tipoRegDupla *seg,*ant; } tipoRegDupla; typedef struct _tipoDupla{ tipoRegDupla inicio; tipoRegDupla fim; } tipoDupla; A NEL DUPLO COM REGISTO SEPARADO PARA A BASE 109 base 1 4 4 9 Figura 4.11: Lista em anel de inteiros. O u´ ltimo elemento aponta para o primeiro base  5 8 9 Figura 4.12: Anel duplo com registo separado para a base. 4.8.4 Aneis Numa lista simplesmente ligada, o u´ ltimo da elemento da lista e´ identificado pelo facto do apontador para o elemento seguinte ser NULL. Uma alternativa e´ colocar o u´ ltimo elemento da lista a apontar para a base (v. figura 4.11). A manipulac¸a˜ o de uma estrutura deste tipo e´ muito semelhante a` de uma lista simples, substituindo-se apenas parte das comparac¸o˜ es com o valor NULL por comparac¸o˜ es com o apontador da base. Uma lista em anel apresenta a vantagem do u´ ltimo elemento apontar para o primeiro, o que pode ser conveniente em aplicac¸o˜ es em que seja necess´ario percorrer os elementos da lista de uma forma c´ıclica. 4.9 4.9.1 Anel duplo com registo separado para a base ˜ Introduc¸ao Um anel duplo com registo separado para a base e´ uma estrutura dinˆamica que combina as diversas variantes abordadas anteriormente: registo separado para a base, ligac¸a˜ o em anel e registos com apontadores bidireccionais (figura 4.12). Embora este tipo estrutura possa sugerir uma manipulac¸a˜ o a` partida mais complexa, verifica-se na pr´atica o inverso: a explorac¸a˜ o correcta desta estrutura origina, de modo geral, c´odigo mais simples. ˆ 110 L ISTAS DIN AMICAS base   Figura 4.13: Anel vazio ap´os a criac¸a˜ o. 4.9.2 ˜ Declarac¸ao A declarac¸a˜ o de um anel e´ feita da forma habitual. Ou seja, typedef struct _tipoAnel { tipoDados dados; struct _tipoAnel *seg,*ant; } tipoAnel; 4.9.3 ˜ Inicializac¸ao A inicializac¸a˜ o de um anel corresponde a` criac¸a˜ o de um registo para a base e ao fecho em anel das suas ligac¸o˜ es (figura 4.13). Esta inicializac¸a˜ o pode ser feita por tipoAnel *inicializa(){ tipoAnel *aux; tipoDados regMudo; aux = novoNo(regMudo); aux -> seg = aux -> ant = aux; return aux; } A func¸a˜ o novoNo(), semelhante a` s anteriores, pode ser definida como tipoAnel *novoNo(tipoDados x){ tipoAnel *novo = calloc(1,sizeof(tipoAnel)); if(novo == NULL) Erro("Erro na reserva de mem´ oria"); novo -> dados = x; A NEL DUPLO COM REGISTO SEPARADO PARA A BASE 111 novo -> seg = novo -> ant = NULL; return novo; } 4.9.4 Listagem A listagem de um anel pode ser feita por ordem directa ou inversa. Deste modo, podem ser desenvolvidas duas func¸o˜ es para este efeito, de estrutura muito semelhante: /* Listagem directa void listar(tipoAnel *base ){ tipoAnel *aux; */ aux = base -> seg; while(aux != base){ printf(" -> "); escreveDados(aux -> dados); printf("\n"); aux = aux -> seg; } } /* Listagem inversa */ void listarInv(tipoAnel *base ){ tipoAnel *aux; aux = base -> ant; while(aux != base){ printf(" -> "); escreveDados(aux -> dados); printf("\n"); aux = aux -> ant; } } Repare-se que em qualquer das duas func¸o˜ es o in´ıcio da listagem se efectua no elemento a seguir a` base (de modo a saltar o registo da base), enquanto que na condic¸a˜ o de manutenc¸a˜ o no ciclo se testa o regresso a` base. 4.9.5 Procura A func¸a˜ o de procura e´ semelhante a` de uma lista ordenada, omitindo o elemento inicial: ˆ 112 L ISTAS DIN AMICAS tipoAnel *procuraOrdenado(tipoAnel *base,tipoDados x){ tipoAnel *aux; base -> dados = x; aux = base -> seg; while(menor(aux -> dados,x)) aux = aux -> seg; if((aux != base) && (igual(aux -> dados,x))) return aux; else return NULL; } Repare-se que, para simplificar a condic¸a˜ o do ciclo, se iniciou o registo da base com o pr´oprio valor do elemento a procurar. Este artif´ıcio garante que, mesmo que o elemento de procura n˜ao se encontre no anel, a procura e´ interrompida quando se atinge a base. Claro que, deste modo, e´ preciso testar a` sa´ıda do ciclo se a interrupc¸a˜ o se verificou por ter encontrado o local real de interrupc¸a˜ o e o elemento de procura, ou por se ter encontrado o elemento artificialmente colocado na base. Neste u´ ltimo caso dever´a retornar-se o apontador NULL. 4.9.6 ˜ Inserc¸ao No caso de inserc¸a˜ o ordenada no anel, e´ particularmente u´ til utilizar uma func¸a˜ o s´o para a inserc¸a˜ o do registo, admitindo que j´a se conhece o local de inserc¸a˜ o, e desenvolver posteriormente a func¸a˜ o de procura o local de inserc¸a˜ o. A primeira destas func¸o˜ es pode ser definida como void insere(tipoAnel *depois,tipoDados x){ tipoAnel *novo = novoNo(x); novo -> seg = depois; /* (1) */ novo -> ant = depois -> ant; /* (2) */ depois -> ant = novo; /* (3) */ novo -> ant -> seg = novo; /* (4) */ } Apesar da aparente complexidade da func¸a˜ o, a sua realizac¸a˜ o corresponde apenas a` realizac¸a˜ o das ligac¸o˜ es necess´arias pela ordem correcta. Na figura 4.14 detalha-se este processo de inserc¸a˜ o no caso de um anel de inteiros, identificando-se a correspondˆencia entre as ligac¸a˜ o e as instruc¸o˜ es da listagem. A NEL DUPLO COM REGISTO SEPARADO PARA A BASE 113 depois 1 8 (2) (4) novo (1) (3) 3 Figura 4.14: Inserc¸a˜ o num anel. Os n´umeros entre () correspondem a` s atribuic¸o˜ es da listagem apresentada no texto. Repare-se que, dado que a lista e´ dupla, e´ suficiente o apontador para o elemento seguinte para estabelecer todas as ligac¸o˜ es. A func¸a˜ o de inserc¸a˜ o propriamente dita realiza apenas a pesquisa do local de inserc¸a˜ o. De forma semelhante ao j´a efectuado no caso da func¸a˜ o de procura, e´ poss´ıvel colocar uma c´opia do elemento a inserir no registo da base para permitir que o caso particular de inserc¸a˜ o no final da lista n˜ao tem que ser tratado como um caso particular. Deste modo, o c´odigo da func¸a˜ o fica particularmente simples: void insereOrdenado(tipoAnel *base,tipoDados x){ tipoAnel *act; base -> dados = x; act = base -> seg; while(menor(act -> dados,x)) act = act -> seg; insere(act,x); } 4.9.7 ˜ Remoc¸ao A operac¸a˜ o de remoc¸a˜ o pode ser realizada por duas func¸o˜ es complementares. A primeira (removeReg()) e´ utilizada ap´os a identificac¸a˜ o do registo a eliminar e e´ respons´avel pela libertac¸a˜ o da mem´oria ocupada e reconstruc¸a˜ o das ligac¸o˜ es. A segunda (apaga()) que corresponde a` func¸a˜ o a utilizar externamente, procura o registo a eliminar e, caso o identifique, chama ˆ 114 L ISTAS DIN AMICAS a primeira. void removeReg(tipoAnel *reg){ reg -> seg -> ant = reg -> ant; reg -> ant -> seg = reg -> seg; free(reg); } void apaga(tipoAnel *base,tipoDados x){ tipoAnel *aux; base -> dados = x; aux = base -> seg; while(menor(aux -> dados,x)) aux = aux -> seg; if((aux != base) && igual(aux -> dados,x)) removeReg(aux); } 4.9.8 Exemplo Utiliza-se neste caso o mesmo exemplo de listagem de racionais j´a apresentado anteriormente. Neste caso, omite-se a listagem dos m´etodos do tipo tipoDados, e os ficheiros util.c e util.h, obviamente idˆentico ao anterior. Ficheiro anel.h /* * Ficheiro: anel.h * Autor: Fernando M. Silva * Data: 7/11/2000 * Conte´ udo: * Ficheiro com declarac ¸˜ ao de tipos e * prot´ otipos dos m´ etodos para manipulac ¸˜ ao * de um anel com registo seoparado * para a base * */ #ifndef _ANEL_H #define _ANEL_H #include A NEL DUPLO COM REGISTO SEPARADO PARA A BASE 115 #include /* * Tipo dos dados da lista */ #include "dados.h" /* * Definic ¸˜ ao de tipoAnel */ typedef struct _tipoAnel { tipoDados dados; struct _tipoAnel *seg,*ant; } tipoAnel; /* * Prot´ otipos dos m´ etodos de acesso */ /* Inicializac ¸˜ ao */ tipoAnel *inicializa(void); /* * Procura x na lista iniciada por base, retornando um apontador * para o registo que contem este valor (ou NULL se n˜ ao existe) */ tipoAnel *procura(tipoAnel *base,tipoDados x); tipoAnel *procuraOrdenado(tipoAnel *base,tipoDados x); /* * Insere x antes do registo "depois" */ void insere(tipoAnel *depois,tipoDados x); /* * Insere x na lista ordenada iniciada por base */ void insereOrdenado(tipoAnel *base,tipoDados x); /* * Lista todos os elementos da estrutura. */ void listar(tipoAnel *base); void listarInv(tipoAnel *base); /* * Remove da lista o elemento apontado por reg ˆ 116 L ISTAS DIN AMICAS */ void libertaReg(tipoAnel *reg); /* * Apaga da lista o registo que cont´ em x */ void apaga(tipoAnel *base,tipoDados x); #endif Ficheiro anel.c /* * * * * * * * * */ Ficheiro: anel.c Autor: Fernando M. Silva Data: 7/11/2000 Conte´ udo: M´ etodos para manipulac ¸˜ ao de um anel com registo separado para a base. /* * Inclui ficheiro com tipo e prot´ otipos */ #include "anel.h" #include "util.h" tipoAnel *novoNo(tipoDados x){ /* * Cria um novo n´ o da lista */ tipoAnel *novo = calloc(1,sizeof(tipoAnel)); if(novo == NULL) Erro("Erro na reserva de mem´ oria"); novo -> dados = x; novo -> seg = novo -> ant = NULL; return novo; } /* Inicializac ¸˜ ao. O anel vazio ´ e constitu´ ıda pelo * registo da base. */ tipoAnel *inicializa(){ A NEL DUPLO COM REGISTO SEPARADO PARA A BASE 117 tipoAnel *aux; tipoDados regMudo; aux = novoNo(regMudo); aux -> seg = aux -> ant = aux; return aux; } /* * Procura x no anel iniciada por base, retornando um apontador * para o registo que contem este valor (ou NULL se n˜ ao existe) * * Este procedimento realiza uma busca exaustiva em todo o anel */ tipoAnel *procura(tipoAnel *base,tipoDados x){ tipoAnel *aux; base -> dados = x; aux = base -> seg; while(!igual(aux -> dados,x)) aux = aux -> seg; return (aux == base ? NULL : aux); } /* * Idˆ entico ao anterior, mas optimizado para aneis * ordenadas (a procura para logo que seja atingido * um elemento igual ou SUPERIOR a x). */ tipoAnel *procuraOrdenado(tipoAnel *base,tipoDados x){ tipoAnel *aux; base -> dados = x; aux = base -> seg; while(menor(aux -> dados,x)) aux = aux -> seg; if((aux != base) && (igual(aux -> dados,x))) return aux; else return NULL; } /* * Insere x antes do registo "depois" */ void insere(tipoAnel *depois,tipoDados x){ tipoAnel *novo = novoNo(x); ˆ 118 L ISTAS DIN AMICAS novo -> seg = depois; novo -> ant = depois -> ant; depois -> ant = novo; novo -> ant -> seg = novo; } /* * Insere x no anel ordenada iniciada por base */ void insereOrdenado(tipoAnel *base,tipoDados x){ tipoAnel *act; base -> dados = x; act = base -> seg; while(menor(act -> dados,x)){ /* * Procura local de inserc ¸˜ ao */ act = act -> seg; } insere(act,x); } /* * Lista todos os elementos da estrutura. */ void listar(tipoAnel *base ){ tipoAnel *aux; aux = base -> seg; while(aux != base){ printf(" -> "); escreveDados(aux -> dados); printf("\n"); aux = aux -> seg; } } /* * Lista todos os elementos da estrutura * por ordem inversa. */ void listarInv(tipoAnel *base ){ tipoAnel *aux; aux = base -> ant; while(aux != base){ printf(" -> "); escreveDados(aux -> dados); A NEL DUPLO COM REGISTO SEPARADO PARA A BASE 119 printf("\n"); aux = aux -> ant; } } /* * Remove do anel o elemento apontado por reg * e liberta a mem´ oria associada. */ void libertaReg(tipoAnel *reg){ reg -> seg -> ant = reg -> ant; reg -> ant -> seg = reg -> seg; free(reg); } /* * Apaga do anel o registo que cont´ em x * Retorna a base, eventualmente alterada */ void apaga(tipoAnel *base,tipoDados x){ tipoAnel *aux; base -> dados = x; aux = base -> seg; while(menor(aux -> dados,x)) aux = aux -> seg; if((aux != base) && igual(aux -> dados,x)) libertaReg(aux); } Ficheiro main.c /* * * * * * * * * * * * * Ficheiro: main.c Autor: Fernando M. Silva Data: 7/11/2000 Conte´ udo: Programa principal simples para teste de estruturas dinˆ amicas ligadas. Neste teste, os dados armazenados na lista s˜ ao fracc ¸˜ oes inteiras, implementadas por um tipo abstracto "tipoDados". Assim: 1. A especificac ¸˜ ao de um racional positivo, ˆ 120 L ISTAS DIN AMICAS * acrescenta o racional ` a lista * 2. A introduc ¸˜ ao de um racional negativo, * especifica que deve ser apagada o seu * sim´ etrico da lista * 3. Com a entrada de um racional com valor 0, * termina o programa * */ #include #include #include "anel.h" int main(){ tipoAnel *base; tipoDados x; base = inicializa(); printf(" Programa para teste de uma lista ordenada de racionais:\n"); printf(" - A introduc ¸˜ ao de um racional positivo conduz ` a sua\n" " inserc ¸˜ ao ordenada na lista.\n"); printf(" - A introduc ¸˜ ao de um racional negativo procura o seu\n" " sim´ etrico na lista e, caso exista, remove-o.\n"); x = leDados("\nIndique o numerador e denominador de um racional:\n" "(dois inteiros na mesma linha)"); while(numerador(x) != 0) { if(valor(x) > 0){ insereOrdenado(base,x); } else{ x = simetrico(x); if(procuraOrdenado(base,x) == NULL){ printf(" ************ Erro: elemento de valor idˆ entico a "); escreveDados(x); printf(" n˜ ao encontrado na lista\n"); } else{ apaga(base,x); } } printf("\nConte´ udo actual do anel (ordem crescente):\n"); listar(base); printf("\nConte´ udo por ordem inversa:\n"); listarInv(base); x = leDados("\nIndique o numerador e denominador de um racional:\n" "(dois inteiros na mesma linha)"); } L ISTAS DE LISTAS 121 base Figura 4.15: Lista de listas. printf("\n Racional com valor 0: fim do programa\n"); exit(0); } 4.10 Listas de listas E´ frequente a implementac¸a˜ o de uma dada aplicac¸a˜ o requerer a utilizac¸a˜ o hier´arquica de diversas listas. Por exemplo, um sistema de arquivo de documentos pode basear-se numa lista de categorias, em que cada categoria tem associada, por sua vez, uma lista de documentos dessa categoria. Eventualmente, cada um destes documentos pode ainda ter associado uma lista espec´ıfica, como uma lista das datas em que o documento foi alterado e as alterac¸o˜ es fundamentais de cada vez. Uma representac¸a˜ o simb´olica de uma lista de listas est´a representada na figura 4.15. Uma lista de listas (ou um anel de an´eis) nada tem de particular do ponto de vista de programac¸a˜ o. Cada lista por si s´o u´ uma estrutura do tipo apontado anteriormente. No entanto, e´ necess´ario ter em atenc¸a˜ o que a lista e as suas sublistas tˆem tipos diferentes e, de acordo com o modelo que temos vindo a desenvolver, cada uma delas precisar´a de um m´etodo espec´ıfico de acesso. Por outras palavras, ser´a necess´ario manter duas func¸o˜ es de listagem, duas func¸o˜ es de inserc¸a˜ o, etc, dado que cada tipo de lista exige um m´etodo espec´ıfico2 . A declarac¸a˜ o de uma lista de lista pode ser efectuada pelo c´odigo typedef struct _tipoSubLista { /* 2 De facto, esta duplicac¸a˜ o de c´odigo pode ser evitada escrevendo c´odigo baseado em apontadores gen´ericos do tipo void*. Esta possibilidade n˜ao ser´a, por agora, abordada ˆ 122 L ISTAS DIN AMICAS tipoDadosSubLista descreve todos os atributos de cada elemento de uma sublista */ tipoDadosSubLista dadosSubLista; struct _tipoSubLista *seg; } tipoSubLista; typedef struct _tipoDadosLista{ /* declarac ¸˜ ao dos campos necess´ arios a cada elemento da lista principal int ... char ... */ tipoSubLista *baseSub; } tipoDadosLista; typedef struct _tipoListaDeListas{ tipoDadosLista dados; struct _tipoListaDeListas *seg; } tipoListaDeListas; Note-se que se adoptou aqui quatro tipos abstractos distintos: o tipo tipoDadosSubLista, que suporta os dados de cada sublista, o tipo tipoSubLista, que suporta as sublistas, o tipo tipoDadosLista que suporta o tipo de dados da lista principal e, finalmente, o tipoListaDeListas, que suporta a lista principal. A manipulac¸a˜ o da lista faz-se pela combinac¸a˜ o das func¸o˜ es (m´etodos) desenvolvidos para cada tipo de objecto. Admita-se, por exemplo, que no programa principal foi declarada uma lista de listas tipoListaDeListas *base; que foi de alguma forma inicializada. Suponha-se agora que e´ necess´ario inserir uma vari´avel x de tipoDados na sublista suportada no elemento da lista principal que tem o valor y. O c´odigo para este efeito seria: tipoListaDeListas *base,*aux; tipoDadosSubLista x; tipoDadosLista y; L ISTAS DE LISTAS 123 /* inicializac ¸˜ ao da lista e dos valores x e y ... */ aux = procuraOrdenadoListaDeListas(base,y); if(aux == NULL) fprintf(stderr,"Erro: valor n˜ ao encontrado na lista principal"); else insereDados(aux -> dados,x); /* ... */ A func¸a˜ o insereDados e´ nova neste contexto, e dever´a ser um m´etodo espec´ıfico de tipoDadosLista. A sua estrutura e´ muito simples, e a sua existˆencia deve-se apenas a` necessidade de manter uma realizac¸a˜ o correcta da abstracc¸a˜ o de dados. Esta func¸a˜ o seria void insereDados(tipoDadosLista dados,tipoDadosSubLista x){ insereDadosSubLista(dados.baseSub,x); } sendo a func¸a˜ o insereDadosSubLista uma func¸a˜ o convencional de inserc¸a˜ o. Cap´ıtulo 5 ˜ Conclusoes O C e´ provavelmente a mais flex´ıvel das linguagens de programac¸a˜ o de alto-n´ıvel, mas apresenta uma relativa complexidade sint´actica. Uma das maiores dificuldades na abordagem do C numa disciplina introdut´oria de programac¸a˜ o e´ a necessidade de introduzir os conceitos de enderec¸o de mem´oria, apontador e mem´oria dinˆamica. Neste texto introduziu-se a noc¸a˜ o de apontador e discutiu-se o problema da manipulac¸a˜ o de estruturas de dados dinˆamicas em C. Apesar da introduc¸a˜ o de diversas estruturas de dados, o primeiro objectivo deste texto foi, sobretudo, o de tentar explicar os mecanismos essenciais de manipulac¸a˜ o de apontadores e gest˜ao de mem´oria dinˆamica em C, utilizando-se algumas estruturas de dados simples para exemplificar estes mecanismos. O aprofundamento destes temas tem o seu seguimento natural numa disciplina espec´ıfica de Algoritmos e Estruturas de Dados, ou em textos espec´ıficos de algoritmia, como (Sedgwick, 1990; Cormen e outros, 1990; Knuth, 1973). Bibliografia Cormen, T. H., Leiserson, C. E., e Rivest, R. L. (1990). Introduction to Algorithms. MIT Press/McGraw-Hill. Kernighan, B. e Ritchie, D. (1978). The C Programming Language. Prentice-Hall. Knuth, D. E. (1973). Fundamental Algorithms, volume 1 of The Art of Computer Programming, section 1.2, p´aginas 10–119. Addison-Wesley, Reading, Massachusetts, second edition. Martins, J. P. (1989). Introduction to Computer Science Using Pascal. Wadsworth Publishing Co., Belmont, California. Ritchie, D. e Thmompson, K. (1974). The unix time sharing system. Commun ACM, p´aginas 365–375. Sedgwick (1990). Algorithms in C. Addison Wesley. B IBLIOGRAFIA 129 ˆ Apendice A Programa de teste que valida a consistˆencia das atribuic¸o˜ es da tabela 2.1, apresentada na secc¸a˜ o 2.6.4. Note-se que o facto de o programa compilar sem erros garante a consistˆencia dos tipos nas atribuic¸o˜ es realizadas. Nos processadores da fam´ılia Intel, cada enderec¸o de mem´oria especifica apenas um byte, apesar de, nos processadores 486 e seguintes, cada operac¸a˜ o de acesso a` mem´oria se realizar em palavras de 4 bytes (32 bits). De forma a obter-se valores equivalentes ao do modelo de mem´oria usado nos exemplos, realiza-se uma divis˜ao por quatro e ajusta-se o enderec¸o escrito no monitor de forma a que o elemento x[0][0] surja com o valor 1001. #include float x[3][2] = {{1.0,2.0},{3.0,4.0}, {5.0,6.0}}; void escreveEndereco(int n){ n = (n - (int) x)/4 + 1001; printf("endereco = %d\n",n); } int main(){ float (*pv3)[2]; float *pf; float f; } pv3 = x; pv3 = x + 1; escreveEndereco((int) pv3); escreveEndereco((int)pv3); pf = *(x+1); pf = *(x+2)+1; f = *(*(x+2)+1); escreveEndereco((int)pf); escreveEndereco((int)pf); printf("%4.1f\n",f); pf = *x; escreveEndereco((int)pf); f = **x; f = *(*x+1); printf("%4.1f\n",f); printf("%4.1f\n",f); pf = x[1]; return 0; escreveEndereco((int)pf);