Вы находитесь на странице: 1из 139

Apontadores e Estruturas de Dados Din micas em C a

Fernando Mira da Silva

Departamento de Engenharia Electrot cnica e de Computadores e Instituto Superior T cnico e Novembro de 2002

Resumo O C e provavelmente a mais exvel das linguagens de programacao de alto-nvel, mas ap resenta uma relativa complexidade sint ctica. Uma das maiores diculdades na abordagem do a C numa disciplina de introdut ria de programacao e a necessidade de introduzir os conceitos de o endereco de mem ria, apontador e mem ria din mica. o o a ` Este texto foi preparado para apoio a disciplina de Introducao a Programacao da Licenciatura ` em Engenharia Electrot cnica e Computadores do Instituto Superior T cnico. Este texto tenta e e focar de modo sistem tico alguns dos t picos que maiores d vidas suscita nas abordagens iniciais a o u da linguagem: apontadores e estruturas de dados din micas. Assim, embora se pressuponha o a conhecimentos dos elementos b sicas da linguagem C por parte do leitor nomeadamente, os a tipos de dados elementares e as estruturas de controlo o texto e mantido ao nvel elementar de uma disciplina introdut ria de inform tica. o a Na apresentacao das estruturas de dados consideradas, que incluem pilhas, las, listas e an is, introduz-se de forma natural a nocao de abstraccao de dados, e os princpios essenciais e de estruturacao e modularidade baseados neste paradigma de programacao. Para o programador experiente em C, alguns dos exemplos de c digo poder o parecer pouco o a optimizados. Trata-se de uma opcao premeditada que tenta beneciar a clareza e a simplicidade algortmica, ainda que em alguns casos esta opcao possa sacricar ligeiramente a eci ncia do e c digo apresentado. Pensamos, no entanto, que esta e a opcao correcta numa abordagem introo dut ria da programacao. o

ndice I

1 Introducao

2 Apontadores 2.1 2.2 2.3 2.4 Motivacao para os apontadores em C . . . . . . . . . . . . . . . . . . . . . . . . Modelos de mem ria em C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . o Apontadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Funcoes e passagem por refer ncia . . . . . . . . . . . . . . . . . . . . . . . . . e 2.4.1 2.4.2 2.5 Passagem por refer ncia . . . . . . . . . . . . . . . . . . . . . . . . . . e Erros frequentes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

5 5 5 7 11 11 16 18 18 22 25 26 28 28

Vectores e apontadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.5.1 2.5.2 2.5.3 2.5.4 Declaracao de vectores . . . . . . . . . . . . . . . . . . . . . . . . . . . Aritm tica de apontadores . . . . . . . . . . . . . . . . . . . . . . . . . e Indices e apontadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . Vectores como argumentos de funcoes . . . . . . . . . . . . . . . . . . .

2.6

Matrizes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.6.1 Declaracao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

IV

INDICE

2.6.2 2.6.3 2.6.4 2.7

Matrizes como argumento de funcoes . . . . . . . . . . . . . . . . . . . Matrizes e vectores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Matrizes e apontadores . . . . . . . . . . . . . . . . . . . . . . . . . . .

29 31 34 38

Generalizacao para mais do que duas dimens es . . . . . . . . . . . . . . . . . . o

Vectores e mem ria din mica o a 3.1 3.2 3.3 3.4 Introducao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Vectores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Vectores din micos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . a Gest o da mem ria din mica . . . . . . . . . . . . . . . . . . . . . . . . . . . . a o a 3.4.1 3.4.2 3.4.3 3.5 Vericacao da reserva de mem ria . . . . . . . . . . . . . . . . . . . . . o Outras funcoes de gest o de mem ria din mica . . . . . . . . . . . . . . a o a Garbbage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

41 41 43 46 48 48 50 52 55 55 55 56 58

Criacao din mica de matrizes . . . . . . . . . . . . . . . . . . . . . . . . . . . . a 3.5.1 3.5.2 3.5.3 3.5.4 Introducao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Matrizes est ticas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . a Matrizes din micas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . a Vectores de apontadores e matrizes . . . . . . . . . . . . . . . . . . . .

Listas din micas a 4.1 4.2 Introducao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Abstraccao de dados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

61 61 62

INDICE

4.3 4.4 4.5

Listas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Listas din micas: listar elementos . . . . . . . . . . . . . . . . . . . . . . . . . a Listas: pilhas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.5.1 4.5.2 4.5.3 4.5.4 4.5.5 4.5.6 4.5.7 Introducao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Declaracao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Inicializacao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Sobreposicao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Remocao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Teste . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Exemplo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

63 66 68 68 70 70 71 72 72 73 80 80 80 81 82 83 83 84 87 87 88

4.6

Listas: las . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.6.1 4.6.2 4.6.3 4.6.4 4.6.5 4.6.6 4.6.7 Introducao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Declaracao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Inicializacao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Insercao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Remocao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Teste . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Exemplo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

4.7

Listas ordenadas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.7.1 4.7.2 Introducao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Declaracao e inicializacao . . . . . . . . . . . . . . . . . . . . . . . . .

VI

INDICE

4.7.3 4.7.4 4.7.5 4.7.6 4.7.7 4.7.8 4.8

Listagem ordenada . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Procura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Abstraccao de dados e m todos de teste . . . . . . . . . . . . . . . . . . e Insercao ordenada . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Remocao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Exemplo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

89 89 91 92 95 96

Variantes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107 4.8.1 4.8.2 4.8.3 4.8.4 Introducao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107 Listas com registo separado para a base . . . . . . . . . . . . . . . . . . 107 Listas duplamente ligadas . . . . . . . . . . . . . . . . . . . . . . . . . 108 Aneis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109

4.9

Anel duplo com registo separado para a base . . . . . . . . . . . . . . . . . . . . 109 4.9.1 4.9.2 4.9.3 4.9.4 4.9.5 4.9.6 4.9.7 4.9.8 Introducao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109 Declaracao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110 Inicializacao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110 Listagem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111 Procura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111 Insercao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112 Remocao . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113 Exemplo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114

4.10 Listas de listas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121

INDICE

VII

Conclus es o

125

Bibliograa

127

Captulo 1 Introducao

At ao aparecimento da linguagem C, as linguagens de alto-nvel tinham por objectivo dise tanciar o programador do hardware especco de trabalho. Pretendia-se, deste modo, que o pro gramador focasse a sua actividade na solucao conceptual e algortmica do problema e, simultane amente, que o c digo nal fosse independente do hardware e, como tal, facilmente port vel entre o a diferentes plataformas. Este princpio te rico fundamental, ainda hoje correcto em muitas areas de aplicacao de soft o ware, 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 crticas do c digo ou, em outros casos, por ser conveniente ou desej vel explorar deter o a minadas facilidades oferecidas pelas instrucoes do processador que n o estavam directamente a disponveis na linguagem de alto-nvel. Nestas situacoes, a unica alternativa era a programacao directa em linguagem m quina destes blocos de c digo, opcao que implicava a revis o do software a o a em cada alteracao ou evolucao de hardware. Por raz es semelhantes, quer o sistema operativo quer ferramentas de sistema como como piladores, gestores de cheiros ou monitores de sistema eram consideradas aplicacoes que, por requisitos de eci ncia do c digo, eram incompatveis com linguagens de alto-nvel. O mesmo e o sucedia com todos os programas que necessitavam, de alguma forma, de controlar directamente dispositivos de hardware. Como corol rio, estas aplicacoes eram tradicionalmente escritas totala mente em linguagem m quina, implicando um enorme esforco de desenvolvimento e manutencao a em cada evolucao 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 xima da linguagem m quina, mas que facilitava exo a traordinariamente a programacao de baixo-nvel. O B adoptava algumas estruturas decisionais e ciclos comuns a linguagens de alto-nvel e, simultaneamente, disponibilizava um conjunto de fa cilidades simples, geralmente s acessveis em linguagem m quina, como o acesso a enderecos de o a mem ria e a m todos de enderecamento indirecto. De facto, os mecanismos de acesso a vari veis o e a e estruturas de dados previstos no B cobriam a esmagadora maioria das necessidades dos progra` madores quando anteriormente eram obrigados a recorrer a linguagem m quina. Ora estes mecana ismos, embora obviamente dependentes do hardware, obedeciam na sua generalidade ao modelo de mem ria previsto na arquitectura de Von Neuman, o qual, nos seus princpios essenciais, est na o a base da maioria das plataformas computacionais desde os anos 50 at aos nossos dias. Foi assim e surgiu a ideia da possibilidade de desenvolvimento de uma linguagem de alto nvel, independente do hardware, que permitisse simultaneamente um acesso exvel aos modos de enderecamento e facilidades disponveis ao nvel da linguagem m quina da maioria dos processadores. E assim a que, em 1983, e inventada a linguagem C (Kernighan e Ritchie, 1978), a qual permitiu a re-escrita de 90% do n cleo do sistema operativo Unix em alto-nvel. u Para al m da manipulacao directa de enderecos de mem ria, uma das facilidades introduzida e o pela linguagem C foi a incorporacao de mecanismos para gest o de mem ria din mica. O conceito a o a de mem ria din mica permite que um programa ajuste de modo exvel a dimens o da mem ria o a a o que utiliza de acordo com as suas necessidades efectivas. Por exemplo, um mesmo programa de processamento de texto pode ocupar pouca mem ria se estiver a tratar um documento de pequena o dimens o, ou ocupar um volume mais signicativo no caso de um documento de maior n mero de a u p ginas. a A invencao da linguagem C (Kernighan e Ritchie, 1978) permitiu a re-escrita de 90% do n cleo do sistema operativo Unix em linguagem de alto-nvel. A possibilidade de manipular u enderecos de mem ria permitiu ainda a implementacao eciente em linguagem de alto nvel de o muitos algoritmos e estruturas de dados at ent o geralmente escritos em linguagem Assembler e a (Knuth, 1973). Neste texto apresenta-se uma introducao aos apontadores e mem ria din mica na linguagem o a de programacao C. Para exemplicar estes conceitos, s o introduzidas algumas estruturas de dados a din micas simples, como pilhas, las, listas e aneis. Durante a exposicao, introduz-se de forma a natural a nocao de abstraccao de dados, e os princpios essenciais de estruturacao e modularidade baseados neste paradigma de programacao. Deste modo, a nocao de abstraccao de dados n o e in a troduzida formalmente num captulo aut nomo, mas sim quando se instroduzem listas din micas, o a altura em que o conceito e fundamental para justicar a estrutura adoptada. Esta abordagem, embora pouco convencional, deriva da nossa experi ncia na doc ncia da disciplina de Introducao a e e ` Programacao durante v rios anos no IST, tendo beneciado das crticas e sugest es de diversas a o

geracoes de alunos. ` Tenta-se neste texto dar-se primazia a clareza algortmica e legibilidade do c digo, ainda o que em alguns casos esta opcao possa sacricar pontualmente a eci ncia do c digo produzido. e o Considera-se, no entanto, que e esta a abordagem mais adequada numa disciplina de Introducao ` a Programacao. Por outro lado, a maioria dos compiladores recentes incluem processos de optimizacao que dispensam a utilizacao explcita dos mecanismos de optimizacao previstos origi nalmente na linguagem C.

Captulo 2 Apontadores

2.1 2.2

Motivacao para os apontadores em C Modelos de memoria em C

` Embora os mecanismos de enderecamento e acesso a mem ria tenham sofrido v rias o a evolucoes nas ultimas d cadas e sejam evidentemente dependentes do hardware, o C requer apenas e hip teses muito simples relativamente ao modelo de mem ria do processador. o o A mem ria de um computador encontra-se organizada em c lulas ou palavras de mem ria o e o individuais. Cada palavra de mem ria e referenciada por um endereco e armazena um dado o conte do. Designa-se por n mero de bits da arquitectura o n mero m ximo de bits que um u u u a ` processador e capaz de ler num unico acesso a mem ria. Cada palavra de mem ria de refer ncia o o e de um processador tem em geral um n mero de bits id ntico ao n mero de bits da arquitectura1 . u e u Admita-se que num dado programa em C declara, entre outras as vari veis i,j,k com o tipo a int, a vari vel f com o tipo float e uma vari vel d do tipo double, n o necessariamente a a a por esta ordem. Admita-se que ap s a declaracao destas vari veis s o realizadas as seguintes o a a atribuicoes:

i = 2450; j = 11331; k = 113; f = 225.345; d = 22.5E+145;


A complexidade dos modos de enderecamento dos processadores actuais conduz a que a denicao e os modelos aqui apresentados pequem por uma simplicidade excessiva, mas s o sucientes para os objectivos propostos. a
1

6 A PONTADORES

Endereo

Contedo

Varivel

. . .
1001 1002 1003 1004 1005 1006 1007 1008 1009

. . .
??? 2450 ??? 225.345 11331 113 22.5E145
(double, 64bits)

. . .
i

f j k d

???

. . .

. . .

Figura 2.1: Modelo de mem ria em C (exemplo). o Um exemplo esquem tico de um modelo mem ria correspondente a esta conguracao encontra-se a o representado na gura 2.1. Admite-se aqui que a palavra de mem ria e o tipo inteiro s o repreo a ` sentados por 32 bits. De acordo com a gura, durante a compilacao foram atribudos as vari veis a ` i,j,k,f os enderecos 1002,1005, 1006 e 1004, respectivamente, enquanto que a vari vel d foi a 2 . Note-se que, com excepcao do tipo double, cada vari vel tem uma atribudo o endereco 1007 a representacao interna de 32 bits, ocupando por isso apenas um endereco de mem ria. A vari vel o a d, de tipo double, tem uma representacao interna de 64 bits, exigindo por isso duas palavras de mem ria: os enderecos 1007 e 1008. Na gura 2.1 encontram-se representados outras posicoes o de mem ria, provavelmente ocupadas por outras vari veis, e cujo conte do e desconhecido do o a u programador.
Estritamente falando, a cada vari vel local, e apenas atribudo um endereco relativo durante a compilacao, sendo o a endereco nal xado durante a execucao da funcao ou activacao do bloco de instrucoes em que a declaracao e realizada. Este tema voltar a ser abordado na seccao 3.2. a
2

A PONTADORES 7

2.3

Apontadores

Um apontador em C e uma vari vel cujo conte do e um endereco de outra posicao de a u mem ria. A declaracao de vari veis do tipo apontador pode ser construda a partir de qualquer o a tipo denido anteriormente, e deve especicar o tipo de vari vel referenciada pelo apontador. a A declaracao de uma vari vel do tipo apontador realiza-se colocando um * antes do nome a da vari vel. Assim, na declaracao a double *pd; int i; int *pi; float f; int j,k; double d; int m,*pi2; double *pd2,d2; as vari veis i,j,k,m s o do tipo int, f e do tipo float, e d,d2 s o do tipo double. Al m a a a e destas vari veis de tipos elementares, s o declaradas as vari veis pi, pi2 do tipo apontador a a a para inteiro, enquanto que pd e pd2 s o do tipo apontador para double. a Admita-se agora se realiza a seguinte sequ ncia de atribuicoes: e i = 2450; f = 225.345; k = 113; d = 22.5E145; m = 9800; Ap s estas instrucoes, o mapa de mem ria poderia ser o representado na gura 2.2, situacao o o A. Sublinhe-se que as vari veis de tipo apontador apenas cont m um endereco de mem ria, ina e o dependentemente do tipo referenciado. Desta forma, todas as vari veis de tipo apontador t m a e uma representacao igual em mem ria, normalmente de dimens o id ntica ao do tipo inteiro (uma o a e palavra de mem ria). Note-se que o conte do dos apontadores, tal como o de algumas vari veis o u a elementares, ainda n o foi inicializado, pelo que surgem representados com ???. E importante a compreender que cada uma destas vari veis tem de facto um conte do arbitr rio, resultante da a u a operacao anterior do computador. Claro que estes valores, n o tido sido ainda inicializados pelo a programa, s o desconhecidos do programador, mas esta situacao n o deve ser confundida com a a a aus ncia de conte do, erro de raciocnio frequentemente cometido por programadores principie u antes. As vari veis de tipo elementar podem ser inicializadas pela atribuicao directa de valores cona stantes. A mesma t cnica pode ser utilizada para a inicializacao de apontadores, embora este e

8 A PONTADORES

Endereo

Contedo

Varivel

Endereo

Contedo

Varivel

Endereo

Contedo

Varivel

. . .
1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013

. . .
??? 2450 ??? 225.345 ??? 113 22.5E145
(double, 64bits)

. . .
pd i pi f j k d

. . .
1001 1002 1003 1004 1005 1006 1007 1008

. . .
1007 2450 1006 225.345 ??? 113 22.5E145
(double, 64bits)

. . .
pd i pi f j k d

. . .
1001 1002 1003 1004 1005 1006 1007 1008

. . .
1007 2450 1006 225.345 ??? 113 22.5E145
(double, 64bits)

. . .
pd i pi f j k d

9800 ??? ??? ???


(double, 64bits)

m pi2 pd2 d2

1009 1010 1011 1012 1013

9800 ??? ??? ???


(double, 64bits)

m pi2 pd2 d2

1009 1010 1011 1012 1013

9800 1006 1007 ???


(double, 64bits)

m pi2 pd2 d2

. . .
Endereo

. . .
A
Contedo Varivel

. . .
Endereo

. . .
B
Contedo Varivel

. . .
Endereo

. . .
C
Contedo Varivel

. . .
1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013

. . .
1007 2450 1006 225.345 113 113 22.5E145
(double, 64bits)

. . .
pd i pi f j k d

. . .
1001 1002 1003 1004 1005 1006 1007 1008

. . .
1007 2450 1006 225.345 113 113 22.5E145
(double, 64bits)

. . .
pd i pi f j k d

. . .
5001 5002 5003 5004 5005 5006 5007 5008

. . .
3 4 5001 5003 5004 10 46 5006 5003

. . .
i1 i2 pi1 pi2 pi3 k1 k2 pk1 pk2

9800 1006 1007 22.5E145


(double, 64bits)

m pi2 pd2 d2

1009 1010 1011 1012 1013

24500 1002 1012 22.5E144


(double, 64bits)

m pi2 pd2 d2

5009 5010 5011 5012 5013

. . .

. . .
D

. . .

. . .
E

. . .

. . .
F

Figura 2.2: Mapa de mem ria ap s diferentes sequ ncias de atribuicao (explicacao no texto). o o e

A PONTADORES 9

m todo raramente seja utilizado: em geral, o programador n o sabe quais os enderecos de mem ria e a o disponveis no sistema, e a manipulacao directa de enderecos absolutos tem reduzidas aplicacoes 3 Assim, A inicializacao de apontadores e geralmente realizada por outros m todos, entre pr ticas. a e os quais a utilizacao do endereco de outras vari veis j declaradas. Este endereco pode ser obtido a a em C aplicando o operador & a uma vari vel. Na continuacao do exemplo anterior, admita-se que a era realizada agora a seguinte sequ ncia de atribuicoes: e pd = &d; pi = &k;

Ap s esta fase, o mapa de mem ria seria o representado na gura 2.2, situacao B, onde as o o vari veis pd e pi surgem agora preenchidas com os enderecos de d e k. Como seria de esa perar, a consist ncia da atribuicao exige que a vari vel referenciada e o tipo do apontador sejam e a compatveis. Por exemplo, a atribuicao pd=&k e incorrecta, atendendo a que k e do tipo int e pd est declarada como um apontador para double. a A partir do momento em que os apontadores s o inicializados, o seu conte do pode ser a u copiado e atribudo a outras vari veis, desde que os tipos ainda sejam compatveis. Assim, as a atribuicoes pd2 = pd; pi2 = pi;

` o conduziriam apenas a c pia dos enderecos guardados em pd e pi para pd2 e pi2. Ap s esta o sequ ncia, a situacao seria a representada na gura 2.2, caso C. e Uma vez estabelecido um mecanismo para preecher o conte do de um apontador, coloca-se a u quest o de como utilizar este apontador de modo a aceder aos dados apontados. Este mecanismo e a suportado no chamado sistema de enderecamento indirecto, o qual e realizado em C pelo operador *. Assim, a atribuicao j = *pi; tem o signicado consultar o endereco guardado em pi (1006) e, seguidamente, ler o inteiro cujo endereco e 1006 e colocar o resultado (113) na vari vel j. Deste a modo, ap s as atribuicoes o j = *pi; d2 = *pd;

a situacao seria a representada na gura 2.2, caso D. Referiu-se anteriomente que um apontador e apenas um endereco de mem ria. Assim, poder o 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 ncia do apontador do tipo apontado n o tem a ver com e a a estrutura do apontador em si, mas sim com o facto de esta informacao ser indispens vel para a desreferenciar (aceder) correctamente o valor enderecado. Por exemplo, na express o d2 = *pd, a e o facto de pd ser um apontador para double que permite ao compilador saber que o valor referenciado ocupa n o apenas uma, mas duas palavras de mem ria e qual a sua representacao. S a o o na posse desta informacao e possvel efectuar a atribuicao correcta a d2. Outras sequ ncias de atribuicao seriam possveis. Por exemplo, ap s a sequ ncia e o e pd2 = &d2; *pd2 = *pd1 / 10.0; pi2 = &i; m= *pi2 * 10; a situacao seria a representada no caso E. Note-se que aqui o operador * foi utilizado no lado esquerdo da atribuicao. Assim, por exemplo, a atribuicao *pd2 = *pd1 e interpretada como ler o real cujo endereco e especicado pelo conte do de pd1 e colocar o resultado no endereco u especicado por pd2. Embora at aqui tenham sido considerados apontadores para tipos elementares, um apontador e pode tamb m enderecar um outro apontador. Assim, na declaracao e 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 inicializacao destes apontadores realiza-se segundo as mesmas regras seguidas para os apontadores simples, sendo apenas necess rio ter em atencao, o nvel correcto de enderecamento a indirecto m ltiplo. Assim, se ap s a declaracao anterior, fosse executada a sequ ncia de instrucoes u o e i1 = 3; i2 = 4; pi1 = &i1; pi2 = &pi1; pi3 = &pi2; k1 = 10; pk1 = &k1; pk2 = pi2; k2 = i1 * ***pi3 + *pi1 + i2 + **pk2 * *pk1; a situacao nal poderia ser a representada na gura 2.2, caso F. Em apontadores m tilpos a possibilidade de desreferenciar um apontador continua a ser deu pendente do tipo apontado. Deste modo, um apontador para um inteiro e um apontador para um apontador para um inteiro s o tipos claramente distintos e cujos conte dos n o podem ser mua u a tuamente trocados ou atribudos. Por outro lado, os nveis de indireccao devem ser claramente

F UNC OES E PASSAGEM POR REFER E NCIA 11

respeitados. Assim, a atribuicao i1 = *pk2; no exemplo precedente seria incorrecta, j que i1 a 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 vel nunca deve ser utilizado antes de esta ser inicializada a explicitamente pelo programa. Com efeito, no incio de um programa, as vari veis t m um valor a e arbitr rio desconhecido. Por maioria de raz o, o mesmo princpio deve ser escrupulosamente a a seguido na manipulacao de apontadores. Suponha-se, por exemplo que, no incio de um programa, s o includas a declaracao e as instrucoes seguintes: a

int *p,k; k = 4; *p = k*2;

Aqui, a segunda atribuicao especica que o dobro de k (valor 8) deve ser colocado no endereco especicado pelo conte do da vari vel p. No entanto, dado que p n o foi inicializada, o seu u a a conte do e arbitr rio. Com elevada probabilidade, o seu valor corresponde a um endereco de u a mem ria inexistente ou inv lido. No entanto, o C n o realiza qualquer juzo de valor sobre esta o a a situacao e, tendo sido instrudo para colocar 8 no endereco indicado por p tentar executar esta a operacao. A tentativa de escrita numa posicao de mem ria inv lida ou protegida conduzir ou ao o a a compromisso da integridade do sistema operativo se o espaco de mem ria n o for conveniente o a mente protegido, como e o caso do DOS. Se o sistema tiver um modo protegido, como o Unix ou o Windows NT, esta situacao pode originar um erro de execucao, devido a uma violacao de mem ria o ` detectada pelo sistema operativo. Os erros de execucao conduzem a interrupcao imediata, em erro, do programa. Sublinhe-se que nas guras desta seccao foram utilizados enderecos especcos nos aponta dores de modo a melhor demonstrar e explicar o mecanismo de funcionamento dos mecanismos de apontadores e indireccao em C. No entanto, na pr tica, o programador n o necessita de conhecer o a a valor absoluto dos apontadores, sendo suciente a manipulacao indirecta do seu conte do atrav s u e dos mecanismos de referenciacao e desreferenciacao descritos.

2.4
2.4.1

Funcoes e passagem por referencia


Passagem por referencia

Em C, na chamada de uma funcao, os par metros formais da funcao recebem sempre uma a c pia dos valores dos par metros actuais. Este mecanismo de passagem de argumentos e desigo a

12 A PONTADORES

nado passagem por valor (Martins, 1989) e est subjacente a todas as chamadas de funcoes em a C. Esta situacao e adequada se se pretender apenas que os argumentos transmitam informacao do m dulo que chama para dentro da funcao. Dado que uma funcao pode tamb m retornar um valor, o e este mecanismo b sico e tamb m adequado quando a funcao recebe v rios valores de entrada e a e a tem apenas um valor de sada. Por exemplo, uma funcao que determina o maior de tr s inteiros e pode ser escrita como

int maior_3(int i1,int i2, int i3){ /* * Funco que determina o maior de trs inteiros a e * */ if((i1 > i2) && (i1 > i3)) return i1; else return (i2 > i3 ? i2 : i3); }

Existem no entanto situacoes em que se pretende que uma funcao devolva mais do que um valor. Uma situacao possvel seria uma variante da funcao maior_3 em que se pretendesse determinar os dois maiores valores, e n o apenas o maior. Outro caso tpico e o de uma funcao a para trocar o valor de duas vari veis entre si. Numa primeira tentativa, poderia haver a tentacao de a escrever esta funcao como

#include <stdio.h> #include <stdlib.h> void trocaMal(int x,int y){ /* * ERRADO */ int aux; aux = x; x = y; y = aux; } int main(){ int a,b; printf("Indique dois nmeros: "); u scanf("%d %d",&a,&b); trocaMal(&a,&b); printf("a = %d, b= %d\n",a,b);

F UNC OES E PASSAGEM POR REFER E NCIA 13

exit(0); } No entanto, este programa n o funciona: o mecanismo de passagem por valor implica que a a funcao troca opera correctamente sobre as vari veis locais x e y, trocando o seu valor, mas estas a vari veis n o s o mais do que c pias das vari veis a e b que, como tal, se mant m inalteradas. a a a o a e Na gura 2.3 representa-se a evolucao do mapa de mem ria e do conte do das vari veis durante a o u a ` chamada a funcao trocaMal. Este aparente problema pode ser resolvido pela utilizacao criteriosa de apontadores. A funcao troca especicada anteriormente pode ser correctamente escrita como se segue: #include <stdio.h> #include <stdlib.h> void troca(int *x,int *y){ /* * Funco que troca dois argumentos a */ int aux; aux = *x; *x = *y; *y = aux; } int main(){ int a,b; printf("Indique dois nmeros: "); u scanf("%d %d",&a,&b); troca(&a,&b); printf("a = %d, b= %d\n",a,b); exit(0); } ` Tal como pode ser observado, a solucao adoptada consiste em passar a funcao n o o valor das a vari veis a e b, mas sim os seus enderecos. Embora estes enderecos sejam passados por valor (ou a ` seja, a funcao recebe uma c pia destes valores), o endereco permite a funcao o conhecimento da o posicao das vari veis a b em mem ria e, deste modo, permite a manipulacao do seu conte do por a o u meio de um enderecamento indirecto. Um hipot tico exemplo de um mapa mem ria associado ao funcionamento do programa e o troca encontra-se representado na gura 2.4. Admita-se que a declaracao de vari veis no pro a

14 A PONTADORES

Endereo

Contedo

Varivel

Endereo

Contedo

Varivel

. . . . . .

. . . . . .

. . . . . .

. . .
2131 Variveis da funao 2132 troca() 2133

. . .
8 2

. . .
x y aux

. . . . . .
Variveis 2135 do bloco main() 2136

. . . . . .
8 2

. . . . . .
a b

. . .
8 2

. . .
a b

. . .
Variveis 2135 do bloco main() 2136

. . .
A
Endereo

. . .
Contedo

. . .
Varivel

. . .
B
Endereo

. . .
Contedo

. . .
Varivel

. . .
2131 Variveis da funao 2132 troca() 2133

. . .
2 8 8 2 8

. . .
x y aux

. . . . . .

. . . . . .

. . . . . .

. . . . . .
Variveis 2135 do bloco main() 2136

. . . . . .
8 2

. . . . . .
a b

. . .
Variveis 2135 do bloco main() 2136

. . .
8 2

. . .
a b

. . .
C

. . .

. . .

. . .
D

. . .

. . .

Figura 2.3: Mapa de mem ria durante as diferentes fase de execucao de um programa que utiliza o ` a funcao trocaMal. A - antes da chamada a funcao, B - no incio de troca, C - no nal de troca, D - ap s o regresso ao programa principal. O mecanismo de passagem por valor conduz o a que os valores do programa principal n o sejam alterados. a

F UNC OES E PASSAGEM POR REFER E NCIA 15

Endereo

Contedo

Varivel

Endereo

Contedo

Varivel

. . . . . .

. . . . . .

. . . . . .

. . .
2131 Variveis da funao 2132 troca() 2133

. . .
2135 2136

. . .
x y aux

. . . . . .
Variveis 2135 do bloco main() 2136

. . . . . .
8 2

. . . . . .
a b

. . .
8 2

. . .
a b

. . .
Variveis 2135 do bloco main() 2136

. . .
A
Endereo

. . .
Contedo

. . .
Varivel

. . .
B
Endereo

. . .
Contedo

. . .
Varivel

. . .
2131 Variveis da funao 2132 troca() 2133

. . .
2135 2136 8

. . .
x y aux

. . . . . .

. . . . . .

. . . . . .

. . . . . .
Variveis 2135 do bloco main() 2136

. . . . . .
2 8 8 2

. . . . . .
a b

. . .
Variveis 2135 do bloco main() 2136

. . .
2 3

. . .
a b

. . .
C

. . .

. . .

. . .
D

. . .

. . .

Figura 2.4: Mapa de mem ria durante as diferentes fase de execucao do programa que utiliza a o ` funcao troca. A - antes da chamada a funcao, B - no incio de troca C - no nal de troca, D ap s o regresso ao programa principal. A passagem de apontadores para as vari veis do programa o a principal (passagem por refer ncia) permite que a funcao altere as vari veis do programa principal. e a

16 A PONTADORES

` grama principal atribuiu as vari veis A e B os enderecos 2135 e 2136, e que estas foram iniciala izadas pelo utilizador com os valores 8 e 2, respectivamente. A situacao imediatamente antes da chamada da funcao troca encontra-se representada em A. Durante a chamada da funcao, realiza ` se a activacao das vari veis x, y e aux, locais a funcao, eventualmente numa zona de mem ria a o afastada daquela onde se encontram as vari veis a e b, sendo as duas primeiras destas vari veis a a inicializadas com os enderecos de a e b (situacao B). Atrav s do enderecamento indirecto atrav s e e das vari veis x e y, s o alterados os valores das vari veis a e b do programa principal, atingido-se a a a a situacao C. Ap s o regresso ao programa principal, as vari veis da funcao troca s o libertadas, o a a atingindo-se a situacao 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 o vari veis locais a funcao, tendo recebido apenas valores correspondentes ao endereco de a a vari veis declaradas no programa principal. No entanto, neste tipo de mecanismo, diz-se tamb m a e que as vari veis a e b foram passadas por refer ncia(Martins, 1989), atendendo a que o seu a e ` endereco (e n o o seu conte do) que foi passadas a funcao. a u Mais genericamente, sempre que e necess rio que uma funcao altere o valor de um ou mais a ` dos seus argumentos, este ou estes dever o ser passados por refer ncia, de forma a ser possvel a a e por este motivo que, funcao modicar o valor das vari veis por um mecanismo de indireccao. E a na chamada da funcao scanf(), todas vari veis a ler s o passados por refer ncia, de modo a ser a a e possvel a esta funcao poder ler e alterar os valores das vari veis do programa principal. a

2.4.2

Erros frequentes

A tentativa de desdobramento do mecanismo de referenciacao de uma vari vel quando uma a dado programa envolve v rios nveis de funcoes e um erro frequente de programadores principia antes ou com reduzida experi ncia de C. Considere-se o seguinte troco de programa, em que se e pretende que a vari vel x do programa principal seja modicada na funcao func2. Neste exema plo, o programador adoptou desnecessariamente uma referenciacao (c lculo do seu endereco) da a vari vel a modicar em cada um dos nveis de chamada da funcao: a /* Utilizaco incorrecta (desnecessria) a a de referncias mltiplas. e u */ void func2(int **p2,int b2){ **p2 = -b2 * b2; } void func1(int *p1,int b1){ b1 = b1 + 1;

F UNC OES E PASSAGEM POR REFER E NCIA 17

func2(&p1,b1); } int main(){ int x; func1(&x,5); return 0; }

Como pode ser facilmente entendido, a referenciacao de uma vari vel uma unica vez e suciente a para que este mesmo endereco possa ser sucessivamente passado entre os v rios nveis de funcoes a e ainda permitir a alteracao da vari vel seja sempre possvel. Assim, embora o programa anterior a u funcione, a referenciacao de p1 na passagem de func1 para func2 e in til: o mecanismo ali adoptado s faria sentido caso se pretendesse que func2 alterasse o conte do da vari vel p1. o u a Como e evidente n o e esse o caso, e o programa anterior correctamente escrito tomaria a seguinte a forma: /* Utilizacao correcta de uma passagem por referncia entre vrios nveis e a de funces. o */ 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 funcao que chama a refer ncia de uma vari vel local a funcao evocada. Este tipo de erro pode e a 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 vel b e local a funcao func e, como tal, e criada quando func e activada e a a sua zona de mem ria libertada quando a funcao termina. Ora o resultado da funcao func e o passada ao programa principal sob a forma do endereco de b. Quando o valor desta vari vel e a lido no programa principal por meio de um enderecamento indirecto na express o x = 2 * *y, a ` a vari vel b j n o est activa, realizando-se por isso um acesso inv lido a posicao de mem ria a a a a a o especicada pelo endereco em y. Com elevada probabilidade, o resultado nal daquela express o a ser incorrecto. a

2.5
2.5.1

Vectores e apontadores
Declaracao de vectores

Um vector em C permite a criacao de uma estrutura com ocorr ncias m ltiplas de uma vari vel e u a de um mesmo tipo. Assim, a declaracao 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 o, indexado entre 0 e 2, inicializados na pr pria declaracao. Um possvel mapa de mem ria a o o correspondente a esta declaracao encontra-se representado na gura 2.5.1. ` Cada elemento individual de um vector pode ser referenciado acrescentando a frente do nome da vari vel o ndice, ou posicao que se pretende aceder, representado por um inteiro entre []. a Para um vector com N posicoes, o ndice de acesso varia entre 0 (primeira posicao) e N 1 ( ltima posicao). Cada elemento de x e y corresponde a uma vari vel de tipo inteiro ou double, u a respectivamente. Deste modo, se se escrever, int x[5] = {123,234,345,456,567};

V ECTORES E APONTADORES 19

Endereo

Contedo

Varivel

. . .
1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011

. . .
123 234 345 456 789 200.0

. . .
x[0] x[1] x[2] x[3] x[4] y[0]

y[1] 200.1 y[2] 200.2

. . .

. . .

` Figura 2.5: Mapa de mem ria correspondente a declaracao de dois vectores o

20 A PONTADORES

double y[3] = {200.0,200.1,200.2}; double a; a = y[1] + x[3];

o conte do nal de a ser o resultado da soma da segunda posicao de y com a quarta posicao de u a x, ou seja 656.1. Dado que cada elemento de um vector e uma vari vel simples, e possvel determinar o seu a endereco. Assim, e possvel 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 situacao representada na gura 2.5.1. Note-se que nas atribuicoes pi = &(x[2]) e pd = &(y[1]) os parenteses poderiam ser omitidos, dado que o operador [] (ndice) tem preced ncia sobre o operador & (endereco de). Assim, aquelas atribuicoes e poderiam ser escritas como pi = &x[2] e pd = &y[1]. Uma das vantagens da utilizacao de vectores e o ndice de acesso poder ser uma vari vel. a Deste modo, inicializacao de um vector de 10 inteiros a 0 pode ser feita pela sequ ncia e

#define NMAX 10 int main(){ int x[NMAX]; int k; for(k = 0; k < NMAX ; k++) x[k] = 0; /* ...*/

Uma utilizacao comum dos vectores e a utilizacao de vectores de caracteres para guardar texto. Por exemplo a declaracao

char texto[18]="Duas palavras";

V ECTORES E APONTADORES 21

Endereo

Contedo

Varivel

. . .
1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013

. . .
123 234 345 456 789 200.0

. . .
x[0] x[1] x[2] x[3] x[4] y[0]

y[1] 200.1 y[2] 200.2

1003 1008

pi pd

. . .

. . .

Figura 2.6: Apontadores e vectores (explicacao no texto).

22 A PONTADORES

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: Utilizacao de vectores de caracteres para armazenar texto. cria um vector de 18 caracteres, e inicia as suas primeiras posicoes com o texto Duas palavras. Uma representacao gr ca deste vector depois de inicializado e apresentada na gura 2.7. a Dado que o texto pode ocupar menos espaco que a totalidade do vector, como sucede neste caso, a ultima posicao ocupada deste vector e assinalada pela colocacao na posicao seguinte ` a ultima do caracter com o c digo ASCII 0, geralmente representado pela constante inteira 0 o ou pelo caracter \0. Note-se que esta ultima representacao n o deve ser confundida com a a representacao do algarismo zero 0, internamente representado por 48, o c digo ASCII de 0. o

2.5.2

Aritmetica de apontadores

E possvel adicionar ou subtrair uma constante inteira de uma vari vel de tipo apontador. Este a tipo de operacao s faz sentido em vectores ou estruturas de dados regulares, em que o avanco ou o recuo de um apontador conduz a um outro elemento do vector (ou estrutura de dados regulares). E da unica e exclusiva responsabilidade do programador garantir que tais operacoes aritm ticas e se mant m dentro dos enderecos v lidos da estrutura de dados apontada. Assim, por exemplo, se e a na sequ ncia do exemplo e da situacao representada na gura 2.5.1 (repetida por conveni ncia na e e gura 2.5.2, A) fossem executadas as instrucoes pi = pi + 2; pd = pd - 1;

a situacao resultante seria a representada na gura 2.5.2, B. No caso do modelo de mem ria adoptado como exemplo, a operacao aritm tica sobre o apono e tador inteiro tem uma correspond ncia directa com a operacao realizada sobre o endereco. No e entanto, o mesmo n o sucede com o apontador para double, onde subtraccao de uma unidade a ao apontador corresponde na realidade a uma reducao de duas unidades no endereco fsico da mem ria. o De facto, a aritm tica de apontadores em C e realizada de forma a que o incremento ou decree mento unit rio corresponda a um avanco ou recuo de uma unidade num vector, independentemente a do tipo dos elementos do vector. O compilador de C garante o escalamento da constante adicionada

V ECTORES E APONTADORES 23

Endereo

Contedo

Varivel

Endereo

Contedo

Varivel

. . .
1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013

. . .
123 234 345 456 789 200.0

. . .
x[0] x[1] x[2] x[3] x[4] y[0]

. . .
1001 1002 1003 1004 1005 1006 1007

. . .
123 234 345 456 789 200.0

. . .
x[0] x[1] x[2] x[3] x[4] y[0]

y[1] 200.1 y[2] 200.2

1008 1009 1010 1011

y[1] 200.1 y[2] 200.2

1003 1008

pi pd

1012 1013

1005 1006

pi pd

. . .

. . .
A

. . .

. . .
B

Figura 2.8: Aritm tica de apontadores (explicacao no texto). e

24 A PONTADORES

ou subtrada de acordo com a dimens o do tipo apontado. Este modo de operacao garante que, a na pr tica, o programador possa realizar operacoes sobre apontadores abstraindo-se do n mero a u efectivo de bytes do elemento apontado. J anteriormente se mencionou que em C o tipo do apontador depende do objecto apontado, de a modo a ser possvel determinar, num enderecamento indirecto, qual o tipo da vari vel referenciada. a A aritm tica de apontadores reforca esta necessidade, dado que o seu incremento ou decremento e exige o conhecimento da dimens o do objecto apontado. a A aritm tica de apontadores dene apenas operacoes relativas de enderecos. Assim, embora e seja possvel adicionar ou subtrair constantes de apontadores, n o faz sentido somar apontadores. a Numa analogia simples, considere-se que os enderecos s o os n meros da porta dos edifcios de a u uma dada rua: faz sentido referir dois n meros depois do pr dio 174 ou tr s n meros antes do u e e u 180, mas n o existe nenuma aplicacao em que faca sentido adicionar dois n meros de porta (174 a u e 180, por exemplo). No entanto, faz sentido referir o n mero de pr dios entre o 174 e o 180: u e de modo equivalente, tamb m em C a subtraccao de apontadores e possvel, desde que sejam do e mesmo tipo. Como e evidente, tal como na adicao, a subtraccao de operadores e convenientemenet escalada pela dimens o do objecto apontado: a 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 ultima nota relativamente aos apontadores de tipo void. O C-ANSI permite a denicao de apontadores gen ricos, cujo tipo e independente do objecto apontado. Um apontador pv deste e tipo manipula um endereco gen rico e pode ser simplesmente declarado por e void *pv;

e encontra utilizacao em situacoes em que se pretende que uma mesma estrutura possa enderecar entidades ou objectos de tipos diferentes. Sempre que possvel, este tipo de situacoes deve ser preferencialmente abordado pela utilizacao de estruturas do tipo union. No entanto, existem ` casos em que tal n o e possvel, obrigando a utilizacao deste tipo de apontadores. E, por exemplo, a o caso das funcoes de gest o de mem ria, que trabalham com apontadores gen ricos (V. seccao a o e 3.3). Note-se, no entanto, que o C n o consegue desreferenciar automaticamente um apontador a

V ECTORES E APONTADORES 25

para void (ou seja, aceder ao conte do apontado por). De igual modo, o desconhecimento da u dimens o do objecto apontado impede que a aritm tica de apontadores seja aplic vel a apontadores a e a deste tipo.

2.5.3 ndices e apontadores I


Dos princpios gerais referidos anteriormente, resulta que atribuicao do 3o elemento de um vector x a uma vari vel a pode ser realizada directamente por a a = x[2]; ou, de forma equivalente, por a = *(&x[0] + 2); sendo o resultado id ntico. Enquanto que no primeiro caso se adopta um mecanismo de e indexacao directa, no segundo caso determina-se um apontador para o primeiro elemento do vector, incrementa-se o apontador de duas unidades para enderecar o 3o elemento e, nalmente, aplica-se o operador * para realizar o enderecamento indirecto. De facto, a segunda express o pode ser simplicada. O C dene que o endereco do primeiro a elemento de um vector pode ser obtido usando simplesmente o nome do vector, sem o operador ` de indexacao ([0]) a frente. Ou seja, no contexto de uma express o em C, &x[0] e equiv a alente 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 o x[k] e sempre equivalente a *(xk)+. Este a facto conduz ao que designamos por regra geral de indexacao em C, que pode ser enunciada pela equival ncia e x[k] <-> *(x + k) Registe-se, como curiosidade, que a regra geral de indexacao 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 utilizacao desta propriedade e geralmente proibido, n o pelo C, mas pelas normas de a boa programacao!

26 A PONTADORES

2.5.4

Vectores como argumentos de funcoes

Para usar um vector como argumento de uma funcao func() basta, no bloco que chama func(), especiar o nome da vari vel que se pretende como argumento. Assim, pode fazer-se, a por exemplo,

int x[10]; /* ... */ func(x); /* ... */

No entanto, e necess rio ter em conta que x, quando usado sem ndice, representa apenas o a endereco da primeira posicao do vector. Como deve ent o ser declarado o argumento formal a correspondente no cabecalho de func? Diversas opcoes existem para realizar esta declaracao. No exemplo seguinte, a funcao 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 funcao e apenas a repeticao da declaracao 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 funcao manipular um vector e suciente conhecer o endereco do primeiro elemento. Dentro da funcao, o modo de aceder ao k- simo elemento do vector e sempre e adicionar k ao endereco da base, independentemente da dimens o do vector. Ou seja, o par metro a a 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 o usa a informacao [NMAX] no par metro formal para aceder a a ` a funcao. Assim, a indicacao explcita da dimens o pode ser omitida, sendo v lido escrever a a

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 o resulta tamb m da funcao n o necessitar de a e a reservar o espaco para o vector: a funcao limita-se a referenciar as c lulas de mem ria reservadas e o no programa principal. Na passagem de vectores como argumento o C utiliza sempre o endereco da primeira posicao, n o existindo nenhum mecanismo previsto que permita passar a totalidade dos elementos do veca ` o tor por valor. Esta limitacao, inerente a pr pria origem do C, tinha por base a justicacao de que a passagem por valor de estruturas de dados de dimens o elevada e pouco eciente, dada a a 4. necessidade de copiar todo o seu conte do u Atendendo a que a, sem inclus o do ndice, especica um apontador para primeiro elemento a 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 ultima forma sugere uma forma alternativa de escrever o corpo da funcao set. Com efeito, para percorrer um vector basta criar um apontador, inicializ -lo com o endereco da primeira a ` posicao do vector e seguidamente increment -lo sucessivamente para aceder as posicoes seguintes. a Esta t cnica pode ser ilustrada escrevendo a funcao e

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 pia do endereco de a no o programa principal, e possvel proceder ao seu incremento na funcao de modo a percorrer todos os elementos do vector. Aqui, a express o *x++ merece um coment rio adicional. Em primeiro a a lugar, o operador ++ tem preced ncia sobre o operador * e, deste modo, o incremento opera sobre e o endereco e n o sobre a posicao de mem ria em si. E este apenas o signicado da preced ncia, a o e
Na vers o original do C, esta limitacao estendia-se ao tratamento de estruturas de dados criadas com a directiva a struct, mas esta limitacao foi levantada pelo C-Ansi
4

28 A PONTADORES

o qual n o deve ser confundido com a forma de funcionamento do operador incremento enquanto a suxo: o suxo estabelece apenas que o conte do de x e utilizado antes da operacao de incremento u se realizar. Ou seja, *x+=k;+ e equivalente a *x=k;x+;+. Um outro exemplo de utilizacao desta t cnica pode ser dado escrevendo uma funcao para e contabilizar o n mero de caracteres usados de uma string. Atendendo a que se sabe que o ultimo u caracter est seguido do c digo ASCII 0, esta funcao pode escrever-se a o int conta(char *s){ int n = 0; while(*s++ != \0) n++; return n; }

De facto, esta funcao e equivalente a funcao strlen, disponvel na biblioteca string.h.

2.6
2.6.1

Matrizes
Declaracao

Na sua forma mais simples, a utilizacao de estruturas multidimensionais em C apenas envolve a declaracao de uma vari vel com v rios ndices, especicando cada um da dimens o pretendida a a a da estrutura. Por exemplo float x[3][2];

declara uma estrutura bidimensional de tr s por dois reais, ocupando um total de seis palavras e de mem ria no modelo de mem ria que temos vindo a usar como refer ncia. E frequente uma o o e estrutura bidimensional ser interpretada como uma matriz, neste exemplo de tr s linhas por duas e colunas. Nos modos de utilizacao mais simples deste tipo de estruturas, o programadador pode abstrair se dos detalhes de implementacao e usar a vari vel bidimensional como uma matriz. Assim, a a inicializacao a zeros da estrutura x pode ser efectuada por float x[3][2]; int k,j;

M ATRIZES 29

Endereo

Contedo

Varivel

. . .
1001 1002 1003 1004 1005 1006

. . .
1.0 2.0 3.0 4.0 5.0 6.0

. . .
x[0][0] x[0][1] x[1][0] x[1][1] x[2][0] x[2][1]

. . .
for(k = 0 ; k < 3 ; k++) for(j = 0 ; j < 2 ; j++) x[k][j] = 0.0;

. . .

` Figura 2.9: Mapa de mem ria correspondente a declaracao de uma estrutura de tr s por dois reais o e

Alternativamente, a inicializacao pode ser feita listando os valores iniciais, sendo apenas necess rio agrupar hierarquicamente as constantes de inicializacao de acordo com as dimens es a o da estrutura: float x[3][2] = {{1.0,2.0},{3.0,4.0}, {5.0,6.0}};

A disposicao em mem ria desta estrutura e representada esquematicamente na gura 2.9. o

2.6.2

Matrizes como argumento de funcoes

O facto de ser possvel omitir a dimens o de um vector na declaracao do par metro formal de a a uma funcao leva por vezes a pensar que o mesmo pode ser feito no caso de uma matriz. De facto, a situacao n o e totalmente equivalente. a ` Comece-se por regressar a declaracao int x[3][2] e observar a forma como e determi nado o endereco de x[k][j]. Analisando a gura 2.9, e evidente que o endereco deste elemento e obtido adicionando ao endereco do primeiro elemento k*2+j. Ou seja,

30 A PONTADORES

x[k][j]

<->

*(&(x[0][0]) + k * 2 +j)

No caso mais gen rico da declaracao ter a forma <tipo> x[N][M] ter-se- ainda e a x[k][j] <-> *(&(x[0][0]) + k * M +j)

Ou seja, para aceder a um elemento gen rico de uma matriz n o basta conhecer o endereco da e a primeira posicao e o os dois ndices de acesso: e necess rio saber tamb m o n mero de colunas da a e u matriz. Deste modo, quando uma matriz e passada como argumento de uma funcao, e necess rio a ` que esta saiba a dimens o das colunas da matriz. Por este motivo, dada a chamada a funcao a /* ... */ #define NLIN 3 #define NCOL 2 /* ... */ int m[NLIN][NCOL]; int a; /* ... */ a = soma(m); /* ... */

o par metro formal da funcao pode repetir na totalidade a declaracao da matriz, como em a 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 mero de linhas (dado que, como se mostrou, este n mero n o e indispens vel u u a a para localizar o endereco de um elemento gen rico da matriz), como em e 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 o simult nea de ambos os ndices como em a a

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 o e possvel, gerando um erro de compilacao. a Viu-se anteriormente que se o vector int x[NMAX] for utilizado na chamada a uma funcao, a declaracao a adoptar nos par metros formais da funcao podia ser ou int a[]) ou int *a. a frequente surgir a d vida se e possvel adoptar uma notacao de apontador equivalente no caso E u de uma matriz. De facto sim, embora esta notacao raramente seja utilizada na pr tica. No caso do a exemplo que tem vindo a ser utilizado a declaracao possvel 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 declaracao, x e um apontador para um vector de NCOL floats. Uma explicacao mais detalhada do signicado desta invulgar declaracao pode ser encontrado na seccao 2.6.4.

2.6.3

Matrizes e vectores

Em C, uma matriz n o e mais do que um vector de vectores. Ou seja, a declaracao a int x[3][2] pode ser interpretada como x e um vector de 3 posicoes, em que cada posicao e um vector de 2 posicoes. Esta interpretacao e tamb m sugerida pela representacao que se adoptou e na gura 2.9. Por outras palavras, x[0], x[1] e x[2] representam vectores de dois elementos constitudos respectivamente por {1.0 , 2.0}, {2.0 , 3.0}, e {4.0 , 5.0}. Na pr tica, este facto signica que cada uma das linhas de uma matriz pode ser tratada india vidualmente como um vector.

32 A PONTADORES

Suponha-se, por exemplo, que dada uma matriz a de dimens o NLIN NCOL, se pretende a escrever uma funcao que determine o m ximo de cada uma das linhas da matriz e coloque o a resultado num vector y. Esta funcao 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 ximo e determinado pela a funcao 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 o N M e x e y s o vectores de dimens o M e N , respectivaa a a mente. 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 o e mais do que o produto interno de cada linha da matriz com o vector operando, a funcao a 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 denido 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 ultima situacao em que e possvel exemplicar a utilizacao de linhas como vectores e o do armazenamento de v rias linhas de texto. Admita-se, por exemplo, que se pretende ler uma a sequ ncia de linhas de texto e imprimir as mesmas linhas por ordem inversas. Tal e possvel atrav s e e da utilizacao de uma matriz de caracteres, utilizando cada linha como uma string convencional:

#include <stdio.h> #include <stdlib.h> #include <string.h> #define NUM_LINHAS 5 #define DIM_LINHA 40 int main(){ char texto[NUM_LINHAS][DIM_LINHA]; int k; /* Leitura */ printf("Escreva uma sequncia de %d linhas:\n",NUM_LINHAS); e 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 utilizacao de estruturas multidimensionais e relativamente simples e intuitiva, j o mesmo a nem sempre sucede quando e necess rio manipular apontadores relacionados com este tipo. 5 O a tratamento de matrizes no C e frequentemente fonte de alvo de d vidas. Se se perguntar a qualquer u programador de C dada a declaracao int x[3], qual e o tipo de x quando usado isoladamente, nenhum ter d vidas em armar que a resposta e apontador para int. Experimente-se, no a u entanto, a colocar a pergunta semelhante dada a declaracao int x[3][2], qual e o tipo de x quando usado isoladamente e, com elevada probabilidade, n o ser obtida a mesma unanimidade a a nas respostas. Dado que, por consist ncia sint ctica, se tem sempre e a

x[k] <-> *(x + k)

ent o, no caso de uma estrutura bidimensional, ter que ser a a

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 gura 2.9, signica isto que x[0] corresponde a um apontador para float com o valor 1001 (e portanto o endereco do primeiro elemento do vector de floats formado pelos reais 1.0 e 2.0, enquanto que x[2] corresponde tamb m a um apontador, cujo e valor e 1003 (primeiro elemento do vector de floats formado pelos reais 3.0 e 4.0). Considere-se agora, novamente, a quest o de qual o tipo de x, quando considerado isoladaa mente. Como j se disse anteriormente em C, uma estrutura multidimensional representa sempre a
5

A leitura desta seccao pode ser omitida numa abordagem introdut ria da linguagem C. o

M ATRIZES 35

uma hierarquia de vectores simples. Ou seja, x[3][2] representa um vector de tr s elementos, e 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 formulacao, resulta claro que x representa um apontador para um vector de dois oats. Isto n o e mais do que a generalizacao da situacao dos vectores, em que a dada a declaracao int a[N], se sabe que a isoladamente e um apontador para inteiro. Assim, e natural que x sem ndices especique o endereco do primeiro elemento de um vector de tr s elementos, em que cada um e um vector de dois floats. Ou seja, no nosso exemplo, x e corresponde ao valor 1001. Mas, anal, qual a diferenca entre um apontador para float e um apontador para um vec tor de floats? Por um lado, a forma de usar este tipo vari vel num enderecamento indirecto a e claramente diferente. Por exemplo, x e &x[0][0] correspondem ao mesmo valor (1001 no nosso exemplo), mas s o tipos diferentes: o primeiro e um apontador para um vector, pelo que *x a e um vector (a primeira linha de x, enquanto que *(&x[0][0]) e um float (o conte do de u x[0][0]. No entanto, e provavelmente mais importante reter que a diferenca fundamental reside na dimens o do objecto apontado. Considere-se novamente o exemplo que temos vindo a cona siderar. Uma vari vel do tipo apontador para float, cujo valor seja 1001, quando incrementada a passa a ter o valor 1002. Mas uma vari vel do tipo apontador para vector de dois float, cujo a valor seja 1001, quando incrementada passa a ter o valor 1003 (j que o incremento e escalado a pela dimens o do objecto apontado). a E interessante vericar que este modelo e o unico que permite manter a consist ncia sint ctica e a do C na equival ncia entre vectores e apontadores. J se disse que sendo sempre e a

x[k] <-> *(x + k)

ent o, a

x[k][j] <-> *(x[k] + j)

O que n o se disse antes, mas que tamb m se verica, e que a primeira destas regras tamb m deve a e e ` ser aplic vel a entidade x[k] que surge na ultima express o. Ou seja, tem-se a a

x[k][j] <-> *(*(x+k) + j)

o que exige por outro lado estabelecer que

36 A PONTADORES

Express o a x x+1 *(x+1) *(x+2)+1 *(*(x+2)+1) *x **x *(*x+1) x[1]

Tipo 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

Valor 1001 1003 1003 1006 6.0 1001 1.0 2.0 1003

Elemento da estrutura x[2][1] x[0][0] x[0][1] -

Tabela 2.1: Exemplos de tipos e valores derivados do exemplo da gura 2.9. Se a e um apontador para um escalar, *a e desse tipo escalar, e o valor de *a e o conte do u do endereco especicado por a. Se a e um apontador para um vector de elementos de um dado tipo, *a e um apontador para e um elemento do tipo constituinte, e o seu valor e id ntico ao de a.

Um factor que contribui frequentemente para alguma confus o deriva do facto de que, ainda a que x n o seja sintacticamente um duplo apontador para float, sendo a x[0][0] <-> *(*(x+0) + 0) <-> **x

verica-se que **x e um float. A consist ncia destas equival ncias podem ser vericada considerando casos particulares do e e exemplo que tem vindo a ser utilizado como refer ncia, tal como listados na tabela 2.1. O c digo e o de um pequeno programa que permite validar esta tabela est listado no ap ndice A. a e Como e natural, e possvel declarar um apontador para um vector de dois floats, sem ser da forma implcita que resulta da declaracao da matriz. A declaracao de uma vari vel y deste tipo a pode ser feita por float (*y)[2];

Por este motivo, que quando a matriz x e passada por argumento para uma funcao func, a

M ATRIZES 37

declaracao do par metro formal poder ser feita repetindo a declaracao total, omitindo a dimens o a a do ndice interior, ou ent o por a

void func(float (*y)[2]);

tal como se referiu na seccao 2.6.2. Dado que este tipo de declaracoes e alvo frequente de confus o, e conveniente saber que existe a uma regra de leitura que ajuda a claricar a sem ntica da declaracao. Com efeito, e suciente a ` seguir as regras de preced ncia, procedendo a leitura na seguinte sequ ncia: e e
float (*y)[3] y um apontador para um vector de trs floats

Sublinhe-se que, face a tudo o que cou dito anteriormente, n o e possvel declarar um apona tador para um vector sem especicar a dimens o do vector: como j foi dito por diversas vezes, a a um apontador tem que conhecer a dimens o do objecto apontado. Isto n o e possvel sem especia a car a dimens o do vector. Como corol rio, um apontador para um vector de tr s inteiros e de a a e tipo distinto de um apontador para um vector para seis inteiros, n o podendo os seus conte dos a u ser mutuamente atribudos. Note-se que a declaracao que se acabou de referir e claramente distinta de float *y[2];

` onde, devido a aus ncia de parenteses, e necess rio ter em atencao a preced ncia de [] sobre o e a e *. Neste caso, y e um vector de dois apontadores para float, podendo a leitura da declaracao ser realizada pela sequ ncia: e
float *y[3] y um vector de trs apontadores para float

A utilizacao de vectores de apontadores e abordada em maior detalhe na seccao 3.5.4. Finalmente, rera-se que os apontadores para vectores podem ainda surgir noutros contextos: dada a declaracao int x[10], x e um apontador para inteiro, mas a express o &x e do tipo a

38 A PONTADORES

apontador para um vector de 10 inteiros.

2.7

Generalizacao para mais do que duas dimensoes

A generalizacao do que cou dito para mais do que duas dimens es e directa. Considere-se, o como refer ncia, a declaracao da estrutura e int x[M][N][L];

1. No c lculo do endereco de qualquer elemento da estrutura tem-se a igualdade: a &(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 NL inteiros. 5. Em geral, x[m][n][l] == *(*(*(x+m)+n)+l)

A passagem de uma estrutura multidimensional como argumento pode ser feita pela repeticao da declaracao completa do tipo. Assim, uma declaracao possvel 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; }

G ENERALIZAC AO PARA MAIS DO QUE DUAS DIMENS OES 39

Como se mostrou anteriormente, o c lculo do endereco de um elemento gen rico de uma esa e trutura tridimensional exige o conhecimento das duas dimens es exteriores da estrutura (N e L no o exemplo). A generalizacao desta regra mostra que para calcular o endereco de um elemento de uma estrutura n-dimensional e necess rio conhecer com rigor os n 1 ndices exteriores da esa trutura. Deste modo, nos argumentos formais de uma funcao e sempre possvel omitir a dimens o a do primeiro ndice de uma estrutura multidimensional, mas n o mais do que esse. No exemplo a anterior, pode ent o escrever-se a

void func(int x[][N][L]){ /* ... */ }

Claro que todas as outras variantes em que exista consist ncia sint ctica entre os argumentos e a formais e actuais do procedimento s o v lidas. Assim, pelas mesmas raz es j detalhadas na a a o a seccao 2.6.4,

void func(int (*x)[N][L]){ /* ... */ }

e uma alternativa sintacticamente correcta neste caso.

Captulo 3 Vectores e memoria dinamica

3.1

Introducao

At ao aparecimento da linguagem C, a maioria das linguagens de alto nvel exigia um dimene sionamento rgido das vari veis envolvidas. Por outras palavras, a quantidade m xima de mem ria a a o necess ria durante a execucao do programa deveria ser denida na altura de compilacao do proa grama. Sempre que os requisitos de mem ria ultrapassavam o limite xado durante a execucao o do programa, este deveria gerar um erro. A solucao nestes casos era recompilar o programa au mentando a dimens o das vari veis, solucao s possvel se o utilizador tivesse acesso e dominasse a a o ` os detalhes do c digo fonte. Por exemplo, um programa destinado a simulacao de um circuito o electr nico poderia ser obrigado a denir no c digo fonte o n mero m ximo de componentes o o u a do circuito. Caso este n mero fosse ultrapassado, o programa deveria gerar um erro, porque a u mem ria reservada durante a compilacao tinha sido ultrapassada. A alternativa nestas situacoes o ` era a reserva a partida de uma dimens o de mem ria suciente para acomodar circuitos de dia o mens o elevada, mas tal traduzia-se obviamente num consumo excessivo de mem ria sempre que a o o programa fosse utilizado para simular circuitos de dimens o reduzida. Em alguns casos, os proa ` gramas recorriam a utilizacao de cheiros para guardar informacao tempor ria, mas esta solucao a implicava geralmente uma complexidade algortmica acrescida. Tal como o nome indica, os sistemas de mem ria din mica permitem gerir de forma din mica o a a os requisitos de mem ria de um dado programa durante a sua execucao. Por exemplo, no caso o do sistema de simulacao referido anteriormente, o programa pode, no incio da execucao, deter minar a dimens o do circuito a simular e s nessa altura reservar a mem ria necess ria. Com a o o a esta metodologia, o programa pode minimizar a quantidade de mem ria reservada e, deste modo, o permitir que o sistema operativo optimize a distribuicao de mem ria pelos v rios programas que o a

42 V ECTORES E MEM ORIA DIN AMICA

se encontram simultaneamente em execucao. No entanto, at ao aparecimento do C este tipo e de mecanismos, caso estivessem disponveis no sistema operativo, eram apenas acessveis em linguagem m quina ou Assembler, mais uma vez pela necessidade de manipular directamente a enderecos de mem ria. o Apesar da exibilidade oferecida, a gest o directa da mem ria din mica exige algumas a o a precaucoes suplementares durante o desenvolvimento do c digo. Por este motivo, muitas lingua o gens de programacao de alto-nvel mais recentes (Lisp, Java, Scheme, Python, Perl, Mathematica, entre outras) efectuam a gest o autom tica da mem ria din mica, permitindo assim que o proa a o a gramador se concentre na implementacao algortmica e nos modelos de dados associados, sem se preocupar explicitamente com os problemas de dimensionamento de vari veis ou os volumes de a mem ria necess rios para armazenamento de dados. o a Neste contexto, poder perguntar-se quais as vantagens de programar em C ou porque e que a o C ainda mant m a popularidade em tantas areas de aplicacao. H diversas respostas para esta e a quest o: a

A gest o directa da mem ria din mica permite normalmente a construcao de programas a o a com maior eci ncia e com menores recursos computacionais. e 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 ximo da linguagem m quina do que linguagens conceptualmente mais elaboradas. o a Por este motivo, o C ainda se reveste hoje de uma import ncia crucial em diversas areas da a Engenharia Electrot cnica, nomeadamente em aplicacoes que implicam o recurso a microe controladores especializados. Dada a sua proximidade com o hardware1 , a maioria dos sistemas operativos actualmente existentes s o ainda hoje programados na linguagem C. a A maioria dos compiladores de linguagens de alto nvel, incluindo o pr prio C, s o actual o a mente escritos e desenvolvidos em C. Ou seja, nesta perspectiva, o C e hoje uma linguagem ` indispens vel a geracao da maioria das outras linguagens, constituindo, neste sentido, a a linguagem das linguagens.
O C e frequentemente designado na gria como um Assembler de alto nvel, apesar do evidente paradoxo contido nesta designacao.
1

V ECTORES 43

3.2

Vectores

Em C, um vector e uma coleccao com um n mero bem denido de elementos do mesmo tipo. u Ao encontrar a declaracao de um vector num programa, o compilador reserva automaticamente espaco em mem ria para todos os seus elementos. Por raz es de clareza e de boa pr tica de o o a programacao, estas constantes s o normalmente declaradas de forma simb lica atrav s de uma a o e directiva define, mas a dimens o continua obviamente a ser uma constante: a #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 ria e necess ria para o vector que se o a pretende utilizar implica que a dimens o deste vector seja uma constante, cujo valor j conhecido a a durante a compilacao do programa. ` Como e sabido, as vari veis locais a uma funcao t m um tempo de vida limitado a execucao a e 2 . O espaco para estas vari veis e reservado na chamada zona da pilha (stack), uma da funcao a regi o de mem ria dedicada pelo programa para armazenar vari veis locais e que normalmente e a o a ocupada por ordem inversa. Por outras palavras, s o usados primeiros os enderecos mais elevados a ` e v o sendo sucessivamente ocupados os enderecos inferiores a medida que s o reservadas mais a a vari veis locais. Dada a forma como se realiza esta ocupacao de mem ria, o limite inferior da a o regi o da pilha e geralmente designada por topo da pilha. Neste sentido, quando uma funcao a e chamada, o espaco para as vari veis locais e reservado no topo da pilha, que assim decresce. a Quando uma funcao termina, todas as vari veis locais s o libertadas e o topo da pilha aumenta a a novamente. Assim, por exemplo, dada a declaracao int function(int z){ int a; int x[5]; int b; /* ... */ a chamada da funcao func poderia dar origem a uma evolucao do preenchimento da mem ria da o forma como se representa na gura 3.1.
2

Excepto se a sua declaracao for precedido do atributo static, mas este s e usado em circunst ncias excepcionais. o a

44 V ECTORES E MEM ORIA DIN AMICA

Endereo

Contedo

Varivel

Endereo

Contedo

Varivel

. . .
1001 1002 1003 1004 1005 1006 1007 1008 1009

. . .

. . .

. . .
1001 1002 1003 1004 1005 1006 1007 1008 1009

. . .

. . .
topo da pilha b x[0] x[1] x[2] x[3] a z ...

. . .

. . .
A

topo da pilha

. . .

. . .
B

` Figura 3.1: Mapa de mem ria antes e ap s a chamada a funcao func (A) e mapa de mem ria o o o durante a execucao da funcao (B). ` A necessidade da dimens o dos vectores ser conhecida na altura da compilacao conduz a a impossibilidade de declaracoes do tipo

int function(int n){ int x[n]; /* MAL: n uma varivel, e o seu valor desconhecido e a e na altura da compilaco */ a /* ... */

Como e evidente, os elementos de um vector podem n o ser escalares simples como no exa emplo 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 declaracao

int mat[10][5]

a matriz mat, do ponto de vista interno do C, n o e mais do que um vector de 10 elementos, em a que cada elemento e por sua vez um vector de 5 inteiro (V. seccao 2.6.3). A necessidade de denir de forma rgida a dimens o dos vectores na altura da compilacao a obriga frequentemente ao estabelecimento de um compromisso entre a dimens o m xima e a a a eci ncia do programa em termos da mem ria utilizada. Uma solucao possvel e utilizar um e o majorante dos valores tpicos. Considere-se novamente um sistema para simulacao de circuitos electr nicos. Se os sistemas que se pretende simular t m em m dia 1000 componentes, podero e e se-ia utilizar um valor de 2000 ou 5000 para dimensionar o vector de componentes. No entanto, a utilizacao de um valor m ximo elevado pode conduzir a situacoes frequentes de desperdcio a ` de mem ria, com o programa a reservar a partida volumes de mem ria muito superiores aos o o necess rios, enquanto que um valor reduzido deste par metro pode limitar seriamente a dimens o a a a dos problemas a abordar. Por outro lado, e por vezes difcil encontrar par metros razo veis para a a denir valor m dio. Em determinadas aplicacoes, 50 componentes pode ser um valor razo vel, e a noutras 10,000 pode ser um valor reduzido. Para agravar a situacao, a dimens o de mem ria que a o ` e razo vel reservar a partida depende da mem ria fsica disponvel no sistema em que se est a a o a trabalhar: sistemas com 8MB ou 8GB de mem ria conduzem obviamente a situacoes distintas. o N o se pretende com esta an lise colocar de parte todas as utilizacoes de vectores convena a cionais com dimens o xa. Frequentemente, esta e uma solucao mais que razo vel. Por exemplo, a a uma linha de texto numa consola de texto tem em geral cerca de 80 caracteres, pelo que a denicao de um valor m ximo para o comprimento de uma linha de 160 ou 200 caracteres e um valor que a pode ser razo vel e que n o e excessivo na maioria das aplicacoes. No entanto, em situacoes a a em que o n mero de elementos pode variar signicativamente, e desej vel que a mem ria seja u a o ` reservada a medida das necessidades.

46 V ECTORES E MEM ORIA DIN AMICA

3.3

Vectores dinamicos

Uma forma de ultrapassar a diculdade criada pelo dimensionamento de vectores e a reserva de blocos de mem ria s ser realizada durante a execucao do programa. Considere-se novamente o o o simulador de circuitos electr nicos. A ideia essencial e iniciar o programa com um volume de o mem ria mnimo, determinar qual o n mero de componentes do sistema a simular e s depois o u o deste passo reservar espaco para manipular o n mero de componentes pretendido. u Recorde-se que ao declarar um vector

int x[MAX_DIM]

a utilizacao do nome do vector sem especicar o ndice e equivalente ao endereco do ndice 0 do vector. Por outras palavras, a utilizacao no programa de x e equivalente a &x[0].Decorre tamb m e daqui a regra geral de indexacao em C:

x[k] <-> *(x + k)

equival ncia que e universal em C. e A utilizacao de um bloco de mem ria criado dinamicamente e que pode ser utilizado com o mecanismos de acesso id nticos aos de um vector pode ser realizado declarando uma vari vel e a de tipo de apontador e solicitando ao sistema operativo a reserva de um bloco de mem ria da o dimens o pretendida. O pedido de reserva de mem ria ao sistema operativo e efectuado atrav s a o e de um conjunto de funcoes declaradas no cheiro stdlib.h. Uma das funcoes utiliz veis para a este efeito tem como prot tipo o

void *calloc(size_t nmemb, size_t size);

A funcao calloc reserva um bloco de mem ria contgua com espaco suciente para armazenar o nmemb elementos de dimens o size cada, devolvendo o endereco (apontador) para a primeira a posicao do bloco. size_t e o tipo utilizado para especicacao de dimens es num ricas em v rias o e a funcoes do C e a sua implementacao pode ser dependente do sistema operativo, mas corresponde geralmente a um inteiro sem sinal. O tipo de retorno, void*, corresponde a um endereco gen rico e de mem ria. o que permite que a funcao seja utilizada independentemente do tipo especco do o apontador que se pretende inicializar. A funcao retorna um apontador para o primeiro endereco de

V ECTORES DIN AMICOS 47

uma regi o de mem ria livre da dimens o solicitada. Caso esta reserva n o seja possvel, a funcao a o a a retorna NULL, para indicar que a reserva n o foi efectuada. a Considere-se, como exemplo, um programa para c lculo da m dia e vari ncia de N valores a e a reais indicados pelo programador. Em vez de utilizar um vector de dimens o xa, e possvel a utilizar apenas um apontador para um real e um programa com a seguinte estrutura: /* Ficheiro: media.c Contedo: Programa para clculo da mdia e varinicia u a e a Autor: Fernando M. Silva, IST (fcr@inesc-id.pt) Histria : 2001/06/01 - Criaco o a */ #include <stdio.h> #include <stdlib.h> int main(){ int n; /* Nmero de valores */ u 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 memria o para um bloco de n reais */

if(f == NULL){ /* Teste da reserva de memria */ o fprintf(stderr,"Erro na reserva de memria\n"); o exit(1); } /* A partir deste ponto, f pode ser tratado como um vector de n posices */ o 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; /* clculo da mdia */ a e 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; /* clculo da var. */ a printf("Mdia = %5.2f, var = %5.2f\n", e media, variancia); free(f); exit(0); }

` Na chamada a funcao calloc(), dois aspectos devem ser considerados. Em primeiro lugar o op erador sizeof() e um operador intrnseco do C que devolve a dimens o (geralmente em bytes) a ` do tipo ou vari vel indicado no argumento. Por outro lado, a esquerda da funcao calloc() foi a acrescentada e express o (float*). Esta express o funciona como um operador de cast, obriga a ` ando a convers o do apontador gen rico devolvido pela funcao calloc para um apontador para a e real. Em geral, na operacao de cast e indicado o tipo do apontador que se encontra do lado es querdo da atribuicao. Embora este cast n o seja obrigat rio, e geralmente utilizado na linguagem a o C como uma garantia adicional da consist ncia das atribuicoes. e Ap s a reserva din mica de mem ria, o apontador f pode ser tratado como um vector cono a o vencional. Como e evidente, o ndice n o deve ultrapassar a posicao n 1, dado que para valores a superiores se estaria a aceder a posicoes de mem ria inv lidas. o a Enquanto que as vari veis locais s o criadas na zona da pilha, toda a mem ria reservada a a o dinamicamente e geralmente criada numa regi o independente de mem ria designada por heap a o (molhe). Admita-se, no caso do programa para c lculo das m dias e vari ncias, que o valor de n a e a especicado pelo utilizador era 4. Uma possvel representacao do mapa de mem ria antes e ap s o o a chamada da funcao calloc encontra-se representado na gura 3.2.

3.4
3.4.1

Gestao da memoria dinamica


Vericacao da reserva de memoria

Como j se referiu, ao efectuar uma reserva din mica de mem ria e essencial testar se os rea a o cursos solicitados foram ou n o disponibilizados pelo sistema. Com efeito, a reserva de mem ria a o pode ser mal sucedida por falta de recursos disponveis no sistema. Este teste teste e deve ser sem pre efectuado testando o apontador devolvido pela funcao calloc(). Quando existe um erro, a funcao devolve o endereco 0 de mem ria, o qual e representado simbolicamente pela constante o

G EST AO DA MEM ORIA DIN AMICA 49

Zona da pilha (var. locais) Endereo Contedo Varivel

Zona do heap (var. dinamicas) Endereo Contedo Varivel

. . .
1001 1002 1003 1004 1005 1006 1007 1008 1009

. . .

. . .
... k variancia media soma f

. . .
2101 2102 2103 2104 2105 2106 2107 2108 2109

. . .

. . .
... topo

n ...

...

. . .

. . .
A
Zona da pilha (var. locais)

. . .

. . .

Zona do heap (var. dinamicas) Endereo Contedo Varivel

Endereo

Contedo

Varivel

. . .
1001 1002 1003 1004 1005 1006 1007 1008 1009

. . .

. . .
... k variancia media soma

. . .
2101 2102 2103 2104 2105 2106 2107 2108 2109

. . .
*f

. . .
... <>f[0]

*(f+1) <> f[1] *(f+2) <> f[2] *(f+3) <> f[3] topo

2102 4

f n ...

...

. . .

. . .
B

. . .

. . .

Figura 3.2: Mapa de mem ria antes da reserva de mem ria (A) e ap s a reserva de mem ria. (B). o o o o

50 V ECTORES E MEM ORIA DIN AMICA

NULL (denida em stdio.h). Se o endereco devolvido tiver qualquer outro valor, a reserva de mem ria foi bem sucedida, e o programa pode continuar a sua execucao normal. o

3.4.2

Outras funcoes de gestao de memoria dinamica

Funcao free()

Enquanto que a funcao calloc() efectua uma reserva din mica de mem ria, a funcao a o free(p) liberta o bloco de mem ria apontado por p. Como seria de esperar, esta funcao s o o tem signicado se o apontador p foi obtido por uma funcao pr via de reserva de mem ria, como e o a funcao calloc() descrita anteriormente. De um modo geral, e boa pr tica de programacao libertar toda a mem ria reservada pelo a o programa sempre que esta deixa de ser necess ria. Isto sucede frequentemente pela necessidade a de libertar mem ria que s foi necess ria temporariamente pelo programa. o o a Note-se que sempre que o programa termina toda a mem ria reservada e automaticamente libo ertada. Deste modo, a libertacao explcita da mem ria reservada durante a execucao do programa o n o e estritamente obrigat ria antes de sair do programa atrav s da funcao exit() ou da direca o e tiva return no bloco main(). Apesar deste facto, alguns autores consideram que, por raz es o ` de consist ncia e arrumacao do c digo, o programa deve proceder a libertacao de toda a mem ria e o o reservada antes de ser concludo. E este o procedimento adoptado no programa para c lculo da a vari ncia, onde a funcao free() e chamada antes do programa terminar. a

Funcao malloc()

A funcao malloc() tem por prot tipo o

void *malloc(size_t total_size);

onde total_size representa a dimens o total da mem ria a reservar, expressa em bytes. A a o ` funcao malloc() e muito semelhante a funcao calloc() A forma calloc(n,d) pode ser simplesmente substituda por malloc(n*d). A unica diferenca formal entre as duas funcoes e que enquanto a funcao calloc() devolve um bloco de mem ria inicializado com zero em todas o as posicoes, a funcao malloc() n o efectua explicitamente esta inicializacao. a

G EST AO DA MEM ORIA DIN AMICA 51

Funcao realloc()

A funcao realloc() pode ser utilizada sempre que e necess rio modicar a dimens o de a a um bloco de mem ria din mica reservado anteriormente. O prot tipo da funcao e o a o

void *realloc(void *old_ptr,size_t total_new_size);

onde old_ptr e o apontador para o bloco de mem ria reservado anteriormente, enquanto que o total_new_size e a dimens o total que se pretende agora para o mesmo bloco. A funcao a retorna um apontador para o bloco de mem ria redimensionado. Note-se que o segundo argumento o tem um signicado semelhante ao da funcao malloc. Suponha-se, por exemplo, que no incio de um programa tinha sido reservado um bloco de mem ria para a n inteiros: o

int main(){ int *x,n; /* Obtenco do valor de n ... */ a x = (int*) calloc(n,sizeof(int));

mas que, mais tarde, se vericou a necessidade de acrescentar 1000 posicoes a este bloco de mem ria. Este resultado pode ser obtido fazendo o

x = (int*) realloc(x,(n+1000)*sizeof(int));

O funcionamento exacto da funcao realloc() depende das disponibilidades de mem ria o existentes. Suponha-se, para assentar ideias, que inicialmente se tinha n=2000, e que o valor de x resultante da funcao calloc era 10000 (gura 3.3, A). Deste modo o bloco de mem ria reservado o inicialmente estendia-se do endereco 10000 ao endereco 11999. Quando mais tarde e chamada a funcao realloc duas situacoes podem ocorrer. Se os enderecos de mem ria entre 12000 e 12999 o estiverem livres, o bloco de mem ria e prolongado, sem mudanca de stio, pelo que o valor de x o permanece inalterado (gura 3.3, B). No entanto, se algum endereco entre 12000 e 12999 estiver ocupado (gura 3.3, C), e necess rio deslocar todo o bloco de mem ria para uma nova regi o. a o a Neste caso, o novo bloco de mem ria pode ser mapeado, por exemplo, entre os enderecos 12500 o e 15499 (gura 3.3, D), retornando a funcao realloc o novo endereco da primeira posicao de mem ria (12500). Como complemento, a funcao realloc trata de copiar automaticamente todo o

52 V ECTORES E MEM ORIA DIN AMICA

o conte do guardado nos enderecos 10000 a 11999 para os enderecos 12500 a 14499 e liberta as u posicoes de mem ria iniciais. o

3.4.3

Garbbage

Como referido anteriormente, o espaco de mem ria para as vari veis locais e reservado na o a zona de mem ria da pilha quando uma funcao e chamada, sendo automaticamente libertado (e o com ele as vari veis locais) quando a funcao termina. a Ao contr rio das vari veis locais, a mem ria din mica e criada e libertada sob controlo do a a o a ` programa, atrav s das chamadas as funcoes calloc(), malloc() e free(). e Em programas complexos, e frequente um programa reservar blocos de mem ria que, por o erro do programador ou pela pr pria estrutura do programa, n o s o libertados mas para os quais o a a se perdem todos os apontadores disponveis. Neste caso, o bloco de mem ria din mica e reser o a vado, mas deixa de ser acessvel porque se perderam todos os apontadores que indicavam a sua localizacao em mem ria. Estes blocos de mem ria reservados mas cuja localizacao se perdeu s o o o a 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)); /* ... Utilizaco de x, free() a no chamado... a */ } void main() int a,b; func(); /* O Bloco reservado em func deixou de ser usado, mas deixou de estar inacessvel */

Um mapa de mem ria ilustrativo desta situacao est representado na gura 3.4. o a

G EST AO DA MEM ORIA DIN AMICA 53

Zona da pilha (var. locais)


Zona da pilha (var. locais) Endereo Contedo Varivel Zona do heap (var. dinamicas) Endereo Contedo Varivel

Zona do heap (var. dinamicas) Endereo Contedo Varivel

Endereo

Contedo

Varivel

. . .
1001 1002 1003 1004 1005 1006 1007 1008 1009

. . .
2000 10000

. . .
... n x

. . .
10000 10001

. . .
666 300

. . .
...

. . .
1001 1002 1003 1004 1005 1006

. . .
2000 10000

. . .
... n x

. . .
10000 10001

. . .
666 300

. . .
...

. . .
11999 12000 12001 Memoria livre 1765

. . .
11999 12000 1765

1007 1008 1009 ... 12999

. . .

...

12002 12003

. . .

. . .

. . .

. . .

. . .

. . .

. . .

. . .

A
Zona da pilha (var. locais) Endereo Contedo Varivel

B
Zona do heap (var. dinamicas) Endereo Contedo Varivel

. . .
1001 1002
Zona da pilha (var. locais) Endereo Contedo Varivel Zona do heap (var. dinamicas)

. . .
2000 12500

. . .
... n x

. . .
10000 10001

. . .
666 300

. . .
...

1003
Endereo Contedo Varivel

. . .
11999 12000 12001 12002 11111111 555555 1765

. . .
1001 1002 1003 1004 1005 1006 1007 1008 1009

. . .
2000 10000

. . .
... n x

. . .
10000 10001

. . .
666 300

. . .
...

1004 1005 1006

Memoria libertada

. . .
11999 12000 12001 11111111 555555 Memoria ocupada por outras var. dinamicas 1765

1007 1008 1009 ...

. . .
12500 12501 666 300 Memoria "realocada"

. . .

. . .

...

12002 12003

. . .

. . .

. . .

. . .

. . . 14599 . . .

. . . . . .

` Figura 3.3: Mapa de mem ria ap s a chamada a funcao calloc() (A) e ap s a funcao o o o realloc() (B), se existir espaco disponvel nos enderecos contguos. Em (C) e (D) representa se a situacao correspondente no caso em que a mem ria din mica imediatamente a seguir ao bloco o a reservado est ocupada, sendo necess rio deslocar todo o bloco para outra zona de mem ria. Neste a a o caso, o apontador retornado pela funcao e diferente do inicial.

54 V ECTORES E MEM ORIA DIN AMICA

Zona da pilha (var. locais) Endereo Contedo Varivel

Zona do heap (var. dinamicas) Endereo Contedo Varivel

Zona da pilha (var. locais) Endereo Contedo Varivel

Zona do heap (var. dinamicas) Endereo Contedo Varivel

. . .
1001 1002 1003 1004 1005 1006 1007 1008 1009

. . .

. . .
...

. . .
2101 2102 2103 2104 topo 2105 2106 2107 2108 2109

. . .

. . .
... topo

. . .
1001 1002 1003 1004 1005 1006 1007

. . .

. . .
... topo

. . .
2101 2102 2103 2104 2105

. . .
x[0] x[1] x[2]

. . .
...

2102

x y

topo

b a

b a

2106 2107 2108 2109 ...

...

1008 1009

. . .

. . .

. . .

. . .

. . .

. . .

. . .

. . .

A
Zona da pilha (var. locais) Endereo Contedo Varivel Zona do heap (var. dinamicas) Endereo Contedo Varivel

. . .
1001 1002 1003 1004 1005 1006 1007 1008 1009

. . .

. . .
...

. . .
2101 2102 2103 2104

. . .
x[0] x[1] x[2]

. . .
...

topo b a

2105 2106 2107 2108 2109

topo

...

. . .

. . .

. . .

. . .

C Figura 3.4: Criacao de garbbage (lixo). Mapa de mem ria (exemplo do texto): (A) antes da o ` chamada a funcao func, (B) no nal da funcao func e (C) ap s retorno ao programa princi o pal. De notar que em (C) a mem ria din mica cou reservada, mas que se perderam todas as o a refer ncias que permitiam o acesso a esta zona de mem ria. e o

C RIAC AO DIN AMICA DE MATRIZES 55

3.5
3.5.1

Criacao dinamica de matrizes


Introducao

A criacao e utilizacao de estruturas din micas em C que possam ter um acesso equivalente a a uma matriz e simples. No entanto, a compreens o detalhada de como funcionam este tipo de a estruturas nem sempre e claro. De modo a simplicar a exposicao, comecar-se- por apresentar na seccao 3.5.2 um resumo do a modo de declaracao e de acesso de matrizes est ticas. Seguidamente, na seccao 3.5.3, descreve-se a uma metodologia simples para criar dinamicamente vectores de apontadores com um comportamento equivalente ao de uma matriz. Seguidamente, na seccao 3.5.4 ser o discutidas as diferencas a e semelhancas entre matrizes e vectores de apontadores, de modo a evidenciar as diferencas entre as estruturas din micas criadas e as matrizes nativas do C. a

3.5.2

Matrizes estaticas

Conforme j se viu na seccao 2.6.1, a utilizacao de estruturas multidimensionais em C apea nas exige a declaracao de uma vari vel com v rios ndices, especicando cada um da dimens o a a a pretendida da estrutura. Por exemplo

float x[3][2];

declara uma estrutura bidimensional de dois por tr s reais, ocupando um total de seis palavras e de mem ria no modelo de mem ria que temos vindo a assumir como refer ncia. E frequente o o e uma estrutura bidimensional ser interpretada como uma matriz, neste exemplo de duas linhas por tr s colunas. A inicializacao de uma matriz pode ser efectuada durante a execucao do programa e ou listando os valores iniciais, sendo apenas necess rio fazer um agrupamento hier rquico das a a constantes de inicializacao de acordo com as dimens es da estrutura: o

float x[3][2] = {{1.0,2.0},{3.0,4.0}, {5.0,6.0}};

Como se mostrou anteriormente, a arrumacao em mem ria desta estrutura encontra-se repre o sentada esquematicamente na gura 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 ncia e

#define N ... #define M ... ... int x[N][M];

por uma estrutura din mica com um mecanismo de acesso equivalente, mas em que os limites n e a m s o vari veis cujo valor s e conhecido durante a execucao do programa. a a o A solucao para este problema e substituir a estrutura x por um vector din mico de apontadores a para inteiros, em que cada posicao e por sua vez inicializada com um vector din mico. Considere a se, por exemplo, que se pretende criar uma matriz de n por m. O c digo para este efeito e dado o por

int main{ int k; int n,m; int **x; /* Inicializaco de n e m */ a x = (int**) calloc(n,sizeof(int*)); if(x == NULL){ printf("Erro na reserva de memria\n"); o exit(1); } for(k = 0; k < n ; k++){ x[k] = (int*) calloc(m,sizeof(int)); if(x == NULL){ printf("Erro na reserva de memria\n"); o exit(1); } }

Por exemplo, se se pretender criar uma matriz de 4 por 2 e inicializ -la com um valores inteiros a ` ` em que a classe das dezenas corresponde a linha e a classe dos algarismos as unidades, poder-se-ia fazer

C RIAC AO DIN AMICA 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 memria\n"); o exit(1); } for(k = 0; k < n ; k++){ x[k] = (int*) calloc(m,sizeof(int)); if(x == NULL){ printf("Erro na reserva de memria\n"); o exit(1); } } for(k = 0; k < n ; k++) for(j = 0; j < m ; j++) x[k][j] = 10 * k + j;

O resultado da execucao deste bloco de c digo seria o que se mostra na gura 3.5. o Este exemplo indica que a solucao geral para simular a criacao din mica de matrizes e declarar a um duplo apontador para o tipo pretendido dos elementos da matriz e, seguidamente, reservar din micamente um vector de apontadores e criar dinamicamente os vectores correspondentes a a cada linha. Como e evidente, neste exemplo x e um duplo apontador para inteiro. Deste modo, se pretender usar esta matriz din mica como argumento de uma funcao fazer-se, no bloco que chama, a

func(x,n,m,/* Outros argumentos */);

enquanto que o cabecalho de func dever ser a

void func(int **x,int n,int m,/* Outros argumentos */);

58 V ECTORES E MEM ORIA DIN AMICA

Zona da pilha Endereo Contedo Varivel Endereo Contedo

Zona do heap Varivel Endereo Contedo Varivel

. . .
1543

. . .
2001

. . .
x

. . .
2001 2002 2003 2004

. . .
2006 2009 2012 2015 x[0] x[1] x[2] x[3]

. . .

2006 2007

0 1

x[0][0] x[0][1]

. . .

. . .

. . .

2009 2010

10 11

x[1][0] x[1][1]

. . .

. . .

. . .

2012 2013

20 21

x[2][0] x[2][1]

2015 2016

30 31

x[3][0] x[3][1]

Figura 3.5: Criacao din mica de matrizes por meio de um vector de apontadores. Os enderecos a indicados s o apenas ilustrativos. a

Esta solucao e t o frequente na pr tica que, por analogia, muitos programadores (mesmo a a experientes) de C pensam que, dada a declaracao tipo x[N][M], x sem qualquer ndice corre sponde a um duplo apontador para tipo. Como se mostrou na seccao 2.6.4, esta suposicao n o a ` corresponde a realidade: nesta situacao, x e um apontador para um vector de elementos de tipo.

3.5.4

Vectores de apontadores e matrizes

Considere-se a declaracao

int *x[4];

Recordando que os [] t m preced ncia sobre o operador *, e seguindo a regra de interpretacao e e sem ntica da declaracao apresentada na seccao 2.6.4, resulta que x e um vector de 4 apontadores a para inteiros. Ou seja, em termos de modelo de mem ria, encontramos uma representacao como a o indicada na gura 3.6, situacao A. Um ponto importante a sublinhar e que esta declaracao apenas reserva mem ria para quatro apontadores. Esta situacao e frequentemente mal interpretada pelo o facto de, sendo x[k] uma apontador para um inteiro, ent o *x[k] e *(x[k]+j) tamb m s o a e a ` do tipo inteiro. Mas, devido as equival ncias sint cticas de enderecamento em C, tem-se e a

C RIAC AO DIN AMICA DE MATRIZES 59

Endereo

Contedo

Varivel

. . .
1001 1002 1003 1004

. . .
??? ??? ??? ??? x[0] x[1] x[2] x[3]

. . .

. . .
A
Endereo Contedo Varivel

. . .
Endereo 2001 2002

. . .
Contedo 1 2 Varivel x[0][0] x[0][1]

. . .
1001 1002 1003 1004

. . .
2001 2005 2010 2013 x[0] x[1] x[2] x[3]

. . .

2005 2006

3 4

x[1][0] x[1][1]

. . .

. . .

. . .

2010 2011

5 6

x[2][0] x[2][1]

2013 2014

7 8

x[3][0] x[3][1]

B Figura 3.6: Vectores de apontadores: A - Antes de inicializado, B- ap s a inicializacao. 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 raciocnio errado de que um vector de apontadores pode ser utilizado indiscriminadamente como se de uma matriz se tratasse. Ora, de facto, tal s e possvel se os elementos de x tiverem sido convenientemente inicializados o de modo a enderecarem a base de um vector de inteiros. Isto, s por si, n o e realizado pela o a declaracao int *x[4], que se limita a declarar um vector de apontadores e a reservar 4 palavras de mem ria para este efeito. Nesta declaracao, n o e reservada mem ria para qualquer inteiro. o a o a Claro que, se o utilizador o pretender, e f cil inicializar as posicoes de mem ria do vector o de apontadores reservando mem ria din mica de modo a armazenar os inteiros pretendido. Ado a mitindo que se pretende que o vector de apontadores anteriores sirva de base a uma estrutura de enderecamento equivalente a uma matriz de 4 por 2, esta inicializacao pode ser feita pela sequ ncia e

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 m da reserva das posicoes de mem ria para os vectores inteiros, se procedeu a sua e o inicializacao. A situacao nal pode ser representada pelo modelo da gura 3.6, situacao B. Em sntese, e conveniente reter que, apesar da manipulacao de matrizes e de vectores de apontadores seja sintacticamente semelhante, os mecanismos de reserva de mem ria s o clarao a mente distintos. No caso da matriz int x[4][2], a pr pria declaracao reserva automaticao mente espaco para oito inteiros, os quais podem ser acedidos pelas regras habituais. No caso da declaracao de um vector de apontadores, como por exemplo int *x[4], apenas e efectuada a reserva de quatro apontadores. A sua utilizacao requer a pr via inicializacao de cada um dos seus e elementos com um endereco v lido de um bloco de mem ria, o qual pode ser obtido a partir de a o um vector j declarado ou, como no exemplo aqui usado, pela reserva de um bloco de mem ria a o din mica atrav s da funcao calloc(). a e

Captulo 4 Listas dinamicas

4.1

Introducao

Como se viu anteriormente, a utilizacao de vectores em C apresenta a desvantagem de exigir o ` conhecimento a partida da dimens o m xima do vector. Em muitas situacoes, a utilizacao de veca a tores din micos, como descrito na seccao 3.3, e uma solucao eciente. No entanto, em situacoes a em que a quantidade de mem ria din mica necess ria varia frequentemente durante a execucao do o a a programa, a utilizacao de vectores din micos revela-se frequentemente pouco ecaz. a Considere-se, por exemplo, um gestor de uma central telef nica que necessita de reservar o dinamicamente mem ria para cada chamada estabelecida (onde e armazenada toda a informacao o sobre a ligacao, como por exemplo os n meros de origem e destino, tempo de ligacao, etc,) e u libertar essa mesma mem ria quando a chamada e terminada. Dado que o n mero de ligacoes o u activas varia frequentemente ao longo do dia, uma solucao baseada num vector din mico exi a giria o seu redimensionamento frequente. Em particular, sempre que n o fosse possvel encontrar a mem ria livre imediatamente a seguir ao m do vector, seria necess rio deslocar todo o vector (v. o a funcao realloc, seccao 3.4.2) para uma nova zona de mem ria, e a c pia exaustiva de todo o o o seu conte do para a nova posicao de mem ria. Este mecanismo, al m de pouco eciente, poderia u o e inviabilizar o funcionamento em tempo real do sistema, dado que uma percentagem signicativa do tempo disponvel seria dispendido na gest o da mem ria usada pelo vector. a o Quando se pretende armazenar entidades individuais de mem ria que possam ser criadas e o libertadas individualmente com uma certa frequ ncia, e normalmente mais adequado utilizar ese truturas de dados criadas din micas e organizadas em listas. a

62 L ISTAS DIN AMICAS

4.2

Abstraccao de dados

Neste captulo descrevem-se listas din micas como uma forma de armazenar ou organizar a informacao. Esta organizacao e, obviamente, independente do tipo de informacao que se pretende ` armazenar. Esta situacao e semelhante a da construcao de um arm rio com gavetas: a estrutura a do m vel e independente do conte do que se ir arrumar nas gavetas. Esta independ ncia entre o u a e m vel e conte do garante a generalidade do m vel, no sentido em que este ser adapt vel a v rias o u o a a a situacoes. De modo semelhante, ao desenhar um programa e importante que as suas componentes sejam o mais independentes possveis, de forma a manterem a generalidade. Tanto quanto possvel, e desej vel que um bloco de c digo desenvolvido para o programa A seja reutiliz vel no programa a o a B ou, pelo menos, que tal seja possvel sem alteracoes ou com alteracoes mnimas. Uma forma de atingir este objectivo e, ao desenhar o programa, adoptar uma metodologia designada por abstraccao de dados(Martins, 1989). Esta metodologia baseia-se na denicao e distincao clara dos v rios tipos de dados utilizados pelo programa (ou, seguindo o exemplo ante a rior, distinguir tanto quanto possvel o m vel do conte do das gavetas). Segundo esta metodologia, o u cada tipo de dados deve ter manipulado apenas por um conjunto de funcoes especcas, designadas m todos, conhecedoras dos detalhes internos do tipo de dados associado. Todos os outros blocos e do programa t m apenas que conhecer as propriedades abstractas ou gen ricas deste tipo. A e e manipulacao do tipo s s o acessveis de outros blocos de programa atrav s dos m todos disponi o a e e bilizados pelo tipo de dados. Esta metodologia de programacao garante que, caso seja necess rio alterar os detalhes inter a nos de um determinado tipo, apenas e necess rio alterar os m todos correspondentes a esse tipo. a e Dado que todos os blocos de programas onde este tipo de dados e utilizado apenas acedem a ele atrav s dos m todos disponveis, requerendo apenas o conhecimento das suas propriedades gerais e e (ou abstractas), e possvel minimizar ou eliminar totalmente as alteracoes necess rias aos out a ros blocos de programa. Deste modo, a metodologia de abstraccao de dados contribui para uma relativa estanquecidade e independ ncia dos diversos m dulos constituintes. e o Neste captulo, utilizar-se- nos diversos exemplos uma metodologia estrita de abstraccao de a dados, chamando-se a atencao caso a caso para aspectos especcos da implementacao.

L ISTAS 63

Zona da pilha (var. locais) Varivel Contedo

Zona do heap (var. dinamicas) Contedo

. . .
base

. . .

dados (pos 1)

dados (pos 2)

dados (pos 3)

dados (pos 4) NULL

Figura 4.1: Lista din mica. Todos os elementos s o criados dinamicamente na zona de heap, a a sendo suciente uma vari vel local de tipo apontador (vari vel base, nas gura) para poder aceder a a a todos os elementos da lista.

4.3

Listas

Uma lista din mica n o e mais do que uma coleccao de estruturas de dados, criadas dinamicaa a mente, em que cada elemento disp e de um apontador para o elemento seguinte (gura 4.1). Cada o elemento da lista e constitudo por uma zona de armazenamento de dados e de um apontador para o pr ximo elemento. Para ser possvel identicar o m da lista, no apontador do ultimo elemento o e colocado o valor NULL. Para criar uma lista e necess rio denir um tipo de dados e criar uma vari vel local (normala a mente 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 mero inteiro. A criacao de uma lista de elementos u deste tipo exigiria uma declaracao de tipos e de vari veis como se segue: a

#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 denicao do apontador seguinte, e necess rio utilizar a construcao a struct _tipoLista *seg e n o tipoLista *seg. De facto, apesar de serem duas fora mas aparentemente semelhantes, a forma

typedef struct _tipoLista{ tipoDados dados; tipoLista *seg; /* DECLARACO INVLIDA */ A A } tipoLista;

e inv lida porque tipoLista s e conhecido no nal da declaracao, e n o pode ser utilizado a o a antes de completamente especicado. 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 ncia do conjunto). Adicionalmente, a ordem e dos elementos da lista e apenas denida pela organizacao dos apontadores. Deste modo, se for necess rio introduzir um elemento entre dois j existentes, n o e necess rio deslocar todos os ela a a a ementos de uma posicao, como sucederia num vector. Basta de facto reservar espaco para um elemento adicional e deslocar todos os outros elementos de uma posicao. Na gura 4.2 apresenta se um exemplo em que se ilustra esta independ ncia entre enderecos de mem ria e sequ ncia da e o e ` lista, e confere a esta uma exibilidade superior a de um vector. Dada esta independ ncia entre enderecos de mem ria e sequ ncia, e frequente adoptar-se e o e representacoes gr cas simplicadas para as listas, como a se mostra na gura 4.3. A cruz no a ultimo elemento corresponde a uma representacao simb lica abreviada para o valor NULL. o

L ISTAS 65

Zona da pilha (var. locais) Varivel Contedo

Zona do heap (var. dinamicas) Contedo

. . .
base

. . .

dados (pos 2)

dados (pos 1)

dados (pos 4) NULL

dados (pos 3)

Figura 4.2: Lista din mica. A sequ ncia dos elementos da lista e apenas denida pelos apontadores a e de cada elemento, independentemente do endereco de mem ria ocupado. o

base

Figura 4.3: Lista din mica. Representacao simplicada a

66 L ISTAS DIN AMICAS

Apesar das vantagens j referidas de uma lista, e necess rio ter em atencao que o programa s a a o disp e de uma vari vel para aceder a toda a lista, e que esta tem que ser percorrida elemento a eleo a mento at se atingir a posicao pretendida. Num vector, dado que todos as posicoes s o adjacentes, e a para aceder a qualquer posicao basta saber o endereco do primeiro elemento e o n mero de ordem u (ndice) do elemento que se pretende aceder para ser possvel calcular o endereco do elemento que se pretende. Ou seja, a exibilidade acrescida da lista em termos de organizacao de mem ria e o ` conseguida a custa do tempo de acesso aos seus elementos.

4.4

Listas dinamicas: listar elementos

Uma operacao 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 do de todos os n meros e nomes da lista do exemplo da u u seccao 4.3. Uma solucao para este efeito seria a sequ ncia de c digo seguinte: e o

#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 no se atingir o final... a escreveDados(aux -> dados); /* Escreve o contedo u aux = aux -> seg; /* Avanca para a prxima posico o a } } int main(){ */ */ */ */

L ISTAS DIN AMICAS :

LISTAR ELEMENTOS

67

tipoLista *base; /* Neste ponto, o apontador base e os restantes elementos da lista so, de alguma forma, a inicializados */

/* Listagem do contedo */ U lista(base);

O princpio essencial da operacao de listagem est includa na funcao lista e e muito sim a ples. Inicializa-se um apontador aux para o incio da lista. Seguidamente, enquanto este apon tador for diferente de NULL (ou seja, n o atingir o m da lista), o apontador e sucessivamente a avancado para o elemento seguinte. Note-se como se adoptou neste bloco de c digo a metodologia de abstraccao de dados. Exo iste neste exemplo uma separacao funcional de tarefas entre as diversas entidades que partici pam o programa. A funcao lista manipula unicamente a estrutura de dados que suporta a lista, percorrendo todos os seus elementos at ao m da lista. No entanto, quando e necess rio e a escrever o conte do de cada n no terminal, esta tarefa e delegada a uma funcao especca ( u o escreveDados), respons vel pela manipulacao e processamento dos detalhes especcos de a vari veis do tipo tipoDados. Neste sentido, tipoDados e, para a lista, um tipo abstracto a gen rico, cuja representacao interna ela desconhece, e da qual s tem que conhecer uma proe o priedade muito gen rica: uma vari vel deste tipo pode de alguma forma ser escrita no terminal, e a havendo um m todo competente para o fazer. e Uma forma alternativa da funcao listar frequentemente utilizada passa pela utilizacao 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); }

68 L ISTAS DIN AMICAS

4.5
4.5.1

Listas: pilhas
Introducao

` As listas t m habitualmente uma estrutura e organizacao em mem ria semelhante a apresene o ` tada na gura 4.1. No entanto, os mecanismos de acesso a lista s o vari veis e, no seu conjunto, a a podem permitir que a lista realize apenas um subconjunto de tarefas bem determinadas. Em teoria da computacao, uma pilha (ou stack, na lietratura anglo-sax nica) e um sistema o que armazena dados de forma sequencial e que permite a sua leitura por ordem inversa da escrita. A designacao pilha vem da semelhanca funcional com uma pilha comum. Admita-se que se disp e de um conjunto de palavras que se pretende armazenar. Considere-se ainda que o sistema o de armazenamento disponvel e um conjunto de pratos, sendo cada palavra manuscrita no fundo ` medida que as palavras v o sendo comunicadas ao sistema de armazenamento, s o do prato. A a 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 obvio, a leitura ocorre por ordem inversa da escrita. Uma pilha e frequentemente designada por uma estrutura LIFO, acr nimo derivado da exo press o Last In First Out. A operacao de armazenamento na pilha e geralmente designada de a operacao de push, enquanto que a leitura e designada de pop. As pilhas t m uma utilizacao frequente em inform tica sempre que se pretende inverter uma e a sequ ncia de dados. Embora a relacao n o seja obvia, pode indicar-se, por exemplo, que o c lculo e a a de uma express o aritm tica em que v rios operadores t m nveis de preced ncia diferentes e a e a e e realizada acumulando numa pilha resultados interm dios. e Considere-se, por exemplo, que se pretende inverter os caracteres da palavra Lus. A forma como uma pilha pode contribuir para este resultado est gracamente sugerido na gura 4.4. a Uma pilha pode ser realizada em C atrav s de uma lista din mica ligada. A colocacao de e a 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 operacao de remocao e leitura corresponde a retirar o elemento da base, cando esta a apontar para o elemento seguinte (v. gura 4.5)

L ISTAS :

PILHAS

69

s s

s s u L

u L L u L

u L

u L u L L

Entrada (push) (sobreposio)

Saida (pop) (remoo)

Figura 4.4: Invers o de uma sequ ncia de caracteres por meio de uma pilha. a e

base

Figura 4.5: Realizacao de uma pilha por uma lista ligada. No exemplo, considera-se uma pilha de caracteres e a insercao do caracter no topo da pilha. A tracejado indica-se a ligacao existente ` antes da insercao, a cheio ap s a insercao. A operacao de remocao corresponde a realizacao o do mecanismo inverso (reposicao da ligacao a tracejado e libertacao do elemento de mem ria o correspondente ao ).

70 L ISTAS DIN AMICAS

4.5.2

Declaracao

Tal como qualquer lista, a realizacao de uma pilha implica a declaracao de um tipo de suporte da lista e a utilizacao de uma vari vel para a base da pilha. A declaracao da pilha pode ser feita, a por exemplo, pela declaracao

typedef struct _tipoPilha { tipoDados dados; struct _tipoPilha *seg; } tipoPilha;

onde se admite que tipoDados j foi denido e caracteriza o tipo de informacao registada em a cada posicao da pilha. A utilizacao da pilha implica que no programa principal, ou no bloco de c digo onde se o pretende utilizar a pilha, seja declarada uma vari vel de tipo apontador para tipoPilha que a registe a base da pilha. Ou seja, ser necess rio dispor de uma vari vel declarada, por exemplo, a a a por

tipoPilha *pilha;

4.5.3

Inicializacao

A primeira operacao necess ria para utilizar a pilha e inicializ -la ou cri -la. Uma pilha vazia a a a deve ter a sua base apontada para NULL. Embora seja possvel realizar directamente uma atribuicao ` a vari vel pilha, tratando-se de um detalhe interno de manipulacao do tipo tipoPilha, esta a inicializacao dever ser delegada numa funcao especca. Deste modo, para respeitar a metodolo a gia de abstraccao de dados, deve declarar-se uma pequena funcao

tipoPilha *inicializa(){ return NULL; }

e utilizar-se esta funcao sempre que seja necess rio inicializar a pilha: a

pilha = inicializa();

L ISTAS :

PILHAS

71

4.5.4

Sobreposicao

A operacao de sobreposicao exige a seguinte sequ ncia de operacoes: e

Reserva de mem ria din mica para armazenar um novo elemento. o a C pia dos dados para o elemento criado. o Insercao do novo elemento na base da pilha.

Para a criacao da mem ria din mica, e conveniente criar uma funcao novoNo() que cria o a espaco para um novo n , vericando a simultaneamente a exist ncia de erros na reserva de o e mem ria. Esta funcao pode ser denida como o tipoPilha *novoNo(tipoDados x){ tipoPilha *novo = calloc(1,sizeof(tipoPilha)); if(novo == NULL) Erro("Erro na reserva de memria"); o novo -> dados = x; novo -> seg = NULL; return novo; }

onde Erro() e uma funcao gen rica simples que imprime uma mensagem de erro e termina o e programa (ver seccao 4.5.7). A operacao de insercao no topo da pilha exige a realizacao das operacoes representadas na gura 4.5. Estas podem ser realizada pela seguinte funcao: tipoPilha *sobrepoe(tipoPilha *pilha,tipoDados x){ tipoPilha *novo; novo = novoNo(x); novo -> seg = pilha; return novo; }

Esta funcao, ap s a criacao do novo elemento, limita-se a colocar o apontador seg a apon o ` tar para o elemento que anteriormente se encontrava no topo, e indicar a base que o novo topo corresponde ao novo elemento inserido.

72 L ISTAS DIN AMICAS

Note-se que esta funcao aceita como argumento o apontador original para o incio da lista (topo da pilha) e devolve a nova base alterada.

4.5.5

Remocao

A operacao de remocao da pilha e acompanhada da leitura. Tal como anteriormente a funcao recebe o apontador para a base da lista e devolve a base alterada. Dado que a remocao e acom panhada de leitura, a funcao recebe um segundo apontador, passado por refer ncia, que deve ser e usado para guardar o valor do elemento removido. Uma funcao possvel para a realizacao desta operacao e tipoPilha *retira(tipoPilha *pilha,tipoDados *x){ tipoPilha *aux; if(pilhaVazia(pilha)) Erro("remoco de uma pilha vazia"); a *x = pilha -> dados; aux = pilha -> seg; free(pilha); return aux; } A operacao de remocao e precedida de um teste para vericacao de pilha vazia, para garantia de integridade da mem ria (v. seccao 4.5.6). Note-se que ap s a leitura da pilha, o bloco de o o mem ria utilizado e libertado pela funcao free(). o

4.5.6

Teste

Antes de ser tentada a remocao 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 rio, a se o apontador da base se encontra com o valor NULL. Esta realizacao, ainda que correcta, violaria o princpio essencial de abstraccao de dados que temos vindo a respeitar: a caracterizacao como vazia de uma pilha e um detalhe interno do tipoPilha, e como tal deve ser delegado numa funcao especca, de tipo booleano. Deste modo, poderia fazer-se int pilhaVazia(tipoPilha *pilha){ return (pilha == NULL); }

L ISTAS :

PILHAS

73

Esta funcao retorna 1 se a pilha estiver vazia, e 0 em caso contr rio. a

4.5.7

Exemplo

Suponha-se que se pretende implementar um sistema de invers o de caracteres baseado na a pilha denida anteriormente. Uma das vantagens de utilizar uma pilha para este efeito consiste ` no facto de n o ser necess rio denir a partida a dimens o m xima da cadeia de caracteres: o a a a a programa efectua a reserva e libertacao de mem ria a medida das necessidades do programa. o ` Neste caso, tipoDados e realizado por um simples caracter. Para respeitar e exemplicar o princpio de abstraccao, iremos escrever os m todos especcos de leitura e escrita deste tipo. e De modo a explorar de forma ecaz a independ ncia dos diversos tipos de dados, os m todos e e correspondentes a cada tipo devem ser declarados em cheiros separados. Deste modo, os m todos e de acesso a tipoDados s o agrupados num cheiro dados.c. Os prot tipos correspondentes a o s o declarados no cheiro dados.h. a

Ficheiro dados.h /* * Ficheiro: dados.h * Autor: Fernando M. Silva * Data: 7/11/2002 * Contedo: u * Ficheiro com declaraco de tipos e a * prottipos dos mtodos para manipulaco o e a * de um tipoDados, concretizados aqui * por um tipo caracter simples. */ #ifndef _DADOS_H #define _DADOS_H #include <stdio.h> #include <stdlib.h>

typedef char tipoDados; /* Mtodos de acesso a tipo dados */ e int leDados(tipoDados *x); void escreveDados(tipoDados x); #endif /* _DADOS_H */

74 L ISTAS DIN AMICAS

Ficheiro dados.c /* * * * * * * * */

Ficheiro: dados.c Autor: Fernando M. Silva Data: 12/11/2002 Contedo: u Mtodos para manipulaco de um e a tipoDados, concretizado por um caracter simples.

#include "dados.h" int leDados(tipoDados *x){ /* L um caracter. Devolve 1 se ler um \n */ e *x = getchar(); if(*x == \n) return 1; else return 0; } void escreveDados(tipoDados x){ putchar(x); }

De igual modo, a metodologia de abstraccao de dados implementada sugere que todos os m todos de acesso a tipoPilha sejam agrupados num cheiro pilha.c, cando as e declaracoes e prot tipos correspondentes acessveis num cheiro pilha.h. o Deste modo, ter-se-ia

Ficheiro pilha.h /* * * * * * *

Ficheiro: pilha.h Autor: Fernando M. Silva Data: 7/11/2000 Contedo: u Ficheiro com declaraco de tipos e a prottipos dos mtodos para manipulaco o e a

L ISTAS :

PILHAS

75

* de uma pilha simples de elementos genricos e * de "tipoDados". */ #ifndef _PILHA_H #define _PILHA_H #include "dados.h" typedef struct _tipoPilha { tipoDados dados; struct _tipoPilha *seg; } tipoPilha; /* Prottipos das funces */ o o 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 * Contedo: u * Mtodos para manipulaco de uma pilha suportada e a * numa estrutura dinmica ligada a */ #include "pilha.h" #include "util.h" tipoPilha *novoNo(tipoDados x){ /* * Cria um novo n para a pilha o */ tipoPilha *novo = calloc(1,sizeof(tipoPilha)); if(novo == NULL) Erro("Erro na reserva de memria"); o novo -> dados = x; novo -> seg = NULL; return novo;

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 contrrio a */ return (pilha == NULL); } tipoPilha *sobrepoe(tipoPilha *pilha,tipoDados x){ /* * Adiciona um elemento ` pilha a * 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 varivel que a * retorna o valor lido da pilha * Retorna: * Pilha modificada

L ISTAS :

PILHAS

77

*/ tipoPilha *aux; if(pilhaVazia(pilha)) Erro("remoco de uma pilha vazia"); a *x = pilha -> dados; aux = pilha -> seg; free(pilha); return aux; }

A funcao gen rica de erro Erro() pode ser declarada num m dulo gen rico util.c, onde e o e s o agrupadas funcoes utilit rias gen ricas (neste exemplo, Erro() e unica). O prot tipos correa a e o spondente deve ser declarado num m dulo util.h. o

Ficheiro util.h /* * Ficheiro: util.h * Autor: Fernando M. Silva * Data: 7/11/2002 * Contedo: u * Ficheiro com declaraco de funces e a o * prottipos genricos o e */ #ifndef _UTIL_H #define _UTIL_H

void Erro(char *msgErro); #endif /* _UTIL_H */

Ficheiro util.c /* * * * * * */

Ficheiro: Autor: Data: Contedo: u Funces o

util.c Fernando M. Silva 1/11/2002 genricas e

78 L ISTAS DIN AMICAS

#include <stdio.h> #include <stdlib.h> #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 ultimo, o programa principal. Este limita-se a inicializar a pilha e a efectuar a leitura de um sequ ncia de dados, acumulando-os sucessivamente na pilha. A leitura e efectuada e at que a funcao leDados() (m todo de tipoDados) identique uma mudanca de linha na e e entrada. Seguidamente, os dados s o sucessivamente removidos (desempilhados) e escritos no a dispositivo de sada, at que a pilha esteja vazia. Deste modo, ter-se-ia: e

Ficheiro main.c /* * Ficheiro: main.c * Autor: Fernando M. Silva * Data: 7/11/2000 * Contedo: u * Programa principal simples para teste * de uma pilha, usada para inverso de a * uma cadeia de caracteres */ #include <stdio.h> #include <stdlib.h> #include "pilha.h" int main(){ tipoPilha *pilha; tipoDados x; pilha = inicializa(); printf("Introduza uma sequncia de caracteres:\n"); e while(!leDados(&x)){ pilha = sobrepoe(pilha,x); }

L ISTAS :

PILHAS

79

printf("Sequncia invertida:\n"); e while(!pilhaVazia(pilha)){ pilha = retira(pilha,&x); escreveDados(x); } printf("\n"); exit(0); }

A compilacao autom tica de todos os m dulos pode ser realizada por meio do utilit rio make. a o a A Makefile correspondente poderia ser escrita como

# # Ficheiro: Makefile # Autor: Fernando M. Silva # Data: 7/11/2000 # Contedo: u # Makefile para teste de estruturas dinmicas a # # A varivel CFLAGS especifica as flags usadas a # por omisso nas regras de compilaco a a # # A varivel SOURCES, que define os ficheiro a # fonte em C, s e usada para permitir a o # evocaco do utilitrio "makedepend" (pelo a a # comando make depend), de modo a actualizar # automaticamente as dependncias dos ficheiros e # .o nos ficheiros .h # # A varivel OBJECTS define o conjunto dos a # 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 executveis a # teste: $(OBJECTS) gcc -o $@ $(OBJECTS)

# # A regra make depend efectua uma actualizaco da a # makefile, actualizando as listas de dependncias dos e

80 L ISTAS DIN AMICAS

# ficheiros .o em .c # # depend:: makedepend $(SOURCES) # # Regra clean: make clean apaga todos os ficheiros # reconstruveis do disco # clean:: rm -f *.o a.out * core teste *.bak

# DO NOT DELETE

4.6
4.6.1

Listas: las
Introducao

Referiu-se anteriormente que uma pilha correspondia a uma estrutura LIFO (Last In, First Out). De forma semelhante, podemos descrever uma la como um sistema FIFO (First In, First Out). O exemplo corrente de funcionamento de uma la e a vulgar la de espera. Cada novo elemento armazenado e colocado no nal da la, enquanto que cada elemento retirado e obtido do incio da la (gura 4.6, A). ` Uma la pode ser realizada por meio de uma lista ligada, relativamente a qual s o mantidos a n o um, mas dois apontadores: um para a base ou incio, por onde s o retirados os elementos, e a a outro para o m ou ultimo elemento da lista, que facilita a insercao de novos elementos na lista.1

4.6.2

Declaracao

De forma a que seja possvel manipular uma unica vari vel do tipoFila, esta e realizada a por uma estrutura de dados que agrupa os dois apontadores, incio e m. Deste modo, a declaracao da la pode ser realizada por
De facto, o apontador para a base seria suciente para realizar a la; no entanto, cada operacao de insercao exigiria percorrer a totalidade da la para realizar a insercao no nal, m todo que seria pouco eciente. e
1

L ISTAS :

FILAS

81

fim inicio

A
fim inicio L u s

B
fim inicio L u s

Figura 4.6: Realizacao de uma la de caracteres com uma lista ligada. A - Estrutura da la, B Fila em A ap s a insercao do caracter s (a tracejado, as ligacoes eliminadas), C - Fila em B ap s o o 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

Inicializacao

A inicializacao da la vazia corresponde simplesmente a colocar a NULL os dois apontadores da la. Deste modo, a inicializacao da la pode ser realizada simplesmente por

void inicializa(tipoFila *fila){ fila -> inicio = NULL; fila -> fim = NULL; }

` Note-se que na chamada a funcao o argumento deve ser efectuada por refer ncia (passando o e endereco da vari vel tipoFila) de modo a que a vari vel seja alter vel pela funcao. a a a

82 L ISTAS DIN AMICAS

4.6.4

Insercao

` A insercao de novos elementos corresponde a colocacao de elementos no nal da la, tal como representado gracamente na gura 4.6, caso B. Tal como no caso da pilha, para a criacao da mem ria din mica, e conveniente criar uma o a funcao auxiliar novoNo().

tipoLista *novoNo(tipoDados x){ tipoLista *novo = calloc(1,sizeof(tipoFila)); if(novo == NULL) Erro("Erro na reserva de memria"); o novo -> dados = x; novo -> seg = NULL; return novo; }

A funcao de insercao 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 funcao e necess rio considerar o caso particular em que a la est vazia e em que, a a como tal, o apontador fim est a NULL. Neste caso particular, ambos os apontadores ( inicio a e fim) devem ser colocados ser inicializados com o endereco do novo elemento inserido. No caso habitual (lista n o vazia), basta adicionar o novo elemento ao ultimo da lista e modicar o a apontador fim.

L ISTAS :

FILAS

83

4.6.5

Remocao

` A insercao de novos elementos corresponde a eliminacao de elementos presentes no nal da la, tal como representado gracamente na gura 4.6, caso C. Esta funcao pode ser realizada por

void retira(tipoFila *fila,tipoDados *x){ tipoLista *aux; if(filaVazia(fila)) Erro("Remoco de uma fila vazia"); a *x = fila -> inicio -> dados; aux = fila -> inicio; fila -> inicio = fila -> inicio -> seg; if(fila -> inicio == NULL) fila -> fim = NULL; free(aux); }

A funcao de retira tem tamb m uma estrutura simples. Tal como na funcao de insercao, e e necess rio considerar o caso particular em que a lista tem apenas um elemento, situacao em que o a apontador fim deve ser colocado a NULL depois da remocao. Tal como no caso da pilha, e testada a condicao de pilha vazia antes de efectuar a remocao (ver seccao 4.6.6).

4.6.6

Teste

Para manipulacao da lista e indispens vel dispor de uma funcao para testar se a la se encontra a vazia. Para este teste basta vericar qualquer um dos apontadores se encontra a NULL. Este teste pode ser efectuado pelo c digo o

int filaVazia(tipoFila *fila){ return (fila -> inicio == NULL); }

84 L ISTAS DIN AMICAS

4.6.7

Exemplo

Para uma maior semelhanca com o exemplo da pilha, utiliza-se tamb m neste exemplo o caso e de uma la de caracteres. Como seria de esperar, a la n o realiza neste caso uma invers o, mas a a apenas um armazenamento tempor rio da informacao, permitindo a sua reproducao pela mesma a ordem de entrada. Tal como no exemplo da pilha, o c digo foi distribudo por tr s cheiros: main.c, fila.c o e e dados.c, tendo para os dois ultimos sido desenvolvido um cheiro de prot tipos associado. o Dada a semelhanca com o exemplo da pilha, apresentam-se aqui apenas o conte do u dos v rios cheiros, sem outros coment rios. Omite-se aqui o conte do dos cheiros a a u dados.h, dados.c, util.h e util.c por serem obviamente id nticos aos anteriores. e

Ficheiro la.h /* * Ficheiro: fila.h * Autor: Fernando M. Silva * Data: 7/11/2002 * Contedo: u * Ficheiro com declaraco de tipos e a * prottipos dos mtodos para manipulaco o e a * de uma fila simples de elementos genricos e * 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; /* Prottipos das funces */ o o

L ISTAS :

FILAS

85

void void void int

inicializa(tipoFila *fila); adiciona(tipoFila *fila,tipoDados x); retira(tipoFila *fila,tipoDados *x); filaVazia(tipoFila *fila);

#endif /* _FILA_H */

Ficheiro la.c /* * Ficheiro: fila.c * Autor: Fernando M. Silva * Data: 1/11/2002 * Contedo: u * Mtodos para manipulaco de uma fila suportada e a * numa estrutura dinmica ligada a */ #include "fila.h" #include "util.h" tipoLista *novoNo(tipoDados x){ /* * Cria um novo n para a fila o */ tipoLista *novo = calloc(1,sizeof(tipoFila)); if(novo == NULL) Erro("Erro na reserva de memria"); o 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:

86 L ISTAS DIN AMICAS

* fila - Apontador para a base da fila * Retorna: * 1 se a fila estiver vazia * 0 em caso contrrio a */ return (fila -> inicio == NULL); } void adiciona(tipoFila *fila,tipoDados x){ /* * Adiciona um elemento ` fila a * 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 varivel que a * retorna o valor lido da fila */ tipoLista *aux; if(filaVazia(fila)) Erro("Remoco de uma fila vazia"); a *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 * Contedo: u * Programa principal simples para teste * de uma fila. */ #include <stdio.h> #include <stdlib.h> #include "fila.h" int main(){ tipoFila fila; tipoDados x; inicializa(&fila); printf("Introduza uma sequncia de caracteres:\n"); e while(!leDados(&x)){ adiciona(&fila,x); } printf("Sequncia lida:\n"); e while(!filaVazia(&fila)){ retira(&fila,&x); escreveDados(x); } printf("\n"); exit(0); }

4.7
4.7.1

Listas ordenadas
Introducao

Um vector pode ser ordenado segundo uma qualquer relacao de ordem utilizando um algo ritmo apropriado(Knuth, 1973) (selection sort, bubble-sort, quicksort ou qualquer outro). Embora estes algoritmos estejam bem estudados e sejam frequentemente necess rios, todos eles requerem a possvel provar que a quantidade de trabalho necess ria um esforco computacional signicativo. E a para esta tarefa n o e apenas proporcional ao n mero de elementos a ordenar: a complexidade da a u

88 L ISTAS DIN AMICAS

tarefa aumenta signicativamente mais que a dimens o do vector. Para compreender melhor a a quantidade de trabalho exigido, podemos comparar esta situacao ao de arrumar alfabeticamente uma biblioteca a partir de uma situacao desorganizada. Se arrumar uma biblioteca com 5,000 livros demorar um dia, arrumar uma biblioteca com 10,000 livros n o demorar apenas dois dias, a a mas sim tr s ou quatro, j que cada ttulo individual ter que ser alfabeticamente comparado com e a a um n mero muito maior de outros livros. u No caso de uma biblioteca, se se pretender evitar a tarefa herc lea de ordenar todos os livros, u o melhor e mant -la sempre ordenada. Desta forma, cada novo livro adicionado ter apenas que e a ser colocado no local certo, tarefa obviamente muito mais r pida. a Em C, quando os elementos a ordenar se encontram armazenados num vector, o m todo ane terior corresponde basicamente a encontrar o local de insercao, deslocar todos os elementos pos teriores de uma posicao, e inserir o elemento na posicao assim aberta. Embora o algoritmo seja simples, a tarefa de deslocar todos os elementos de uma posicao e computacionalmente pesada, sobretudo quando a insercao se efectua nas posicoes iniciais. O paralelo que podemos encontrar no exemplo da biblioteca e o de uma estante repleta de livros, excepto na ultima prateleira. A introducao ordenada de um novo ttulo na primeira prateleira exige deslocar todos os ttulos de uma posicao. Dado que s existe espaco disponvel na ultima prateleira, este processo pode exigir o de facto movimentar livros em todas as prateleiras da estante. As listas din micas oferecem uma forma simples de criar e manter ecientemente uma a coleccao de objectos ordenados. Ao contr rio do vector, em que todas as posicoes se encon a tram em enderecos de mem ria contguos, a ordem dos elementos da lista n o depende do seu o a endereco de mem ria, mas apenas da ordem denida pela sequ ncia de apontadores, tal como j o e a se mostrou 4.2. As operacoes necess rias para a manipulacao e manutencao de uma lista ordenada s o a a generalizacoes relativamente simples dos casos da pilha e da la. Algumas destas operacoes, como criar a lista ou listar os seus elementos, j foram apresentadas anteriormente. Apenas algua mas das operacoes descritas seguidamente, como a insercao, procura e remocao no meio da lista s o de facto totalmente novas. a

4.7.2

Declaracao e inicializacao

A declaracao e inicializacao de uma lista segue os passos j vistos para o caso da pilha. A a declaracao 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 dene a informacao armazenada em cada elemento. ` De igual modo, a inicializacao da lista corresponde apenas a inicializacao do apontador a NULL, por meio da funcao de inicializacao tipoLista *inicializa(){ return NULL; }

4.7.3

Listagem ordenada

A listagem ordenada corresponde apenas a uma listagem convencional do conte do. Deste u 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 encontrar o e elemento que se procura, e retornar um apontador para o respectivo elemento. Convenciona-se que a funcao de procura deve retornar NULL caso o elemento a procurar n o exista. a Para assentar ideias, admita-se temporariamente que se est a lidar com uma lista de inteiros. a Deste modo, presume-se que se deniu previamente typedef int tipoDados;

90 L ISTAS DIN AMICAS

Neste caso, a funcao de procura pode ser realizada por tipoLista *procura(tipoLista *base,tipoDados x){ tipoLista *aux; aux = base; while((aux!=NULL) && aux = aux -> seg; return aux; } A funcao anterior, embora funcione, apresenta o inconveniente de n o explorar o facto da lista a 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 mero 15 ser necess rio ir at ao m da lista u a a e para concluir que o n mero n o est presente. Como e obvio, esta conclus o poderia ter sido tirada u a a a muito mais cedo, logo que fosse atingido o n mero 16 na lista. u Deste modo, uma vers o mais optimizada da funcao de procura pode ser escrita como a tipoLista *procuraOrdenado(tipoLista *base,tipoDados x){ tipoLista *aux = base; while((aux!=NULL) && (aux -> dados<x)) aux = aux -> 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 posicao actual da lista. Quando o ciclo e interrompido, e testado se se se atingiu o m da lista ou se se encontrou o elemento procurado. Nesta funcao, vale a pena considerar a estrutura do teste if((aux != NULL) && ... } (aux -> dados == x)){

aux -> dados != x)

e vericar a forma como se toma partido do modo como o C avalia express es l gicas. Repare-se o o que o segundo operando da disjuncao ( aux -> dados == x) s pode ser avaliado se aux o

L ISTAS ORDENADAS 91

for diferente de NULL, j que de outra forma se poderia estar a gerar uma violacao de mem ria a o (tentativa de acesso atrav s do endereco 0, o que se encontra fora do controlo do programador). e No entanto, o C garante que realiza a avaliacao de express es l gicas da esquerda para a direita e o o que interrompe a sua avaliacao assim que for possvel determinar univocamente o resultado nal. Neste exemplo, se aux for NULL, o primeiro operando da conjuncao l gica && e falso, o que o implica que o resultado global da express o tamb m o e. Assim, n o sendo necess rio o c lculo a e a a a do segundo operando, n o h o risco de se produzir a violacao de mem ria decorrente do acesso a a o atrav s de um apontador NULL. e Rera-se, por ultimo, que num problema pr tico podem coexistir v rias funcoes de procura, a a consoante o que se pretende encontrar. Por exemplo, se os elementos de uma lista s o estruturas a que incluem, por exemplo, um n mero e um nome, podem ser escritas duas funcoes de procura, u uma para o n mero e outra para o nome. Claro que se a lista estiver ordenada por n meros, a u u busca pelo n mero poder ser optimizada (procura s at encontrar um elemento de n mero igual u a o e u ou superior ao procurado), mas a busca pelo nome ter obviamente que ser exaustiva se o nome a n o existir, j que s no nal da lista ser possvel ter a certeza de que determinado nome n o faz a a o a a parte da lista.

4.7.5

Abstraccao de dados e metodos de teste

Nas duas funcoes de procura anteriores admitiu-se que o tipoDados era um inteiro. Esta hip tese permitiu utilizar os operadores relacionais == e < de uma forma intuitiva. o No caso mais geral em que tipoDados e um tipo abstracto gen rico esta comparacao e n o pode ser realizada directamente pelos operadores relacionais. Para demonstrar este facto, a considere-se que tipoDados corresponde a uma estrutura com um n mero e um nome de um u aluno. Neste caso, n o faz sentido usar o operador relacional < para comparar dois alunos a e a b. De facto, o C desconhece o que se pretende comparar: e o nome dos alunos a e b ou os seus n meros? u Para contornar esta diculdade, seria possvel utilizar o operador . (membro de estrutura) e aceder directamente ao campo num rico, utilizando o operador < para a comparacao, ou aceder e ao campo com o nome e utilizar a funcao strcmp() de forma adequada. No entanto, qualquer destas solucoes estaria a violar o princpio de abstraccao de dados, j que os m todos de procura a e da lista, para quem tipoDados deveria ser um tipo abstracto gen rico, estariam a aceder direce tamente a detalhes internos do tipo. A forma de resolver esta quest o e as funcoes procura delegarem o processo de comparacao a

92 L ISTAS DIN AMICAS

em m todos (funcoes) especcos internos de tipoDados, respons veis pela implementacao dos e a detalhes pr ticos da comparacao. Deste modo, admitindo que foram associados ao tipo abstracto a tipoDados um m todo com prot tipo e o

int menor(tipoDados a,tipoDados b);

que devolve 1 se a for de algum modo anterior a b e 0 em caso contr rio (as funcoes da lista n o a a precisam de conhecer os detalhes desta comparacao) e um outro

int igual(tipoDados a,tipoDados b);

que devolve 1 se a for igual b segundo um dado crit rio e 0 em caso contr rio. e a Deste modo, uma solucao mais geral da funcao 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 unicar todos os m todos de comparacao numa unica funcao e que devolve -1, 0 ou 1 consoante o primeiro elemento e anterior, igual ou posterior ao segundo. E esta solucao que e adoptada na funcao strcmp() para comparacao de strings.

4.7.6

Insercao ordenada

Para realizar uma insercao ordenada e suciente percorrer a lista at encontrar um elemento e que seja superior ao que se pretende inserir. O novo elemento dever ser colocado antes deste. a Apesar desta metodologia simples, a insercao ordenada requer algumas consideracoes suple mentares. Um dos pontos a ter em conta e que para posicionar o novo elemento antes do que foi identicado e necess rio alterar o apontador do elemento que se encontra antes deste. Ou seja, ao a

L ISTAS ORDENADAS 93

antes

depois

10

novo

Figura 4.7: Insercao entre dois elementos da lista. O apontador antes n o aponta para o elemento a anterior da lista, mas sim para o campo apontador seg desse elemento. percorrer a lista, e necess rio manter uma refer ncia n o apenas para o elemento da lista que est a e a a a ser comparado (elemento actual) mas tamb m para o seu predecessor (elemento anterior). e De modo a melhor compreender esta operacao, e conveniente comecar por desenvolver uma funcao auxiliar de insercao em que se admite que a posicao de insercao j foi determinada e, como a tal, que j existem refer ncias para o apontador do elemento anterior e para o elemento actual a e (v. gura 4.7). Designaremos estas vari veis de refer ncia por antes e depois. Neste caso o a e c digo desta funcao pode ser escrito o void insere(tipoLista **antes, tipoDados x, tipoLista *depois){ tipoLista *novo = novoNo(x); *antes = novo; novo -> seg = depois; } ` onde a funcao novoNo() e semelhantes as anteriores tipoLista *novoNo(tipoDados x){ tipoLista *novo = calloc(1,sizeof(tipoLista)); if(novo == NULL) Erro("Erro na reserva de memria"); o novo -> dados = x; novo -> seg = NULL; return novo; } Uma vez denida esta funcao, a insercao ordenada apenas tem que ter em conta alguns casos particulares, nomeadamente a insercao numa la vazia ou a insercao antes do incio. 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 funcao insereOrdenado() pode ent o ser escrita a 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 funcao recebe como um dos argumentos o valor da base e devolve o valor desta depois e ` da insercao. A base retornada e id ntica a base inicial, excepto quando a insercao e feita no incio da lista. O par de funcoes insere() e insereOrdenado() foi escrita de modo a sublinhar os ` diversos aspectos necess rios a insercao numa lista. No entanto, o C permite escrever uma funcao a de insercao 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

Figura 4.8: Remocao. O apontador reg referencia o campo seg do registo anterior da lista. }

Aqui, o primeiro if abrange simultaneamente a insercao 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

Remocao

Tal como a insercao, a remocao pode ser simplicada se for escrita uma funcao auxiliar libertaReg que e executada quando j e conhecido o local de remocao. Esta funcao (v. gura a 4.8) pode ser escrita tipoLista *libertaReg(tipoLista *reg){ tipoLista *aux; aux = reg; reg = reg -> seg; free(aux); return reg; }

Seguidamente, e necess rio escrever uma funcao que procure o o elemento a apagar e chame a a funcao anterior. Esta funcao 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 digo completo de um programa que manipula uma o 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 ncia de racionais. Um racional positivo e e inserido na lista, enquanto que um racional negativo provoca uma tentativa de remocao do seu 3 3 sim trico, caso este exista (ou seja, a indicacao de 5 provoca a remocao do racional 5 ). e Omite-se aqui a listagem dos cheiros util.h e util.c, id nticos ao introduzido no exe emplo da pilha (v. seccao 4.5.7).

Ficheiro dados.h /* * * * * * * * * * * * # */

Ficheiro: lista.h Autor: Fernando M. Silva Data: 7/11/2000 Contedo: u Definico do tipo genrico a e "tipoDados" usado nos exemplos de estruturas de dados dinmicas. a Para fins de exemplo, "tipoDados" realizado por uma fracco inteira, e a a qual e especificada por um numerador e um denominador

#ifndef _DADOS_H

L ISTAS ORDENADAS 97

#define _DADOS_H #include <stdio.h> 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 Contedo: u Mtodos de acesso exemplificativos e da definico de um tipo abstracto "tipoDados" a 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 um racional e 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 invlido\n"); a } }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 contrrio a */ 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 contrrio a */ 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 simtrico do racional x e * Argumento:

100 L ISTAS DIN AMICAS

* x - racional * Retorna * simtrico de x e */ 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 * Contedo: u * Ficheiro com declaraco de tipos e a * prottipos dos mtodos para manipulaco o e a * de uma lista dinmica simples a * */ #ifndef _LISTA_H #define _LISTA_H #include <stdio.h> #include <stdlib.h> /* * Tipo dos dados da lista */ #include "dados.h" /* * Definico de tipoLista a */ typedef struct _tipoLista { tipoDados dados; struct _tipoLista *seg; } tipoLista; /* * Prottipos dos mtodos de acesso o e */

L ISTAS ORDENADAS 101

/* Inicializaco */ a tipoLista *inicializa(void); /* * Procura x na lista iniciada por base, retornando um apontador * para o registo que contem este valor (ou NULL se no existe) a */ 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 contm x e * 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

* Contedo: u * Mtodos para manipulaco e a * de uma lista dinmica a * simples (ordenada) * */ /* * Inclui ficheiro com tipo e prottipos o */ #include "lista.h" #include "util.h" tipoLista *novoNo(tipoDados x){ /* * Cria um novo n da lista o */ tipoLista *novo = calloc(1,sizeof(tipoLista)); if(novo == NULL) Erro("Erro na reserva de memria"); o 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){ /* * Idntico ao anterior, mas optimizado para listas e * 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 inserco a */ 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 memria associada. o * Argumentos: * reg - apontador para a varivel da lista que a * 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 contm x e * 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 Contedo: u Programa principal simples para teste de estruturas dinmicas ligadas. a Neste teste, os dados armazenados na lista so fracces inteiras, implementadas a o por um tipo abstracto "tipoDados". Assim: 1. A especificaco de um racional positivo, a

106 L ISTAS DIN AMICAS

` * acrescenta o racional a lista * 2. A introduco de um racional negativo, a * especifica que deve ser apagada o seu * simtrico da lista e * 3. Com a entrada de um racional com valor 0, * termina o programa * */ #include <stdio.h> #include <stdlib.h> #include "lista.h" int main(){ tipoLista *base; tipoDados x; base = inicializa(); printf(" Programa para teste de uma lista " "ordenada de racionais:\n"); printf(" - A introduco de um racional positivo " a "conduz ` sua\n" a " inserco ordenada na lista.\n"); a printf(" - A introduco de um racional " a "negativo procura o seu\n" " simtrico na lista e, " 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 idntico a "); e escreveDados(x); printf(" no encontrado na lista\n"); a } else{ base = apaga(base,x); } } printf("\nContedo actual da lista:\n"); u

VARIANTES 107

base

Figura 4.9: Lista de inteiros com registo separado para a base. Nesta gura, a lista tem apenas tr s e 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
4.8.1

Variantes
Introducao

Embora a estrutura fundamental das listas din micas seja no essencial a que se viu anteriora mente, existem diversas variantes que t m como objectivo simplicar os mecanismos de acesso e ou ajustar a lista a objectivos especcos.

4.8.2

Listas com registo separado para a base

Conforme se viu anteriormente, a manipulacao 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 incio da lista (registo separado para a base) que, embora n o a seja utilizado de facto, permite simplicar o acesso aos restantes elementos. Adicionalmente, este tipo de estrutura (v. gura 4.9 tem a vantagem do apontador para a base n o ser modicado depois a da criacao da lista. ` As funcoes de manipulacao da lista s o no essencial semelhantes as da lista ordenada simples. a Embora sem listar aqui todas as funcoes de acesso, referem-se algumas das que s o modicadas a pela exist ncia de um registo permanente na base. e ` A inicializacao da lista corresponde neste caso a criacao do registo da base. Deste modo, a

108 L ISTAS DIN AMICAS

fim inicio

Figura 4.10: Lista dupla de inteiros. funcao de inicializacao e dada por

Por outro lado, a funcao de insercao ordenada pode ser simplicada. Admitindo a utilizacao da mesma funcao insere() j apresentado para a lista simples, a insercao pode ser feita por a

` onde se pode constatar a aus ncia do teste particular a base que se realiza na lista ordenada simples. e

4.8.3

Listas duplamente ligadas

Um dos inconvenientes das listas simplesmente ligadas e serem unidireccionais. Por outras a palavras, ao aceder a um dado elemento da lista e f cil aceder ao elemento seguinte, mas o acesso ao elemento anterior n o e possvel. a Para evitar este inconveniente e possvel desenvolver uma lista duplamente ligada, em que cada elemento disp e de dois apontadores, um para o elemento seguinte e outro para o anterior. o ` Adicionalmente, tal como no caso da la, o acesso a lista e geralmente mantido por dois apontadores, um para o incio e outro para o m da lista (v. gura 4.10). A declaracao 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

Figura 4.11: Lista em anel de inteiros. O ultimo elemento aponta para o primeiro
base

Figura 4.12: Anel duplo com registo separado para a base.

4.8.4

Aneis

Numa lista simplesmente ligada, o ultimo da elemento da lista e identicado pelo facto do apontador para o elemento seguinte ser NULL. Uma alternativa e colocar o ultimo elemento da lista a apontar para a base (v. gura 4.11). A manipulacao de uma estrutura deste tipo e muito ` semelhante a de uma lista simples, substituindo-se apenas parte das comparacoes com o valor NULL por comparacoes com o apontador da base. Uma lista em anel apresenta a vantagem do ultimo elemento apontar para o primeiro, o que pode ser conveniente em aplicacoes em que seja necess rio percorrer os elementos da lista de uma a forma cclica.

4.9
4.9.1

Anel duplo com registo separado para a base


Introducao

Um anel duplo com registo separado para a base e uma estrutura din mica que combina as a diversas variantes abordadas anteriormente: registo separado para a base, ligacao em anel e reg istos com apontadores bidireccionais (gura 4.12). Embora este tipo estrutura possa sugerir uma manipulacao a partida mais complexa, verica-se na pr tica o inverso: a exploracao correcta desta ` a estrutura origina, de modo geral, c digo mais simples. o

110 L ISTAS DIN AMICAS

base

Figura 4.13: Anel vazio ap s a criacao. o

4.9.2

Declaracao

A declaracao de um anel e feita da forma habitual. Ou seja,

typedef struct _tipoAnel { tipoDados dados; struct _tipoAnel *seg,*ant; } tipoAnel;

4.9.3

Inicializacao

` A inicializacao de um anel corresponde a criacao de um registo para a base e ao fecho em anel das suas ligacoes (gura 4.13). Esta inicializacao pode ser feita por

tipoAnel *inicializa(){ tipoAnel *aux; tipoDados regMudo; aux = novoNo(regMudo); aux -> seg = aux -> ant = aux; return aux; }

` A funcao novoNo(), semelhante as anteriores, pode ser denida como

tipoAnel *novoNo(tipoDados x){ tipoAnel *novo = calloc(1,sizeof(tipoAnel)); if(novo == NULL) Erro("Erro na reserva de memria"); o 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 funcoes 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 funcoes o incio da listagem se efectua no elemento a seguir ` a base (de modo a saltar o registo da base), enquanto que na condicao de manutencao no ciclo se ` testa o regresso a base.

4.9.5

Procura

` A funcao 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 simplicar a condicao do ciclo, se iniciou o registo da base com o pr prio o valor do elemento a procurar. Este artifcio garante que, mesmo que o elemento de procura n o a se encontre no anel, a procura e interrompida quando se atinge a base. Claro que, deste modo, ` e preciso testar a sada do ciclo se a interrupcao se vericou por ter encontrado o local real de interrupcao e o elemento de procura, ou por se ter encontrado o elemento articialmente colocado na base. Neste ultimo caso dever retornar-se o apontador NULL. a

4.9.6

Insercao

No caso de insercao ordenada no anel, e particularmente util utilizar uma funcao s para a o insercao do registo, admitindo que j se conhece o local de insercao, e desenvolver posteriormente a a funcao de procura o local de insercao. A primeira destas funcoes pode ser denida 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 funcao, a sua realizacao corresponde apenas a realizacao das ligacoes necess rias pela ordem correcta. Na gura 4.14 detalha-se este processo de insercao a no caso de um anel de inteiros, identicando-se a correspond ncia entre as ligacao e as instrucoes e da listagem.

A NEL DUPLO COM REGISTO SEPARADO PARA A BASE 113

depois

(2) novo

(4)

(1)

(3)

` Figura 4.14: Insercao num anel. Os n meros entre () correspondem as atribuicoes da listagem u apresentada no texto. Repare-se que, dado que a lista e dupla, e suciente o apontador para o elemento seguinte para estabelecer todas as ligacoes. A funcao de insercao propriamente dita realiza apenas a pesquisa do local de insercao. De forma semelhante ao j efectuado no caso da funcao de procura, e possvel colocar uma c pia a o do elemento a inserir no registo da base para permitir que o caso particular de insercao no nal da lista n o tem que ser tratado como um caso particular. Deste modo, o c digo da funcao ca a o 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

Remocao

A operacao de remocao pode ser realizada por duas funcoes complementares. A primeira (removeReg()) e utilizada ap s a identicacao do registo a eliminar e e respons vel pela o a libertacao da mem ria ocupada e reconstrucao das ligacoes. A segunda (apaga()) que corre o ` sponde a funcao a utilizar externamente, procura o registo a eliminar e, caso o identique, 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 apresentado anteriormente. a Neste caso, omite-se a listagem dos m todos do tipo tipoDados, e os cheiros util.c e e util.h, obviamente id ntico ao anterior. e

Ficheiro anel.h /* * Ficheiro: anel.h * Autor: Fernando M. Silva * Data: 7/11/2000 * Contedo: u * Ficheiro com declaraco de tipos e a * prottipos dos mtodos para manipulaco o e a * de um anel com registo seoparado * para a base * */ #ifndef _ANEL_H #define _ANEL_H #include <stdio.h>

A NEL DUPLO COM REGISTO SEPARADO PARA A BASE 115

#include <stdlib.h> /* * Tipo dos dados da lista */ #include "dados.h" /* * Definico de tipoAnel a */ typedef struct _tipoAnel { tipoDados dados; struct _tipoAnel *seg,*ant; } tipoAnel; /* * Prottipos dos mtodos de acesso o e */ /* Inicializaco */ a tipoAnel *inicializa(void); /* * Procura x na lista iniciada por base, retornando um apontador * para o registo que contem este valor (ou NULL se no existe) a */ 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 contm x e */ void apaga(tipoAnel *base,tipoDados x); #endif

Ficheiro anel.c /* * * * * * * * * */

Ficheiro: anel.c Autor: Fernando M. Silva Data: 7/11/2000 Contedo: u Mtodos para manipulaco e a de um anel com registo separado para a base.

/* * Inclui ficheiro com tipo e prottipos o */ #include "anel.h" #include "util.h" tipoAnel *novoNo(tipoDados x){ /* * Cria um novo n da lista o */ tipoAnel *novo = calloc(1,sizeof(tipoAnel)); if(novo == NULL) Erro("Erro na reserva de memria"); o novo -> dados = x; novo -> seg = novo -> ant = NULL; return novo; } /* Inicializaco. O anel vazio constituda pelo a e * 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 no existe) a * * 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); } /* * Idntico ao anterior, mas optimizado para aneis e * 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 inserco a */ 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 memria associada. o */ void libertaReg(tipoAnel *reg){ reg -> seg -> ant = reg -> ant; reg -> ant -> seg = reg -> seg; free(reg); } /* * Apaga do anel o registo que contm x e * 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 Contedo: u Programa principal simples para teste de estruturas dinmicas ligadas. a Neste teste, os dados armazenados na lista so fracces inteiras, implementadas a o por um tipo abstracto "tipoDados". Assim: 1. A especificaco de um racional positivo, a

120 L ISTAS DIN AMICAS

` * acrescenta o racional a lista * 2. A introduco de um racional negativo, a * especifica que deve ser apagada o seu * simtrico da lista e * 3. Com a entrada de um racional com valor 0, * termina o programa * */ #include <stdio.h> #include <stdlib.h> #include "anel.h" int main(){ tipoAnel *base; tipoDados x; base = inicializa(); printf(" Programa para teste de uma lista ordenada de racionais:\n"); printf(" - A introduco de um racional positivo conduz ` sua\n" a a " inserco ordenada na lista.\n"); a printf(" - A introduco de um racional negativo procura o seu\n" a " simtrico na lista e, caso exista, remove-o.\n"); e 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 idntico a "); e escreveDados(x); printf(" no encontrado na lista\n"); a } else{ apaga(base,x); } } printf("\nContedo actual do anel (ordem crescente):\n"); u listar(base); printf("\nContedo por ordem inversa:\n"); u 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 implementacao de uma dada aplicacao requerer a utilizacao hier rquica de di a versas 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 especca, como uma lista das datas em que o documento foi alterado e as alteracoes fundamentais de cada vez. Uma representacao simb lica de uma lista de listas est representada na gura 4.15. o a Uma lista de listas (ou um anel de an is) nada tem de particular do ponto de vista de e programacao. Cada lista por si s u uma estrutura do tipo apontado anteriormente. No entanto, o e necess rio ter em atencao que a lista e as suas sublistas t m tipos diferentes e, de acordo com a e o modelo que temos vindo a desenvolver, cada uma delas precisar de um m todo especco de a e acesso. Por outras palavras, ser necess rio manter duas funcoes de listagem, duas funcoes de a a 2. insercao, etc, dado que cada tipo de lista exige um m todo especco e A declaracao de uma lista de lista pode ser efectuada pelo c digo o

typedef struct _tipoSubLista { /*


De facto, esta duplicacao de c digo pode ser evitada escrevendo c digo baseado em apontadores gen ricos do tipo o o e void*. Esta possibilidade n o ser , por agora, abordada a a
2

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{ /* declaraco dos campos necessrios a a 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, nalmente, o tipoListaDeListas, que suporta a lista principal. A manipulacao da lista faz-se pela combinacao das funcoes (m todos) desenvolvidos para e 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 rio inserir uma vari vel a a x de tipoDados na sublista suportada no elemento da lista principal que tem o valor y. O c digo o para este efeito seria:

tipoListaDeListas *base,*aux; tipoDadosSubLista x; tipoDadosLista y;

L ISTAS DE LISTAS 123

/* inicializaco da lista e dos valores x e y a ... */ aux = procuraOrdenadoListaDeListas(base,y); if(aux == NULL) fprintf(stderr,"Erro: valor no encontrado na lista principal"); a else insereDados(aux -> dados,x); /* ... */

A funcao insereDados e nova neste contexto, e dever ser um m todo especco de a e ` tipoDadosLista. A sua estrutura e muito simples, e a sua exist ncia deve-se apenas a nee cessidade de manter uma realizacao correcta da abstraccao de dados. Esta funcao seria

void insereDados(tipoDadosLista dados,tipoDadosSubLista x){ insereDadosSubLista(dados.baseSub,x); }

sendo a funcao insereDadosSubLista uma funcao convencional de insercao.

Captulo 5 Conclusoes

O C e provavelmente a mais exvel das linguagens de programacao de alto-nvel, mas ap resenta uma relativa complexidade sint ctica. Uma das maiores diculdades na abordagem do a C numa disciplina introdut ria de programacao e a necessidade de introduzir os conceitos de o endereco de mem ria, apontador e mem ria din mica. o o a Neste texto introduziu-se a nocao de apontador e discutiu-se o problema da manipulacao de estruturas de dados din micas em C. Apesar da introducao de diversas estruturas de dados, o a primeiro objectivo deste texto foi, sobretudo, o de tentar explicar os mecanismos essenciais de manipulacao de apontadores e gest o de mem ria din mica em C, utilizando-se algumas estruturas a o a de dados simples para exemplicar estes mecanismos. O aprofundamento destes temas tem o seu seguimento natural numa disciplina especca de Algoritmos e Estruturas de Dados, ou em textos especcos de algoritmia, como (Sedgwick, 1990; Cormen e outros, 1990; Knuth, 1973).

Bibliograa

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 ginas 10119. Addison-Wesley, Reading, Massachusetts, second edition. a 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 ginas a 365375. Sedgwick (1990). Algorithms in C. Addison Wesley.

B IBLIOGRAFIA 129

Apendice A
Programa de teste que valida a consist ncia das atribuicoes da tabela 2.1, apresentada na e seccao 2.6.4. Note-se que o facto de o programa compilar sem erros garante a consist ncia dos e tipos nas atribuicoes realizadas. Nos processadores da famlia Intel, cada endereco de mem ria especica apenas um byte, o ` apesar de, nos processadores 486 e seguintes, cada operacao de acesso a mem ria se realizar em o palavras de 4 bytes (32 bits). De forma a obter-se valores equivalentes ao do modelo de mem ria o usado nos exemplos, realiza-se uma divis o por quatro e ajusta-se o endereco escrito no monitor a de forma a que o elemento x[0][0] surja com o valor 1001.

#include <stdio.h> 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; pf = *(x+1); pf = *(x+2)+1; f = *(*(x+2)+1); pf = *x; f = **x; f = *(*x+1); pf = x[1]; return 0; } escreveEndereco((int) pv3); escreveEndereco((int)pv3); escreveEndereco((int)pf); escreveEndereco((int)pf); printf("%4.1f\n",f); escreveEndereco((int)pf); printf("%4.1f\n",f); printf("%4.1f\n",f); escreveEndereco((int)pf);

Вам также может понравиться