Você está na página 1de 139

Apontadores e Estruturas de Dados Dinamicas em C

Fernando Mira da Silva

Departamento de Engenharia Electrotecnica e de Computadores


Instituto Superior Tecnico
Novembro de 2002

Resumo
O C e provavelmente a mais flexvel das linguagens de programaca o de alto-nvel, mas apresenta uma relativa complexidade sintactica. Uma das maiores dificuldades na abordagem do
C numa disciplina de introdutoria de programaca o e a necessidade de introduzir os conceitos de
endereco de memoria, apontador e memoria dinamica.
Este texto foi preparado para apoio a` disciplina de Introduca o a` Programaca o da Licenciatura
em Engenharia Electrotecnica e Computadores do Instituto Superior Tecnico. Este texto tenta
focar de modo sistematico alguns dos topicos que maiores duvidas suscita nas abordagens iniciais
da linguagem: apontadores e estruturas de dados dinamicas. Assim, embora se pressuponha o
conhecimentos dos elementos basicas da linguagem C por parte do leitor nomeadamente, os
tipos de dados elementares e as estruturas de controlo o texto e mantido ao nvel elementar de
uma disciplina introdutoria de informatica.
Na apresentaca o das estruturas de dados consideradas, que incluem pilhas, filas, listas e
aneis, introduz-se de forma natural a noca o de abstracca o de dados, e os princpios essenciais
de estruturaca o e modularidade baseados neste paradigma de programaca o.
Para o programador experiente em C, alguns dos exemplos de codigo poderao parecer pouco
optimizados. Trata-se de uma opca o premeditada que tenta beneficiar a clareza e a simplicidade
algortmica, ainda que em alguns casos esta opca o possa sacrificar ligeiramente a eficiencia do
codigo apresentado. Pensamos, no entanto, que esta e a opca o correcta numa abordagem introdutoria da programaca o.

Indice

1 Introduca o

2 Apontadores

2.1

Motivaca o para os apontadores em C . . . . . . . . . . . . . . . . . . . . . . . .

2.2

Modelos de memoria em C . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

2.3

Apontadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

2.4

Funco es e passagem por referencia . . . . . . . . . . . . . . . . . . . . . . . . .

11

2.4.1

Passagem por referencia . . . . . . . . . . . . . . . . . . . . . . . . . .

11

2.4.2

Erros frequentes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

16

Vectores e apontadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

18

2.5.1

Declaraca o de vectores . . . . . . . . . . . . . . . . . . . . . . . . . . .

18

2.5.2

Aritmetica de apontadores . . . . . . . . . . . . . . . . . . . . . . . . .

22

2.5.3

Indices e apontadores . . . . . . . . . . . . . . . . . . . . . . . . . . . .

25

2.5.4

Vectores como argumentos de funco es . . . . . . . . . . . . . . . . . . .

26

Matrizes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

28

Declaraca o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

28

2.5

2.6

2.6.1

IV

INDICE

2.7

Matrizes como argumento de funco es . . . . . . . . . . . . . . . . . . .

29

2.6.3

Matrizes e vectores . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

31

2.6.4

Matrizes e apontadores . . . . . . . . . . . . . . . . . . . . . . . . . . .

34

Generalizaca o para mais do que duas dimensoes . . . . . . . . . . . . . . . . . .

38

Vectores e memoria dinamica

41

3.1

Introduca o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

41

3.2

Vectores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

43

3.3

Vectores dinamicos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

46

3.4

Gestao da memoria dinamica . . . . . . . . . . . . . . . . . . . . . . . . . . . .

48

3.4.1

Verificaca o da reserva de memoria . . . . . . . . . . . . . . . . . . . . .

48

3.4.2

Outras funco es de gestao de memoria dinamica . . . . . . . . . . . . . .

50

3.4.3

Garbbage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

52

Criaca o dinamica de matrizes . . . . . . . . . . . . . . . . . . . . . . . . . . . .

55

3.5.1

Introduca o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

55

3.5.2

Matrizes estaticas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

55

3.5.3

Matrizes dinamicas . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

56

3.5.4

Vectores de apontadores e matrizes . . . . . . . . . . . . . . . . . . . .

58

3.5

2.6.2

Listas dinamicas

61

4.1

Introduca o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

61

4.2

Abstracca o de dados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

62

INDICE

4.3

Listas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

63

4.4

Listas dinamicas: listar elementos . . . . . . . . . . . . . . . . . . . . . . . . .

66

4.5

Listas: pilhas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

68

4.5.1

Introduca o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

68

4.5.2

Declaraca o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

70

4.5.3

Inicializaca o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

70

4.5.4

Sobreposica o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

71

4.5.5

Remoca o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

72

4.5.6

Teste . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

72

4.5.7

Exemplo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

73

Listas: filas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

80

4.6.1

Introduca o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

80

4.6.2

Declaraca o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

80

4.6.3

Inicializaca o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

81

4.6.4

Inserca o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

82

4.6.5

Remoca o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

83

4.6.6

Teste . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

83

4.6.7

Exemplo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

84

Listas ordenadas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

87

4.7.1

Introduca o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

87

4.7.2

Declaraca o e inicializaca o . . . . . . . . . . . . . . . . . . . . . . . . .

88

4.6

4.7

VI

INDICE

4.8

4.9

4.7.3

Listagem ordenada . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

89

4.7.4

Procura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

89

4.7.5

Abstracca o de dados e metodos de teste . . . . . . . . . . . . . . . . . .

91

4.7.6

Inserca o ordenada . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

92

4.7.7

Remoca o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

95

4.7.8

Exemplo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

96

Variantes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
4.8.1

Introduca o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107

4.8.2

Listas com registo separado para a base . . . . . . . . . . . . . . . . . . 107

4.8.3

Listas duplamente ligadas . . . . . . . . . . . . . . . . . . . . . . . . . 108

4.8.4

Aneis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109

Anel duplo com registo separado para a base . . . . . . . . . . . . . . . . . . . . 109


4.9.1

Introduca o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109

4.9.2

Declaraca o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110

4.9.3

Inicializaca o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110

4.9.4

Listagem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111

4.9.5

Procura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111

4.9.6

Inserca o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112

4.9.7

Remoca o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113

4.9.8

Exemplo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114

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

INDICE

Conclusoes

Bibliografia

125

127

VII

Captulo 1

Introducao

Ate ao aparecimento da linguagem C, as linguagens de alto-nvel tinham por objectivo distanciar o programador do hardware especfico de trabalho. Pretendia-se, deste modo, que o programador focasse a sua actividade na soluca o conceptual e algortmica do problema e, simultaneamente, que o codigo final fosse independente do hardware e, como tal, facilmente portavel entre
diferentes plataformas.
Este princpio teorico fundamental, ainda hoje correcto em muitas a reas de aplicaca o de software, conduzia no entanto a problemas diversos sempre que o programador, por qualquer motivo,
necessitava de explorar determinadas regularidades das estruturas de dados de modo a optimizar
zonas crticas do codigo ou, em outros casos, por ser conveniente ou desejavel explorar determinadas facilidades oferecidas pelas instruco es do processador que nao estavam directamente
disponveis na linguagem de alto-nvel. Nestas situaco es, a u nica alternativa era a programaca o
directa em linguagem maquina destes blocos de codigo, opca o que implicava a revisao do software
em cada alteraca o ou evoluca o de hardware.
Por razoes semelhantes, quer o sistema operativo quer ferramentas de sistema como compiladores, gestores de ficheiros ou monitores de sistema eram consideradas aplicaco es que, por
requisitos de eficiencia do codigo, eram incompatveis com linguagens de alto-nvel. O mesmo
sucedia com todos os programas que necessitavam, de alguma forma, de controlar directamente
dispositivos de hardware. Como corolario, estas aplicaco es eram tradicionalmente escritas totalmente em linguagem maquina, implicando um enorme esforco de desenvolvimento e manutenca o
em cada evoluca o do hardware.
Quando Ken Thompson iniciou a escrita do sistema operativo Unix (Ritchie e Thmompson,
1974), desenvolveu uma linguagem, designada B, que funcionava no hardware de um computador


2 I NTRODUC AO

Digital PDP-10. O B era uma linguagem proxima da linguagem maquina, mas que facilitava extraordinariamente a programaca o de baixo-nvel. O B adoptava algumas estruturas decisionais e
ciclos comuns a linguagens de alto-nvel e, simultaneamente, disponibilizava um conjunto de facilidades simples, geralmente so acessveis em linguagem maquina, como o acesso a enderecos de
memoria e a metodos de enderecamento indirecto. De facto, os mecanismos de acesso a variaveis
e estruturas de dados previstos no B cobriam a esmagadora maioria das necessidades dos programadores quando anteriormente eram obrigados a recorrer a` linguagem maquina. Ora estes mecanismos, embora obviamente dependentes do hardware, obedeciam na sua generalidade ao modelo
de memoria previsto na arquitectura de Von Neuman, o qual, nos seus princpios essenciais, esta na
base da maioria das plataformas computacionais desde os anos 50 ate aos nossos dias. Foi assim
surgiu a ideia da possibilidade de desenvolvimento de uma linguagem de alto nvel, independente
do hardware, que permitisse simultaneamente um acesso flexvel aos modos de enderecamento
e facilidades disponveis ao nvel da linguagem maquina da maioria dos processadores. E assim
que, em 1983, e inventada a linguagem C (Kernighan e Ritchie, 1978), a qual permitiu a re-escrita
de 90% do nucleo do sistema operativo Unix em alto-nvel.
Para alem da manipulaca o directa de enderecos de memoria, uma das facilidades introduzida
pela linguagem C foi a incorporaca o de mecanismos para gestao de memoria dinamica. O conceito
de memoria dinamica permite que um programa ajuste de modo flexvel a dimensao da memoria
que utiliza de acordo com as suas necessidades efectivas. Por exemplo, um mesmo programa de
processamento de texto pode ocupar pouca memoria se estiver a tratar um documento de pequena
dimensao, ou ocupar um volume mais significativo no caso de um documento de maior numero de
paginas.
A invenca o da linguagem C (Kernighan e Ritchie, 1978) permitiu a re-escrita de 90% do
nucleo do sistema operativo Unix em linguagem de alto-nvel. A possibilidade de manipular
enderecos de memoria permitiu ainda a implementaca o eficiente em linguagem de alto nvel de
muitos algoritmos e estruturas de dados ate entao geralmente escritos em linguagem Assembler
(Knuth, 1973).
Neste texto apresenta-se uma introduca o aos apontadores e memoria dinamica na linguagem
de programaca o C. Para exemplificar estes conceitos, sao introduzidas algumas estruturas de dados
dinamicas simples, como pilhas, filas, listas e aneis. Durante a exposica o, introduz-se de forma
natural a noca o de abstracca o de dados, e os princpios essenciais de estruturaca o e modularidade
baseados neste paradigma de programaca o. Deste modo, a noca o de abstracca o de dados nao e introduzida formalmente num captulo autonomo, mas sim quando se instroduzem listas dinamicas,
altura em que o conceito e fundamental para justificar a estrutura adoptada. Esta abordagem, embora pouco convencional, deriva da nossa experiencia na docencia da disciplina de Introduca o a`
Programaca o durante varios anos no IST, tendo beneficiado das crticas e sugestoes de diversas

geraco es de alunos.
Tenta-se neste texto dar-se primazia a` clareza algortmica e legibilidade do codigo, ainda
que em alguns casos esta opca o possa sacrificar pontualmente a eficiencia do codigo produzido.
Considera-se, no entanto, que e esta a abordagem mais adequada numa disciplina de Introduca o
a` Programaca o. Por outro lado, a maioria dos compiladores recentes incluem processos de
optimizaca o que dispensam a utilizaca o explcita dos mecanismos de optimizaca o previstos originalmente na linguagem C.

Captulo 2
Apontadores

2.1

para os apontadores em C
Motivacao

2.2

Modelos de memoria
em C

Embora os mecanismos de enderecamento e acesso a` memoria tenham sofrido varias


evoluco es nas u ltimas decadas e sejam evidentemente dependentes do hardware, o C requer apenas
hipoteses muito simples relativamente ao modelo de memoria do processador.
A memoria de um computador encontra-se organizada em celulas ou palavras de memoria
individuais. Cada palavra de memoria e referenciada por um endereco e armazena um dado
conteudo. Designa-se por numero de bits da arquitectura o numero maximo de bits que um
processador e capaz de ler num u nico acesso a` memoria. Cada palavra de memoria de referencia
de um processador tem em geral um numero de bits identico ao numero de bits da arquitectura1 .
Admita-se que num dado programa em C declara, entre outras as variaveis i,j,k com o tipo
int, a variavel f com o tipo float e uma variavel d do tipo double, nao necessariamente
por esta ordem. Admita-se que apos a declaraca o destas variaveis sao realizadas as seguintes
atribuico es:

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


1

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

6 A PONTADORES

Endereo

..
.

Contedo

Varivel

..
.

..
.

1001

???

1002

2450

1003

???

1004

225.345

1005

11331

1006

113

1007

22.5E145

1008

(double, 64bits)

1009

???

..
.

..
.

Figura 2.1: Modelo de memoria em C (exemplo).


Um exemplo esquematico de um modelo memoria correspondente a esta configuraca o encontra-se
representado na figura 2.1. Admite-se aqui que a palavra de memoria e o tipo inteiro sao representados por 32 bits. De acordo com a figura, durante a compilaca o foram atribudos a` s variaveis
i,j,k,f os enderecos 1002,1005, 1006 e 1004, respectivamente, enquanto que a` variavel d foi
atribudo o endereco 10072 . Note-se que, com excepca o do tipo double, cada variavel tem uma
representaca o interna de 32 bits, ocupando por isso apenas um endereco de memoria. A variavel
d, de tipo double, tem uma representaca o interna de 64 bits, exigindo por isso duas palavras de
memoria: os enderecos 1007 e 1008. Na figura 2.1 encontram-se representados outras posico es
de memoria, provavelmente ocupadas por outras variaveis, e cujo conteudo e desconhecido do
programador.
2

Estritamente falando, a cada variavel local, e apenas atribudo um endereco relativo durante a compilaca o, sendo o
endereco final fixado durante a execuca o da funca o ou activaca o do bloco de instruco es em que a declaraca o e realizada.
Este tema voltara a ser abordado na secca o 3.2.

A PONTADORES 7

2.3

Apontadores

Um apontador em C e uma variavel cujo conteudo e um endereco de outra posica o de


memoria. A declaraca o de variaveis do tipo apontador pode ser construda a partir de qualquer
tipo definido anteriormente, e deve especificar o tipo de variavel referenciada pelo apontador.
A declaraca o de uma variavel do tipo apontador realiza-se colocando um * antes do nome
da variavel. Assim, na declaraca o
double *pd;
int
i;
int
*pi;
float
f;
int
j,k;
double d;
int
m,*pi2;
double *pd2,d2;
as variaveis i,j,k,m sao do tipo int, f e do tipo float, e d,d2 sao do tipo double. Alem
destas variaveis de tipos elementares, sao declaradas as variaveis pi, pi2 do tipo apontador
para inteiro, enquanto que pd e pd2 sao do tipo apontador para double.
Admita-se agora se realiza a seguinte sequencia de atribuico es:
i = 2450; f = 225.345; k = 113; d = 22.5E145; m = 9800;
Apos estas instruco es, o mapa de memoria poderia ser o representado na figura 2.2, situaca o
A. Sublinhe-se que as variaveis de tipo apontador apenas contem um endereco de memoria, independentemente do tipo referenciado. Desta forma, todas as variaveis de tipo apontador tem
uma representaca o igual em memoria, normalmente de dimensao identica ao do tipo inteiro (uma
palavra de memoria). Note-se que o conteudo dos apontadores, tal como o de algumas variaveis
elementares, ainda nao foi inicializado, pelo que surgem representados com ???. E importante
compreender que cada uma destas variaveis tem de facto um conteudo arbitrario, resultante da
operaca o anterior do computador. Claro que estes valores, nao tido sido ainda inicializados pelo
programa, sao desconhecidos do programador, mas esta situaca o nao deve ser confundida com a
ausencia de conteudo, erro de raciocnio frequentemente cometido por programadores principiantes.
As variaveis de tipo elementar podem ser inicializadas pela atribuica o directa de valores constantes. A mesma tecnica pode ser utilizada para a inicializaca o de apontadores, embora este

8 A PONTADORES

Endereo

..
.

Contedo

Varivel

..
.

..
.

Endereo

..
.

Contedo

Varivel

..
.

..
.

Endereo

..
.

Contedo

Varivel

..
.

..
.

pd

1001

1007

pd

1001

1007

pd

1002

2450

1002

2450

pi

1003

1006

pi

1003

1006

pi

225.345

1004

225.345

1004

225.345

1005

???

1005

???

1005

???

1006

113

1006

113

1006

113

1007

1007

1001

???

1002

2450

1003

???

1004

1007

22.5E145

1008

(double, 64bits)

1009

9800

1010
1011
1012

1009

9800

???

pi2

1010

1006

pi2

???

pd2

1011

1007

pd2

d2

1012

???

d2

1009

9800

???

pi2

1010

???

pd2

1011

d2

1012

(double, 64bits)

1013

..
.

..
.

..
.

..
.

1008

1008

???

Contedo

???
(double, 64bits)

1013

..
.

..
.

Varivel

Endereo

..
.

Contedo

Varivel

..
.

..
.

(double, 64bits)

..
.
C

..
.

..
.

22.5E145
(double, 64bits)

(double, 64bits)

1013

Endereo

22.5E145

Endereo

..
.

Contedo

Varivel

..
.

..
.

1001

1007

pd

1001

1007

pd

5001

i1

1002

2450

1002

2450

5002

i2

1003

1006

pi

1003

1006

pi

5003

5001

pi1

5004

5003

pi2

1004

225.345

1005

113

1005

113

5005

5004

pi3

1006

113

1006

113

5006

10

k1

1007

5007

46

k2

5008

5006

pk1

5003

pk2

1007

22.5E145

1008

(double, 64bits)

1009

9800

1010
1011
1012
1013

..
.

1004

225.345

22.5E145

1008

(double, 64bits)

1009

24500

5009

1006

pi2

1010

1002

pi2

5010

1007

pd2

1011

1012

pd2

5011

d2

1012

d2

5012

22.5E145
(double, 64bits)

..
.
D

1013

..
.

22.5E144
(double, 64bits)

..
.
E

5013

..
.

..
.
F

Figura 2.2: Mapa de memoria apos diferentes sequencias de atribuica o (explicaca o no texto).

A PONTADORES 9

metodo raramente seja utilizado: em geral, o programador nao sabe quais os enderecos de memoria
disponveis no sistema, e a manipulaca o directa de enderecos absolutos tem reduzidas aplicaco es
praticas.3 Assim, A inicializaca o de apontadores e geralmente realizada por outros metodos, entre
os quais a utilizaca o do endereco de outras variaveis ja declaradas. Este endereco pode ser obtido
em C aplicando o operador & a uma variavel. Na continuaca o do exemplo anterior, admita-se que
era realizada agora a seguinte sequencia de atribuico es:
pd = &d; pi = &k;

Apos esta fase, o mapa de memoria seria o representado na figura 2.2, situaca o B, onde as
variaveis pd e pi surgem agora preenchidas com os enderecos de d e k. Como seria de esperar, a consistencia da atribuica o exige que a variavel referenciada e o tipo do apontador sejam
compatveis. Por exemplo, a atribuica o pd=&k e incorrecta, atendendo a que k e do tipo int e
pd esta declarada como um apontador para double.
A partir do momento em que os apontadores sao inicializados, o seu conteudo pode ser
copiado e atribudo a outras variaveis, desde que os tipos ainda sejam compatveis. Assim, as
atribuico es
pd2 = pd; pi2 = pi;

conduziriam apenas a` copia dos enderecos guardados em pd e pi para pd2 e pi2. Apos esta
sequencia, a situaca o seria a representada na figura 2.2, caso C.
Uma vez estabelecido um mecanismo para preecher o conteudo de um apontador, coloca-se a
questao de como utilizar este apontador de modo a aceder aos dados apontados. Este mecanismo e
suportado no chamado sistema de enderecamento indirecto, o qual e realizado em C pelo operador
*. Assim, a atribuica o j = *pi; tem o significado consultar o endereco guardado em pi (1006)
e, seguidamente, ler o inteiro cujo endereco e 1006 e colocar o resultado (113) na variavel j. Deste
modo, apos as atribuico es
j = *pi; d2 = *pd;

a situaca o seria a representada na figura 2.2, caso D.


Referiu-se anteriomente que um apontador e apenas um endereco de memoria. Assim, podera
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 dependencia do apontador do tipo apontado nao tem a ver com
a estrutura do apontador em si, mas sim com o facto de esta informaca o ser indispensavel para
desreferenciar (aceder) correctamente o valor enderecado. Por exemplo, na expressao d2 = *pd,
e o facto de pd ser um apontador para double que permite ao compilador saber que o valor
referenciado ocupa nao apenas uma, mas duas palavras de memoria e qual a sua representaca o. So
na posse desta informaca o e possvel efectuar a atribuica o correcta a d2.
Outras sequencias de atribuica o seriam possveis. Por exemplo, apos a sequencia
pd2 = &d2; *pd2 = *pd1 / 10.0; pi2 = &i; m= *pi2 * 10;
a situaca o seria a representada no caso E. Note-se que aqui o operador * foi utilizado no lado
esquerdo da atribuica o. Assim, por exemplo, a atribuica o *pd2 = *pd1 e interpretada como
ler o real cujo endereco e especificado pelo conteudo de pd1 e colocar o resultado no endereco
especificado por pd2.
Embora ate aqui tenham sido considerados apontadores para tipos elementares, um apontador
pode tambem enderecar um outro apontador. Assim, na declaraca o
int i1,i2,*pi1,**pi2,***pi3;
int k1,k2,*pk1,**pk2;
enquanto que p1 e um apontador para um inteiro, p2 e um apontador para um apontador para um
inteiro e p3 e um apontador para um apontador para um apontador para um inteiro. Como seria de
esperar, a inicializaca o destes apontadores realiza-se segundo as mesmas regras seguidas para os
apontadores simples, sendo apenas necessario ter em atenca o, o nvel correcto de enderecamento
indirecto multiplo. Assim, se apos a declaraca o anterior, fosse executada a sequencia de instruco es
i1 = 3; i2 = 4;
pi1 = &i1; pi2 = &pi1; pi3 = &pi2;
k1 = 10;
pk1 = &k1; pk2 = pi2;
k2 = i1 * ***pi3 + *pi1 + i2 + **pk2 * *pk1;
a situaca o final poderia ser a representada na figura 2.2, caso F.
Em apontadores mutilpos a possibilidade de desreferenciar um apontador continua a ser dependente do tipo apontado. Deste modo, um apontador para um inteiro e um apontador para um
apontador para um inteiro sao tipos claramente distintos e cujos conteudos nao podem ser mutuamente trocados ou atribudos. Por outro lado, os nveis de indirecca o devem ser claramente

E PASSAGEM POR REFER E NCIA 11


F UNC OES

respeitados. Assim, a atribuica o i1 = *pk2; no exemplo precedente seria incorrecta, ja que i1


e do tipo inteiro e, *pk2 e um apontador para inteiro (recorde-se que pk2 e um apontador para um
apontador para um inteiro).
Como e sabido, o valor de uma variavel nunca deve ser utilizado antes de esta ser inicializada
explicitamente pelo programa. Com efeito, no incio de um programa, as variaveis tem um valor
arbitrario desconhecido. Por maioria de razao, o mesmo princpio deve ser escrupulosamente
seguido na manipulaca o de apontadores. Suponha-se, por exemplo que, no incio de um programa,
sao includas a declaraca o e as instruco es seguintes:

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

Aqui, a segunda atribuica o especifica que o dobro de k (valor 8) deve ser colocado no endereco
especificado pelo conteudo da variavel p. No entanto, dado que p nao foi inicializada, o seu
conteudo e arbitrario. Com elevada probabilidade, o seu valor corresponde a um endereco de
memoria inexistente ou invalido. No entanto, o C nao realiza qualquer juzo de valor sobre esta
situaca o e, tendo sido instrudo para colocar 8 no endereco indicado por p tentara executar esta
operaca o. A tentativa de escrita numa posica o de memoria invalida ou protegida conduzira ou ao
compromisso da integridade do sistema operativo se o espaco de memoria nao for convenientemente protegido, como e o caso do DOS. Se o sistema tiver um modo protegido, como o Unix ou o
Windows NT, esta situaca o pode originar um erro de execuca o, devido a uma violaca o de memoria
detectada pelo sistema operativo. Os erros de execuca o conduzem a` interrupca o imediata, em erro,
do programa.
Sublinhe-se que nas figuras desta secca o foram utilizados enderecos especficos nos apontadores de modo a melhor demonstrar e explicar o mecanismo de funcionamento dos mecanismos de
apontadores e indirecca o em C. No entanto, na pratica, o programador nao necessita de conhecer o
valor absoluto dos apontadores, sendo suficiente a manipulaca o indirecta do seu conteudo atraves
dos mecanismos de referenciaca o e desreferenciaca o descritos.

2.4
2.4.1

Funcoes
e passagem por referencia

Passagem por referencia

Em C, na chamada de uma funca o, os parametros formais da funca o recebem sempre uma


copia dos valores dos parametros actuais. Este mecanismo de passagem de argumentos e desig-

12 A PONTADORES

nado passagem por valor (Martins, 1989) e esta subjacente a todas as chamadas de funco es em
C. Esta situaca o e adequada se se pretender apenas que os argumentos transmitam informaca o do
modulo que chama para dentro da funca o. Dado que uma funca o pode tambem retornar um valor,
este mecanismo basico e tambem adequado quando a funca o recebe varios valores de entrada e
tem apenas um valor de sada. Por exemplo, uma funca o que determina o maior de tres inteiros
pode ser escrita como

int maior_3(int i1,int i2, int i3){


/*
* Func

ao que determina o maior de tr


es inteiros
*
*/
if((i1 > i2) && (i1 > i3))
return i1;
else
return (i2 > i3 ? i2 : i3);
}

Existem no entanto situaco es em que se pretende que uma funca o devolva mais do que um
valor. Uma situaca o possvel seria uma variante da funca o maior_3 em que se pretendesse
determinar os dois maiores valores, e nao apenas o maior. Outro caso tpico e o de uma funca o
para trocar o valor de duas variaveis entre si. Numa primeira tentativa, poderia haver a tentaca o de
escrever esta funca o 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 n
umeros: ");
scanf("%d %d",&a,&b);
trocaMal(&a,&b);
printf("a = %d, b= %d\n",a,b);

E PASSAGEM POR REFER E NCIA 13


F UNC OES

exit(0);
}
No entanto, este programa nao funciona: o mecanismo de passagem por valor implica que a
funca o troca opera correctamente sobre as variaveis locais x e y, trocando o seu valor, mas estas
variaveis nao sao mais do que copias das variaveis a e b que, como tal, se mantem inalteradas.
Na figura 2.3 representa-se a evoluca o do mapa de memoria e do conteudo das variaveis durante a
chamada a` funca o trocaMal.
Este aparente problema pode ser resolvido pela utilizaca o criteriosa de apontadores. A funca o
troca especificada anteriormente pode ser correctamente escrita como se segue:
#include <stdio.h>
#include <stdlib.h>
void troca(int *x,int *y){
/*
* Func

ao que troca dois argumentos


*/
int aux;
aux = *x;
*x = *y;
*y = aux;
}
int main(){
int a,b;
printf("Indique dois n
umeros: ");
scanf("%d %d",&a,&b);
troca(&a,&b);
printf("a = %d, b= %d\n",a,b);
exit(0);
}
Tal como pode ser observado, a soluca o adoptada consiste em passar a` funca o nao o valor das
variaveis a e b, mas sim os seus enderecos. Embora estes enderecos sejam passados por valor (ou
seja, a funca o recebe uma copia destes valores), o endereco permite a` funca o o conhecimento da
posica o das variaveis a b em memoria e, deste modo, permite a manipulaca o do seu conteudo por
meio de um enderecamento indirecto.
Um hipotetico exemplo de um mapa memoria associado ao funcionamento do programa
troca encontra-se representado na figura 2.4. Admita-se que a declaraca o de variaveis no pro-

14 A PONTADORES

Endereo

Contedo

..
.

..
.

..
.

..
.

..
.

..
.

..
.

..
.

..
.

Variveis 2135
do bloco
main()
2136

..
.

Varivel

..
.

..
.

Endereo

..
.

..
.

2131
Variveis
da funao 2132
troca()
2133

Varivel

..
.

aux

..
.

..
.

..
.

..
.

..
.

..
.

Variveis 2135
do bloco
main()
2136

..
.

..
.
C

Varivel

..
.
8
2

x
y
aux

..
.

..
.

..
.

..
.

..
.

..
.

Variveis 2135
do bloco
main()
2136

..
.

..
.

..
.

Contedo

..
.

..
.

2131
Variveis
da funao 2132
troca()
2133

A
Endereo

Contedo

..
.

Endereo

Contedo

Varivel

..
.

..
.

..
.

..
.

..
.

..
.

..
.

..
.

..
.

Variveis 2135
do bloco
main()
2136

..
.

..
.

..
.

Figura 2.3: Mapa de memoria durante as diferentes fase de execuca o de um programa que utiliza
a funca o trocaMal. A - antes da chamada a` funca o, B - no incio de troca, C - no final de
troca, D - apos o regresso ao programa principal. O mecanismo de passagem por valor conduz
a que os valores do programa principal nao sejam alterados.

E PASSAGEM POR REFER E NCIA 15


F UNC OES

Endereo

Contedo

..
.

..
.

..
.

..
.

..
.

..
.

..
.

..
.

..
.

Variveis 2135
do bloco
main()
2136

..
.

Varivel

..
.

..
.

Endereo

..
.

..
.

2131
Variveis
da funao 2132
troca()
2133

Varivel

..
.
2135
2136
8

y
aux

..
.

..
.

..
.

..
.

..
.

..
.

..
.
C

..
.
2135
2136

x
y
aux

..
.

..
.

..
.

..
.

..
.

..
.

Variveis 2135
do bloco
main()
2136

..
.

..
.

..
.

Endereo

Contedo

Varivel

..
.

..
.

..
.

..
.

..
.

..
.

..
.

..
.

..
.

..
.

Variveis 2135
do bloco
main()
2136

Varivel

Contedo

..
.

..
.

2131
Variveis
da funao 2132
troca()
2133

A
Endereo

Contedo

..
.

Variveis 2135
do bloco
main()
2136

..
.

..
.

..
.

Figura 2.4: Mapa de memoria durante as diferentes fase de execuca o do programa que utiliza a
funca o troca. A - antes da chamada a` funca o, B - no incio de troca C - no final de troca, D apos o regresso ao programa principal. A passagem de apontadores para as variaveis do programa
principal (passagem por referencia) permite que a funca o altere as variaveis do programa principal.

16 A PONTADORES

grama principal atribuiu a` s variaveis A e B os enderecos 2135 e 2136, e que estas foram inicializadas pelo utilizador com os valores 8 e 2, respectivamente. A situaca o imediatamente antes da
chamada da funca o troca encontra-se representada em A. Durante a chamada da funca o, realizase a activaca o das variaveis x, y e aux, locais a` funca o, eventualmente numa zona de memoria
afastada daquela onde se encontram as variaveis a e b, sendo as duas primeiras destas variaveis
inicializadas com os enderecos de a e b (situaca o B). Atraves do enderecamento indirecto atraves
das variaveis x e y, sao alterados os valores das variaveis a e b do programa principal, atingido-se
a situaca o C. Apos o regresso ao programa principal, as variaveis da funca o troca sao libertadas,
atingindo-se a situaca o representada em D, com as
Note-se que, estritamente falando, a passagem de argumento se deu por valor, atendendo a que
x e y sao variaveis locais a` funca o, tendo recebido apenas valores correspondentes ao endereco de
variaveis declaradas no programa principal. No entanto, neste tipo de mecanismo, diz-se tambem
que as variaveis a e b foram passadas por referencia(Martins, 1989), atendendo a que o seu
endereco (e nao o seu conteudo) que foi passadas a` funca o.
Mais genericamente, sempre que e necessario que uma funca o altere o valor de um ou mais
dos seus argumentos, este ou estes deverao ser passados por referencia, de forma a ser possvel a`
funca o modificar o valor das variaveis por um mecanismo de indirecca o. E por este motivo que,
na chamada da funca o scanf(), todas variaveis a ler sao passados por referencia, de modo a ser
possvel a esta funca o poder ler e alterar os valores das variaveis do programa principal.

2.4.2

Erros frequentes

A tentativa de desdobramento do mecanismo de referenciaca o de uma variavel quando uma


dado programa envolve varios nveis de funco es e um erro frequente de programadores principiantes ou com reduzida experiencia de C. Considere-se o seguinte troco de programa, em que se
pretende que a variavel x do programa principal seja modificada na funca o func2. Neste exemplo, o programador adoptou desnecessariamente uma referenciaca o (calculo do seu endereco) da
variavel a modificar em cada um dos nveis de chamada da funca o:
/*
Utilizac

ao incorrecta (desnecess
aria)
de refer
encias m
ultiplas.
*/
void func2(int **p2,int b2){
**p2 = -b2 * b2;
}
void func1(int *p1,int b1){
b1 = b1 + 1;

E PASSAGEM POR REFER E NCIA 17


F UNC OES

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

Como pode ser facilmente entendido, a referenciaca o de uma variavel uma u nica vez e suficiente
para que este mesmo endereco possa ser sucessivamente passado entre os varios nveis de funco es
e ainda permitir a alteraca o da variavel seja sempre possvel. Assim, embora o programa anterior
funcione, a referenciaca o de p1 na passagem de func1 para func2 e inutil: o mecanismo ali
adoptado so faria sentido caso se pretendesse que func2 alterasse o conteudo da variavel p1.
Como e evidente nao e esse o caso, e o programa anterior correctamente escrito tomaria a seguinte
forma:
/*
o correcta de uma passagem
Utilizac
a
por refer
encia entre v
arios n
veis
de func

oes.
*/
void func2(int *p2,int b2){
*p2 = -b2 * b2;
}
void func1(int *p1,int b1){
b1 = b1 + 1;
func2(p1,b1);
}
int main(){
int x;
func1(&x,5);
return 0;
}

Um outro erro frequente em programadores principiantes e passagem ao programa principal


ou funca o que chama a referencia de uma variavel local a` funca o evocada. Este tipo de erro pode
ser esquematizado pelo seguinte programa:
int *func(int a){
int b,*c;
b = 2 * a;
c = &b;
return c;

18 A PONTADORES

}
int main(){
int x,*y;
y = func1(1);
x = 2 * *y;
return 0;
}

Aqui, a variavel b e local a` funca o func e, como tal, e criada quando func e activada e a
sua zona de memoria libertada quando a funca o termina. Ora o resultado da funca o func e
passada ao programa principal sob a forma do endereco de b. Quando o valor desta variavel e
lido no programa principal por meio de um enderecamento indirecto na expressao x = 2 * *y,
a variavel b ja nao esta activa, realizando-se por isso um acesso invalido a` posica o de memoria
especificada pelo endereco em y. Com elevada probabilidade, o resultado final daquela expressao
sera incorrecto.

2.5
2.5.1

Vectores e apontadores
de vectores
Declaracao

Um vector em C permite a criaca o de uma estrutura com ocorrencias multiplas de uma variavel
de um mesmo tipo. Assim, a declaraca o
int
x[5] = {123,234,345,456,567};
double y[3] = {200.0,200.1,200.2};

declara um vector x de 5 inteiros, indexado entre 0 e 4, e um vector y de 3 reais de dupla precisao, indexado entre 0 e 2, inicializados na propria declaraca o. Um possvel mapa de memoria
correspondente a esta declaraca o encontra-se representado na figura 2.5.1.
Cada elemento individual de um vector pode ser referenciado acrescentando a` frente do nome
da variavel o ndice, ou posica o que se pretende aceder, representado por um inteiro entre [].
Para um vector com N posico es, o ndice de acesso varia entre 0 (primeira posica o) e N 1
(ultima posica o). Cada elemento de x e y corresponde a uma variavel de tipo inteiro ou double,
respectivamente. Deste modo, se se escrever,
int

x[5] = {123,234,345,456,567};

V ECTORES E APONTADORES 19

Endereo

..
.

Contedo

Varivel

..
.

..
.

1001

123

x[0]

1002

234

x[1]

1003

345

x[2]

1004

456

x[3]

1005

789

x[4]

1006

200.0

y[0]

1007
1008

y[1]
200.1

1009
1010
1011

..
.

y[2]
200.2

..
.

Figura 2.5: Mapa de memoria correspondente a` declaraca o de dois vectores

20 A PONTADORES

double y[3] = {200.0,200.1,200.2};


double a;
a = y[1] + x[3];

o conteudo final de a sera o resultado da soma da segunda posica o de y com a quarta posica o de
x, ou seja 656.1.
Dado que cada elemento de um vector e uma variavel simples, e possvel determinar o seu
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` situaca o representada na figura 2.5.1. Note-se que nas atribuico es
pi = &(x[2]) e pd = &(y[1]) os parenteses poderiam ser omitidos, dado que o operador [] (ndice) tem precedencia sobre o operador & (endereco de). Assim, aquelas atribuico es
poderiam ser escritas como pi = &x[2] e pd = &y[1].
Uma das vantagens da utilizaca o de vectores e o ndice de acesso poder ser uma variavel.
Deste modo, inicializaca o de um vector de 10 inteiros a 0 pode ser feita pela sequencia

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

Uma utilizaca o comum dos vectores e a utilizaca o de vectores de caracteres para guardar
texto. Por exemplo a declaraca o

char texto[18]="Duas palavras";

V ECTORES E APONTADORES 21

Endereo

..
.

Contedo

Varivel

..
.

..
.

1001

123

x[0]

1002

234

x[1]

1003

345

x[2]

1004

456

x[3]

1005

789

x[4]

1006

200.0

y[0]

1007
1008

y[1]
200.1

1009
1010

y[2]
200.2

1011
1012

1003

pi

1013

1008

pd

..
.

..
.

Figura 2.6: Apontadores e vectores (explicaca o 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: Utilizaca o de vectores de caracteres para armazenar texto.


cria um vector de 18 caracteres, e inicia as suas primeiras posico es com o texto Duas palavras.
Uma representaca o grafica deste vector depois de inicializado e apresentada na figura 2.7.
Dado que o texto pode ocupar menos espaco que a totalidade do vector, como sucede neste
caso, a u ltima posica o ocupada deste vector e assinalada pela colocaca o na posica o seguinte
a` u ltima do caracter com o codigo ASCII 0, geralmente representado pela constante inteira 0
ou pelo caracter \0. Note-se que esta u ltima representaca o nao deve ser confundida com a
representaca o do algarismo zero 0, internamente representado por 48, o codigo ASCII de 0.

2.5.2

Aritmetica
de apontadores

E possvel adicionar ou subtrair uma constante inteira de uma variavel de tipo apontador. Este
tipo de operaca o so faz sentido em vectores ou estruturas de dados regulares, em que o avanco ou
recuo de um apontador conduz a um outro elemento do vector (ou estrutura de dados regulares).
E da u nica e exclusiva responsabilidade do programador garantir que tais operaco es aritmeticas
se mantem dentro dos enderecos validos da estrutura de dados apontada. Assim, por exemplo, se
na sequencia do exemplo e da situaca o representada na figura 2.5.1 (repetida por conveniencia na
figura 2.5.2, A) fossem executadas as instruco es
pi = pi + 2;
pd = pd - 1;

a situaca o resultante seria a representada na figura 2.5.2, B.


No caso do modelo de memoria adoptado como exemplo, a operaca o aritmetica sobre o apontador inteiro tem uma correspondencia directa com a operaca o realizada sobre o endereco. No
entanto, o mesmo nao sucede com o apontador para double, onde subtracca o de uma unidade
ao apontador corresponde na realidade a uma reduca o de duas unidades no endereco fsico da
memoria.
De facto, a aritmetica de apontadores em C e realizada de forma a que o incremento ou decremento unitario corresponda a um avanco ou recuo de uma unidade num vector, independentemente
do tipo dos elementos do vector. O compilador de C garante o escalamento da constante adicionada

V ECTORES E APONTADORES 23

Endereo

..
.

Contedo

Varivel

..
.

..
.

Endereo

..
.

Contedo

Varivel

..
.

..
.

1001

123

x[0]

1001

123

x[0]

1002

234

x[1]

1002

234

x[1]

1003

345

x[2]

1003

345

x[2]

1004

456

x[3]

1004

456

x[3]

1005

789

x[4]

1005

789

x[4]

y[0]

1006

1006

200.0

1007
1008

y[0]

1007
y[1]
200.1

1009
1010

200.0

1008

y[1]
200.1

1009
y[2]
200.2

1011

1010

y[2]
200.2

1011

1012

1003

pi

1012

1005

pi

1013

1008

pd

1013

1006

pd

..
.

..
.
A

..
.

..
.
B

Figura 2.8: Aritmetica de apontadores (explicaca o no texto).

24 A PONTADORES

ou subtrada de acordo com a dimensao do tipo apontado. Este modo de operaca o garante que,
na pratica, o programador possa realizar operaco es sobre apontadores abstraindo-se do numero
efectivo de bytes do elemento apontado.
Ja anteriormente se mencionou que em C o tipo do apontador depende do objecto apontado, de
modo a ser possvel determinar, num enderecamento indirecto, qual o tipo da variavel referenciada.
A aritmetica de apontadores reforca esta necessidade, dado que o seu incremento ou decremento
exige o conhecimento da dimensao do objecto apontado.
A aritmetica de apontadores define apenas operaco es relativas de enderecos. Assim, embora
seja possvel adicionar ou subtrair constantes de apontadores, nao faz sentido somar apontadores.
Numa analogia simples, considere-se que os enderecos sao os numeros da porta dos edifcios de
uma dada rua: faz sentido referir dois numeros depois do predio 174 ou tres numeros antes do
180, mas nao existe nenuma aplicaca o em que faca sentido adicionar dois numeros de porta (174
e 180, por exemplo). No entanto, faz sentido referir o numero de predios entre o 174 e o 180:
de modo equivalente, tambem em C a subtracca o de apontadores e possvel, desde que sejam do
mesmo tipo. Como e evidente, tal como na adica o, a subtracca o de operadores e convenientemenet
escalada pela dimensao do objecto apontado:
int
x[5] = {123,234,345,456,567};
double y[3] = {200.0,200.1,200.2};
int
*pi1,*pi2; double *pd1,*pd2;
int
di,dd;
pi1 = &x[2]; pi2 = &x[0]; di = pi1 - pi2; /* di <- 2 */
pd1 = &y[1]; pd2 = &y[3]; dd = pd1 - pd2; /* dd <- -1 */

.
Uma u ltima nota relativamente aos apontadores de tipo void. O C-ANSI permite a definica o
de apontadores genericos, cujo tipo e independente do objecto apontado. Um apontador pv deste
tipo manipula um endereco generico e pode ser simplesmente declarado por
void *pv;

e encontra utilizaca o em situaco es em que se pretende que uma mesma estrutura possa enderecar
entidades ou objectos de tipos diferentes. Sempre que possvel, este tipo de situaco es deve ser
preferencialmente abordado pela utilizaca o de estruturas do tipo union. No entanto, existem
por exemplo,
casos em que tal nao e possvel, obrigando a` utilizaca o deste tipo de apontadores. E,
o caso das funco es de gestao de memoria, que trabalham com apontadores genericos (V. secca o
3.3). Note-se, no entanto, que o C nao consegue desreferenciar automaticamente um apontador

V ECTORES E APONTADORES 25

para void (ou seja, aceder ao conteudo apontado por). De igual modo, o desconhecimento da
dimensao do objecto apontado impede que a aritmetica de apontadores seja aplicavel a apontadores
deste tipo.

2.5.3 Indices e apontadores


Dos princpios gerais referidos anteriormente, resulta que atribuica o do 3o elemento de um
vector x a uma variavel a pode ser realizada directamente por
a = x[2];
ou, de forma equivalente, por
a = *(&x[0] + 2);
sendo o resultado identico. Enquanto que no primeiro caso se adopta um mecanismo de
indexaca o 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, finalmente,
aplica-se o operador * para realizar o enderecamento indirecto.
De facto, a segunda expressao pode ser simplificada. O C define que o endereco do primeiro
elemento de um vector pode ser obtido usando simplesmente o nome do vector, sem o operador
de indexaca o ([0]) a` frente. Ou seja, no contexto de uma expressao em C, &x[0] e equivalente a usar simplesmente x. Por outras palavras, se x[] e um vector do tipo xpto, x e um
apontador para o tipo xpto. Assim, a expressao x[k] e sempre equivalente a *(xk)+. Este
facto conduz ao que designamos por regra geral de indexaca o em C, que pode ser enunciada pela
equivalencia
x[k] <-> *(x + k)
Registe-se, como curiosidade, que a regra geral de indexaca o conduz a que, por exemplo,
x[3] seja equivalente a 3[x]. Com efeito,
x[3] <-> *(x+3) <-> *(3+x) <-> 3[x]
Claro que a utilizaca o desta propriedade e geralmente proibido, nao pelo C, mas pelas normas de
boa programaca o!

26 A PONTADORES

2.5.4

Vectores como argumentos de funcoes

Para usar um vector como argumento de uma funca o func() basta, no bloco que chama
func(), especifiar o nome da variavel que se pretende como argumento. Assim, pode fazer-se,
por exemplo,

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

No entanto, e necessario ter em conta que x, quando usado sem ndice, representa apenas o
endereco da primeira posica o do vector. Como deve entao ser declarado o argumento formal
correspondente no cabecalho de func?
Diversas opco es existem para realizar esta declaraca o. No exemplo seguinte, a funca o set e
utilizada para inicializar os NMAX elementos do vector a do programa principal com os inteiros
entre 1 e NMAX. Numa primeira variante, o argumento formal da funca o e apenas a repetica o da
declaraca o efectuada no programa principal. Assim,

#define NMAX 100


void set(int x[NMAX],int n){
int k;
for(k = 0; k < n; k++) x[k] = k+1;
}
int main(){
int a[NMAX];
set(a,NMAX);
/* ... */
}

Note-se que para uma funca o manipular um vector e suficiente conhecer o endereco do
primeiro elemento. Dentro da funca o, o modo de aceder ao k-esimo elemento do vector e sempre
adicionar k ao endereco da base, independentemente da dimensao do vector. Ou seja, o parametro
formal int x[NMAX] apenas indica que x e um apontador para o primeiro elemento de um vector de inteiros. Deste modo, o C nao usa a informaca o [NMAX] no parametro formal para aceder
a` funca o. Assim, a indicaca o explcita da dimensao pode ser omitida, sendo valido escrever

void set(int x[],int n){

V ECTORES E APONTADORES 27

int k;
for(k = 0; k < n; k++) x[k] = k+1;
}

Note-se que a possibilidade de omitir a dimensao resulta tambem da funca o nao necessitar de
reservar o espaco para o vector: a funca o limita-se a referenciar as celulas de memoria reservadas
no programa principal.
Na passagem de vectores como argumento o C utiliza sempre o endereco da primeira posica o,
nao existindo nenhum mecanismo previsto que permita passar a totalidade dos elementos do vector por valor. Esta limitaca o, inerente a` propria origem do C, tinha por base a justificaca o de
que a passagem por valor de estruturas de dados de dimensao elevada e pouco eficiente, dada a
necessidade de copiar todo o seu conteudo4 .
Atendendo a que a, sem inclusao do ndice, especifica um apontador para primeiro elemento
do vector, uma terceira forma de declarar o argumento formal e

void set(int *x,int n){


int k;
for(k = 0; k < n; k++) x[k] = k+1;
}

Esta u ltima forma sugere uma forma alternativa de escrever o corpo da funca o set. Com
efeito, para percorrer um vector basta criar um apontador, inicializa-lo com o endereco da primeira
posica o do vector e seguidamente incrementa-lo sucessivamente para aceder a` s posico es seguintes.
Esta tecnica pode ser ilustrada escrevendo a funca o

void set(int *x,int n){


int k;
for(k = 0; k < n; k++) *x++ = k+1;
}

Note-se que sendo x um ponteiro cujo valor resulta de uma passagem copia do endereco de a no
programa principal, e possvel proceder ao seu incremento na funca o de modo a percorrer todos
os elementos do vector. Aqui, a expressao *x++ merece um comentario adicional. Em primeiro
lugar, o operador ++ tem precedencia sobre o operador * e, deste modo, o incremento opera sobre
o endereco e nao sobre a posica o de memoria em si. E este apenas o significado da precedencia,
4

Na versao original do C, esta limitaca o estendia-se ao tratamento de estruturas de dados criadas com a directiva
struct, mas esta limitaca o foi levantada pelo C-Ansi

28 A PONTADORES

o qual nao deve ser confundido com a forma de funcionamento do operador incremento enquanto
sufixo: o sufixo estabelece apenas que o conteudo de x e utilizado antes da operaca o de incremento
se realizar. Ou seja, *x+=k;+ e equivalente a *x=k;x+;+.
Um outro exemplo de utilizaca o desta tecnica pode ser dado escrevendo uma funca o para
contabilizar o numero de caracteres usados de uma string. Atendendo a que se sabe que o u ltimo
caracter esta seguido do codigo ASCII 0, esta funca o pode escrever-se
int conta(char *s){
int n = 0;
while(*s++ != \0) n++;
return n;
}

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

2.6
2.6.1

Matrizes

Declaracao

Na sua forma mais simples, a utilizaca o de estruturas multidimensionais em C apenas envolve


a declaraca o de uma variavel com varios ndices, especificando cada um da dimensao pretendida
da estrutura. Por exemplo
float x[3][2];

declara uma estrutura bidimensional de tres por dois reais, ocupando um total de seis palavras
de memoria no modelo de memoria que temos vindo a usar como referencia. E frequente uma
estrutura bidimensional ser interpretada como uma matriz, neste exemplo de tres linhas por duas
colunas.
Nos modos de utilizaca o mais simples deste tipo de estruturas, o programadador pode abstrairse dos detalhes de implementaca o e usar a variavel bidimensional como uma matriz. Assim, a
inicializaca o a zeros da estrutura x pode ser efectuada por
float x[3][2];
int k,j;

M ATRIZES 29

Endereo

..
.

Contedo

Varivel

..
.

..
.

1001

1.0

x[0][0]

1002

2.0

x[0][1]

1003

3.0

x[1][0]

1004

4.0

x[1][1]

1005

5.0

x[2][0]

1006

6.0

x[2][1]

..
.

..
.

Figura 2.9: Mapa de memoria correspondente a` declaraca o de uma estrutura de tres por dois reais

for(k = 0 ; k < 3 ; k++)


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

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

A disposica o em memoria desta estrutura e representada esquematicamente na figura 2.9.

2.6.2

Matrizes como argumento de funcoes

O facto de ser possvel omitir a dimensao de um vector na declaraca o do parametro formal de


uma funca o leva por vezes a pensar que o mesmo pode ser feito no caso de uma matriz. De facto,
a situaca o nao e totalmente equivalente.
Comece-se por regressar a` declaraca o int x[3][2] e observar a forma como e determinado o endereco de x[k][j]. Analisando a figura 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 generico da declaraca o ter a forma <tipo> x[N][M] ter-se-a ainda
x[k][j]

<->

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

Ou seja, para aceder a um elemento generico de uma matriz nao basta conhecer o endereco da
primeira posica o e o os dois ndices de acesso: e necessario saber tambem o numero de colunas da
matriz. Deste modo, quando uma matriz e passada como argumento de uma funca o, e necessario
que esta saiba a dimensao das colunas da matriz. Por este motivo, dada a chamada a` funca o
/* ... */
#define NLIN 3
#define NCOL 2
/* ... */
int m[NLIN][NCOL];
int a;
/* ... */
a = soma(m);
/* ... */

o parametro formal da funca o pode repetir na totalidade a declaraca o da matriz, como em


float norma(float x[NLIN][NCOL]){
int s = 0,k,j;
for(k = 0; k < NLIN ; k++)
for(j = 0; j < NCOL ; j++)
s += x[k][j];
return s;
}

ou pode omitir o numero de linhas (dado que, como se mostrou, este numero nao e indispensavel
para localizar o endereco de um elemento generico da matriz), como em
float norma(float x[][NCOL]){
int s = 0,k,j;
for(k = 0; k < NLIN ; k++)
for(j = 0; j < NCOL ; j++)
s += x[k][j];
return s;
}

M ATRIZES 31

No entanto, a omissao simultanea de ambos os ndices como em

float norma(float x[][]){ /* ERRADO */


int s = 0,k,j;
for(k = 0; k < NLIN ; k++)
for(j = 0; j < NCOL ; j++)
s += x[k][j];
return s;
}

nao e possvel, gerando um erro de compilaca o.


Viu-se anteriormente que se o vector int x[NMAX] for utilizado na chamada a uma funca o,
a declaraca o a adoptar nos parametros formais da funca o podia ser ou int a[]) ou int *a.
E frequente surgir a duvida se e possvel adoptar uma notaca o de apontador equivalente no caso
de uma matriz. De facto sim, embora esta notaca o raramente seja utilizada na pratica. No caso do
exemplo que tem vindo a ser utilizado a declaraca o 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 declaraca o, x e um apontador para um vector de NCOL floats. Uma explicaca o mais
detalhada do significado desta invulgar declaraca o pode ser encontrado na secca o 2.6.4.

2.6.3

Matrizes e vectores

Em C, uma matriz nao e mais do que um vector de vectores. Ou seja, a declaraca o


int x[3][2] pode ser interpretada como x e um vector de 3 posico es, em que cada posica o e
um vector de 2 posico es. Esta interpretaca o e tambem sugerida pela representaca o que se adoptou
na figura 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 pratica, este facto significa que cada uma das linhas de uma matriz pode ser tratada individualmente como um vector.

32 A PONTADORES

Suponha-se, por exemplo, que dada uma matriz a de dimensao NLIN NCOL, se pretende
escrever uma funca o que determine o maximo de cada uma das linhas da matriz e coloque o
resultado num vector y. Esta funca o pode ser escrita como
void maxMat(float y[],float a[][NCOL]){
int k;
for(k = 0; k < NLIN ; k++)
y[k] = maxVec(a[k],NCOL);
}
onde cada linha foi tratada individualmente como um vector, cujo maximo e determinado pela
funca o vectorial
float maxVec(float v[],int n){
float m;
int k;
m = v[0];
for(k = 1; k < n; k++)
m = (v[k] > m ? v[k] : m);
return m;
}
De igual forma, considere-se que se pretende calcular o produto matricial
y = Ax
onde A e uma matriz de dimensao N M e x e y sao vectores de dimensao M e N , respectivamente.
Admita-se que o programa principal era escrito como
#define N 3
#define M 2
int main(){
float A[N][M] = {{1,2},{3,4},{5,6}};
float x[M] = {10,20};
float y[N];
int k;
matVecProd(y,A,x);
for(k = 0; k < N ; k++)
printf("%f\n",y[k]);
exit(0);
}

M ATRIZES 33

Atendendo a que o produto de uma matriz por um vector e um vector em que cada elemento
nao e mais do que o produto interno de cada linha da matriz com o vector operando, a funca o
prodMatVec poderia ser escrita como

void matVecProd(float y[],float A[][M],float x[]){


int k;
for(k = 0; k < N ; k++)
y[k] = prodInt(A[k],x,M);
}

com o produto interno definido por

float prodInt(float a[],float b[],int n){


float s = 0.0;
int k;
for(k = 0; k < n; k++)
s += a[k] * b[k];
return s;
}

Uma u ltima situaca o em que e possvel exemplificar a utilizaca o de linhas como vectores e
o do armazenamento de varias linhas de texto. Admita-se, por exemplo, que se pretende ler uma
sequencia de linhas de texto e imprimir as mesmas linhas por ordem inversas. Tal e possvel atraves
da utilizaca o 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 sequ
encia de %d linhas:\n",NUM_LINHAS);
for(k = 0;k < NUM_LINHAS; k++){
fgets(texto[k],DIM_LINHA,stdin);
if(texto[k][strlen(texto[k])-1] != \n){
printf("Erro: linha demasiado comprida.\n");
exit(1);

34 A PONTADORES

}
}
/* Escrita */
printf("\nLinhas por ordem inversa:\n");
for(k = NUM_LINHAS-1;k >= 0; k--)
printf("%s",texto[k]);
exit(0);
}

2.6.4

Matrizes e apontadores

Se a utilizaca o de estruturas multidimensionais e relativamente simples e intuitiva, ja o mesmo


nem sempre sucede quando e necessario manipular apontadores relacionados com este tipo. 5 O
tratamento de matrizes no C e frequentemente fonte de alvo de duvidas. Se se perguntar a qualquer
programador de C dada a declaraca o int x[3], qual e o tipo de x quando usado isoladamente,
nenhum tera duvidas em afirmar que a resposta e apontador para int. Experimente-se, no
entanto, a colocar a pergunta semelhante dada a declaraca o int x[3][2], qual e o tipo de x
quando usado isoladamente e, com elevada probabilidade, nao sera obtida a mesma unanimidade
nas respostas.
Dado que, por consistencia sintactica, se tem sempre

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

entao, no caso de uma estrutura bidimensional, tera que ser

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

e, portanto, se x[k][j] e por exemplo do tipo float, x[k] e um apontador para float. Consultando novamente o exemplo da figura 2.9, significa isto que x[0] corresponde a um apontador
para float com o valor 1001 (e portanto o endereco do primeiro elemento do vector de floats
formado pelos reais 1.0 e 2.0, enquanto que x[2] corresponde tambem a um apontador, cujo
valor e 1003 (primeiro elemento do vector de floats formado pelos reais 3.0 e 4.0).
Considere-se agora, novamente, a questao de qual o tipo de x, quando considerado isoladamente. Como ja se disse anteriormente em C, uma estrutura multidimensional representa sempre
5

A leitura desta secca o pode ser omitida numa abordagem introdutoria da linguagem C.

M ATRIZES 35

uma hierarquia de vectores simples. Ou seja, x[3][2] representa um vector de tres elementos,
em que cada um e por sua vez um vector de dois elementos. Assim, x[k] representa sempre um
vector de dois floats. Com esta formulaca o, resulta claro que x representa um apontador para
um vector de dois floats. Isto nao e mais do que a generalizaca o da situaca o dos vectores, em que
dada a declaraca o int a[N], se sabe que a isoladamente e um apontador para inteiro.
Assim, e natural que x sem ndices especifique o endereco do primeiro elemento de um vector
de tres elementos, em que cada um e um vector de dois floats. Ou seja, no nosso exemplo, x
corresponde ao valor 1001.
Mas, afinal, qual a diferenca entre um apontador para float e um apontador para um vector de floats? Por um lado, a forma de usar este tipo variavel num enderecamento indirecto
e claramente diferente. Por exemplo, x e &x[0][0] correspondem ao mesmo valor (1001 no
nosso exemplo), mas sao tipos diferentes: o primeiro e um apontador para um vector, pelo que *x
e um vector (a primeira linha de x, enquanto que *(&x[0][0]) e um float (o conteudo de
x[0][0]. No entanto, e provavelmente mais importante reter que a diferenca fundamental reside
na dimensao do objecto apontado. Considere-se novamente o exemplo que temos vindo a considerar. Uma variavel do tipo apontador para float, cujo valor seja 1001, quando incrementada
passa a ter o valor 1002. Mas uma variavel do tipo apontador para vector de dois float, cujo
valor seja 1001, quando incrementada passa a ter o valor 1003 (ja que o incremento e escalado
pela dimensao do objecto apontado).
E interessante verificar que este modelo e o u nico que permite manter a consistencia sintactica
do C na equivalencia entre vectores e apontadores. Ja se disse que sendo sempre

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

entao,

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

O que nao se disse antes, mas que tambem se verifica, e que a primeira destas regras tambem deve
ser aplicavel a` entidade x[k] que surge na u ltima expressao. Ou seja, tem-se

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

o que exige por outro lado estabelecer que

36 A PONTADORES

Expressao

Tipo

Valor

Apontador para vector


de dois floats
Apontador para vector
de dois floats
Apontador para float
Apontador para float
float
Apontador para float
float
float
Apontador para float

1001

Elemento da
estrutura
-

1003

1003
1006
6.0
1001
1.0
2.0
1003

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

x+1
*(x+1)
*(x+2)+1
*(*(x+2)+1)
*x
**x
*(*x+1)
x[1]

Tabela 2.1: Exemplos de tipos e valores derivados do exemplo da figura 2.9.


Se a e um apontador para um escalar, *a e desse tipo escalar, e o valor de *a e o conteudo
do endereco especificado por a.
Se a e um apontador para um vector de elementos de um dado tipo, *a e um apontador para
um elemento do tipo constituinte, e o seu valor e identico ao de a.

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

verifica-se que **x e um float.


A consistencia destas equivalencias podem ser verificada considerando casos particulares do
exemplo que tem vindo a ser utilizado como referencia, tal como listados na tabela 2.1. O codigo
de um pequeno programa que permite validar esta tabela esta listado no apendice A.
Como e natural, e possvel declarar um apontador para um vector de dois floats, sem ser
da forma implcita que resulta da declaraca o da matriz. A declaraca o de uma variavel y deste tipo
pode ser feita por
float (*y)[2];

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

M ATRIZES 37

declaraca o do parametro formal poder ser feita repetindo a declaraca o total, omitindo a dimensao
do ndice interior, ou entao por

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

tal como se referiu na secca o 2.6.2.


Dado que este tipo de declaraco es e alvo frequente de confusao, e conveniente saber que existe
uma regra de leitura que ajuda a clarificar a semantica da declaraca o. Com efeito, e suficiente
seguir as regras de precedencia, procedendo a` leitura na seguinte sequencia:
float (*y)[3]
y
um apontador
para um vector de trs
floats

Sublinhe-se que, face a tudo o que ficou dito anteriormente, nao e possvel declarar um apontador para um vector sem especificar a dimensao do vector: como ja foi dito por diversas vezes,
um apontador tem que conhecer a dimensao do objecto apontado. Isto nao e possvel sem especificar a dimensao do vector. Como corolario, um apontador para um vector de tres inteiros e de
tipo distinto de um apontador para um vector para seis inteiros, nao podendo os seus conteudos
ser mutuamente atribudos.
Note-se que a declaraca o que se acabou de referir e claramente distinta de
float *y[2];

onde, devido a` ausencia de parenteses, e necessario ter em atenca o a precedencia de [] sobre o


*. Neste caso, y e um vector de dois apontadores para float, podendo a leitura da declaraca o
ser realizada pela sequencia:
float *y[3]
y
um vector de trs
apontadores
para float

A utilizaca o de vectores de apontadores e abordada em maior detalhe na secca o 3.5.4.


Finalmente, refira-se que os apontadores para vectores podem ainda surgir noutros contextos:
dada a declaraca o int x[10], x e um apontador para inteiro, mas a expressao &x e do tipo

38 A PONTADORES

apontador para um vector de 10 inteiros.

2.7

para mais do que duas dimensoes

Generalizacao

A generalizaca o do que ficou dito para mais do que duas dimensoes e directa. Considere-se,
como referencia, a declaraca o da estrutura
int x[M][N][L];

1. No calculo do endereco de qualquer elemento da estrutura tem-se a igualdade:


&(x[m][n][l]) == (&x[0][0][0]) + m * (N*L) + n * L + l
2. x[k][j] e um apontador para inteiro.
3. x[k] e um apontador para um vector de L inteiros.
4. x e um apontador para uma matriz de 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 repetica o
da declaraca o completa do tipo. Assim, uma declaraca o 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;
}

PARA MAIS DO QUE DUAS DIMENS OES

G ENERALIZAC AO
39

Como se mostrou anteriormente, o calculo do endereco de um elemento generico de uma estrutura tridimensional exige o conhecimento das duas dimensoes exteriores da estrutura (N e L no
exemplo). A generalizaca o desta regra mostra que para calcular o endereco de um elemento de
uma estrutura n-dimensional e necessario conhecer com rigor os n 1 ndices exteriores da estrutura. Deste modo, nos argumentos formais de uma funca o e sempre possvel omitir a dimensao
do primeiro ndice de uma estrutura multidimensional, mas nao mais do que esse. No exemplo
anterior, pode entao escrever-se

void func(int x[][N][L]){


/* ... */
}

Claro que todas as outras variantes em que exista consistencia sintactica entre os argumentos
formais e actuais do procedimento sao validas. Assim, pelas mesmas razoes ja detalhadas na
secca o 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

Ate ao aparecimento da linguagem C, a maioria das linguagens de alto nvel exigia um dimensionamento rgido das variaveis envolvidas. Por outras palavras, a quantidade maxima de memoria
necessaria durante a execuca o do programa deveria ser definida na altura de compilaca o do programa. Sempre que os requisitos de memoria ultrapassavam o limite fixado durante a execuca o
do programa, este deveria gerar um erro. A soluca o nestes casos era recompilar o programa aumentando a dimensao das variaveis, soluca o so possvel se o utilizador tivesse acesso e dominasse
os detalhes do codigo fonte. Por exemplo, um programa destinado a` simulaca o de um circuito
electronico poderia ser obrigado a definir no codigo fonte o numero maximo de componentes
do circuito. Caso este numero fosse ultrapassado, o programa deveria gerar um erro, porque a
memoria reservada durante a compilaca o tinha sido ultrapassada. A alternativa nestas situaco es
era a reserva a` partida de uma dimensao de memoria suficiente para acomodar circuitos de dimensao elevada, mas tal traduzia-se obviamente num consumo excessivo de memoria sempre que
o programa fosse utilizado para simular circuitos de dimensao reduzida. Em alguns casos, os programas recorriam a` utilizaca o de ficheiros para guardar informaca o temporaria, mas esta soluca o
implicava geralmente uma complexidade algortmica acrescida.
Tal como o nome indica, os sistemas de memoria dinamica permitem gerir de forma dinamica
os requisitos de memoria de um dado programa durante a sua execuca o. Por exemplo, no caso
do sistema de simulaca o referido anteriormente, o programa pode, no incio da execuca o, determinar a dimensao do circuito a simular e so nessa altura reservar a memoria necessaria. Com
esta metodologia, o programa pode minimizar a quantidade de memoria reservada e, deste modo,
permitir que o sistema operativo optimize a distribuica o de memoria pelos varios programas que

42 V ECTORES E MEM ORIA


DIN AMICA

se encontram simultaneamente em execuca o. No entanto, ate ao aparecimento do C este tipo


de mecanismos, caso estivessem disponveis no sistema operativo, eram apenas acessveis em
linguagem maquina ou Assembler, mais uma vez pela necessidade de manipular directamente
enderecos de memoria.
Apesar da flexibilidade oferecida, a gestao directa da memoria dinamica exige algumas
precauco es suplementares durante o desenvolvimento do codigo. Por este motivo, muitas linguagens de programaca o de alto-nvel mais recentes (Lisp, Java, Scheme, Python, Perl, Mathematica,
entre outras) efectuam a gestao automatica da memoria dinamica, permitindo assim que o programador se concentre na implementaca o algortmica e nos modelos de dados associados, sem se
preocupar explicitamente com os problemas de dimensionamento de variaveis ou os volumes de
memoria necessarios para armazenamento de dados.
Neste contexto, podera perguntar-se quais as vantagens de programar em C ou porque e que
o C ainda mantem a popularidade em tantas a reas de aplicaca o. Ha diversas respostas para esta
questao:

A gestao directa da memoria dinamica permite normalmente a construca o de programas


com maior eficiencia e com menores recursos computacionais.
Dada a evidente complexidade dos compiladores de linguagens mais elaboradas, e frequente
microprocessadores e controladores especializados (processadores digitais de sinal, microcontroladores, etc) apenas disporem de compiladores para a linguagem C, a qual se encontra
mais proximo da linguagem maquina do que linguagens conceptualmente mais elaboradas.
Por este motivo, o C ainda se reveste hoje de uma importancia crucial em diversas a reas da
Engenharia Electrotecnica, nomeadamente em aplicaco es que implicam o recurso a microcontroladores especializados.
Dada a sua proximidade com o hardware1 , a maioria dos sistemas operativos actualmente
existentes sao ainda hoje programados na linguagem C.
A maioria dos compiladores de linguagens de alto nvel, incluindo o proprio C, sao actualmente escritos e desenvolvidos em C. Ou seja, nesta perspectiva, o C e hoje uma linguagem
indispensavel a` geraca o da maioria das outras linguagens, constituindo, neste sentido, a
linguagem das linguagens.
1

O C e frequentemente designado na gria como um Assembler de alto nvel, apesar do evidente paradoxo contido
nesta designaca o.

V ECTORES 43

3.2

Vectores

Em C, um vector e uma colecca o com um numero bem definido de elementos do mesmo tipo.
Ao encontrar a declaraca o de um vector num programa, o compilador reserva automaticamente
espaco em memoria para todos os seus elementos. Por razoes de clareza e de boa pratica de
programaca o, estas constantes sao normalmente declaradas de forma simbolica atraves de uma
directiva define, mas a dimensao continua obviamente a ser uma constante:
#define DIM_MAX 100
#define DIM_MAX_STRING 200

int
char

x[DIM_MAX];
s[DIM_MAX_STRING];

Por outras palavras, a necessidade de saber quanta memoria e necessaria para o vector que se
pretende utilizar implica que a dimensao deste vector seja uma constante, cujo valor ja conhecido
durante a compilaca o do programa.
Como e sabido, as variaveis locais a uma funca o tem um tempo de vida limitado a` execuca o
da funca o2 . O espaco para estas variaveis e reservado na chamada zona da pilha (stack), uma
regiao de memoria dedicada pelo programa para armazenar variaveis locais e que normalmente e
ocupada por ordem inversa. Por outras palavras, sao usados primeiros os enderecos mais elevados
e vao sendo sucessivamente ocupados os enderecos inferiores a` medida que sao reservadas mais
variaveis locais. Dada a forma como se realiza esta ocupaca o de memoria, o limite inferior da
regiao da pilha e geralmente designada por topo da pilha. Neste sentido, quando uma funca o
e chamada, o espaco para as variaveis locais e reservado no topo da pilha, que assim decresce.
Quando uma funca o termina, todas as variaveis locais sao libertadas e o topo da pilha aumenta
novamente. Assim, por exemplo, dada a declaraca o
int function(int z){
int a;
int x[5];
int b;
/* ... */
a chamada da funca o func poderia dar origem a uma evoluca o do preenchimento da memoria da
forma como se representa na figura 3.1.
2

Excepto se a sua declaraca o for precedido do atributo static, mas este so e usado em circunstancias excepcionais.

44 V ECTORES E MEM ORIA


DIN AMICA

Endereo

..
.

Contedo

Varivel

..
.

..
.

Endereo

..
.

Contedo

Varivel

..
.

..
.

1001

1001

topo da pilha

1002

1002

1003

1003

x[0]

1004

1004

x[1]

1005

1005

x[2]

1006

1006

x[3]

1007

1007

1008

1008

1009

1009

...

..
.

..
.

topo da pilha

..
.

..
.
B

Figura 3.1: Mapa de memoria antes e apos a chamada a` funca o func (A) e mapa de memoria
durante a execuca o da funca o (B).
A necessidade da dimensao dos vectores ser conhecida na altura da compilaca o conduz a`
impossibilidade de declaraco es do tipo

int function(int n){


int x[n]; /* MAL: n
e uma vari
avel, e o seu valor
e desconhecido
na altura da compilac

ao */
/* ... */

Como e evidente, os elementos de um vector podem nao ser escalares simples como no exemplo anterior. Os elementos de um vector podem ser estruturas de dados, como por exemplo
em
#define MAX_NOME
200
#define MAX_ALUNOS 500
typedef struct _tipoAluno {
int numero;
char nome[MAX_NOME];
} tipoAluno;

V ECTORES 45

int main(){
tipoAluno alunos[MAX_ALUNOS];
/* ... */

ou mesmo outro vector. Com efeito, na declaraca o

int mat[10][5]

a matriz mat, do ponto de vista interno do C, nao e mais do que um vector de 10 elementos, em
que cada elemento e por sua vez um vector de 5 inteiro (V. secca o 2.6.3).
A necessidade de definir de forma rgida a dimensao dos vectores na altura da compilaca o
obriga frequentemente ao estabelecimento de um compromisso entre a dimensao maxima e a
eficiencia do programa em termos da memoria utilizada. Uma soluca o possvel e utilizar um
majorante dos valores tpicos. Considere-se novamente um sistema para simulaca o de circuitos
electronicos. Se os sistemas que se pretende simular tem em media 1000 componentes, poderse-ia utilizar um valor de 2000 ou 5000 para dimensionar o vector de componentes. No entanto,
a utilizaca o de um valor maximo elevado pode conduzir a situaco es frequentes de desperdcio
de memoria, com o programa a reservar a` partida volumes de memoria muito superiores aos
necessarios, enquanto que um valor reduzido deste parametro pode limitar seriamente a dimensao
dos problemas a abordar. Por outro lado, e por vezes difcil encontrar parametros razoaveis para
definir valor medio. Em determinadas aplicaco es, 50 componentes pode ser um valor razoavel,
noutras 10,000 pode ser um valor reduzido. Para agravar a situaca o, a dimensao de memoria que
e razoavel reservar a` partida depende da memoria fsica disponvel no sistema em que se esta a
trabalhar: sistemas com 8MB ou 8GB de memoria conduzem obviamente a situaco es distintas.
Nao se pretende com esta analise colocar de parte todas as utilizaco es de vectores convencionais com dimensao fixa. Frequentemente, esta e uma soluca o mais que razoavel. Por exemplo,
uma linha de texto numa consola de texto tem em geral cerca de 80 caracteres, pelo que a definica o
de um valor maximo para o comprimento de uma linha de 160 ou 200 caracteres e um valor que
pode ser razoavel e que nao e excessivo na maioria das aplicaco es. No entanto, em situaco es
em que o numero de elementos pode variar significativamente, e desejavel que a memoria seja
reservada a` medida das necessidades.

46 V ECTORES E MEM ORIA


DIN AMICA

3.3

Vectores dinamicos

Uma forma de ultrapassar a dificuldade criada pelo dimensionamento de vectores e a reserva


de blocos de memoria so ser realizada durante a execuca o do programa. Considere-se novamente
o simulador de circuitos electronicos. A ideia essencial e iniciar o programa com um volume de
memoria mnimo, determinar qual o numero de componentes do sistema a simular e so depois
deste passo reservar espaco para manipular o numero de componentes pretendido.
Recorde-se que ao declarar um vector

int x[MAX_DIM]

a utilizaca o do nome do vector sem especificar o ndice e equivalente ao endereco do ndice 0 do


vector. Por outras palavras, a utilizaca o no programa de x e equivalente a &x[0].Decorre tambem
daqui a regra geral de indexaca o em C:

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

equivalencia que e universal em C.


A utilizaca o de um bloco de memoria criado dinamicamente e que pode ser utilizado com
mecanismos de acesso identicos aos de um vector pode ser realizado declarando uma variavel
de tipo de apontador e solicitando ao sistema operativo a reserva de um bloco de memoria da
dimensao pretendida. O pedido de reserva de memoria ao sistema operativo e efectuado atraves
de um conjunto de funco es declaradas no ficheiro stdlib.h. Uma das funco es utilizaveis para
este efeito tem como prototipo

void *calloc(size_t nmemb, size_t size);

A funca o calloc reserva um bloco de memoria contgua com espaco suficiente para armazenar
nmemb elementos de dimensao size cada, devolvendo o endereco (apontador) para a primeira
posica o do bloco. size_t e o tipo utilizado para especificaca o de dimensoes numericas em varias
funco es do C e a sua implementaca o pode ser dependente do sistema operativo, mas corresponde
geralmente a um inteiro sem sinal. O tipo de retorno, void*, corresponde a um endereco generico
de memoria. o que permite que a funca o seja utilizada independentemente do tipo especfico do
apontador que se pretende inicializar. A funca o retorna um apontador para o primeiro endereco de


V ECTORES DIN AMICOS
47

uma regiao de memoria livre da dimensao solicitada. Caso esta reserva nao seja possvel, a funca o
retorna NULL, para indicar que a reserva nao foi efectuada.
Considere-se, como exemplo, um programa para calculo da media e variancia de N valores
reais indicados pelo programador. Em vez de utilizar um vector de dimensao fixa, e possvel
utilizar apenas um apontador para um real e um programa com a seguinte estrutura:
/* Ficheiro: media.c
Conte
udo: Programa para c
alculo da m
edia e vari
anicia
Autor:
Fernando M. Silva, IST (fcr@inesc-id.pt)
Hist
oria : 2001/06/01 - Criac

ao
*/
#include <stdio.h>
#include <stdlib.h>
int main(){
int n;
/* N
umero de valores */
float *f; /* Apontador para o primeiro valor */
float soma,media,variancia;
int k;
printf("Indique quantos valores pretende utilizar: ");
scanf("%d",&n);
f = (float*) calloc(n,sizeof(float));

/* reserva de mem
oria
para um bloco de n
reais */

if(f == NULL){
/* Teste da reserva de mem
oria */
fprintf(stderr,"Erro na reserva de mem
oria\n");
exit(1);
}
/* A partir deste ponto, f pode ser tratado como um
vector de n posic

oes */
for( k = 0 ; k < n ; k++){
printf("Indique o valor
ndice %d : ",k);
scanf("%f",&f[k]);
}
soma = 0.0;
for( k = 0 ; k < n ; k++)
soma += f[k];
media = soma / n;
/* c
alculo da m
edia */
soma = 0.0;

48 V ECTORES E MEM ORIA


DIN AMICA

for( k = 0 ; k < n ; k++)


soma += (f[k] - media) * (f[k] - media);
variancia = soma / n;
/* c
alculo da var. */
printf("M
edia = %5.2f, var = %5.2f\n",
media, variancia);
free(f);
exit(0);
}

Na chamada a` funca o calloc(), dois aspectos devem ser considerados. Em primeiro lugar o operador sizeof() e um operador intrnseco do C que devolve a dimensao (geralmente em bytes)
do tipo ou variavel indicado no argumento. Por outro lado, a` esquerda da funca o calloc() foi
acrescentada e expressao (float*). Esta expressao funciona como um operador de cast, obrigando a` conversao do apontador generico devolvido pela funca o calloc para um apontador para
real. Em geral, na operaca o de cast e indicado o tipo do apontador que se encontra do lado esquerdo da atribuica o. Embora este cast nao seja obrigatorio, e geralmente utilizado na linguagem
C como uma garantia adicional da consistencia das atribuico es.
Apos a reserva dinamica de memoria, o apontador f pode ser tratado como um vector convencional. Como e evidente, o ndice nao deve ultrapassar a posica o n 1, dado que para valores
superiores se estaria a aceder a posico es de memoria invalidas.
Enquanto que as variaveis locais sao criadas na zona da pilha, toda a memoria reservada
dinamicamente e geralmente criada numa regiao independente de memoria designada por heap
(molhe). Admita-se, no caso do programa para calculo das medias e variancias, que o valor de n
especificado pelo utilizador era 4. Uma possvel representaca o do mapa de memoria antes e apos
a chamada da funca o calloc encontra-se representado na figura 3.2.

3.4

da memoria

Gestao
dinamica

3.4.1

da reserva de memoria

Verificacao

Como ja se referiu, ao efectuar uma reserva dinamica de memoria e essencial testar se os recursos solicitados foram ou nao disponibilizados pelo sistema. Com efeito, a reserva de memoria
pode ser mal sucedida por falta de recursos disponveis no sistema. Este teste teste e deve ser sempre efectuado testando o apontador devolvido pela funca o calloc(). Quando existe um erro,
a funca o devolve o endereco 0 de memoria, o qual e representado simbolicamente pela constante

DA MEM ORIA

G EST AO
DIN AMICA
49

Zona da pilha (var. locais)


Endereo

..
.

Contedo

Zona do heap (var. dinamicas)

Varivel

Endereo

..
.

..
.

..
.

...

1001

2102

1003

variancia

2103

1004

media

2104

1005

soma

2105

1006

2106

2107

...

2108

1008
1009

..
.

Varivel

..
.

..
.

...

2101

1002

1007

Contedo

topo

...

2109

..
.

..
.

..
.

A
Zona da pilha (var. locais)
Endereo

..
.

Contedo

..
.

Zona do heap (var. dinamicas)

Varivel

Endereo

..
.

..
.

...

1001

Contedo

Varivel

..
.

..
.

...

2101

1002

2102

*f

1003

variancia

2103

*(f+1) <> f[1]

1004

media

2104

*(f+2) <> f[2]

1005

soma

2105

*(f+3) <> f[3]

2106

2107

...

2108

1006
1007

2102
4

1008
1009

..
.

<>f[0]

topo

...

2109

..
.

..
.

..
.

B
Figura 3.2: Mapa de memoria antes da reserva de memoria (A) e apos a reserva de memoria. (B).

50 V ECTORES E MEM ORIA


DIN AMICA

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

3.4.2

de memoria

Outras funcoes
de gestao
dinamica

free()
Funcao

Enquanto que a funca o calloc() efectua uma reserva dinamica de memoria, a funca o
free(p) liberta o bloco de memoria apontado por p. Como seria de esperar, esta funca o so
tem significado se o apontador p foi obtido por uma funca o previa de reserva de memoria, como
a funca o calloc() descrita anteriormente.
De um modo geral, e boa pratica de programaca o libertar toda a memoria reservada pelo
programa sempre que esta deixa de ser necessaria. Isto sucede frequentemente pela necessidade
de libertar memoria que so foi necessaria temporariamente pelo programa.
Note-se que sempre que o programa termina toda a memoria reservada e automaticamente libertada. Deste modo, a libertaca o explcita da memoria reservada durante a execuca o do programa
nao e estritamente obrigatoria antes de sair do programa atraves da funca o exit() ou da directiva return no bloco main(). Apesar deste facto, alguns autores consideram que, por razoes
de consistencia e arrumaca o do codigo, o programa deve proceder a` libertaca o de toda a memoria
reservada antes de ser concludo. E este o procedimento adoptado no programa para calculo da
variancia, onde a funca o free() e chamada antes do programa terminar.

malloc()
Funcao

A funca o malloc() tem por prototipo

void *malloc(size_t total_size);

onde total_size representa a dimensao total da memoria a reservar, expressa em bytes. A


funca o malloc() e muito semelhante a` funca o calloc() A forma calloc(n,d) pode ser
simplesmente substituda por malloc(n*d). A u nica diferenca formal entre as duas funco es e
que enquanto a funca o calloc() devolve um bloco de memoria inicializado com zero em todas
as posico es, a funca o malloc() nao efectua explicitamente esta inicializaca o.

DA MEM ORIA

G EST AO
DIN AMICA
51

realloc()
Funcao

A funca o realloc() pode ser utilizada sempre que e necessario modificar a dimensao de
um bloco de memoria dinamica reservado anteriormente. O prototipo da funca o e

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

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

int main(){
int *x,n;
/* Obtenc

ao do valor de n ... */
x = (int*) calloc(n,sizeof(int));

mas que, mais tarde, se verificou a necessidade de acrescentar 1000 posico es a este bloco de
memoria. Este resultado pode ser obtido fazendo

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

O funcionamento exacto da funca o realloc() depende das disponibilidades de memoria


existentes. Suponha-se, para assentar ideias, que inicialmente se tinha n=2000, e que o valor de x
resultante da funca o calloc era 10000 (figura 3.3, A). Deste modo o bloco de memoria reservado
inicialmente estendia-se do endereco 10000 ao endereco 11999. Quando mais tarde e chamada a
funca o realloc duas situaco es podem ocorrer. Se os enderecos de memoria entre 12000 e 12999
estiverem livres, o bloco de memoria e prolongado, sem mudanca de stio, pelo que o valor de x
permanece inalterado (figura 3.3, B). No entanto, se algum endereco entre 12000 e 12999 estiver
ocupado (figura 3.3, C), e necessario deslocar todo o bloco de memoria para uma nova regiao.
Neste caso, o novo bloco de memoria pode ser mapeado, por exemplo, entre os enderecos 12500
e 15499 (figura 3.3, D), retornando a funca o realloc o novo endereco da primeira posica o de
memoria (12500). Como complemento, a funca o realloc trata de copiar automaticamente todo

52 V ECTORES E MEM ORIA


DIN AMICA

o conteudo guardado nos enderecos 10000 a 11999 para os enderecos 12500 a 14499 e liberta as
posico es de memoria iniciais.

3.4.3

Garbbage

Como referido anteriormente, o espaco de memoria para as variaveis locais e reservado na


zona de memoria da pilha quando uma funca o e chamada, sendo automaticamente libertado (e
com ele as variaveis locais) quando a funca o termina.
Ao contrario das variaveis locais, a memoria dinamica e criada e libertada sob controlo do
programa, atraves das chamadas a` s funco es calloc(), malloc() e free().
Em programas complexos, e frequente um programa reservar blocos de memoria que, por
erro do programador ou pela propria estrutura do programa, nao sao libertados mas para os quais
se perdem todos os apontadores disponveis. Neste caso, o bloco de memoria dinamica e reservado, mas deixa de ser acessvel porque se perderam todos os apontadores que indicavam a sua
localizaca o em memoria. Estes blocos de memoria reservados mas cuja localizaca o se perdeu sao
geralmente designados por garbbage (lixo).
Considere-se, por exemplo, o seguinte, programa:

void func(){
int *x,y;
y = 3;
x = (int*) calloc(y,sizeof(int));
/* ... Utilizac

ao de x, free()
n
ao chamado...
*/
}
void main()
int a,b;
func();
/* O Bloco reservado em func deixou de ser
usado, mas deixou de estar inacess
vel */

Um mapa de memoria ilustrativo desta situaca o esta representado na figura 3.4.

DA MEM ORIA

G EST AO
DIN AMICA
53

Zona da pilha (var. locais)


Zona da pilha (var. locais)
Endereo

Contedo

..
.

Varivel

Endereo

..
.

..
.
2000

1003

10000

Contedo

..
.

...

1001
1002

10000

10001

11999

1006

12000

1007

12001
...

..
.

...

Endereo

..
.

Contedo

Varivel

Endereo

..
.

..
.

1001
2000

..
.

1003

10000

1765

1005

1008

12003

1009

..
.

..
.

...

12999

..
.

Endereo

..
.

...

1001
1002

Contedo

..
.

2000

1003

10000

Contedo

..
.

10000

666

10001

300

1004

2000

1003

12500

..
.

..
.

..
.
...

10001

300

..
.
11999

1005

1009

1006

12000

1007

12001

11111111

12002

555555

...

1009

..
.

12001

11111111

12002

555555

...

Memoria
libertada

..
.

..
.

Memoria ocupada
por outras var. dinamicas

12500

666

12501

300

..
.
14599
..
.

12003

..
.

1765

...

12000

1008

..
.

Varivel

..
.

..
.
666

1004

1765

Contedo

10000

Varivel

1007

11999

..
.

Endereo

...

1002

..
.

1005

1008

Zona do heap (var. dinamicas)

Varivel

1006

..
.

Zona do heap (var. dinamicas)

..
.

1765

..
.

1001

..
.

...

12000

..
.

..
.

Varivel

..
.

..
.

Zona da pilha (var. locais)

..
.

300

1007

Endereo

Contedo

666

10001

11999

Endereo

10000

1006

12002

Varivel

..
.

1004

Memoria livre

..
.

Zona da pilha (var. locais)

Contedo

..
.

...

1002

300

1005

1009

..
.

666

1008

Varivel

..
.

1004

..
.

Zona do heap (var. dinamicas)

Zona do heap (var. dinamicas)

..
.

Memoria
"realocada"

..
.
..
.

Figura 3.3: Mapa de memoria apos a chamada a` funca o calloc() (A) e apos a funca o
realloc() (B), se existir espaco disponvel nos enderecos contguos. Em (C) e (D) representase a situaca o correspondente no caso em que a memoria dinamica imediatamente a seguir ao bloco
reservado esta ocupada, sendo necessario deslocar todo o bloco para outra zona de memoria. Neste
caso, o apontador retornado pela funca o e diferente do inicial.

54 V ECTORES E MEM ORIA


DIN AMICA

Zona da pilha (var. locais)


Endereo

..
.

Contedo

Zona do heap (var. dinamicas)

Varivel

Endereo

..
.

..
.

..
.

...

1001

Contedo

Zona da pilha (var. locais)

Varivel

Endereo

..
.

..
.

..
.

...

2101

Contedo

Zona do heap (var. dinamicas)

Varivel

..
.

..
.

..
.

...

1001

2102

1003

2103

1003

1004

2104

1004

2105

1005
1006

2106

1007

2107

topo

1006

2106

1007

2107

1008

2108

1009

2109

..
.

..
.

..
.

1002

...

2102

..
.
...

2102

x[0]

2103

x[1]

2104

x[2]

2105

2108

1009

2109

..
.

Varivel

..
.

1008

..
.

Contedo

2101

topo

1002

1005

topo

Endereo

..
.

..
.

topo

...

..
.

B
Zona da pilha (var. locais)
Endereo

..
.

Contedo

Zona do heap (var. dinamicas)

Varivel

Endereo

..
.

..
.

..
.

...

1001

Contedo

Varivel

..
.

..
.

...

2101

1002

2102

x[0]

1003

2103

x[1]

1004

2104

x[2]

1005

topo

2105

1006

2106

1007

2107

1008

2108

1009

2109

..
.

..
.

..
.

topo

...

..
.

C
Figura 3.4: Criaca o de garbbage (lixo). Mapa de memoria (exemplo do texto): (A) antes da
chamada a` funca o func, (B) no final da funca o func e (C) apos retorno ao programa principal. De notar que em (C) a memoria dinamica ficou reservada, mas que se perderam todas as
referencias que permitiam o acesso a esta zona de memoria.

DIN AMICA

C RIAC AO
DE MATRIZES 55

3.5
3.5.1

dinamica

Criacao
de matrizes

Introducao

A criaca o e utilizaca o de estruturas dinamicas em C que possam ter um acesso equivalente


a uma matriz e simples. No entanto, a compreensao detalhada de como funcionam este tipo de
estruturas nem sempre e claro.
De modo a simplificar a exposica o, comecar-se-a por apresentar na secca o 3.5.2 um resumo do
modo de declaraca o e de acesso de matrizes estaticas. Seguidamente, na secca o 3.5.3, descreve-se
uma metodologia simples para criar dinamicamente vectores de apontadores com um comportamento equivalente ao de uma matriz. Seguidamente, na secca o 3.5.4 serao discutidas as diferencas
e semelhancas entre matrizes e vectores de apontadores, de modo a evidenciar as diferencas entre
as estruturas dinamicas criadas e as matrizes nativas do C.

3.5.2

Matrizes estaticas

Conforme ja se viu na secca o 2.6.1, a utilizaca o de estruturas multidimensionais em C apenas exige a declaraca o de uma variavel com varios ndices, especificando cada um da dimensao
pretendida da estrutura. Por exemplo

float x[3][2];

declara uma estrutura bidimensional de dois por tres reais, ocupando um total de seis palavras
de memoria no modelo de memoria que temos vindo a assumir como referencia. E frequente
uma estrutura bidimensional ser interpretada como uma matriz, neste exemplo de duas linhas por
tres colunas. A inicializaca o de uma matriz pode ser efectuada durante a execuca o do programa
ou listando os valores iniciais, sendo apenas necessario fazer um agrupamento hierarquico das
constantes de inicializaca o de acordo com as dimensoes da estrutura:

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


{5.0,6.0}};

Como se mostrou anteriormente, a arrumaca o em memoria desta estrutura encontra-se representada esquematicamente na figura 2.9.

56 V ECTORES E MEM ORIA


DIN AMICA

3.5.3

Matrizes dinamicas

Admita-se que num dado programa se pretende substituir a sequencia

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

por uma estrutura dinamica com um mecanismo de acesso equivalente, mas em que os limites n e
m sao variaveis cujo valor so e conhecido durante a execuca o do programa.
A soluca o para este problema e substituir a estrutura x por um vector dinamico de apontadores
para inteiros, em que cada posica o e por sua vez inicializada com um vector dinamico. Considerese, por exemplo, que se pretende criar uma matriz de n por m. O codigo para este efeito e dado
por

int main{
int k;
int n,m;
int **x;
/* Inicializac

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

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

DIN AMICA

C RIAC AO
DE MATRIZES 57

int main{
int k,j;
int n,m;
int **x;
n = 4; m = 3;
x = (int**) calloc(n,sizeof(int*));
if(x == NULL){
printf("Erro na reserva de mem
oria\n");
exit(1);
}
for(k = 0; k < n ; k++){
x[k] = (int*) calloc(m,sizeof(int));
if(x == NULL){
printf("Erro na reserva de mem
oria\n");
exit(1);
}
}
for(k = 0; k < n ; k++)
for(j = 0; j < m ; j++)
x[k][j] = 10 * k + j;

O resultado da execuca o deste bloco de codigo seria o que se mostra na figura 3.5.
Este exemplo indica que a soluca o geral para simular a criaca o dinamica de matrizes e declarar
um duplo apontador para o tipo pretendido dos elementos da matriz e, seguidamente, reservar
dinamicamente um vector de apontadores e criar dinamicamente os vectores correspondentes a
cada linha.
Como e evidente, neste exemplo x e um duplo apontador para inteiro. Deste modo, se pretender usar esta matriz dinamica como argumento de uma funca o fazer-se, no bloco que chama,

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

enquanto que o cabecalho de func devera ser

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

58 V ECTORES E MEM ORIA


DIN AMICA

Zona da pilha

Zona do heap

Endereo

Contedo

Varivel

..
.

..
.

..
.

1543

2001

..
.

..
.

..
.

Endereo

..
.

Contedo

Varivel

..
.

..
.

2001

2006

x[0]

2002

2009

x[1]

2003

2012

x[2]

2004

2015

x[3]

..
.

..
.

..
.

Endereo

Contedo

Varivel

2006

x[0][0]

2007

x[0][1]

2009

10

x[1][0]

2010

11

x[1][1]

2012

20

x[2][0]

2013

21

x[2][1]

2015

30

x[3][0]

2016

31

x[3][1]

Figura 3.5: Criaca o dinamica de matrizes por meio de um vector de apontadores. Os enderecos
indicados sao apenas ilustrativos.

Esta soluca o e tao frequente na pratica que, por analogia, muitos programadores (mesmo
experientes) de C pensam que, dada a declaraca o tipo x[N][M], x sem qualquer ndice corresponde a um duplo apontador para tipo. Como se mostrou na secca o 2.6.4, esta suposica o nao
corresponde a` realidade: nesta situaca o, x e um apontador para um vector de elementos de tipo.

3.5.4

Vectores de apontadores e matrizes

Considere-se a declaraca o

int *x[4];

Recordando que os [] tem precedencia sobre o operador *, e seguindo a regra de interpretaca o


semantica da declaraca o apresentada na secca o 2.6.4, resulta que x e um vector de 4 apontadores
para inteiros. Ou seja, em termos de modelo de memoria, encontramos uma representaca o como a
indicada na figura 3.6, situaca o A. Um ponto importante a sublinhar e que esta declaraca o apenas
reserva memoria para quatro apontadores. Esta situaca o e frequentemente mal interpretada pelo
facto de, sendo x[k] uma apontador para um inteiro, entao *x[k] e *(x[k]+j) tambem sao
do tipo inteiro. Mas, devido a` s equivalencias sintacticas de enderecamento em C, tem-se

DIN AMICA

C RIAC AO
DE MATRIZES 59

Endereo

Contedo

Varivel

..
.

..
.

..
.
1001

???

x[0]

1002

???

x[1]

1003

???

x[2]

1004

???

x[3]

..
.

..
.

..
.

A
Endereo

..
.

Contedo

Varivel

Endereo

..
.

2001

x[0][0]

2002

x[0][1]

2005

x[1][0]

2006

x[1][1]

2010

x[2][0]

2011

x[2][1]

2013

x[3][0]

2014

x[3][1]

..
.

1001

2001

x[0]

1002

2005

x[1]

1003

2010

x[2]

1004

2013

x[3]

..
.

..
.

..
.

Contedo

Varivel

B
Figura 3.6: Vectores de apontadores: A - Antes de inicializado, B- apos a inicializaca 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 so e possvel se os elementos de x tiverem sido convenientemente inicializados
de modo a enderecarem a base de um vector de inteiros. Isto, so por si, nao e realizado pela
declaraca o int *x[4], que se limita a declarar um vector de apontadores e a reservar 4 palavras
de memoria para este efeito. Nesta declaraca o, nao e reservada memoria para qualquer inteiro.
Claro que, se o utilizador o pretender, e facil inicializar as posico es de memoria do vector
de apontadores reservando memoria dinamica de modo a armazenar os inteiros pretendido. Admitindo 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 inicializaca o pode ser feita pela
sequencia

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, alem da reserva das posico es de memoria para os vectores inteiros, se procedeu a` sua
inicializaca o. A situaca o final pode ser representada pelo modelo da figura 3.6, situaca o B.
Em sntese, e conveniente reter que, apesar da manipulaca o de matrizes e de vectores de
apontadores seja sintacticamente semelhante, os mecanismos de reserva de memoria sao claramente distintos. No caso da matriz int x[4][2], a propria declaraca o reserva automaticamente espaco para oito inteiros, os quais podem ser acedidos pelas regras habituais. No caso da
declaraca o de um vector de apontadores, como por exemplo int *x[4], apenas e efectuada a
reserva de quatro apontadores. A sua utilizaca o requer a previa inicializaca o de cada um dos seus
elementos com um endereco valido de um bloco de memoria, o qual pode ser obtido a partir de
um vector ja declarado ou, como no exemplo aqui usado, pela reserva de um bloco de memoria
dinamica atraves da funca o calloc().

Captulo 4

Listas dinamicas

4.1

Introducao

Como se viu anteriormente, a utilizaca o de vectores em C apresenta a desvantagem de exigir o


conhecimento a` partida da dimensao maxima do vector. Em muitas situaco es, a utilizaca o de vectores dinamicos, como descrito na secca o 3.3, e uma soluca o eficiente. No entanto, em situaco es
em que a quantidade de memoria dinamica necessaria varia frequentemente durante a execuca o do
programa, a utilizaca o de vectores dinamicos revela-se frequentemente pouco eficaz.
Considere-se, por exemplo, um gestor de uma central telefonica que necessita de reservar
dinamicamente memoria para cada chamada estabelecida (onde e armazenada toda a informaca o
sobre a ligaca o, como por exemplo os numeros de origem e destino, tempo de ligaca o, etc,) e
libertar essa mesma memoria quando a chamada e terminada. Dado que o numero de ligaco es
activas varia frequentemente ao longo do dia, uma soluca o baseada num vector dinamico exigiria o seu redimensionamento frequente. Em particular, sempre que nao fosse possvel encontrar
memoria livre imediatamente a seguir ao fim do vector, seria necessario deslocar todo o vector (v.
funca o realloc, secca o 3.4.2) para uma nova zona de memoria, e a copia exaustiva de todo o
seu conteudo para a nova posica o de memoria. Este mecanismo, alem de pouco eficiente, poderia
inviabilizar o funcionamento em tempo real do sistema, dado que uma percentagem significativa
do tempo disponvel seria dispendido na gestao da memoria usada pelo vector.
Quando se pretende armazenar entidades individuais de memoria que possam ser criadas e
libertadas individualmente com uma certa frequencia, e normalmente mais adequado utilizar estruturas de dados criadas dinamicas e organizadas em listas.


62 L ISTAS DIN AMICAS

4.2

de dados
Abstraccao

Neste captulo descrevem-se listas dinamicas como uma forma de armazenar ou organizar
informaca o. Esta organizaca o e , obviamente, independente do tipo de informaca o que se pretende
armazenar. Esta situaca o e semelhante a` da construca o de um armario com gavetas: a estrutura
do movel e independente do conteudo que se ira arrumar nas gavetas. Esta independencia entre
movel e conteudo garante a generalidade do movel, no sentido em que este sera adaptavel a varias
situaco es.
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
desejavel que um bloco de codigo desenvolvido para o programa A seja reutilizavel no programa
B ou, pelo menos, que tal seja possvel sem alteraco es ou com alteraco es mnimas.
Uma forma de atingir este objectivo e , ao desenhar o programa, adoptar uma metodologia
designada por abstracca o de dados(Martins, 1989). Esta metodologia baseia-se na definica o e
distinca o clara dos varios tipos de dados utilizados pelo programa (ou, seguindo o exemplo anterior, distinguir tanto quanto possvel o movel do conteudo das gavetas). Segundo esta metodologia,
cada tipo de dados deve ter manipulado apenas por um conjunto de funco es especficas, designadas
metodos, conhecedoras dos detalhes internos do tipo de dados associado. Todos os outros blocos
do programa tem apenas que conhecer as propriedades abstractas ou genericas deste tipo. A
manipulaca o do tipo so sao acessveis de outros blocos de programa atraves dos metodos disponibilizados pelo tipo de dados.
Esta metodologia de programaca o garante que, caso seja necessario alterar os detalhes internos de um determinado tipo, apenas e necessario alterar os metodos correspondentes a esse tipo.
Dado que todos os blocos de programas onde este tipo de dados e utilizado apenas acedem a ele
atraves dos metodos disponveis, requerendo apenas o conhecimento das suas propriedades gerais
(ou abstractas), e possvel minimizar ou eliminar totalmente as alteraco es necessarias aos outros blocos de programa. Deste modo, a metodologia de abstracca o de dados contribui para uma
relativa estanquecidade e independencia dos diversos modulos constituintes.
Neste captulo, utilizar-se-a nos diversos exemplos uma metodologia estrita de abstracca o de
dados, chamando-se a atenca o caso a caso para aspectos especficos da implementaca o.

L ISTAS 63

Zona da pilha (var. locais)


Varivel

Contedo

..
.
base

Zona do heap (var. dinamicas)


Contedo

..
.

dados
(pos 1)

dados
(pos 2)

dados
(pos 3)

dados
(pos 4)
NULL

Figura 4.1: Lista dinamica. Todos os elementos sao criados dinamicamente na zona de heap,
sendo suficiente uma variavel local de tipo apontador (variavel base, nas figura) para poder aceder
a todos os elementos da lista.

4.3

Listas

Uma lista dinamica nao e mais do que uma colecca o de estruturas de dados, criadas dinamicamente, em que cada elemento dispoe de um apontador para o elemento seguinte (figura 4.1). Cada
elemento da lista e constitudo por uma zona de armazenamento de dados e de um apontador para
o proximo elemento. Para ser possvel identificar o fim da lista, no apontador do u ltimo elemento
e colocado o valor NULL.
Para criar uma lista e necessario definir um tipo de dados e criar uma variavel local (normalmente designada base ou raiz. Admita-se, por exemplo, que se pretendia que cada elemento da
lista guardasse uma string com um nome e um numero inteiro. A criaca o de uma lista de elementos
deste tipo exigiria uma declaraca o de tipos e de variaveis como se segue:

#define MAX_NOME 200


64 L ISTAS DIN AMICAS

typedef struct _tipoDados {


char nome[MAX_NOME];
int num;
} _tipoDados;
typedef struct _tipoLista{
tipoDados dados;
struct _tipoLista *seg;
} tipoLista;
int main(){
tipoLista *base;
/* ... */

Note-se que na definica o do apontador seguinte, e necessario utilizar a construca o


struct _tipoLista *seg e nao tipoLista *seg. De facto, apesar de serem duas formas aparentemente semelhantes, a forma

typedef struct _tipoLista{


tipoDados dados;
tipoLista *seg; /* DECLARAC

AO INV
ALIDA */
} tipoLista;

e invalida porque tipoLista so e conhecido no final da declaraca o, e nao pode ser utilizado
antes de completamente especificado.
Uma das vantagens deste tipo de estruturas e que cada um dos seus elementos pode ser reservado e libertado individualmente, sem afectar os restantes elementos (exigindo apenas ajustar um
ou dois apontadores, de forma a garantir a consistencia do conjunto). Adicionalmente, a ordem
dos elementos da lista e apenas definida pela organizaca o dos apontadores. Deste modo, se for
necessario introduzir um elemento entre dois ja existentes, nao e necessario deslocar todos os elementos de uma posica o, como sucederia num vector. Basta de facto reservar espaco para um
elemento adicional e deslocar todos os outros elementos de uma posica o. Na figura 4.2 apresentase um exemplo em que se ilustra esta independencia entre enderecos de memoria e sequencia da
lista, e confere a esta uma flexibilidade superior a` de um vector.
Dada esta independencia entre enderecos de memoria e sequencia, e frequente adoptar-se
representaco es graficas simplificadas para as listas, como a se mostra na figura 4.3. A cruz no
u ltimo elemento corresponde a uma representaca o simbolica abreviada para o valor NULL.

L ISTAS 65

Zona da pilha (var. locais)


Varivel

Contedo

..
.
base

Zona do heap (var. dinamicas)


Contedo

..
.

dados
(pos 2)

dados
(pos 1)

dados
(pos 4)
NULL

dados
(pos 3)

Figura 4.2: Lista dinamica. A sequencia dos elementos da lista e apenas definida pelos apontadores
de cada elemento, independentemente do endereco de memoria ocupado.

base

Figura 4.3: Lista dinamica. Representaca o simplificada


66 L ISTAS DIN AMICAS

Apesar das vantagens ja referidas de uma lista, e necessario ter em atenca o que o programa so
dispoe de uma variavel para aceder a toda a lista, e que esta tem que ser percorrida elemento a elemento ate se atingir a posica o pretendida. Num vector, dado que todos as posico es sao adjacentes,
para aceder a qualquer posica o basta saber o endereco do primeiro elemento e o numero de ordem
(ndice) do elemento que se pretende aceder para ser possvel calcular o endereco do elemento que
se pretende. Ou seja, a flexibilidade acrescida da lista em termos de organizaca o de memoria e
conseguida a` custa do tempo de acesso aos seus elementos.

4.4

Listas dinamicas:
listar elementos

Uma operaca o particularmente simples sobre uma lista, mas ilustrativa do mecanismos de
acesso geralmente adoptados, e a listagem de todos os seus elementos. Considere-se, por exemplo,
que se pretende listar no terminal o conteudo de todos os numeros e nomes da lista do exemplo da
secca o 4.3. Uma soluca o para este efeito seria a sequencia de codigo seguinte:

#define MAX_NOME 200


typedef struct _tipoDados {
char nome[MAX_NOME];
int num;
} tipoDados;
typedef struct _tipoLista{
tipoDados dados;
struct _tipoLista *seg;
} tipoLista;
void escreveDados(tipoDados dados){
printf("Num = %5d Nome = %s\n",dados.num, dados.nome);
}
void lista(tipoLista *base){
tipoLista *aux; /* Apontador que percorre a lista */
aux = base;
/* Incializa com o primeiro elemento
while(aux != NULL){ /* Enquanto n
ao se atingir o final...
escreveDados(aux -> dados); /* Escreve o conte
udo
aux = aux -> seg; /* Avanc
a para a pr
oxima posic

ao
}
}
int main(){

*/
*/
*/
*/


L ISTAS DIN AMICAS
:

LISTAR ELEMENTOS

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

/* Listagem do conte
Udo */
lista(base);

O princpio essencial da operaca o de listagem esta includa na funca o lista e e muito simples. Inicializa-se um apontador aux para o incio da lista. Seguidamente, enquanto este apontador for diferente de NULL (ou seja, nao atingir o fim da lista), o apontador e sucessivamente
avancado para o elemento seguinte.
Note-se como se adoptou neste bloco de codigo a metodologia de abstracca o de dados. Existe neste exemplo uma separaca o funcional de tarefas entre as diversas entidades que participam o programa. A funca o lista manipula unicamente a estrutura de dados que suporta a
lista, percorrendo todos os seus elementos ate ao fim da lista. No entanto, quando e necessario
escrever o conteudo de cada no no terminal, esta tarefa e delegada a uma funca o especfica (
escreveDados), responsavel pela manipulaca o e processamento dos detalhes especficos de
variaveis do tipo tipoDados. Neste sentido, tipoDados e , para a lista, um tipo abstracto
generico, cuja representaca o interna ela desconhece, e da qual so tem que conhecer uma propriedade muito generica: uma variavel deste tipo pode de alguma forma ser escrita no terminal,
havendo um metodo competente para o fazer.
Uma forma alternativa da funca o listar frequentemente utilizada passa pela utilizaca o de um
ciclo for em vez do ciclo while:

void lista(tipoLista *base){


tipoLista *aux; /* Apont. que percorre */
/* a lista.
*/
for(aux = base ; aux != NULL; aux = aux -> seg)
escreveDados(aux -> dados);
}

67


68 L ISTAS DIN AMICAS

4.5
4.5.1

Listas: pilhas

Introducao

As listas tem habitualmente uma estrutura e organizaca o em memoria semelhante a` apresentada na figura 4.1. No entanto, os mecanismos de acesso a` lista sao variaveis e, no seu conjunto,
podem permitir que a lista realize apenas um subconjunto de tarefas bem determinadas.
Em teoria da computaca o, uma pilha (ou stack, na lietratura anglo-saxonica) e um sistema
que armazena dados de forma sequencial e que permite a sua leitura por ordem inversa da escrita.
A designaca o pilha vem da semelhanca funcional com uma pilha comum. Admita-se que se
dispoe de um conjunto de palavras que se pretende armazenar. Considere-se ainda que o sistema
de armazenamento disponvel e um conjunto de pratos, sendo cada palavra manuscrita no fundo
` medida que as palavras vao sendo comunicadas ao sistema de armazenamento, sao
do prato. A
escritas no fundo de um prato e este e colocado numa pilha. A leitura posterior das palavras
registadas e feita desempilhando pratos sucessivamente e lendo o valor registado em cada um.
Como e o bvio, a leitura ocorre por ordem inversa da escrita.
Uma pilha e frequentemente designada por uma estrutura LIFO, acronimo derivado da expressao Last In First Out. A operaca o de armazenamento na pilha e geralmente designada de
operaca o de push, enquanto que a leitura e designada de pop.
As pilhas tem uma utilizaca o frequente em informatica sempre que se pretende inverter uma
sequencia de dados. Embora a relaca o nao seja o bvia, pode indicar-se, por exemplo, que o calculo
de uma expressao aritmetica em que varios operadores tem nveis de precedencia diferentes e
realizada acumulando numa pilha resultados intermedios.
Considere-se, por exemplo, que se pretende inverter os caracteres da palavra Lus. A forma
como uma pilha pode contribuir para este resultado esta graficamente sugerido na figura 4.4.
Uma pilha pode ser realizada em C atraves de uma lista dinamica ligada. A colocaca o de
um novo elemento no topo da pilha corresponde a criar um novo elemento para a lista e inseri-lo
junto a` base. De forma correspondente, a operaca o de remoca o e leitura corresponde a retirar o
elemento da base, ficando esta a apontar para o elemento seguinte (v. figura 4.5)

L ISTAS :

Entrada (push)
(sobreposio)

PILHAS

Saida (pop)
(remoo)

Figura 4.4: Inversao de uma sequencia de caracteres por meio de uma pilha.

base

Figura 4.5: Realizaca o de uma pilha por uma lista ligada. No exemplo, considera-se uma pilha de
caracteres e a inserca o do caracter no topo da pilha. A tracejado indica-se a ligaca o existente
antes da inserca o, a cheio apos a inserca o. A operaca o de remoca o corresponde a` realizaca o
do mecanismo inverso (reposica o da ligaca o a tracejado e libertaca o do elemento de memoria
correspondente ao ).

69


70 L ISTAS DIN AMICAS

4.5.2

Declaracao

Tal como qualquer lista, a realizaca o de uma pilha implica a declaraca o de um tipo de suporte
da lista e a utilizaca o de uma variavel para a base da pilha. A declaraca o da pilha pode ser feita,
por exemplo, pela declaraca o

typedef struct _tipoPilha {


tipoDados dados;
struct _tipoPilha *seg;
} tipoPilha;

onde se admite que tipoDados ja foi definido e caracteriza o tipo de informaca o registada em
cada posica o da pilha.
A utilizaca o da pilha implica que no programa principal, ou no bloco de codigo onde se
pretende utilizar a pilha, seja declarada uma variavel de tipo apontador para tipoPilha que
registe a base da pilha. Ou seja, sera necessario dispor de uma variavel declarada, por exemplo,
por

tipoPilha *pilha;

4.5.3

Inicializacao

A primeira operaca o necessaria para utilizar a pilha e inicializa-la ou cria-la. Uma pilha vazia
deve ter a sua base apontada para NULL. Embora seja possvel realizar directamente uma atribuica o
a` variavel pilha, tratando-se de um detalhe interno de manipulaca o do tipo tipoPilha, esta
inicializaca o devera ser delegada numa funca o especfica. Deste modo, para respeitar a metodologia de abstracca o de dados, deve declarar-se uma pequena funca o

tipoPilha *inicializa(){
return NULL;
}

e utilizar-se esta funca o sempre que seja necessario inicializar a pilha:

pilha = inicializa();

L ISTAS :

4.5.4

PILHAS

Sobreposicao

A operaca o de sobreposica o exige a seguinte sequencia de operaco es:

Reserva de memoria dinamica para armazenar um novo elemento.


Copia dos dados para o elemento criado.
Inserca o do novo elemento na base da pilha.

Para a criaca o da memoria dinamica, e conveniente criar uma funca o novoNo() que cria
espaco para um novo no, verificando a simultaneamente a existencia de erros na reserva de
memoria. Esta funca o pode ser definida como
tipoPilha *novoNo(tipoDados x){
tipoPilha *novo = calloc(1,sizeof(tipoPilha));
if(novo == NULL)
Erro("Erro na reserva de mem
oria");
novo -> dados = x;
novo -> seg = NULL;
return novo;
}

onde Erro() e uma funca o generica simples que imprime uma mensagem de erro e termina o
programa (ver secca o 4.5.7).
A operaca o de inserca o no topo da pilha exige a realizaca o das operaco es representadas na
figura 4.5. Estas podem ser realizada pela seguinte funca o:
tipoPilha *sobrepoe(tipoPilha *pilha,tipoDados x){
tipoPilha *novo;
novo = novoNo(x);
novo -> seg = pilha;
return novo;
}

Esta funca o, apos a criaca o do novo elemento, limita-se a colocar o apontador seg a apontar para o elemento que anteriormente se encontrava no topo, e indicar a` base que o novo topo
corresponde ao novo elemento inserido.

71


72 L ISTAS DIN AMICAS

Note-se que esta funca o 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 operaca o de remoca o da pilha e acompanhada da leitura. Tal como anteriormente a funca o


recebe o apontador para a base da lista e devolve a base alterada. Dado que a remoca o e acompanhada de leitura, a funca o recebe um segundo apontador, passado por referencia, que deve ser
usado para guardar o valor do elemento removido. Uma funca o possvel para a realizaca o desta
operaca o e
tipoPilha *retira(tipoPilha *pilha,tipoDados *x){
tipoPilha *aux;
if(pilhaVazia(pilha))
Erro("remoc

ao de uma pilha vazia");


*x = pilha -> dados;
aux = pilha -> seg;
free(pilha);
return aux;
}
A operaca o de remoca o e precedida de um teste para verificaca o de pilha vazia, para garantia
de integridade da memoria (v. secca o 4.5.6). Note-se que apos a leitura da pilha, o bloco de
memoria utilizado e libertado pela funca o free().

4.5.6

Teste

Antes de ser tentada a remoca o de um elemento da pilha, e conveniente testar se a pilha se


encontra vazia. Numa primeira abordagem, poder-se-ia ser tentado a testar, sempre que necessario,
se o apontador da base se encontra com o valor NULL. Esta realizaca o, ainda que correcta, violaria
o princpio essencial de abstracca o de dados que temos vindo a respeitar: a caracterizaca o como
vazia de uma pilha e um detalhe interno do tipoPilha, e como tal deve ser delegado numa
funca o especfica, de tipo booleano. Deste modo, poderia fazer-se
int pilhaVazia(tipoPilha *pilha){
return (pilha == NULL);
}

L ISTAS :

PILHAS

Esta funca o retorna 1 se a pilha estiver vazia, e 0 em caso contrario.

4.5.7

Exemplo

Suponha-se que se pretende implementar um sistema de inversao de caracteres baseado na


pilha definida anteriormente. Uma das vantagens de utilizar uma pilha para este efeito consiste
no facto de nao ser necessario definir a` partida a dimensao maxima da cadeia de caracteres: o
programa efectua a reserva e libertaca o de memoria a` medida das necessidades do programa.
Neste caso, tipoDados e realizado por um simples caracter. Para respeitar e exemplificar
o princpio de abstracca o, iremos escrever os metodos especficos de leitura e escrita deste tipo.
De modo a explorar de forma eficaz a independencia dos diversos tipos de dados, os metodos
correspondentes a cada tipo devem ser declarados em ficheiros separados. Deste modo, os metodos
de acesso a tipoDados sao agrupados num ficheiro dados.c. Os prototipos correspondentes
sao declarados no ficheiro dados.h.

Ficheiro dados.h
/*
* Ficheiro: dados.h
* Autor:
Fernando M. Silva
* Data:
7/11/2002
* Conte
udo:
*
Ficheiro com declarac

ao de tipos e
*
prot
otipos dos m
etodos para manipulac

ao
*
de um tipoDados, concretizados aqui
*
por um tipo caracter simples.
*/
#ifndef _DADOS_H
#define _DADOS_H
#include <stdio.h>
#include <stdlib.h>

typedef char tipoDados;


/* M
etodos de acesso a tipo dados */
int
leDados(tipoDados *x);
void
escreveDados(tipoDados x);
#endif /* _DADOS_H */

73


74 L ISTAS DIN AMICAS

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

Ficheiro: dados.c
Autor:
Fernando M. Silva
Data:
12/11/2002
Conte
udo:
M
etodos para manipulac

ao de um
tipoDados, concretizado por um
caracter simples.

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

De igual modo, a metodologia de abstracca o de dados implementada sugere que todos


os metodos de acesso a tipoPilha sejam agrupados num ficheiro pilha.c, ficando as
declaraco es e prototipos correspondentes acessveis num ficheiro pilha.h.
Deste modo, ter-se-ia

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

Ficheiro: pilha.h
Autor:
Fernando M. Silva
Data:
7/11/2000
Conte
udo:
Ficheiro com declarac

ao de tipos e
prot
otipos dos m
etodos para manipulac

ao

L ISTAS :

*
de uma pilha simples de elementos gen
ericos
*
de "tipoDados".
*/
#ifndef _PILHA_H
#define _PILHA_H
#include "dados.h"
typedef struct _tipoPilha {
tipoDados dados;
struct _tipoPilha *seg;
} tipoPilha;
/* Prot
otipos das func

oes */
tipoPilha *inicializa(void);
tipoPilha *sobrepoe(tipoPilha *pilha,tipoDados x);
tipoPilha *retira(tipoPilha *pilha,tipoDados *x);
int
pilhaVazia(tipoPilha *pilha);
#endif /* _PILHA_H */

Ficheiro pilha.c
/*
* Ficheiro: pilha.c
* Autor:
Fernando M. Silva
* Data:
1/12/2000
* Conte
udo:
*
M
etodos para manipulac

ao de uma pilha suportada


*
numa estrutura din
amica ligada
*/
#include "pilha.h"
#include "util.h"
tipoPilha *novoNo(tipoDados x){
/*
* Cria um novo n
o para a pilha
*/
tipoPilha *novo = calloc(1,sizeof(tipoPilha));
if(novo == NULL)
Erro("Erro na reserva de mem
oria");
novo -> dados = x;
novo -> seg = NULL;
return novo;

PILHAS

75


76 L ISTAS DIN AMICAS

}
tipoPilha *inicializa(){
/*
* Cria uma nova pilha
* Retorna:
*
Pilha inicializada
*/
return NULL;
}
int pilhaVazia(tipoPilha *pilha){
/*
* Verifica o estado da pilha
* Argumentos:
*
pilha - Apontador para a base da pilha
* Retorna:
*
1 se a pilha estiver vazia
*
0 em caso contr
ario
*/
return (pilha == NULL);
}
tipoPilha *sobrepoe(tipoPilha *pilha,tipoDados x){
/*
* Adiciona um elemento `
a pilha
* Argumentos:
*
pilha - Apontador para a base da pilha
*
x
- dados a inserir
* Retorna:
*
Pilha modificada
*/
tipoPilha *novo;
novo = novoNo(x);
novo -> seg = pilha;
return novo;
}
tipoPilha *retira(tipoPilha *pilha,tipoDados *x){
/*
* Retira um elemento da pilha
* Argumentos:
*
pilha - Apontador para a base da pilha
*
x
- Apontador para vari
avel que
*
retorna o valor lido da pilha
* Retorna:
*
Pilha modificada

L ISTAS :

PILHAS

*/
tipoPilha *aux;
if(pilhaVazia(pilha))
Erro("remoc

ao de uma pilha vazia");


*x = pilha -> dados;
aux = pilha -> seg;
free(pilha);
return aux;
}

A funca o generica de erro Erro() pode ser declarada num modulo generico util.c, onde
sao agrupadas funco es utilitarias genericas (neste exemplo, Erro() e u nica). O prototipos correspondente deve ser declarado num modulo util.h.

Ficheiro util.h
/*
* Ficheiro: util.h
* Autor:
Fernando M. Silva
* Data:
7/11/2002
* Conte
udo:
*
Ficheiro com declarac

ao de func

oes e
*
prot
otipos gen
ericos
*/
#ifndef _UTIL_H
#define _UTIL_H

void Erro(char *msgErro);


#endif /* _UTIL_H */

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

Ficheiro:
Autor:
Data:
Conte
udo:
Func

oes

util.c
Fernando M. Silva
1/11/2002
gen
ericas

77


78 L ISTAS DIN AMICAS

#include <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 u ltimo, o programa principal. Este limita-se a inicializar a pilha e a efectuar
a leitura de um sequencia de dados, acumulando-os sucessivamente na pilha. A leitura e efectuada
ate que a funca o leDados() (metodo de tipoDados) identifique uma mudanca de linha na
entrada. Seguidamente, os dados sao sucessivamente removidos (desempilhados) e escritos no
dispositivo de sada, ate que a pilha esteja vazia. Deste modo, ter-se-ia:

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

L ISTAS :

PILHAS

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

A compilaca o automatica de todos os modulos pode ser realizada por meio do utilitario make.
A Makefile correspondente poderia ser escrita como

#
# Ficheiro: Makefile
# Autor:
Fernando M. Silva
# Data:
7/11/2000
# Conte
udo:
#
Makefile para teste de estruturas din
amicas
#
#
A vari
avel CFLAGS especifica as flags usadas
#
por omiss
ao nas regras de compilac

ao
#
#
A vari
avel SOURCES, que define os ficheiro
#
fonte em C, s
o e usada para permitir a
#
evocac

ao do utilit
ario "makedepend" (pelo
#
comando make depend), de modo a actualizar
#
automaticamente as depend
encias dos ficheiros
#
.o nos ficheiros .h
#
#
A vari
avel OBJECTS define o conjunto dos
#
ficheiros objectos
#
CFLAGS=-g -Wall -ansi -pedantic
SOURCES=main.c pilha.c dados.c util.c
OBJECTS=main.o pilha.o dados.o util.o
#
# Comando de linkagem dos execut
aveis
#
teste: $(OBJECTS)
gcc -o $@ $(OBJECTS)

#
# A regra make depend efectua uma actualizac

ao da
# makefile, actualizando as listas de depend
encias dos

79


80 L ISTAS DIN AMICAS

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

# DO NOT DELETE

4.6
4.6.1

Listas: filas

Introducao

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

4.6.2

Declaracao

De forma a que seja possvel manipular uma u nica variavel do tipoFila, esta e realizada
por uma estrutura de dados que agrupa os dois apontadores, incio e fim. Deste modo, a declaraca o
da fila pode ser realizada por
1

De facto, o apontador para a base seria suficiente para realizar a fila; no entanto, cada operaca o de inserca o exigiria
percorrer a totalidade da fila para realizar a inserca o no final, metodo que seria pouco eficiente.

L ISTAS :

fim

FILAS

inicio

A
fim

inicio

B
fim

inicio

Figura 4.6: Realizaca o de uma fila de caracteres com uma lista ligada. A - Estrutura da fila, B Fila em A apos a inserca o do caracter s (a tracejado, as ligaco es eliminadas), C - Fila em B apos
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 inicializaca o da fila vazia corresponde simplesmente a colocar a NULL os dois apontadores


da fila. Deste modo, a inicializaca o da fila pode ser realizada simplesmente por

void inicializa(tipoFila *fila){


fila -> inicio = NULL;
fila -> fim
= NULL;
}

Note-se que na chamada a` funca o o argumento deve ser efectuada por referencia (passando o
endereco da variavel tipoFila) de modo a que a variavel seja alteravel pela funca o.

81


82 L ISTAS DIN AMICAS

4.6.4

Insercao

A inserca o de novos elementos corresponde a` colocaca o de elementos no final da fila, tal como
representado graficamente na figura 4.6, caso B.
Tal como no caso da pilha, para a criaca o da memoria dinamica, e conveniente criar uma
funca o auxiliar novoNo().

tipoLista *novoNo(tipoDados x){


tipoLista *novo = calloc(1,sizeof(tipoFila));
if(novo == NULL)
Erro("Erro na reserva de mem
oria");
novo -> dados = x;
novo -> seg = NULL;
return novo;
}

A funca o de inserca o propriamente dita pode ser realizada por

void adiciona(tipoFila *fila,tipoDados x){


tipoLista *novo;
novo = novoNo(x);
novo -> dados = x;
if(fila -> fim != NULL){
fila -> fim -> seg = novo;
}
else{
fila -> inicio = novo;
}
fila -> fim = novo;
}

Nesta funca o e necessario considerar o caso particular em que a fila esta vazia e em que,
como tal, o apontador fim esta a NULL. Neste caso particular, ambos os apontadores ( inicio
e fim) devem ser colocados ser inicializados com o endereco do novo elemento inserido. No
caso habitual (lista nao vazia), basta adicionar o novo elemento ao u ltimo da lista e modificar o
apontador fim.

L ISTAS :

4.6.5

FILAS

Remocao

A inserca o de novos elementos corresponde a` eliminaca o de elementos presentes no final da


fila, tal como representado graficamente na figura 4.6, caso C.
Esta funca o pode ser realizada por

void retira(tipoFila *fila,tipoDados *x){


tipoLista *aux;
if(filaVazia(fila))
Erro("Remoc

ao de uma fila vazia");


*x = fila -> inicio -> dados;
aux = fila -> inicio;
fila -> inicio = fila -> inicio -> seg;
if(fila -> inicio == NULL)
fila -> fim = NULL;
free(aux);
}

A funca o de retira tem tambem uma estrutura simples. Tal como na funca o de inserca o, e
necessario considerar o caso particular em que a lista tem apenas um elemento, situaca o em que o
apontador fim deve ser colocado a NULL depois da remoca o.
Tal como no caso da pilha, e testada a condica o de pilha vazia antes de efectuar a remoca o
(ver secca o 4.6.6).

4.6.6

Teste

Para manipulaca o da lista e indispensavel dispor de uma funca o para testar se a fila se encontra
vazia. Para este teste basta verificar qualquer um dos apontadores se encontra a NULL. Este teste
pode ser efectuado pelo codigo

int filaVazia(tipoFila *fila){


return (fila -> inicio == NULL);
}

83


84 L ISTAS DIN AMICAS

4.6.7

Exemplo

Para uma maior semelhanca com o exemplo da pilha, utiliza-se tambem neste exemplo o caso
de uma fila de caracteres. Como seria de esperar, a fila nao realiza neste caso uma inversao, mas
apenas um armazenamento temporario da informaca o, permitindo a sua reproduca o pela mesma
ordem de entrada.
Tal como no exemplo da pilha, o codigo foi distribudo por tres ficheiros: main.c, fila.c
e dados.c, tendo para os dois u ltimos sido desenvolvido um ficheiro de prototipos associado.
Dada a semelhanca com o exemplo da pilha, apresentam-se aqui apenas o conteudo
dos varios ficheiros, sem outros comentarios. Omite-se aqui o conteudo dos ficheiros
dados.h, dados.c, util.h e util.c por serem obviamente identicos aos anteriores.

Ficheiro fila.h
/*
* Ficheiro: fila.h
* Autor:
Fernando M. Silva
* Data:
7/11/2002
* Conte
udo:
*
Ficheiro com declarac

ao de tipos e
*
prot
otipos dos m
etodos para manipulac

ao
*
de uma fila simples de elementos gen
ericos
*
de "tipoDados".
*/
#ifndef _FILA_H
#define _FILA_H
#include "dados.h"
typedef struct _tipoLista {
tipoDados dados;
struct _tipoLista *seg;
} tipoLista;
typedef struct _tipoFila {
tipoLista *fim;
tipoLista *inicio;
} tipoFila;
/* Prot
otipos das func

oes */

L ISTAS :

void
void
void
int

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

#endif /* _FILA_H */

Ficheiro fila.c
/*
* Ficheiro: fila.c
* Autor:
Fernando M. Silva
* Data:
1/11/2002
* Conte
udo:
*
M
etodos para manipulac

ao de uma fila suportada


*
numa estrutura din
amica ligada
*/
#include "fila.h"
#include "util.h"
tipoLista *novoNo(tipoDados x){
/*
* Cria um novo n
o para a fila
*/
tipoLista *novo = calloc(1,sizeof(tipoFila));
if(novo == NULL)
Erro("Erro na reserva de mem
oria");
novo -> dados = x;
novo -> seg = NULL;
return novo;
}
void inicializa(tipoFila *fila){
/*
* Inicializa uma nova fila
*/
fila -> inicio = NULL;
fila -> fim
= NULL;
}
int filaVazia(tipoFila *fila){
/*
* Verifica o estado da fila
* Argumentos:

FILAS

85


86 L ISTAS DIN AMICAS

*
fila - Apontador para a base da fila
* Retorna:
*
1 se a fila estiver vazia
*
0 em caso contr
ario
*/
return (fila -> inicio == NULL);
}
void adiciona(tipoFila *fila,tipoDados x){
/*
* Adiciona um elemento `
a fila
* Argumentos:
*
fila - Apontador para a fila
*
x
- dados a inserir
*/
tipoLista *novo;
novo = novoNo(x);
if(fila -> fim != NULL){
fila -> fim -> seg = novo;
}
else{
fila -> inicio = novo;
}
fila -> fim = novo;
}
void retira(tipoFila *fila,tipoDados *x){
/*
* Retira um elemento fila
* Argumentos:
*
fila - Apontador para a fila
*
x
- Apontador para vari
avel que
*
retorna o valor lido da fila
*/
tipoLista *aux;
if(filaVazia(fila))
Erro("Remoc

ao de uma fila vazia");


*x = fila -> inicio -> dados;
aux = fila -> inicio;
fila -> inicio = fila -> inicio -> seg;
if(fila -> inicio == NULL)
fila -> fim = NULL;
free(aux);
}

L ISTAS ORDENADAS 87

Ficheiro main.c
/*
* Ficheiro: main.c
* Autor:
Fernando M. Silva
* Data:
7/11/2000
* Conte
udo:
*
Programa principal simples para teste
*
de uma fila.
*/
#include <stdio.h>
#include <stdlib.h>
#include "fila.h"
int main(){
tipoFila fila;
tipoDados x;
inicializa(&fila);
printf("Introduza uma sequ
encia de caracteres:\n");
while(!leDados(&x)){
adiciona(&fila,x);
}
printf("Sequ
encia lida:\n");
while(!filaVazia(&fila)){
retira(&fila,&x);
escreveDados(x);
}
printf("\n");
exit(0);
}

4.7
4.7.1

Listas ordenadas

Introducao

Um vector pode ser ordenado segundo uma qualquer relaca o de ordem utilizando um algoritmo apropriado(Knuth, 1973) (selection sort, bubble-sort, quicksort ou qualquer outro). Embora
estes algoritmos estejam bem estudados e sejam frequentemente necessarios, todos eles requerem
um esforco computacional significativo. E possvel provar que a quantidade de trabalho necessaria
para esta tarefa nao e apenas proporcional ao numero de elementos a ordenar: a complexidade da


88 L ISTAS DIN AMICAS

tarefa aumenta significativamente mais que a dimensao do vector. Para compreender melhor a
quantidade de trabalho exigido, podemos comparar esta situaca o ao de arrumar alfabeticamente
uma biblioteca a partir de uma situaca o desorganizada. Se arrumar uma biblioteca com 5,000
livros demorar um dia, arrumar uma biblioteca com 10,000 livros nao demorara apenas dois dias,
mas sim tres ou quatro, ja que cada ttulo individual tera que ser alfabeticamente comparado com
um numero muito maior de outros livros.
No caso de uma biblioteca, se se pretender evitar a tarefa herculea de ordenar todos os livros,
o melhor e mante-la sempre ordenada. Desta forma, cada novo livro adicionado tera apenas que
ser colocado no local certo, tarefa obviamente muito mais rapida.
Em C, quando os elementos a ordenar se encontram armazenados num vector, o metodo anterior corresponde basicamente a encontrar o local de inserca o, deslocar todos os elementos posteriores de uma posica o, e inserir o elemento na posica o assim aberta. Embora o algoritmo seja
simples, a tarefa de deslocar todos os elementos de uma posica o e computacionalmente pesada,
sobretudo quando a inserca o se efectua nas posico es iniciais. O paralelo que podemos encontrar
no exemplo da biblioteca e o de uma estante repleta de livros, excepto na u ltima prateleira. A
introduca o ordenada de um novo ttulo na primeira prateleira exige deslocar todos os ttulos de
uma posica o. Dado que so existe espaco disponvel na u ltima prateleira, este processo pode exigir
de facto movimentar livros em todas as prateleiras da estante.
As listas dinamicas oferecem uma forma simples de criar e manter eficientemente uma
colecca o de objectos ordenados. Ao contrario do vector, em que todas as posico es se encontram em enderecos de memoria contguos, a ordem dos elementos da lista nao depende do seu
endereco de memoria, mas apenas da ordem definida pela sequencia de apontadores, tal como ja
se mostrou 4.2.
As operaco es necessarias para a manipulaca o e manutenca o de uma lista ordenada sao
generalizaco es relativamente simples dos casos da pilha e da fila. Algumas destas operaco es,
como criar a lista ou listar os seus elementos, ja foram apresentadas anteriormente. Apenas algumas das operaco es descritas seguidamente, como a inserca o, procura e remoca o no meio da lista
sao de facto totalmente novas.

4.7.2

e inicializacao

Declaracao

A declaraca o e inicializaca o de uma lista segue os passos ja vistos para o caso da pilha. A
declaraca o da lista pode ser realizada por
typedef struct _tipoLista {

L ISTAS ORDENADAS 89

tipoDados dados;
struct _tipoLista *seg;
} tipoLista;

onde, tal como nos exemplos anteriores, tipoDados e um tipo abstracto que define a informaca o
armazenada em cada elemento.
De igual modo, a inicializaca o da lista corresponde apenas a` inicializaca o do apontador a
NULL, por meio da funca o de inicializaca o
tipoLista *inicializa(){
return NULL;
}

4.7.3

Listagem ordenada

A listagem ordenada corresponde apenas a uma listagem convencional do conteudo. Deste


modo, a listagem pode ser feita por

void listar(tipoLista *base ){


tipoLista *aux = base;
while(aux){
printf(" -> ");
escreveDados(aux -> dados);
printf("\n");
aux = aux -> seg;
}
}

4.7.4

Procura

A procura e um procedimento relativamente simples. Basta percorrer a lista ate encontrar o


elemento que se procura, e retornar um apontador para o respectivo elemento. Convenciona-se
que a funca o de procura deve retornar NULL caso o elemento a procurar nao exista.
Para assentar ideias, admita-se temporariamente que se esta a lidar com uma lista de inteiros.
Deste modo, presume-se que se definiu previamente
typedef int tipoDados;


90 L ISTAS DIN AMICAS

Neste caso, a funca o de procura pode ser realizada por


tipoLista *procura(tipoLista *base,tipoDados x){
tipoLista *aux;
aux = base;
while((aux!=NULL) &&
aux = aux -> seg;
return aux;

aux -> dados != x)

}
A funca o anterior, embora funcione, apresenta o inconveniente de nao explorar o facto da lista
estar ordenada. Por exemplo, no caso de uma lista ordenada de inteiros onde estejam todos os
valores pares entre 10 e 10000, se se procurar o numero 15 sera necessario ir ate ao fim da lista
para concluir que o numero nao esta presente. Como e o bvio, esta conclusao poderia ter sido tirada
muito mais cedo, logo que fosse atingido o numero 16 na lista.
Deste modo, uma versao mais optimizada da funca o de procura pode ser escrita como
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` posica o actual
da lista. Quando o ciclo e interrompido, e testado se se se atingiu o fim da lista ou se se encontrou
o elemento procurado.
Nesta funca o, vale a pena considerar a estrutura do teste
if((aux != NULL) &&
...
}

(aux -> dados

== x)){

e verificar a forma como se toma partido do modo como o C avalia expressoes logicas. Repare-se
que o segundo operando da disjunca o ( aux -> dados == x) so pode ser avaliado se aux

L ISTAS ORDENADAS 91

for diferente de NULL, ja que de outra forma se poderia estar a gerar uma violaca o de memoria
(tentativa de acesso atraves do endereco 0, o que se encontra fora do controlo do programador).
No entanto, o C garante que realiza a avaliaca o de expressoes logicas da esquerda para a direita e
que interrompe a sua avaliaca o assim que for possvel determinar univocamente o resultado final.
Neste exemplo, se aux for NULL, o primeiro operando da conjunca o logica && e falso, o que
implica que o resultado global da expressao tambem o e . Assim, nao sendo necessario o calculo
do segundo operando, nao ha o risco de se produzir a violaca o de memoria decorrente do acesso
atraves de um apontador NULL.
Refira-se, por u ltimo, que num problema pratico podem coexistir varias funco es de procura,
consoante o que se pretende encontrar. Por exemplo, se os elementos de uma lista sao estruturas
que incluem, por exemplo, um numero e um nome, podem ser escritas duas funco es de procura,
uma para o numero e outra para o nome. Claro que se a lista estiver ordenada por numeros, a
busca pelo numero podera ser optimizada (procura so ate encontrar um elemento de numero igual
ou superior ao procurado), mas a busca pelo nome tera obviamente que ser exaustiva se o nome
nao existir, ja que so no final da lista sera possvel ter a certeza de que determinado nome nao faz
parte da lista.

4.7.5

de dados e metodos

Abstraccao
de teste

Nas duas funco es de procura anteriores admitiu-se que o tipoDados era um inteiro. Esta
hipotese permitiu utilizar os operadores relacionais == e < de uma forma intuitiva.
No caso mais geral em que tipoDados e um tipo abstracto generico esta comparaca o
nao pode ser realizada directamente pelos operadores relacionais. Para demonstrar este facto,
considere-se que tipoDados corresponde a uma estrutura com um numero e um nome de um
aluno. Neste caso, nao faz sentido usar o operador relacional < para comparar dois alunos a e
b. De facto, o C desconhece o que se pretende comparar: e o nome dos alunos a e b ou os seus
numeros?
Para contornar esta dificuldade, seria possvel utilizar o operador . (membro de estrutura)
e aceder directamente ao campo numerico, utilizando o operador < para a comparaca o, ou aceder
ao campo com o nome e utilizar a funca o strcmp() de forma adequada. No entanto, qualquer
destas soluco es estaria a violar o princpio de abstracca o de dados, ja que os metodos de procura
da lista, para quem tipoDados deveria ser um tipo abstracto generico, estariam a aceder directamente a detalhes internos do tipo.
A forma de resolver esta questao e as funco es procura delegarem o processo de comparaca o


92 L ISTAS DIN AMICAS

em metodos (funco es) especficos internos de tipoDados, responsaveis pela implementaca o dos
detalhes praticos da comparaca o. Deste modo, admitindo que foram associados ao tipo abstracto
tipoDados um metodo com prototipo

int menor(tipoDados a,tipoDados b);

que devolve 1 se a for de algum modo anterior a b e 0 em caso contrario (as funco es da lista nao
precisam de conhecer os detalhes desta comparaca o) e um outro

int igual(tipoDados a,tipoDados b);

que devolve 1 se a for igual b segundo um dado criterio e 0 em caso contrario.


Deste modo, uma soluca o mais geral da funca o de procura ordenada deveria ser escrita como
tipoLista *procuraOrdenado(tipoLista *base,tipoDados x){
tipoLista *aux = base;
while((aux!=NULL) && menor(aux -> dados,x))
aux = aux -> seg;
if((aux != NULL) && igual(aux -> dados ,x))
return aux;
else
return NULL;
}

Alternativamente, e frequente unificar todos os metodos de comparaca o numa u nica funca o


que devolve -1, 0 ou 1 consoante o primeiro elemento e anterior, igual ou posterior ao segundo. E
esta soluca o que e adoptada na funca o strcmp() para comparaca o de strings.

4.7.6

ordenada
Insercao

Para realizar uma inserca o ordenada e suficiente percorrer a lista ate encontrar um elemento
que seja superior ao que se pretende inserir. O novo elemento devera ser colocado antes deste.
Apesar desta metodologia simples, a inserca o ordenada requer algumas consideraco es suplementares. Um dos pontos a ter em conta e que para posicionar o novo elemento antes do que foi
identificado e necessario alterar o apontador do elemento que se encontra antes deste. Ou seja, ao

L ISTAS ORDENADAS 93

antes

depois

novo

10

Figura 4.7: Inserca o entre dois elementos da lista. O apontador antes nao aponta para o elemento
anterior da lista, mas sim para o campo apontador seg desse elemento.
percorrer a lista, e necessario manter uma referencia nao apenas para o elemento da lista que esta
a ser comparado (elemento actual) mas tambem para o seu predecessor (elemento anterior).
De modo a melhor compreender esta operaca o, e conveniente comecar por desenvolver uma
funca o auxiliar de inserca o em que se admite que a posica o de inserca o ja foi determinada e, como
tal, que ja existem referencias para o apontador do elemento anterior e para o elemento actual
(v. figura 4.7). Designaremos estas variaveis de referencia por antes e depois. Neste caso o
codigo desta funca o pode ser escrito
void insere(tipoLista **antes, tipoDados x, tipoLista *depois){
tipoLista *novo = novoNo(x);
*antes = novo;
novo -> seg = depois;
}
onde a funca o novoNo() e semelhantes a` s anteriores
tipoLista *novoNo(tipoDados x){
tipoLista *novo = calloc(1,sizeof(tipoLista));
if(novo == NULL)
Erro("Erro na reserva de mem
oria");
novo -> dados = x;
novo -> seg = NULL;
return novo;
}
Uma vez definida esta funca o, a inserca o ordenada apenas tem que ter em conta alguns casos
particulares, nomeadamente a inserca o numa fila vazia ou a inserca o 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 funca o insereOrdenado() pode entao ser escrita
tipoLista *insereOrdenado(tipoLista *base,tipoDados x){
tipoLista *act,*ant;
if(base == NULL){
insere(&base,x,NULL); /* Lista vazia */
}
else if(menor(x,base -> dados)){
insere(&base,x,base); /* Insere *antes* da base */
}
else{
ant = base; act = base -> seg;
while((act != NULL) && (menor(act -> dados,x))){
ant = act; act = act -> seg;
}
insere(&(ant -> seg),x,act);
}
return base;
}

Esta funca o recebe como um dos argumentos o valor da base e devolve o valor desta depois
da inserca o. A base retornada e identica a` base inicial, excepto quando a inserca o e feita no incio
da lista.
O par de funco es insere() e insereOrdenado() foi escrita de modo a sublinhar os
diversos aspectos necessarios a` inserca o numa lista. No entanto, o C permite escrever uma funca o
de inserca o ordenada de modo muito mais compacto:
tipoLista *insereOrdenado(tipoLista *base,tipoDados x){
tipoLista *aux = base, *novo = novoNo(x);
if(aux == NULL || menor(x,aux -> dados)) {
novo -> seg = base;
return novo;
}
aux = base;
while( aux -> seg!= NULL && menor(aux -> seg -> dados,x))
aux = aux -> seg;
novo -> seg = aux -> seg;
aux -> seg = novo;
return base;

L ISTAS ORDENADAS 95

reg

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

Aqui, o primeiro if abrange simultaneamente a inserca o numa lista vazia e antes do primeiro
elemento. Seguidamente, o ciclo while percorre a lista, mantendo apenas um apontador (equivalente ao apontador ant do exemploanterior).

4.7.7

Remocao

Tal como a inserca o, a remoca o pode ser simplificada se for escrita uma funca o auxiliar
libertaReg que e executada quando ja e conhecido o local de remoca o. Esta funca o (v. figura
4.8) pode ser escrita
tipoLista *libertaReg(tipoLista *reg){
tipoLista *aux;
aux = reg;
reg = reg -> seg;
free(aux);
return reg;
}

Seguidamente, e necessario escrever uma funca o que procure o o elemento a apagar e chame
a funca o anterior. Esta funca o e dada por
tipoLista *apaga(tipoLista *base,tipoDados x){
tipoLista *aux;
aux = base;
if(base != NULL){
if(igual(base -> dados,x)){
base = libertaReg(base);
}
else{


96 L ISTAS DIN AMICAS

aux = base;
while((aux -> seg != NULL) &&
(menor(aux -> seg -> dados,x)))
aux = aux -> seg;
if((aux -> seg != NULL) &&
igual(aux -> seg -> dados,x))
aux -> seg = libertaReg(aux -> seg);
}
}
return base;
}

4.7.8

Exemplo

Como exemplo, apresenta-se aqui o codigo completo de um programa que manipula uma
lista ordenada de racionais. O tipo racional e representado por dois inteiros, que descrevem o seu
numerador e o denominador. O programa aceita uma sequencia de racionais. Um racional positivo
e inserido na lista, enquanto que um racional negativo provoca uma tentativa de remoca o do seu
simetrico, caso este exista (ou seja, a indicaca o de 35 provoca a remoca o do racional 53 ).
Omite-se aqui a listagem dos ficheiros util.h e util.c, identicos ao introduzido no exemplo da pilha (v. secca o 4.5.7).

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

Ficheiro: lista.h
Autor:
Fernando M. Silva
Data:
7/11/2000
Conte
udo:
Definic

ao do tipo gen
erico
"tipoDados" usado nos exemplos de
estruturas de dados din
amicas.
Para fins de exemplo, "tipoDados"

e realizado por uma fracc

ao inteira,
a qual e especificada por um numerador
e um denominador

#ifndef _DADOS_H

L ISTAS ORDENADAS 97

#define _DADOS_H
#include <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
Conte
udo:
M
etodos de acesso exemplificativos
da definic

ao de um tipo abstracto "tipoDados"


Neste exemplo, "tipoDados" implementa um numero
racional, especificado por um numerador e um
denominador

#include "dados.h"
#define DIM_LINE 100
tipoDados leDados(char mensagem[]){
/*
* Escreve a mensagem passada por argumento, l
e um racional e
* valida a entrada
* Argumento:
*
mensagem - mensagem a escrever
* Retorna:
*
racional lido


98 L ISTAS DIN AMICAS

*/
char line[DIM_LINE];
tipoDados x;
int ni;
do{
printf("%s\n",mensagem);
fgets(line,DIM_LINE,stdin);
ni=sscanf(line,"%d %d",&x.numerador,&x.denominador);
if(ni !=2){
printf("Erro na leitura\n");
}
else{
if(x.denominador == 0)
printf("Racional inv
alido\n");
}
}while((ni != 2) || (x.denominador == 0));
return x;
}
void
escreveDados(tipoDados x){
/*
* Escreve x em stdout, no formato
* numerador / denominador
* Argumento:
*
x - valor a escrever
*/
printf("%d/%d",x.numerador,x.denominador);
}
float
valor(tipoDados x){
/*
* Retorna um real com o valor (aproximado) do racional x
* Argumento:
*
x - racional
* Retorna
*
valor aproximado de x
*/
return (float) x.numerador / (float) x.denominador;
}
int
menor(tipoDados x,tipoDados y){
/*
* Retorna 1 caso o racional x seja menor que y
* Argumentos:
*
x,y - racionais a comparar
* Retorna

L ISTAS ORDENADAS 99

*
1 se x < y
*
0 em caso contr
ario
*/
if(x.denominador * y.denominador > 0)
return x.numerador * y.denominador < y.numerador * x.denominador;
else
return x.numerador * y.denominador > y.numerador * x.denominador;
}
int
igual(tipoDados x,tipoDados y){
/*
* Retorna 1 caso o racional x seja igual a y
* Argumentos:
*
x,y - racionais a comparar
* Retorna
*
1 se x = y
*
0 em caso contr
ario
*/
return x.numerador * y.denominador == x.denominador * y.numerador;
}
int
numerador(tipoDados x){
/*
* Retorna o numerador do racional x
* Argumento:
*
x - racional
* Retorna
*
numerador de x
*/
return x.numerador;
}
int
denominador(tipoDados x){
/*
* Retorna o denominador do racional x
* Argumento:
*
x - racional
* Retorna
*
denominador de x
*/
return x.denominador;
}
tipoDados simetrico(tipoDados x){
/*
* Retorna o sim
etrico do racional x
* Argumento:


100 L ISTAS DIN AMICAS

*
x - racional
* Retorna
*
sim
etrico de x
*/
tipoDados aux;
aux.numerador
= -x.numerador;
aux.denominador = x.denominador;
return aux;
}

Ficheiro lista.h
/*
* Ficheiro: lista.h
* Autor:
Fernando M. Silva
* Data:
7/11/2000
* Conte
udo:
*
Ficheiro com declarac

ao de tipos e
*
prot
otipos dos m
etodos para manipulac

ao
*
de uma lista din
amica simples
*
*/
#ifndef _LISTA_H
#define _LISTA_H
#include <stdio.h>
#include <stdlib.h>
/*
* Tipo dos dados da lista
*/
#include "dados.h"
/*
* Definic

ao de tipoLista
*/
typedef struct _tipoLista {
tipoDados dados;
struct _tipoLista *seg;
} tipoLista;
/*
* Prot
otipos dos m
etodos de acesso
*/

L ISTAS ORDENADAS 101

/* Inicializac

ao */
tipoLista *inicializa(void);
/*
* Procura x na lista iniciada por base, retornando um apontador
* para o registo que contem este valor (ou NULL se n
ao existe)
*/
tipoLista *procura(tipoLista *base,tipoDados x);
tipoLista *procuraOrdenado(tipoLista *base,tipoDados x);
/*
* Insere x antes do registo "depois" e modificando o apontador "antes".
*/
void insere(tipoLista **antes, tipoDados x, tipoLista *depois);
/*
* Insere x na lista ordenada iniciada por base
* Devolve a base, eventualmente alterada
*/
tipoLista *insereOrdenado(tipoLista *base,tipoDados x);
/*
* Lista todos os elementos da estrutura.
*/
void listar(tipoLista *base);
/*
* Liberta da lista o elemento especificado por reg
*/
tipoLista *libertaReg(tipoLista *reg);
/*
* Apaga da lista o registo que cont
em x
* Retorna a base, eventualmente alterada
*/
tipoLista *apaga(tipoLista *base,tipoDados x);
#endif

Ficheiro lista.c
/*
*
*
*

Ficheiro: lista.c
Autor:
Fernando M. Silva
Data:
7/11/2000


102 L ISTAS DIN AMICAS

* Conte
udo:
*
M
etodos para manipulac

ao
*
de uma lista din
amica
*
simples (ordenada)
*
*/
/*
* Inclui ficheiro com tipo e prot
otipos
*/
#include "lista.h"
#include "util.h"
tipoLista *novoNo(tipoDados x){
/*
* Cria um novo n
o da lista
*/
tipoLista *novo = calloc(1,sizeof(tipoLista));
if(novo == NULL)
Erro("Erro na reserva de mem
oria");
novo -> dados = x;
novo -> seg = NULL;
return novo;
}
tipoLista *inicializa(){
/*
* Cria uma nova lista
* Retorna:
*
lista inicializada
*/
return NULL;
}
tipoLista *procura(tipoLista *base,tipoDados x){
/*
* Procura um elemento na lista iniciada por base
* Argumentos:
*
base - apontador para a base da lista
*
x
- elemento a procurar
* Retorna
*
apontador para x (ou NULL caso nao encontre)
*/
tipoLista *aux = base;
while((aux!=NULL) && !igual(aux -> dados,x))
aux = aux -> seg;
return aux;

L ISTAS ORDENADAS 103

}
tipoLista *procuraOrdenado(tipoLista *base,tipoDados x){
/*
* Id
entico ao anterior, mas optimizado para listas
* ordenadas
* Argumentos:
*
base - apontador para a base da lista
*
x
- elemento a procurar
* Retorna
*
apontador para x (ou NULL caso nao encontre)
*/
tipoLista *aux = base;
while((aux!=NULL) && menor(aux -> dados,x))
aux = aux -> seg;
if((aux != NULL) && igual(aux -> dados ,x))
return aux;
else
return NULL;
}
void insere(tipoLista **antes, tipoDados x, tipoLista *depois){
/*
* Insere x entre dois registos
* Argumentos:
*
antes - predecessor na lista
*
x
- elemento a inserir
*
depois - elemento seguinte a x
*/
tipoLista *novo = novoNo(x);
*antes = novo;
novo -> seg = depois;
}
tipoLista *insereOrdenado(tipoLista *base,tipoDados x){
/*
* Insere x na lista ordenada
* Argumentos:
*
base - apontador para a base da lista
*
x
- elemento a procurar
* Retorna
*
base, eventualmente alterada
*/
tipoLista *act,*ant;
if(base == NULL){
insere(&base,x,NULL); /* Lista vazia */


104 L ISTAS DIN AMICAS

}
else if(menor(x,base -> dados)){
insere(&base,x,base); /* Insere *antes* da base */
}
else{
ant = base; act = base -> seg;
while((act != NULL) &&
(menor(act -> dados,x))){
/*
* Procura local de inserc

ao
*/
ant = act; act = act -> seg;
}
insere(&(ant -> seg),x,act);
}
return base;
}
void listar(tipoLista *base ){
/*
* Lista todos os elementos
* Argumentos:
*
base - apontador para a base da lista
*/
while(base){
printf(" -> ");
escreveDados(base -> dados);
printf("\n");
base = base -> seg;
}
}
tipoLista *libertaReg(tipoLista *reg){
/*
* Remove da lista o elemento apontado por reg e
* liberta a mem
oria associada.
* Argumentos:
*
reg - apontador para a vari
avel da lista que
*
aponta para o registo a libertar
* Retorna - novo valor do apontandor
*/
tipoLista *aux;
aux = reg;
reg = reg -> seg;
free(aux);
return reg;
}

L ISTAS ORDENADAS 105

tipoLista *apaga(tipoLista *base,tipoDados x){


/*
* Apaga da lista o registo que cont
em x
* Argumentos:
*
base - apontador para a base da lista
*
x
- elemento a eliminar
* Retorna
*
base, eventualmente alterada
*/
tipoLista *aux;
aux = base;
if(base != NULL){
if(igual(base -> dados,x)){
base = libertaReg(base);
}
else{
aux = base;
while((aux -> seg != NULL) &&
(menor(aux -> seg -> dados,x)))
aux = aux -> seg;
if((aux -> seg != NULL) &&
igual(aux -> seg -> dados,x))
aux -> seg = libertaReg(aux -> seg);
}
}
return base;
}

Ficheiro main.c
/*
*
*
*
*
*
*
*
*
*
*
*
*

Ficheiro: main.c
Autor:
Fernando M. Silva
Data:
7/11/2000
Conte
udo:
Programa principal simples para teste
de estruturas din
amicas ligadas.
Neste teste, os dados armazenados na lista
s
ao fracc

oes inteiras, implementadas


por um tipo abstracto "tipoDados".
Assim:
1. A especificac

ao de um racional positivo,


106 L ISTAS DIN AMICAS

*
acrescenta o racional `
a lista
*
2. A introduc

ao de um racional negativo,
*
especifica que deve ser apagada o seu
*
sim
etrico da lista
*
3. Com a entrada de um racional com valor 0,
*
termina o programa
*
*/
#include <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 introduc

ao de um racional positivo "


`
"conduz a sua\n"
"
inserc

ao ordenada na lista.\n");
printf("
- A introduc

ao de um racional "
"negativo procura o seu\n"
"
sim
etrico na lista e, "
"caso exista, remove-o.\n");
x = leDados("\nIndique o numerador e denominador "
"de um racional:\n"
"(dois inteiros na mesma linha)");
while(numerador(x) != 0) {
if(valor(x) > 0){
base = insereOrdenado(base,x);
}
else{
x = simetrico(x);
if(procuraOrdenado(base,x) == NULL){
printf(" ************ Erro: elemento de "
"valor id
entico a ");
escreveDados(x);
printf(" n
ao encontrado na lista\n");
}
else{
base = apaga(base,x);
}
}
printf("\nConte
udo actual da lista:\n");

VARIANTES 107

base





 


Figura 4.9: Lista de inteiros com registo separado para a base. Nesta figura, a lista tem apenas tres
elementos efectivos (1, 4 e 9), sendo o primeiro registo utilizado apenas para o suporte da base da
lista.
listar(base);
x = leDados("\nIndique o numerador "
"e denominador de um racional:\n"
"(dois inteiros na mesma linha)");
}
printf("\n Racional com valor 0: fim do programa\n");
exit(0);
}

4.8

Variantes

4.8.1

Introducao

Embora a estrutura fundamental das listas dinamicas seja no essencial a que se viu anteriormente, existem diversas variantes que tem como objectivo simplificar os mecanismos de acesso
ou ajustar a lista a objectivos especficos.

4.8.2

Listas com registo separado para a base

Conforme se viu anteriormente, a manipulaca o de listas ordenadas exige que a base da lista
seja tratada como um caso particular. Uma forma de evitar estes testes adicionais e manter permanentemente um registo mudo no incio da lista (registo separado para a base) que, embora nao
seja utilizado de facto, permite simplificar o acesso aos restantes elementos. Adicionalmente, este
tipo de estrutura (v. figura 4.9 tem a vantagem do apontador para a base nao ser modificado depois
da criaca o da lista.
As funco es de manipulaca o da lista sao no essencial semelhantes a` s da lista ordenada simples.
Embora sem listar aqui todas as funco es de acesso, referem-se algumas das que sao modificadas
pela existencia de um registo permanente na base.
A inicializaca o da lista corresponde neste caso a` criaca o do registo da base. Deste modo, a


108 L ISTAS DIN AMICAS

fim

inicio

Figura 4.10: Lista dupla de inteiros.


funca o de inicializaca o e dada por

Por outro lado, a funca o de inserca o ordenada pode ser simplificada. Admitindo a utilizaca o
da mesma funca o insere() ja apresentado para a lista simples, a inserca o pode ser feita por

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

4.8.3

Listas duplamente ligadas

Um dos inconvenientes das listas simplesmente ligadas e serem unidireccionais. Por outras
palavras, ao aceder a um dado elemento da lista e facil aceder ao elemento seguinte, mas o acesso
ao elemento anterior nao e possvel.
Para evitar este inconveniente e possvel desenvolver uma lista duplamente ligada, em que
cada elemento dispoe de dois apontadores, um para o elemento seguinte e outro para o anterior.
Adicionalmente, tal como no caso da fila, o acesso a` lista e geralmente mantido por dois apontadores, um para o incio e outro para o fim da lista (v. figura 4.10). A declaraca o de uma lista
duplamente ligada pode ser realizada simplesmente por

typedef struct _tipoRegDupla{


tipoDados dados;
struct _tipoRegDupla *seg,*ant;
} tipoRegDupla;
typedef struct _tipoDupla{
tipoRegDupla inicio;
tipoRegDupla fim;
} tipoDupla;

A NEL DUPLO COM REGISTO SEPARADO PARA A BASE 109

base

Figura 4.11: Lista em anel de inteiros. O u ltimo 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 u ltimo da elemento da lista e identificado pelo facto do
apontador para o elemento seguinte ser NULL. Uma alternativa e colocar o u ltimo elemento da
lista a apontar para a base (v. figura 4.11). A manipulaca o de uma estrutura deste tipo e muito
semelhante a` de uma lista simples, substituindo-se apenas parte das comparaco es com o valor
NULL por comparaco es com o apontador da base.
Uma lista em anel apresenta a vantagem do u ltimo elemento apontar para o primeiro, o que
pode ser conveniente em aplicaco es em que seja necessario percorrer os elementos da lista de uma
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 dinamica que combina as
diversas variantes abordadas anteriormente: registo separado para a base, ligaca o em anel e registos com apontadores bidireccionais (figura 4.12). Embora este tipo estrutura possa sugerir uma
manipulaca o a` partida mais complexa, verifica-se na pratica o inverso: a exploraca o correcta desta
estrutura origina, de modo geral, codigo mais simples.


110 L ISTAS DIN AMICAS

base




Figura 4.13: Anel vazio apos a criaca o.

4.9.2

Declaracao

A declaraca o 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 inicializaca o de um anel corresponde a` criaca o de um registo para a base e ao fecho em anel


das suas ligaco es (figura 4.13). Esta inicializaca o pode ser feita por

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

A funca o novoNo(), semelhante a` s anteriores, pode ser definida como

tipoAnel *novoNo(tipoDados x){


tipoAnel *novo = calloc(1,sizeof(tipoAnel));
if(novo == NULL)
Erro("Erro na reserva de mem
oria");
novo -> dados = x;

A NEL DUPLO COM REGISTO SEPARADO PARA A BASE 111

novo -> seg = novo -> ant = NULL;


return novo;
}

4.9.4

Listagem

A listagem de um anel pode ser feita por ordem directa ou inversa. Deste modo, podem ser
desenvolvidas duas funco es para este efeito, de estrutura muito semelhante:
/*
Listagem directa
void listar(tipoAnel *base ){
tipoAnel *aux;

*/

aux = base -> seg;


while(aux != base){
printf(" -> ");
escreveDados(aux -> dados);
printf("\n");
aux = aux -> seg;
}
}
/*
Listagem inversa
*/
void listarInv(tipoAnel *base ){
tipoAnel *aux;
aux = base -> ant;
while(aux != base){
printf(" -> ");
escreveDados(aux -> dados);
printf("\n");
aux = aux -> ant;
}
}

Repare-se que em qualquer das duas funco es o incio da listagem se efectua no elemento a seguir
a` base (de modo a saltar o registo da base), enquanto que na condica o de manutenca o no ciclo se
testa o regresso a` base.

4.9.5

Procura

A funca o de procura e semelhante a` de uma lista ordenada, omitindo o elemento inicial:


112 L ISTAS DIN AMICAS

tipoAnel *procuraOrdenado(tipoAnel *base,tipoDados x){


tipoAnel *aux;
base -> dados = x;
aux = base -> seg;
while(menor(aux -> dados,x))
aux = aux -> seg;
if((aux != base) && (igual(aux -> dados,x)))
return aux;
else
return NULL;
}
Repare-se que, para simplificar a condica o do ciclo, se iniciou o registo da base com o proprio
valor do elemento a procurar. Este artifcio garante que, mesmo que o elemento de procura nao
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 interrupca o se verificou por ter encontrado o local real de
interrupca o e o elemento de procura, ou por se ter encontrado o elemento artificialmente colocado
na base. Neste u ltimo caso devera retornar-se o apontador NULL.

4.9.6

Insercao

No caso de inserca o ordenada no anel, e particularmente u til utilizar uma funca o so para a
inserca o do registo, admitindo que ja se conhece o local de inserca o, e desenvolver posteriormente
a funca o de procura o local de inserca o.
A primeira destas funco es pode ser definida como
void insere(tipoAnel *depois,tipoDados x){
tipoAnel *novo = novoNo(x);
novo -> seg = depois;
/* (1) */
novo -> ant = depois -> ant;
/* (2) */
depois -> ant = novo;
/* (3) */
novo -> ant -> seg = novo;
/* (4) */
}
Apesar da aparente complexidade da funca o, a sua realizaca o corresponde apenas a` realizaca o
das ligaco es necessarias pela ordem correcta. Na figura 4.14 detalha-se este processo de inserca o
no caso de um anel de inteiros, identificando-se a correspondencia entre as ligaca o e as instruco es
da listagem.

A NEL DUPLO COM REGISTO SEPARADO PARA A BASE 113

depois

(2)

(4)

novo

(1)

(3)

Figura 4.14: Inserca o num anel. Os numeros entre () correspondem a` s atribuico es da listagem
apresentada no texto.
Repare-se que, dado que a lista e dupla, e suficiente o apontador para o elemento seguinte para
estabelecer todas as ligaco es.
A funca o de inserca o propriamente dita realiza apenas a pesquisa do local de inserca o. De
forma semelhante ao ja efectuado no caso da funca o de procura, e possvel colocar uma copia
do elemento a inserir no registo da base para permitir que o caso particular de inserca o no final
da lista nao tem que ser tratado como um caso particular. Deste modo, o codigo da funca o fica
particularmente simples:

void insereOrdenado(tipoAnel *base,tipoDados x){


tipoAnel *act;
base -> dados = x;
act = base -> seg;
while(menor(act -> dados,x))
act = act -> seg;
insere(act,x);
}

4.9.7

Remocao

A operaca o de remoca o pode ser realizada por duas funco es complementares. A primeira
(removeReg()) e utilizada apos a identificaca o do registo a eliminar e e responsavel pela
libertaca o da memoria ocupada e reconstruca o das ligaco es. A segunda (apaga()) que corresponde a` funca o a utilizar externamente, procura o registo a eliminar e, caso o identifique, chama


114 L ISTAS DIN AMICAS

a primeira.
void removeReg(tipoAnel *reg){
reg -> seg -> ant = reg -> ant;
reg -> ant -> seg = reg -> seg;
free(reg);
}
void apaga(tipoAnel *base,tipoDados x){
tipoAnel *aux;
base -> dados = x;
aux = base -> seg;
while(menor(aux -> dados,x))
aux = aux -> seg;
if((aux != base) && igual(aux -> dados,x))
removeReg(aux);
}

4.9.8

Exemplo

Utiliza-se neste caso o mesmo exemplo de listagem de racionais ja apresentado anteriormente.


Neste caso, omite-se a listagem dos metodos do tipo tipoDados, e os ficheiros util.c e
util.h, obviamente identico ao anterior.

Ficheiro anel.h
/*
* Ficheiro: anel.h
* Autor:
Fernando M. Silva
* Data:
7/11/2000
* Conte
udo:
*
Ficheiro com declarac

ao de tipos e
*
prot
otipos dos m
etodos para manipulac

ao
*
de um anel com registo seoparado
*
para a base
*
*/
#ifndef _ANEL_H
#define _ANEL_H
#include <stdio.h>

A NEL DUPLO COM REGISTO SEPARADO PARA A BASE 115

#include <stdlib.h>
/*
* Tipo dos dados da lista
*/
#include "dados.h"
/*
* Definic

ao de tipoAnel
*/
typedef struct _tipoAnel {
tipoDados dados;
struct _tipoAnel *seg,*ant;
} tipoAnel;
/*
* Prot
otipos dos m
etodos de acesso
*/
/* Inicializac

ao */
tipoAnel *inicializa(void);
/*
* Procura x na lista iniciada por base, retornando um apontador
* para o registo que contem este valor (ou NULL se n
ao existe)
*/
tipoAnel *procura(tipoAnel *base,tipoDados x);
tipoAnel *procuraOrdenado(tipoAnel *base,tipoDados x);
/*
* Insere x antes do registo "depois"
*/
void insere(tipoAnel *depois,tipoDados x);
/*
* Insere x na lista ordenada iniciada por base
*/
void insereOrdenado(tipoAnel *base,tipoDados x);
/*
* Lista todos os elementos da estrutura.
*/
void listar(tipoAnel *base);
void listarInv(tipoAnel *base);
/*
*

Remove da lista o elemento apontado por reg


116 L ISTAS DIN AMICAS

*/
void libertaReg(tipoAnel *reg);
/*
* Apaga da lista o registo que cont
em x
*/
void apaga(tipoAnel *base,tipoDados x);
#endif

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

Ficheiro: anel.c
Autor:
Fernando M. Silva
Data:
7/11/2000
Conte
udo:
M
etodos para manipulac

ao
de um anel com registo separado
para a base.

/*
* Inclui ficheiro com tipo e prot
otipos
*/
#include "anel.h"
#include "util.h"
tipoAnel *novoNo(tipoDados x){
/*
* Cria um novo n
o da lista
*/
tipoAnel *novo = calloc(1,sizeof(tipoAnel));
if(novo == NULL)
Erro("Erro na reserva de mem
oria");
novo -> dados = x;
novo -> seg = novo -> ant = NULL;
return novo;
}
/* Inicializac

ao. O anel vazio


e constitu
da pelo
* registo da base.
*/
tipoAnel *inicializa(){

A NEL DUPLO COM REGISTO SEPARADO PARA A BASE 117

tipoAnel *aux;
tipoDados regMudo;
aux = novoNo(regMudo);
aux -> seg = aux -> ant = aux;
return aux;
}
/*
* Procura x no anel iniciada por base, retornando um apontador
* para o registo que contem este valor (ou NULL se n
ao existe)
*
* Este procedimento realiza uma busca exaustiva em todo o anel
*/
tipoAnel *procura(tipoAnel *base,tipoDados x){
tipoAnel *aux;
base -> dados = x;
aux = base -> seg;
while(!igual(aux -> dados,x))
aux = aux -> seg;
return (aux == base ? NULL : aux);
}
/*
* Id
entico ao anterior, mas optimizado para aneis
* ordenadas (a procura para logo que seja atingido
* um elemento igual ou SUPERIOR a x).
*/
tipoAnel *procuraOrdenado(tipoAnel *base,tipoDados x){
tipoAnel *aux;
base -> dados = x;
aux = base -> seg;
while(menor(aux -> dados,x))
aux = aux -> seg;
if((aux != base) && (igual(aux -> dados,x)))
return aux;
else
return NULL;
}
/*
* Insere x antes do registo "depois"
*/
void insere(tipoAnel *depois,tipoDados x){
tipoAnel *novo = novoNo(x);


118 L ISTAS DIN AMICAS

novo -> seg = depois;


novo -> ant = depois -> ant;
depois -> ant = novo;
novo -> ant -> seg = novo;
}
/*
* Insere x no anel ordenada iniciada por base
*/
void insereOrdenado(tipoAnel *base,tipoDados x){
tipoAnel *act;
base -> dados = x;
act = base -> seg;
while(menor(act -> dados,x)){
/*
* Procura local de inserc

ao
*/
act = act -> seg;
}
insere(act,x);
}
/*
* Lista todos os elementos da estrutura.
*/
void listar(tipoAnel *base ){
tipoAnel *aux;
aux = base -> seg;
while(aux != base){
printf(" -> ");
escreveDados(aux -> dados);
printf("\n");
aux = aux -> seg;
}
}
/*
* Lista todos os elementos da estrutura
* por ordem inversa.
*/
void listarInv(tipoAnel *base ){
tipoAnel *aux;
aux = base -> ant;
while(aux != base){
printf(" -> ");
escreveDados(aux -> dados);

A NEL DUPLO COM REGISTO SEPARADO PARA A BASE 119

printf("\n");
aux = aux -> ant;
}
}
/*
* Remove do anel o elemento apontado por reg
* e liberta a mem
oria associada.
*/
void libertaReg(tipoAnel *reg){
reg -> seg -> ant = reg -> ant;
reg -> ant -> seg = reg -> seg;
free(reg);
}
/*
* Apaga do anel o registo que cont
em x
* Retorna a base, eventualmente alterada
*/
void apaga(tipoAnel *base,tipoDados x){
tipoAnel *aux;
base -> dados = x;
aux = base -> seg;
while(menor(aux -> dados,x))
aux = aux -> seg;
if((aux != base) && igual(aux -> dados,x))
libertaReg(aux);
}

Ficheiro main.c
/*
*
*
*
*
*
*
*
*
*
*
*
*

Ficheiro: main.c
Autor:
Fernando M. Silva
Data:
7/11/2000
Conte
udo:
Programa principal simples para teste
de estruturas din
amicas ligadas.
Neste teste, os dados armazenados na lista
s
ao fracc

oes inteiras, implementadas


por um tipo abstracto "tipoDados".
Assim:
1. A especificac

ao de um racional positivo,


120 L ISTAS DIN AMICAS

*
acrescenta o racional `
a lista
*
2. A introduc

ao de um racional negativo,
*
especifica que deve ser apagada o seu
*
sim
etrico da lista
*
3. Com a entrada de um racional com valor 0,
*
termina o programa
*
*/
#include <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 introduc

ao de um racional positivo conduz `


a sua\n"
"
inserc

ao ordenada na lista.\n");
printf("
- A introduc

ao de um racional negativo procura o seu\n"


"
sim
etrico na lista e, caso exista, remove-o.\n");
x = leDados("\nIndique o numerador e denominador de um racional:\n"
"(dois inteiros na mesma linha)");
while(numerador(x) != 0) {
if(valor(x) > 0){
insereOrdenado(base,x);
}
else{
x = simetrico(x);
if(procuraOrdenado(base,x) == NULL){
printf(" ************ Erro: elemento de valor id
entico a ");
escreveDados(x);
printf(" n
ao encontrado na lista\n");
}
else{
apaga(base,x);
}
}
printf("\nConte
udo actual do anel (ordem crescente):\n");
listar(base);
printf("\nConte
udo por ordem inversa:\n");
listarInv(base);
x = leDados("\nIndique o numerador e denominador de um racional:\n"
"(dois inteiros na mesma linha)");
}

L ISTAS DE LISTAS 121

base

Figura 4.15: Lista de listas.


printf("\n Racional com valor 0: fim do programa\n");
exit(0);
}

4.10

Listas de listas

E frequente a implementaca o de uma dada aplicaca o requerer a utilizaca o hierarquica de diversas listas. Por exemplo, um sistema de arquivo de documentos pode basear-se numa lista de
categorias, em que cada categoria tem associada, por sua vez, uma lista de documentos dessa categoria. Eventualmente, cada um destes documentos pode ainda ter associado uma lista especfica,
como uma lista das datas em que o documento foi alterado e as alteraco es fundamentais de cada
vez. Uma representaca o simbolica de uma lista de listas esta representada na figura 4.15.
Uma lista de listas (ou um anel de aneis) nada tem de particular do ponto de vista de
programaca o. Cada lista por si so u uma estrutura do tipo apontado anteriormente. No entanto,
e necessario ter em atenca o que a lista e as suas sublistas tem tipos diferentes e, de acordo com
o modelo que temos vindo a desenvolver, cada uma delas precisara de um metodo especfico de
acesso. Por outras palavras, sera necessario manter duas funco es de listagem, duas funco es de
inserca o, etc, dado que cada tipo de lista exige um metodo especfico2 .
A declaraca o de uma lista de lista pode ser efectuada pelo codigo

typedef struct _tipoSubLista {


/*
2

De facto, esta duplicaca o de codigo pode ser evitada escrevendo codigo baseado em apontadores genericos do tipo
void*. Esta possibilidade nao sera, por agora, abordada


122 L ISTAS DIN AMICAS

tipoDadosSubLista descreve todos os


atributos de cada elemento de uma
sublista
*/
tipoDadosSubLista dadosSubLista;
struct _tipoSubLista *seg;
} tipoSubLista;
typedef struct _tipoDadosLista{
/*
declarac

ao dos campos necess


arios
a cada elemento da lista
principal
int ...
char ...
*/
tipoSubLista *baseSub;
} tipoDadosLista;
typedef struct _tipoListaDeListas{
tipoDadosLista dados;
struct _tipoListaDeListas *seg;
} tipoListaDeListas;

Note-se que se adoptou aqui quatro tipos abstractos distintos: o tipo tipoDadosSubLista,
que suporta os dados de cada sublista, o tipo tipoSubLista, que suporta as sublistas, o tipo
tipoDadosLista que suporta o tipo de dados da lista principal e, finalmente, o tipoListaDeListas, que suporta a lista principal.
A manipulaca o da lista faz-se pela combinaca o das funco es (metodos) desenvolvidos para
cada tipo de objecto. Admita-se, por exemplo, que no programa principal foi declarada uma lista
de listas

tipoListaDeListas *base;

que foi de alguma forma inicializada. Suponha-se agora que e necessario inserir uma variavel
x de tipoDados na sublista suportada no elemento da lista principal que tem o valor y. O codigo
para este efeito seria:

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

L ISTAS DE LISTAS 123

/*
inicializac

ao da lista e dos valores x e y


...
*/
aux = procuraOrdenadoListaDeListas(base,y);
if(aux == NULL)
fprintf(stderr,"Erro: valor n
ao encontrado na lista principal");
else
insereDados(aux -> dados,x);
/*
...
*/

A funca o insereDados e nova neste contexto, e devera ser um metodo especfico de


tipoDadosLista. A sua estrutura e muito simples, e a sua existencia deve-se apenas a` necessidade de manter uma realizaca o correcta da abstracca o de dados. Esta funca o seria

void insereDados(tipoDadosLista dados,tipoDadosSubLista x){


insereDadosSubLista(dados.baseSub,x);
}

sendo a funca o insereDadosSubLista uma funca o convencional de inserca o.

Captulo 5

Conclusoes

O C e provavelmente a mais flexvel das linguagens de programaca o de alto-nvel, mas apresenta uma relativa complexidade sintactica. Uma das maiores dificuldades na abordagem do
C numa disciplina introdutoria de programaca o e a necessidade de introduzir os conceitos de
endereco de memoria, apontador e memoria dinamica.
Neste texto introduziu-se a noca o de apontador e discutiu-se o problema da manipulaca o de
estruturas de dados dinamicas em C. Apesar da introduca o de diversas estruturas de dados, o
primeiro objectivo deste texto foi, sobretudo, o de tentar explicar os mecanismos essenciais de
manipulaca o de apontadores e gestao de memoria dinamica em C, utilizando-se algumas estruturas
de dados simples para exemplificar estes mecanismos. O aprofundamento destes temas tem o seu
seguimento natural numa disciplina especfica de Algoritmos e Estruturas de Dados, ou em textos
especficos de algoritmia, como (Sedgwick, 1990; Cormen e outros, 1990; Knuth, 1973).

Bibliografia

Cormen, T. H., Leiserson, C. E., e Rivest, R. L. (1990). Introduction to Algorithms. MIT


Press/McGraw-Hill.
Kernighan, B. e Ritchie, D. (1978). The C Programming Language. Prentice-Hall.
Knuth, D. E. (1973). Fundamental Algorithms, volume 1 of The Art of Computer Programming,
section 1.2, paginas 10119. Addison-Wesley, Reading, Massachusetts, second edition.
Martins, J. P. (1989). Introduction to Computer Science Using Pascal. Wadsworth Publishing Co.,
Belmont, California.
Ritchie, D. e Thmompson, K. (1974). The unix time sharing system. Commun ACM, paginas
365375.
Sedgwick (1990). Algorithms in C. Addison Wesley.

B IBLIOGRAFIA 129

Apendice
A
Programa de teste que valida a consistencia das atribuico es da tabela 2.1, apresentada na
secca o 2.6.4. Note-se que o facto de o programa compilar sem erros garante a consistencia dos
tipos nas atribuico es realizadas.
Nos processadores da famlia Intel, cada endereco de memoria especifica apenas um byte,
apesar de, nos processadores 486 e seguintes, cada operaca o de acesso a` memoria se realizar em
palavras de 4 bytes (32 bits). De forma a obter-se valores equivalentes ao do modelo de memoria
usado nos exemplos, realiza-se uma divisao por quatro e ajusta-se o endereco escrito no monitor
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;

escreveEndereco((int) pv3);
escreveEndereco((int)pv3);

pf = *(x+1);
pf = *(x+2)+1;
f = *(*(x+2)+1);

escreveEndereco((int)pf);
escreveEndereco((int)pf);
printf("%4.1f\n",f);

pf = *x;

escreveEndereco((int)pf);

f = **x;
f = *(*x+1);

printf("%4.1f\n",f);
printf("%4.1f\n",f);

pf = x[1];
return 0;

escreveEndereco((int)pf);

Você também pode gostar