Escolar Documentos
Profissional Documentos
Cultura Documentos
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
2.2
Modelos de memoria em C . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.3
Apontadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.4
11
2.4.1
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
26
Matrizes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
28
Declaraca o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
28
2.5
2.6
2.6.1
IV
INDICE
2.7
29
2.6.3
Matrizes e vectores . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
31
2.6.4
Matrizes e apontadores . . . . . . . . . . . . . . . . . . . . . . . . . . .
34
38
41
3.1
Introduca o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
41
3.2
Vectores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
43
3.3
Vectores dinamicos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
46
3.4
48
3.4.1
48
3.4.2
50
3.4.3
Garbbage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
52
55
3.5.1
Introduca o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
55
3.5.2
Matrizes estaticas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
55
3.5.3
Matrizes dinamicas . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
56
3.5.4
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
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
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
4.8.3
4.8.4
Aneis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
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
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
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
???
..
.
..
.
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
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;
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
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
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
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);
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
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.
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
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;
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;
}
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
..
.
20 A PONTADORES
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
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
..
.
..
.
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
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;
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
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.
26 A PONTADORES
2.5.4
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,
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
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
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
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;
}
2.6
2.6.1
Matrizes
Declaracao
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
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}};
2.6.2
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);
/* ... */
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
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
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
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
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
entao,
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
36 A PONTADORES
Expressao
Tipo
Valor
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]
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
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
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];
38 A PONTADORES
2.7
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];
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;
}
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
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,
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
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.
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
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];
/* ... */
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.
3.3
Vectores dinamicos
int x[MAX_DIM]
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;
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
..
.
Contedo
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
..
.
Varivel
Endereo
..
.
..
.
...
1001
Contedo
Varivel
..
.
..
.
...
2101
1002
2102
*f
1003
variancia
2103
1004
media
2104
1005
soma
2105
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).
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
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
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 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
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 */
DA MEM ORIA
G EST AO
DIN AMICA
53
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
Varivel
1006
..
.
..
.
1765
..
.
1001
..
.
...
12000
..
.
..
.
Varivel
..
.
..
.
..
.
300
1007
Endereo
Contedo
666
10001
11999
Endereo
10000
1006
12002
Varivel
..
.
1004
Memoria livre
..
.
Contedo
..
.
...
1002
300
1005
1009
..
.
666
1008
Varivel
..
.
1004
..
.
..
.
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.
..
.
Contedo
Varivel
Endereo
..
.
..
.
..
.
...
1001
Contedo
Varivel
Endereo
..
.
..
.
..
.
...
2101
Contedo
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
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
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:
Como se mostrou anteriormente, a arrumaca o em memoria desta estrutura encontra-se representada esquematicamente na figura 2.9.
3.5.3
Matrizes dinamicas
#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,
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
Considere-se a declaraca o
int *x[4];
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.
*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
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
Contedo
..
.
base
..
.
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:
64 L ISTAS DIN AMICAS
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
Contedo
..
.
base
..
.
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
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:
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:
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
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;
}
pilha = inicializa();
L ISTAS :
4.5.4
PILHAS
Sobreposicao
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
4.5.6
Teste
L ISTAS :
PILHAS
4.5.7
Exemplo
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>
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);
}
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
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
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
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
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().
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 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
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
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
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
4.7.4
Procura
90 L ISTAS DIN AMICAS
}
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) &&
...
}
== 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
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
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"
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
#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
*/
/* 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;
}
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;
}
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
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 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
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
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
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
base
Figura 4.11: Lista em anel de inteiros. O u ltimo elemento aponta para o primeiro
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
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
4.9.2
Declaracao
4.9.3
Inicializacao
tipoAnel *inicializa(){
tipoAnel *aux;
tipoDados regMudo;
aux = novoNo(regMudo);
aux -> seg = aux -> ant = aux;
return aux;
}
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;
*/
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
112 L ISTAS DIN AMICAS
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.
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:
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
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>
#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);
/*
*
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
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
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);
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
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 ordenada na lista.\n");
printf("
- A introduc
base
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
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
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;
/*
inicializac
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
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);