Escolar Documentos
Profissional Documentos
Cultura Documentos
Este texto foi preparado para apoio à disciplina de Introdução à Programação da Licenciatura
em Engenharia Electrotécnica e Computadores do Instituto Superior Técnico. Este texto tenta
focar de modo sistemático alguns dos tópicos que maiores dúvidas suscita nas abordagens iniciais
da linguagem: apontadores e estruturas de dados dinâmicas. Assim, embora se pressuponha o
conhecimentos dos elementos básicas da linguagem C por parte do leitor – nomeadamente, os
tipos de dados elementares e as estruturas de controlo – o texto é mantido ao nı́vel elementar de
uma disciplina introdutória de informática.
Na apresentação das estruturas de dados consideradas, que incluem pilhas, filas, listas e
anéis, introduz-se de forma natural a noção de abstracção de dados, e os princı́pios essenciais
de estruturação e modularidade baseados neste paradigma de programação.
Para o programador experiente em C, alguns dos exemplos de código poderão parecer pouco
optimizados. Trata-se de uma opção premeditada que tenta beneficiar a clareza e a simplicidade
algorı́tmica, ainda que em alguns casos esta opção possa sacrificar ligeiramente a eficiência do
código apresentado. Pensamos, no entanto, que esta é a opção correcta numa abordagem intro-
dutória da programação.
Índice
1 Introdução 1
2 Apontadores 5
2.3 Apontadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.6 Matrizes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
2.6.1 Declaração . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
IV ÍNDICE
3.1 Introdução . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
3.2 Vectores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
3.4.3 Garbbage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
3.5.1 Introdução . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
4 Listas dinâmicas 61
4.1 Introdução . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
4.3 Listas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
4.5.1 Introdução . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
4.5.2 Declaração . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
4.5.3 Inicialização . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
4.5.4 Sobreposição . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
4.5.5 Remoção . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
4.5.6 Teste . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
4.5.7 Exemplo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
4.6.1 Introdução . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
4.6.2 Declaração . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
4.6.3 Inicialização . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
4.6.4 Inserção . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
4.6.5 Remoção . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
4.6.6 Teste . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
4.6.7 Exemplo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
4.7.1 Introdução . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
4.7.4 Procura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
4.7.7 Remoção . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
4.7.8 Exemplo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
5 Conclusões 125
Bibliografia 127
Capı́tulo 1
Introdução
Este princı́pio teórico fundamental, ainda hoje correcto em muitas áreas de aplicação de soft-
ware, conduzia no entanto a problemas diversos sempre que o programador, por qualquer motivo,
necessitava de explorar determinadas regularidades das estruturas de dados de modo a optimizar
zonas crı́ticas do código ou, em outros casos, por ser conveniente ou desejável explorar deter-
minadas facilidades oferecidas pelas instruções do processador que não estavam directamente
disponı́veis na linguagem de alto-nı́vel. Nestas situações, a única alternativa era a programação
directa em linguagem máquina destes blocos de código, opção que implicava a revisão do software
em cada alteração ou evolução de hardware.
Por razões semelhantes, quer o sistema operativo quer ferramentas de sistema como com-
piladores, gestores de ficheiros ou monitores de sistema eram consideradas aplicações que, por
requisitos de eficiência do código, eram incompatı́veis com linguagens de alto-nı́vel. O mesmo
sucedia com todos os programas que necessitavam, de alguma forma, de controlar directamente
dispositivos de hardware. Como corolário, estas aplicações eram tradicionalmente escritas total-
mente em linguagem máquina, implicando um enorme esforço de desenvolvimento e manutenção
em cada evoluçã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 NTRODUÇ ÃO
Digital PDP-10. O B era uma linguagem próxima da linguagem máquina, mas que facilitava ex-
traordinariamente a programação de baixo-nı́vel. O B adoptava algumas estruturas decisionais e
ciclos comuns a linguagens de alto-nı́vel e, simultaneamente, disponibilizava um conjunto de fa-
cilidades simples, geralmente só acessı́veis em linguagem máquina, como o acesso a endereços de
memória e a métodos de endereçamento indirecto. De facto, os mecanismos de acesso a variáveis
e estruturas de dados previstos no B cobriam a esmagadora maioria das necessidades dos progra-
madores quando anteriormente eram obrigados a recorrer à linguagem máquina. Ora estes mecan-
ismos, embora obviamente dependentes do hardware, obedeciam na sua generalidade ao modelo
de memória previsto na arquitectura de Von Neuman, o qual, nos seus princı́pios essenciais, está na
base da maioria das plataformas computacionais desde os anos 50 até aos nossos dias. Foi assim
surgiu a ideia da possibilidade de desenvolvimento de uma linguagem de alto nı́vel, independente
do hardware, que permitisse simultaneamente um acesso flexı́vel aos modos de endereçamento
e facilidades disponı́veis ao nı́vel da linguagem máquina da maioria dos processadores. É assim
que, em 1983, é inventada a linguagem C (Kernighan e Ritchie, 1978), a qual permitiu a re-escrita
de 90% do núcleo do sistema operativo Unix em alto-nı́vel.
Para além da manipulação directa de endereços de memória, uma das facilidades introduzida
pela linguagem C foi a incorporação de mecanismos para gestão de memória dinâmica. O conceito
de memória dinâmica permite que um programa ajuste de modo flexı́vel a dimensão da memória
que utiliza de acordo com as suas necessidades efectivas. Por exemplo, um mesmo programa de
processamento de texto pode ocupar pouca memória se estiver a tratar um documento de pequena
dimensão, ou ocupar um volume mais significativo no caso de um documento de maior número de
páginas.
Neste texto apresenta-se uma introdução aos apontadores e memória dinâmica na linguagem
de programação C. Para exemplificar estes conceitos, são introduzidas algumas estruturas de dados
dinâmicas simples, como pilhas, filas, listas e aneis. Durante a exposição, introduz-se de forma
natural a noção de abstracção de dados, e os princı́pios essenciais de estruturação e modularidade
baseados neste paradigma de programação. Deste modo, a noção de abstracção de dados não é in-
troduzida formalmente num capı́tulo autónomo, mas sim quando se instroduzem listas dinâmicas,
altura em que o conceito é fundamental para justificar a estrutura adoptada. Esta abordagem, em-
bora pouco convencional, deriva da nossa experiência na docência da disciplina de Introdução à
Programação durante vários anos no IST, tendo beneficiado das crı́ticas e sugestões de diversas
3
gerações de alunos.
Tenta-se neste texto dar-se primazia à clareza algorı́tmica e legibilidade do código, ainda
que em alguns casos esta opção possa sacrificar pontualmente a eficiência do código produzido.
Considera-se, no entanto, que é esta a abordagem mais adequada numa disciplina de Introdução
à Programação. Por outro lado, a maioria dos compiladores recentes incluem processos de
optimização que dispensam a utilização explı́cita dos mecanismos de optimização previstos origi-
nalmente na linguagem C.
Capı́tulo 2
Apontadores
Admita-se que num dado programa em C declara, entre outras as variáveis i,j,k com o tipo
int, a variável f com o tipo float e uma variável d do tipo double, não necessariamente
por esta ordem. Admita-se que após a declaração destas variáveis são realizadas as seguintes
atribuições:
1002 2450 i
1003 ???
1004 225.345 f
1005 11331 j
1006 113 k
1007 22.5E145 d
(double, 64bits)
1008
1009 ???
.. ..
. .
Figura 2.1: Modelo de memória em C (exemplo).
2.3 Apontadores
A declaração de uma variável do tipo apontador realiza-se colocando um “*” antes do nome
da variável. Assim, na declaração
double *pd;
int i;
int *pi;
float f;
int j,k;
double d;
int m,*pi2;
double *pd2,d2;
as variáveis i,j,k,m são do tipo int, f é do tipo float, e d,d2 são do tipo double. Além
destas variáveis de tipos elementares, são declaradas as variáveis pi, pi2 do tipo apontador
para inteiro, enquanto que pd e pd2 são do tipo apontador para double.
Após estas instruções, o mapa de memória poderia ser o representado na figura 2.2, situação
A. Sublinhe-se que as variáveis de tipo apontador apenas contêm um endereço de memória, in-
dependentemente do tipo referenciado. Desta forma, todas as variáveis de tipo apontador têm
uma representação igual em memória, normalmente de dimensão idêntica ao do tipo inteiro (uma
palavra de memória). Note-se que o conteúdo dos apontadores, tal como o de algumas variáveis
elementares, ainda não foi inicializado, pelo que surgem representados com ???. É importante
compreender que cada uma destas variáveis tem de facto um conteúdo arbitrário, resultante da
operação anterior do computador. Claro que estes valores, não tido sido ainda inicializados pelo
programa, são desconhecidos do programador, mas esta situação não deve ser confundida com a
“ausência de conteúdo”, erro de raciocı́nio frequentemente cometido por programadores principi-
antes.
As variáveis de tipo elementar podem ser inicializadas pela atribuição directa de valores con-
stantes. A mesma técnica pode ser utilizada para a inicialização de apontadores, embora este
8 A PONTADORES
.. .. .. .. .. ..
. . . . . .
A B C
.. .. .. .. .. ..
. . . . . .
D E F
Figura 2.2: Mapa de memória após diferentes sequências de atribuição (explicação no texto).
A PONTADORES 9
método raramente seja utilizado: em geral, o programador não sabe quais os endereços de memória
disponı́veis no sistema, e a manipulação directa de endereços absolutos tem reduzidas aplicações
práticas.3 Assim, A inicialização de apontadores é geralmente realizada por outros métodos, entre
os quais a utilização do endereço de outras variáveis já declaradas. Este endereço pode ser obtido
em C aplicando o operador & a uma variável. Na continuação do exemplo anterior, admita-se que
era realizada agora a seguinte sequência de atribuições:
pd = &d; pi = &k;
Após esta fase, o mapa de memória seria o representado na figura 2.2, situação B, onde as
variáveis pd e pi surgem agora preenchidas com os endereços de d e k. Como seria de es-
perar, a consistência da atribuição exige que a variável referenciada e o tipo do apontador sejam
compatı́veis. Por exemplo, a atribuição pd=&k é incorrecta, atendendo a que k é do tipo int e
pd está declarada como um apontador para double.
A partir do momento em que os apontadores são inicializados, o seu conteúdo pode ser
copiado e atribuı́do a outras variáveis, desde que os tipos ainda sejam compatı́veis. Assim, as
atribuições
conduziriam apenas à cópia dos endereços guardados em pd e pi para pd2 e pi2. Após esta
sequência, a situação seria a representada na figura 2.2, caso C.
j = *pi; d2 = *pd;
real, por exemplo. De facto, a dependência do apontador do tipo apontado não tem a ver com
a estrutura do apontador em si, mas sim com o facto de esta informação ser indispensável para
desreferenciar (aceder) correctamente o valor endereçado. Por exemplo, na expressão d2 = *pd,
é o facto de pd ser um apontador para double que permite ao compilador saber que o valor
referenciado ocupa não apenas uma, mas duas palavras de memória e qual a sua representação. Só
na posse desta informação é possı́vel efectuar a atribuição correcta a d2.
a situação seria a representada no caso E. Note-se que aqui o operador * foi utilizado no lado
esquerdo da atribuição. Assim, por exemplo, a atribuição *pd2 = *pd1 é interpretada como
ler o real cujo endereço é especificado pelo conteúdo de pd1 e colocar o resultado no endereço
especificado por pd2.
Embora até aqui tenham sido considerados apontadores para tipos elementares, um apontador
pode também endereçar um outro apontador. Assim, na declaração
int i1,i2,*pi1,**pi2,***pi3;
int k1,k2,*pk1,**pk2;
i1 = 3; i2 = 4;
pi1 = &i1; pi2 = &pi1; pi3 = &pi2;
k1 = 10;
pk1 = &k1; pk2 = pi2;
k2 = i1 * ***pi3 + *pi1 + i2 + **pk2 * *pk1;
respeitados. Assim, a atribuição i1 = *pk2; no exemplo precedente seria incorrecta, já que i1
é do tipo inteiro e, *pk2 é um apontador para inteiro (recorde-se que pk2 é um apontador para um
apontador para um inteiro).
Como é sabido, o valor de uma variável nunca deve ser utilizado antes de esta ser inicializada
explicitamente pelo programa. Com efeito, no inı́cio de um programa, as variáveis têm um valor
arbitrário desconhecido. Por maioria de razão, o mesmo princı́pio deve ser escrupulosamente
seguido na manipulação de apontadores. Suponha-se, por exemplo que, no inı́cio de um programa,
são incluı́das a declaração e as instruções seguintes:
int *p,k;
k = 4; *p = k*2;
Aqui, a segunda atribuição especifica que o dobro de k (valor 8) deve ser colocado no endereço
especificado pelo conteúdo da variável p. No entanto, dado que p não foi inicializada, o seu
conteúdo é arbitrário. Com elevada probabilidade, o seu valor corresponde a um endereço de
memória inexistente ou inválido. No entanto, o C não realiza qualquer juı́zo de valor sobre esta
situação e, tendo sido instruı́do para “colocar 8 no endereço indicado por p” tentará executar esta
operação. A tentativa de escrita numa posição de memória inválida ou protegida conduzirá ou ao
compromisso da integridade do sistema operativo se o espaço de memória não for conveniente-
mente protegido, como é o caso do DOS. Se o sistema tiver um modo protegido, como o Unix ou o
Windows NT, esta situação pode originar um erro de execução, devido a uma violação de memória
detectada pelo sistema operativo. Os erros de execução conduzem à interrupção imediata, em erro,
do programa.
Sublinhe-se que nas figuras desta secção foram utilizados endereços especı́ficos nos aponta-
dores de modo a melhor demonstrar e explicar o mecanismo de funcionamento dos mecanismos de
apontadores e indirecção em C. No entanto, na prática, o programador não necessita de conhecer o
valor absoluto dos apontadores, sendo suficiente a manipulação indirecta do seu conteúdo através
dos mecanismos de referenciação e desreferenciação descritos.
nado passagem por valor (Martins, 1989) e está subjacente a todas as chamadas de funções em
C. Esta situação é adequada se se pretender apenas que os argumentos transmitam informação do
módulo que chama para dentro da função. Dado que uma função pode também retornar um valor,
este mecanismo básico é também adequado quando a função recebe vários valores de entrada e
tem apenas um valor de saı́da. Por exemplo, uma função que determina o maior de três inteiros
pode ser escrita como
Existem no entanto situações em que se pretende que uma função devolva mais do que um
valor. Uma situação possı́vel seria uma variante da função maior_3 em que se pretendesse
determinar os dois maiores valores, e não apenas o maior. Outro caso tı́pico é o de uma função
para trocar o valor de duas variáveis entre si. Numa primeira tentativa, poderia haver a tentação de
escrever esta funçã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úmeros: ");
scanf("%d %d",&a,&b);
trocaMal(&a,&b);
printf("a = %d, b= %d\n",a,b);
F UNÇ ÕES E PASSAGEM POR REFER ÊNCIA 13
exit(0);
}
No entanto, este programa não funciona: o mecanismo de passagem por valor implica que a
função troca opera correctamente sobre as variáveis locais x e y, trocando o seu valor, mas estas
variáveis não são mais do que cópias das variáveis a e b que, como tal, se mantêm inalteradas.
Na figura 2.3 representa-se a evolução do mapa de memória e do conteúdo das variáveis durante a
chamada à função trocaMal.
Este aparente problema pode ser resolvido pela utilização criteriosa de apontadores. A função
troca especificada anteriormente pode ser correctamente escrita como se segue:
#include <stdio.h>
#include <stdlib.h>
void troca(int *x,int *y){
/*
* Função que troca dois argumentos
*/
int aux;
aux = *x;
*x = *y;
*y = aux;
}
int main(){
int a,b;
exit(0);
}
Tal como pode ser observado, a solução adoptada consiste em passar à função não o valor das
variáveis a e b, mas sim os seus endereços. Embora estes endereços sejam passados por valor (ou
seja, a função recebe uma cópia destes valores), o endereço permite à função o conhecimento da
posição das variáveis a b em memória e, deste modo, permite a manipulação do seu conteúdo por
meio de um endereçamento indirecto.
Figura 2.3: Mapa de memória durante as diferentes fase de execução de um programa que utiliza
a função trocaMal. A - antes da chamada à função, B - no inı́cio de troca, C - no final de
troca, D - após o regresso ao programa principal. O mecanismo de passagem por valor conduz
a que os valores do programa principal não sejam alterados.
F UNÇ ÕES E PASSAGEM POR REFER ÊNCIA 15
Figura 2.4: Mapa de memória durante as diferentes fase de execução do programa que utiliza a
função troca. A - antes da chamada à função, B - no inı́cio de troca C - no final de troca, D -
após o regresso ao programa principal. A passagem de apontadores para as variáveis do programa
principal (passagem por referência) permite que a função altere as variáveis do programa principal.
16 A PONTADORES
grama principal atribuiu às variáveis A e B os endereços 2135 e 2136, e que estas foram inicial-
izadas pelo utilizador com os valores 8 e 2, respectivamente. A situação imediatamente antes da
chamada da função troca encontra-se representada em A. Durante a chamada da função, realiza-
se a activação das variáveis x, y e aux, locais à função, eventualmente numa zona de memória
afastada daquela onde se encontram as variáveis a e b, sendo as duas primeiras destas variáveis
inicializadas com os endereços de a e b (situação B). Através do endereçamento indirecto através
das variáveis x e y, são alterados os valores das variáveis a e b do programa principal, atingido-se
a situação C. Após o regresso ao programa principal, as variáveis da função troca são libertadas,
atingindo-se a situação representada em D, com as
Note-se que, estritamente falando, a passagem de argumento se deu por valor, atendendo a que
x e y são variáveis locais à função, tendo recebido apenas valores correspondentes ao endereço de
variáveis declaradas no programa principal. No entanto, neste tipo de mecanismo, diz-se também
que as variáveis a e b foram passadas por referência(Martins, 1989), atendendo a que o seu
endereço (e não o seu conteúdo) que foi passadas à função.
Mais genericamente, sempre que é necessário que uma função altere o valor de um ou mais
dos seus argumentos, este ou estes deverão ser passados por referência, de forma a ser possı́vel à
função modificar o valor das variáveis por um mecanismo de indirecção. É por este motivo que,
na chamada da função scanf(), todas variáveis a ler são passados por referência, de modo a ser
possı́vel a esta função poder ler e alterar os valores das variáveis do programa principal.
/*
Utilização incorrecta (desnecessária)
de referências múltiplas.
*/
void func2(int **p2,int b2){
**p2 = -b2 * b2;
}
void func1(int *p1,int b1){
b1 = b1 + 1;
F UNÇ ÕES E PASSAGEM POR REFER ÊNCIA 17
func2(&p1,b1);
}
int main(){
int x;
func1(&x,5);
return 0;
}
Como pode ser facilmente entendido, a referenciação de uma variável uma única vez é suficiente
para que este mesmo endereço possa ser sucessivamente passado entre os vários nı́veis de funções
e ainda permitir a alteração da variável seja sempre possı́vel. Assim, embora o programa anterior
funcione, a referenciação de p1 na passagem de func1 para func2 é inútil: o mecanismo ali
adoptado só faria sentido caso se pretendesse que func2 alterasse o conteúdo da variável p1.
Como é evidente não é esse o caso, e o programa anterior correctamente escrito tomaria a seguinte
forma:
/*
Utilização correcta de uma passagem
por referência entre vários nı́veis
de funções.
*/
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;
}
int main(){
int x,*y;
y = func1(1);
x = 2 * *y;
return 0;
}
Aqui, a variável b é local à função func e, como tal, é criada quando func é activada e a
sua zona de memória libertada quando a função termina. Ora o resultado da função func é
passada ao programa principal sob a forma do endereço de b. Quando o valor desta variável é
lido no programa principal por meio de um endereçamento indirecto na expressão x = 2 * *y,
a variável b já não está activa, realizando-se por isso um acesso inválido à posição de memória
especificada pelo endereço em y. Com elevada probabilidade, o resultado final daquela expressão
será incorrecto.
Um vector em C permite a criação de uma estrutura com ocorrências múltiplas de uma variável
de um mesmo tipo. Assim, a declaração
Cada elemento individual de um vector pode ser referenciado acrescentando à frente do nome
da variável o ı́ndice, ou posição que se pretende aceder, representado por um inteiro entre [].
Para um vector com N posições, o ı́ndice de acesso varia entre 0 (primeira posição) e N − 1
(última posição). Cada elemento de x e y corresponde a uma variável de tipo inteiro ou double,
respectivamente. Deste modo, se se escrever,
1006 y[0]
200.0
1007
1008 y[1]
200.1
1009
y[2]
1010
200.2
1011
.. ..
. .
Figura 2.5: Mapa de memória correspondente à declaração de dois vectores
20 A PONTADORES
a = y[1] + x[3];
o conteúdo final de a será o resultado da soma da segunda posição de y com a quarta posição de
x, ou seja 656.1.
Dado que cada elemento de um vector é uma variável simples, é possı́vel determinar o seu
endereço. Assim, é possı́vel fazer
pi = &(x[2]);
pd = &(y[1]);
conduzindo-se assim à situação representada na figura 2.5.1. Note-se que nas atribuições
pi = &(x[2]) e pd = &(y[1]) os parenteses poderiam ser omitidos, dado que o oper-
ador [] (ı́ndice) tem precedência sobre o operador & (endereço de). Assim, aquelas atribuições
poderiam ser escritas como pi = &x[2] e pd = &y[1].
Uma das vantagens da utilização de vectores é o ı́ndice de acesso poder ser uma variável.
Deste modo, inicialização de um vector de 10 inteiros a 0 pode ser feita pela sequência
#define NMAX 10
int main(){
int x[NMAX];
int k;
/* ...*/
Uma utilização comum dos vectores é a utilização de vectores de caracteres para guardar
texto. Por exemplo a declaração
1006 y[0]
200.0
1007
1008 y[1]
200.1
1009
y[2]
1010
200.2
1011
1012 1003 pi
1013 1008 pd
.. ..
. .
Figura 2.6: Apontadores e vectores (explicação no texto).
22 A PONTADORES
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
texto[18] ’D’ ’u’ ’a’ ’s’ ’ ’ ’p’ ’a’ ’l’ ’a’ ’v’ ’r’ ’a’ ’s’’\0’
cria um vector de 18 caracteres, e inicia as suas primeiras posições com o texto Duas palavras.
Uma representação gráfica deste vector depois de inicializado é apresentada na figura 2.7.
Dado que o texto pode ocupar menos espaço que a totalidade do vector, como sucede neste
caso, a última posição ocupada deste vector é assinalada pela colocação na posição seguinte
à última do caracter com o código ASCII 0, geralmente representado pela constante inteira 0
ou pelo caracter ’\0’. Note-se que esta última representação não deve ser confundida com a
representação do algarismo zero ’0’, internamente representado por 48, o código ASCII de 0.
É possı́vel adicionar ou subtrair uma constante inteira de uma variável de tipo apontador. Este
tipo de operação só faz sentido em vectores ou estruturas de dados regulares, em que o avanço ou
recuo de um apontador conduz a um outro elemento do vector (ou estrutura de dados regulares).
É da única e exclusiva responsabilidade do programador garantir que tais operações aritméticas
se mantêm dentro dos endereços válidos da estrutura de dados apontada. Assim, por exemplo, se
na sequência do exemplo e da situação representada na figura 2.5.1 (repetida por conveniência na
figura 2.5.2, A) fossem executadas as instruções
pi = pi + 2;
pd = pd - 1;
No caso do modelo de memória adoptado como exemplo, a operação aritmética sobre o apon-
tador inteiro tem uma correspondência directa com a operação realizada sobre o endereço. No
entanto, o mesmo não sucede com o apontador para double, onde subtracção de uma unidade
ao apontador corresponde na realidade a uma redução de duas unidades no endereço fı́sico da
memória.
.. .. .. ..
. . . .
A B
ou subtraı́da de acordo com a dimensão do tipo apontado. Este modo de operação garante que,
na prática, o programador possa realizar operações sobre apontadores abstraindo-se do número
efectivo de bytes do elemento apontado.
Uma última nota relativamente aos apontadores de tipo void. O C-ANSI permite a definição
de apontadores genéricos, cujo tipo é independente do objecto apontado. Um apontador pv deste
tipo manipula um endereço genérico e pode ser simplesmente declarado por
void *pv;
e encontra utilização em situações em que se pretende que uma mesma estrutura possa endereçar
entidades ou objectos de tipos diferentes. Sempre que possı́vel, este tipo de situações deve ser
preferencialmente abordado pela utilização de estruturas do tipo union. No entanto, existem
casos em que tal não é possı́vel, obrigando à utilização deste tipo de apontadores. É, por exemplo,
o caso das funções de gestão de memória, que trabalham com apontadores genéricos (V. secção
3.3). Note-se, no entanto, que o C não consegue desreferenciar automaticamente um apontador
V ECTORES E APONTADORES 25
para void (ou seja, aceder ao conteúdo apontado por). De igual modo, o desconhecimento da
dimensão do objecto apontado impede que a aritmética de apontadores seja aplicável a apontadores
deste tipo.
a = x[2];
a = *(&x[0] + 2);
De facto, a segunda expressão pode ser simplificada. O C define que o endereço do primeiro
elemento de um vector pode ser obtido usando simplesmente o nome do vector, sem o operador
de indexação (“[0]”) à frente. Ou seja, no contexto de uma expressão em C, “ &x[0]” é equiv-
alente a usar simplesmente “ x”. Por outras palavras, se x[] é um vector do tipo xpto, x é um
apontador para o tipo xpto. Assim, a expressão “ x[k]” é sempre equivalente a “ *(xk)+”. Este
facto conduz ao que designamos por regra geral de indexação em C, que pode ser enunciada pela
equivalência
Registe-se, como curiosidade, que a regra geral de indexação conduz a que, por exemplo,
x[3] seja equivalente a 3[x]. Com efeito,
Claro que a utilização desta propriedade é geralmente proibido, não pelo C, mas pelas normas de
boa programação!
26 A PONTADORES
Para usar um vector como argumento de uma função func() basta, no bloco que chama
func(), especifiar o nome da variável que se pretende como argumento. Assim, pode fazer-se,
por exemplo,
int x[10];
/* ... */
func(x);
/* ... */
No entanto, é necessário ter em conta que x, quando usado sem ı́ndice, representa apenas o
endereço da primeira posição do vector. Como deve então ser declarado o argumento formal
correspondente no cabeçalho de func?
Diversas opções existem para realizar esta declaração. No exemplo seguinte, a função set é
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 função é apenas a repetição da
declaração efectuada no programa principal. Assim,
Note-se que para uma função manipular um vector é suficiente conhecer o endereço do
primeiro elemento. Dentro da função, o modo de aceder ao k-ésimo elemento do vector é sempre
adicionar k ao endereço da base, independentemente da dimensão do vector. Ou seja, o parâmetro
formal int x[NMAX] apenas indica que x é um apontador para o primeiro elemento de um vec-
tor de inteiros. Deste modo, o C não usa a informação “[NMAX]” no parâmetro formal para aceder
à função. Assim, a indicação explı́cita da dimensão pode ser omitida, sendo válido escrever
int k;
for(k = 0; k < n; k++) x[k] = k+1;
}
Note-se que a possibilidade de omitir a dimensão resulta também da função não necessitar de
reservar o espaço para o vector: a função limita-se a referenciar as células de memória reservadas
no programa principal.
Atendendo a que a, sem inclusão do ı́ndice, especifica um apontador para primeiro elemento
do vector, uma terceira forma de declarar o argumento formal é
Esta última forma sugere uma forma alternativa de escrever o corpo da função set. Com
efeito, para percorrer um vector basta criar um apontador, inicializá-lo com o endereço da primeira
posição do vector e seguidamente incrementá-lo sucessivamente para aceder às posições seguintes.
Esta técnica pode ser ilustrada escrevendo a função
Note-se que sendo x um ponteiro cujo valor resulta de uma passagem cópia do endereço de a no
programa principal, é possı́vel proceder ao seu incremento na função de modo a percorrer todos
os elementos do vector. Aqui, a expressão *x++ merece um comentário adicional. Em primeiro
lugar, o operador ++ tem precedência sobre o operador * e, deste modo, o incremento opera sobre
o endereço e não sobre a posição de memória em si. É este apenas o significado da precedência,
4
Na versão original do C, esta limitação estendia-se ao tratamento de estruturas de dados criadas com a directiva
struct, mas esta limitação foi levantada pelo C-Ansi
28 A PONTADORES
o qual não deve ser confundido com a forma de funcionamento do operador incremento enquanto
sufixo: o sufixo estabelece apenas que o conteúdo de x é utilizado antes da operação de incremento
se realizar. Ou seja, *x+=k;+ é equivalente a *x=k;x+;+.
Um outro exemplo de utilização desta técnica pode ser dado escrevendo uma função para
contabilizar o número de caracteres usados de uma string. Atendendo a que se sabe que o último
caracter está seguido do código ASCII 0, esta função pode escrever-se
2.6 Matrizes
2.6.1 Declaração
float x[3][2];
declara uma estrutura bidimensional de três por dois reais, ocupando um total de seis palavras
de memória no modelo de memória que temos vindo a usar como referência. É frequente uma
estrutura bidimensional ser interpretada como uma matriz, neste exemplo de três linhas por duas
colunas.
Nos modos de utilização mais simples deste tipo de estruturas, o programadador pode abstrair-
se dos detalhes de implementação e usar a variável bidimensional como uma matriz. Assim, a
inicialização a zeros da estrutura x pode ser efectuada por
float x[3][2];
int k,j;
M ATRIZES 29
.. ..
. .
Figura 2.9: Mapa de memória correspondente à declaração de uma estrutura de três por dois reais
Alternativamente, a inicialização pode ser feita listando os valores iniciais, sendo apenas
necessário agrupar hierarquicamente as constantes de inicialização de acordo com as dimensões
da estrutura:
Comece-se por regressar à declaração int x[3][2] e observar a forma como é determi-
nado o endereço de x[k][j]. Analisando a figura 2.9, é evidente que o endereço deste elemento é
obtido adicionando ao endereço do primeiro elemento k*2+j. Ou seja,
30 A PONTADORES
No caso mais genérico da declaração ter a forma <tipo> x[N][M] ter-se-á ainda
Ou seja, para aceder a um elemento genérico de uma matriz não basta conhecer o endereço da
primeira posição e o os dois ı́ndices de acesso: é necessário saber também o número de colunas da
matriz. Deste modo, quando uma matriz é passada como argumento de uma função, é necessário
que esta saiba a dimensão das colunas da matriz. Por este motivo, dada a chamada à função
/* ... */
#define NLIN 3
#define NCOL 2
/* ... */
int m[NLIN][NCOL];
int a;
/* ... */
a = soma(m);
/* ... */
ou pode omitir o número de linhas (dado que, como se mostrou, este número não é indispensável
para localizar o endereço de um elemento genérico da matriz), como em
Viu-se anteriormente que se o vector int x[NMAX] for utilizado na chamada a uma função,
a declaração a adoptar nos parâmetros formais da função podia ser ou int a[]) ou int *a.
É frequente surgir a dúvida se é possı́vel adoptar uma notação de apontador equivalente no caso
de uma matriz. De facto sim, embora esta notação raramente seja utilizada na prática. No caso do
exemplo que tem vindo a ser utilizado a declaração possı́vel seria
Nesta declaração, x é um apontador para um vector de NCOL floats. Uma explicação mais
detalhada do significado desta invulgar declaração pode ser encontrado na secção 2.6.4.
Na prática, este facto significa que cada uma das linhas de uma matriz pode ser tratada indi-
vidualmente como um vector.
32 A PONTADORES
Suponha-se, por exemplo, que dada uma matriz a de dimensão NLIN × NCOL, se pretende
escrever uma função que determine o máximo de cada uma das linhas da matriz e coloque o
resultado num vector y. Esta função pode ser escrita como
onde cada linha foi tratada individualmente como um vector, cujo máximo é determinado pela
função vectorial
y = Ax
#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 é um vector em que cada elemento
não é mais do que o produto interno de cada linha da matriz com o vector operando, a função
prodMatVec poderia ser escrita como
Uma última situação em que é possı́vel exemplificar a utilização de linhas como vectores é
o do armazenamento de várias linhas de texto. Admita-se, por exemplo, que se pretende ler uma
sequência de linhas de texto e imprimir as mesmas linhas por ordem inversas. Tal é possı́vel através
da utilizaçã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ência 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);
}
e, portanto, se x[k][j] é por exemplo do tipo float, x[k] é um apontador para float. Con-
sultando 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 endereço do primeiro elemento do vector de floats
formado pelos reais 1.0 e 2.0, enquanto que x[2] corresponde também a um apontador, cujo
valor é 1003 (primeiro elemento do vector de floats formado pelos reais 3.0 e 4.0).
uma hierarquia de vectores simples. Ou seja, x[3][2] representa um vector de três elementos,
em que cada um é por sua vez um vector de dois elementos. Assim, x[k] representa sempre um
vector de dois floats. Com esta formulação, resulta claro que x representa um apontador para
um vector de dois floats. Isto não é mais do que a generalização da situação dos vectores, em que
dada a declaração int a[N], se sabe que a isoladamente é um apontador para inteiro.
Assim, é natural que x sem ı́ndices especifique o endereço do primeiro elemento de um vector
de três elementos, em que cada um é um vector de dois floats. Ou seja, no nosso exemplo, x
corresponde ao valor 1001.
Mas, afinal, qual a diferença entre um apontador para float e um apontador para um vec-
tor de floats? Por um lado, a forma de usar este tipo variável num endereçamento indirecto
é claramente diferente. Por exemplo, x e &x[0][0] correspondem ao mesmo valor (1001 no
nosso exemplo), mas são tipos diferentes: o primeiro é um apontador para um vector, pelo que *x
é um vector (a primeira linha de x, enquanto que *(&x[0][0]) é um float (o conteúdo de
x[0][0]. No entanto, é provavelmente mais importante reter que a diferença fundamental reside
na dimensão do objecto apontado. Considere-se novamente o exemplo que temos vindo a con-
siderar. Uma variável do tipo apontador para float, cujo valor seja 1001, quando incrementada
passa a ter o valor 1002. Mas uma variável do tipo apontador para vector de dois float, cujo
valor seja 1001, quando incrementada passa a ter o valor 1003 (já que o incremento é escalado
pela dimensão do objecto apontado).
É interessante verificar que este modelo é o único que permite manter a consistência sintáctica
do C na equivalência entre vectores e apontadores. Já se disse que sendo sempre
então,
O que não se disse antes, mas que também se verifica, é que a primeira destas regras também deve
ser aplicável à entidade x[k] que surge na última expressão. Ou seja, tem-se
Um factor que contribui frequentemente para alguma confusão deriva do facto de que, ainda
que x não seja sintacticamente um duplo apontador para float, sendo
Como é natural, é possı́vel declarar um apontador para um vector de dois floats, sem ser
da forma implı́cita que resulta da declaração da matriz. A declaração de uma variável y deste tipo
pode ser feita por
float (*y)[2];
Por este motivo, que quando a matriz x é passada por argumento para uma função func, a
M ATRIZES 37
declaração do parâmetro formal poder ser feita repetindo a declaração total, omitindo a dimensão
do ı́ndice interior, ou então por
Dado que este tipo de declarações é alvo frequente de confusão, é conveniente saber que existe
uma regra de leitura que ajuda a clarificar a semântica da declaração. Com efeito, é suficiente
“seguir” as regras de precedência, procedendo à leitura na seguinte sequência:
float (*y)[3]
yé
um apontador
para um vector de três
floats
Sublinhe-se que, face a tudo o que ficou dito anteriormente, não é possı́vel declarar um apon-
tador para um vector sem especificar a dimensão do vector: como já foi dito por diversas vezes,
um apontador tem que conhecer a dimensão do objecto apontado. Isto não é possı́vel sem especi-
ficar a dimensão do vector. Como corolário, um apontador para um vector de três inteiros é de
tipo distinto de um apontador para um vector para seis inteiros, não podendo os seus conteúdos
ser mutuamente atribuı́dos.
float *y[2];
onde, devido à ausência de parenteses, é necessário ter em atenção a precedência de “[]” sobre o
“*”. Neste caso, y é um vector de dois apontadores para float, podendo a leitura da declaração
ser realizada pela sequência:
float *y[3]
yé
um vector de três
apontadores
para float
Finalmente, refira-se que os apontadores para vectores podem ainda surgir noutros contextos:
dada a declaração int x[10], x é um apontador para inteiro, mas a expressão &x é do tipo
38 A PONTADORES
A generalização do que ficou dito para mais do que duas dimensões é directa. Considere-se,
como referência, a declaração da estrutura
int x[M][N][L];
5. Em geral,
x[m][n][l] == *(*(*(x+m)+n)+l)
A passagem de uma estrutura multidimensional como argumento pode ser feita pela repetição
da declaração completa do tipo. Assim, uma declaração possı́vel é
#define M ...
#define N ...
#define L ...
int main(){
int x[M][N][L];
/*...*/
func(x);
/*...*/
return 0;
}
G ENERALIZAÇ ÃO PARA MAIS DO QUE DUAS DIMENS ÕES 39
Claro que todas as outras variantes em que exista consistência sintáctica entre os argumentos
formais e actuais do procedimento são válidas. Assim, pelas mesmas razões já detalhadas na
secção 2.6.4,
3.1 Introdução
Até ao aparecimento da linguagem C, a maioria das linguagens de alto nı́vel exigia um dimen-
sionamento rı́gido das variáveis envolvidas. Por outras palavras, a quantidade máxima de memória
necessária durante a execução do programa deveria ser definida na altura de compilação do pro-
grama. Sempre que os requisitos de memória ultrapassavam o limite fixado durante a execução
do programa, este deveria gerar um erro. A solução nestes casos era recompilar o programa au-
mentando a dimensão das variáveis, solução só possı́vel se o utilizador tivesse acesso e dominasse
os detalhes do código fonte. Por exemplo, um programa destinado à simulação de um circuito
electrónico poderia ser obrigado a definir no código fonte o número máximo de componentes
do circuito. Caso este número fosse ultrapassado, o programa deveria gerar um erro, porque a
memória reservada durante a compilação tinha sido ultrapassada. A alternativa nestas situações
era a reserva à partida de uma dimensão de memória suficiente para acomodar circuitos de di-
mensão elevada, mas tal traduzia-se obviamente num consumo excessivo de memória sempre que
o programa fosse utilizado para simular circuitos de dimensão reduzida. Em alguns casos, os pro-
gramas recorriam à utilização de ficheiros para guardar informação temporária, mas esta solução
implicava geralmente uma complexidade algorı́tmica acrescida.
Tal como o nome indica, os sistemas de memória dinâmica permitem gerir de forma dinâmica
os requisitos de memória de um dado programa durante a sua execução. Por exemplo, no caso
do sistema de simulação referido anteriormente, o programa pode, no inı́cio da execução, deter-
minar a dimensão do circuito a simular e só nessa altura reservar a memória necessária. Com
esta metodologia, o programa pode minimizar a quantidade de memória reservada e, deste modo,
permitir que o sistema operativo optimize a distribuição de memória pelos vários programas que
42 V ECTORES E MEM ÓRIA DIN ÂMICA
• Dada a sua proximidade com o hardware1 , a maioria dos sistemas operativos actualmente
existentes são ainda hoje programados na linguagem C.
• A maioria dos compiladores de linguagens de alto nı́vel, incluindo o próprio C, são actual-
mente escritos e desenvolvidos em C. Ou seja, nesta perspectiva, o C é hoje uma linguagem
indispensável à geração da maioria das outras linguagens, constituindo, neste sentido, a
“linguagem das linguagens”.
1
O C é frequentemente designado na gı́ria como um Assembler de alto nı́vel, apesar do evidente paradoxo contido
nesta designação.
V ECTORES 43
3.2 Vectores
Em C, um vector é uma colecção com um número bem definido de elementos do mesmo tipo.
Ao encontrar a declaração de um vector num programa, o compilador reserva automaticamente
espaço em memória para todos os seus elementos. Por razões de clareza e de boa prática de
programação, estas constantes são normalmente declaradas de forma simbólica através de uma
directiva define, mas a dimensão continua obviamente a ser uma constante:
int x[DIM_MAX];
char s[DIM_MAX_STRING];
Por outras palavras, a necessidade de saber quanta memória é necessária para o vector que se
pretende utilizar implica que a dimensão deste vector seja uma constante, cujo valor já conhecido
durante a compilação do programa.
Como é sabido, as variáveis locais a uma função têm um tempo de vida limitado à execução
da função2 . O espaço para estas variáveis é reservado na chamada zona da pilha (stack), uma
região de memória dedicada pelo programa para armazenar variáveis locais e que normalmente é
ocupada por ordem inversa. Por outras palavras, são usados primeiros os endereços mais elevados
e vão sendo sucessivamente ocupados os endereços inferiores à medida que são reservadas mais
variáveis locais. Dada a forma como se realiza esta ocupação de memória, o limite inferior da
região da pilha é geralmente designada por topo da pilha. Neste sentido, quando uma função
é chamada, o espaço para as variáveis locais é reservado no topo da pilha, que assim decresce.
Quando uma função termina, todas as variáveis locais são libertadas e o topo da pilha aumenta
novamente. Assim, por exemplo, dada a declaração
a chamada da função func poderia dar origem a uma evolução do preenchimento da memória da
forma como se representa na figura 3.1.
2
Excepto se a sua declaração for precedido do atributo static, mas este só é usado em circunstâncias excepcionais.
44 V ECTORES E MEM ÓRIA DIN ÂMICA
.. .. .. .. .. ..
. . . . . .
1001 1001 topo da pilha
1002 1002 b
1007 1007 a
1008 1008 z
.. .. topo da pilha .. ..
. . . .
A B
Figura 3.1: Mapa de memória antes e após a chamada à função func (A) e mapa de memória
durante a execução da função (B).
Como é evidente, os elementos de um vector podem não ser escalares simples como no ex-
emplo anterior. Os elementos de um vector podem ser estruturas de dados, como por exemplo
em
int main(){
tipoAluno alunos[MAX_ALUNOS];
/* ... */
int mat[10][5]
a matriz mat, do ponto de vista interno do C, não é mais do que um vector de 10 elementos, em
que cada elemento é por sua vez um vector de 5 inteiro (V. secção 2.6.3).
Não se pretende com esta análise colocar de parte todas as utilizações de vectores conven-
cionais com dimensão fixa. Frequentemente, esta é uma solução mais que razoável. Por exemplo,
uma linha de texto numa consola de texto tem em geral cerca de 80 caracteres, pelo que a definição
de um valor máximo para o comprimento de uma linha de 160 ou 200 caracteres é um valor que
pode ser razoável e que não é excessivo na maioria das aplicações. No entanto, em situações
em que o número de elementos pode variar significativamente, é desejável que a memória seja
reservada à medida das necessidades.
46 V ECTORES E MEM ÓRIA DIN ÂMICA
int x[MAX_DIM]
A utilização de um bloco de memória criado dinamicamente e que pode ser utilizado com
mecanismos de acesso idênticos aos de um vector pode ser realizado declarando uma variável
de tipo de apontador e solicitando ao sistema operativo a reserva de um bloco de memória da
dimensão pretendida. O pedido de reserva de memória ao sistema operativo é efectuado através
de um conjunto de funções declaradas no ficheiro stdlib.h. Uma das funções utilizáveis para
este efeito tem como protótipo
A função calloc reserva um bloco de memória contı́gua com espaço suficiente para armazenar
nmemb elementos de dimensão size cada, devolvendo o endereço (apontador) para a primeira
posição do bloco. size_t é o tipo utilizado para especificação de dimensões numéricas em várias
funções do C e a sua implementação pode ser dependente do sistema operativo, mas corresponde
geralmente a um inteiro sem sinal. O tipo de retorno, void*, corresponde a um endereço genérico
de memória. o que permite que a função seja utilizada independentemente do tipo especı́fico do
apontador que se pretende inicializar. A função retorna um apontador para o primeiro endereço de
“V ECTORES ” DIN ÂMICOS 47
uma região de memória livre da dimensão solicitada. Caso esta reserva não seja possı́vel, a função
retorna NULL, para indicar que a reserva não foi efectuada.
/* Ficheiro: media.c
Conteúdo: Programa para cálculo da média e variânicia
Autor: Fernando M. Silva, IST (fcr@inesc-id.pt)
História : 2001/06/01 - Criação
*/
#include <stdio.h>
#include <stdlib.h>
int main(){
int n; /* Número de valores */
float *f; /* Apontador para o primeiro valor */
float soma,media,variancia;
int k;
soma = 0.0;
for( k = 0 ; k < n ; k++)
soma += f[k];
media = soma / n; /* cálculo da média */
soma = 0.0;
48 V ECTORES E MEM ÓRIA DIN ÂMICA
Na chamada à função calloc(), dois aspectos devem ser considerados. Em primeiro lugar o op-
erador sizeof() é um operador intrı́nseco do C que devolve a dimensão (geralmente em bytes)
do tipo ou variável indicado no argumento. Por outro lado, à esquerda da função calloc() foi
acrescentada e expressão (float*). Esta expressão funciona como um operador de cast, obrig-
ando à conversão do apontador genérico devolvido pela função calloc para um apontador para
real. Em geral, na operação de cast é indicado o tipo do apontador que se encontra do lado es-
querdo da atribuição. Embora este cast não seja obrigatório, é geralmente utilizado na linguagem
C como uma garantia adicional da consistência das atribuições.
Após a reserva dinâmica de memória, o apontador f pode ser tratado como um vector con-
vencional. Como é evidente, o ı́ndice não deve ultrapassar a posição n − 1, dado que para valores
superiores se estaria a aceder a posições de memória inválidas.
Enquanto que as variáveis locais são criadas na zona da pilha, toda a memória reservada
dinamicamente é geralmente criada numa região independente de memória designada por heap
(molhe). Admita-se, no caso do programa para cálculo das médias e variâncias, que o valor de n
especificado pelo utilizador era 4. Uma possı́vel representação do mapa de memória antes e após
a chamada da função calloc encontra-se representado na figura 3.2.
Como já se referiu, ao efectuar uma reserva dinâmica de memória é essencial testar se os re-
cursos solicitados foram ou não disponibilizados pelo sistema. Com efeito, a reserva de memória
pode ser mal sucedida por falta de recursos disponı́veis no sistema. Este teste teste e deve ser sem-
pre efectuado testando o apontador devolvido pela função calloc(). Quando existe um erro,
a função devolve o endereço 0 de memória, o qual é representado simbolicamente pela constante
G EST ÃO DA MEM ÓRIA DIN ÂMICA 49
1006 f 2106
1007 4 n 2107
1009 2109
.. .. .. ..
. . . .
A
1007 4 n 2107
1009 2109
.. .. .. ..
. . . .
B
Figura 3.2: Mapa de memória antes da reserva de memória (A) e após a reserva de memória. (B).
50 V ECTORES E MEM ÓRIA DIN ÂMICA
NULL (definida em stdio.h). Se o endereço devolvido tiver qualquer outro valor, a reserva de
memória foi bem sucedida, e o programa pode continuar a sua execução normal.
Função free()
Enquanto que a função calloc() efectua uma reserva dinâmica de memória, a função
free(p) liberta o bloco de memória apontado por p. Como seria de esperar, esta função só
tem significado se o apontador p foi obtido por uma função prévia de reserva de memória, como
a função calloc() descrita anteriormente.
De um modo geral, é boa prática de programação libertar toda a memória reservada pelo
programa sempre que esta deixa de ser necessária. Isto sucede frequentemente pela necessidade
de libertar memória que só foi necessária temporariamente pelo programa.
Note-se que sempre que o programa termina toda a memória reservada é automaticamente lib-
ertada. Deste modo, a libertação explı́cita da memória reservada durante a execução do programa
não é estritamente obrigatória antes de sair do programa através da função exit() ou da direc-
tiva return no bloco main(). Apesar deste facto, alguns autores consideram que, por razões
de consistência e arrumação do código, o programa deve proceder à libertação de toda a memória
reservada antes de ser concluı́do. É este o procedimento adoptado no programa para cálculo da
variância, onde a função free() é chamada antes do programa terminar.
Função malloc()
Função realloc()
A função realloc() pode ser utilizada sempre que é necessário modificar a dimensão de
um bloco de memória dinâmica reservado anteriormente. O protótipo da função é
onde old_ptr é o apontador para o bloco de memória reservado anteriormente, enquanto que
total_new_size é a dimensão total que se pretende agora para o mesmo bloco. A função
retorna um apontador para o bloco de memória redimensionado. Note-se que o segundo argumento
tem um significado semelhante ao da função malloc.
Suponha-se, por exemplo, que no inı́cio de um programa tinha sido reservado um bloco de
memória para a n inteiros:
int main(){
int *x,n;
mas que, mais tarde, se verificou a necessidade de acrescentar 1000 posições a este bloco de
memória. Este resultado pode ser obtido fazendo
x = (int*) realloc(x,(n+1000)*sizeof(int));
o conteúdo guardado nos endereços 10000 a 11999 para os endereços 12500 a 14499 e liberta as
posições de memória iniciais.
3.4.3 Garbbage
Ao contrário das variáveis locais, a memória dinâmica é criada e libertada sob controlo do
programa, através das chamadas às funções calloc(), malloc() e free().
void func(){
int *x,y;
y = 3;
x = (int*) calloc(y,sizeof(int));
/* ... Utilização de x, free()
não chamado... */
}
void main()
int a,b;
func();
/* O Bloco reservado em func deixou de ser
usado, mas deixou de estar inacessı́vel */
Figura 3.3: Mapa de memória após a chamada à função calloc() (A) e após a função
realloc() (B), se existir espaço disponı́vel nos endereços contı́guos. Em (C) e (D) representa-
se a situação correspondente no caso em que a memória dinâmica imediatamente a seguir ao bloco
reservado está ocupada, sendo necessário deslocar todo o bloco para outra zona de memória. Neste
caso, o apontador retornado pela função é diferente do inicial.
54 V ECTORES E MEM ÓRIA DIN ÂMICA
Zona da pilha (var. locais) Zona do heap (var. dinamicas) Zona da pilha (var. locais) Zona do heap (var. dinamicas)
Endereço Conteúdo Variável Endereço Conteúdo Variável Endereço Conteúdo Variável Endereço Conteúdo Variável
.. .. .. .. .. .. .. .. .. .. .. ..
. . . . . . . . . . . .
... ... ... ...
1001 2101 1001 2101
1006 b 2106
1007 a 2107
1009 2109
.. .. .. ..
. . . .
C
Figura 3.4: Criação de “garbbage” (lixo). Mapa de memória (exemplo do texto): (A) antes da
chamada à função func, (B) no final da função func e (C) após retorno ao programa princi-
pal. De notar que em (C) a memória dinâmica ficou reservada, mas que se perderam todas as
referências que permitiam o acesso a esta zona de memória.
C RIAÇ ÃO DIN ÂMICA DE MATRIZES 55
3.5.1 Introdução
float x[3][2];
declara uma estrutura bidimensional de dois por três reais, ocupando um total de seis palavras
de memória no modelo de memória que temos vindo a assumir como referência. É frequente
uma estrutura bidimensional ser interpretada como uma matriz, neste exemplo de duas linhas por
três colunas. A inicialização de uma matriz pode ser efectuada durante a execução do programa
ou listando os valores iniciais, sendo apenas necessário fazer um agrupamento hierárquico das
constantes de inicialização de acordo com as dimensões da estrutura:
#define N ...
#define M ...
...
int x[N][M];
por uma estrutura dinâmica com um mecanismo de acesso equivalente, mas em que os limites n e
m são variáveis cujo valor só é conhecido durante a execução do programa.
A solução para este problema é substituir a estrutura x por um vector dinâmico de apontadores
para inteiros, em que cada posição é por sua vez inicializada com um vector dinâmico. Considere-
se, por exemplo, que se pretende criar uma matriz de n por m. O código para este efeito é dado
por
int main{
int k;
int n,m;
int **x;
/* Inicialização de n e m */
x = (int**) calloc(n,sizeof(int*));
if(x == NULL){
printf("Erro na reserva de memória\n");
exit(1);
}
Por exemplo, se se pretender criar uma matriz de 4 por 2 e inicializá-la com um valores inteiros
em que a classe das dezenas corresponde à linha e a classe dos algarismos às unidades, poder-se-ia
fazer
C RIAÇ ÃO DIN ÂMICA 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ória\n");
exit(1);
}
O resultado da execução deste bloco de código seria o que se mostra na figura 3.5.
Este exemplo indica que a solução geral para simular a criação dinâmica de matrizes é declarar
um duplo apontador para o tipo pretendido dos elementos da matriz e, seguidamente, reservar
dinâmicamente um vector de apontadores e criar dinamicamente os vectores correspondentes a
cada linha.
Como é evidente, neste exemplo x é um duplo apontador para inteiro. Deste modo, se pre-
tender usar esta “matriz dinâmica” como argumento de uma função fazer-se, no bloco que chama,
.. .. .. .. .. ..
. . . . . . 2006 0 x[0][0]
2007 1 x[0][1]
1543 2001 x 2001 2006 x[0]
2013 21 x[2][1]
2015 30 x[3][0]
2016 31 x[3][1]
Figura 3.5: Criação dinâmica de matrizes por meio de um vector de apontadores. Os endereços
indicados são apenas ilustrativos.
Esta solução é tão frequente na prática que, por analogia, muitos programadores (mesmo
experientes) de C pensam que, dada a declaração tipo x[N][M], x sem qualquer ı́ndice corre-
sponde a um duplo apontador para tipo. Como se mostrou na secção 2.6.4, esta suposição não
corresponde à realidade: nesta situação, x é um apontador para um vector de elementos de tipo.
Considere-se a declaração
int *x[4];
.. .. .. 2001 1 x[0][0]
. . .
2002 2 x[0][1]
1001 2001 x[0]
2011 6 x[2][1]
2013 7 x[3][0]
2014 8 x[3][1]
e, portanto, x[k][j] é um inteiro. O que conduz, por vezes, ao raciocı́nio errado de que um
vector de apontadores pode ser utilizado indiscriminadamente como se de uma matriz se tratasse.
Ora, de facto, tal só é possı́vel se os elementos de x tiverem sido convenientemente inicializados
de modo a endereçarem a base de um vector de inteiros. Isto, só por si, não é realizado pela
declaração int *x[4], que se limita a declarar um vector de apontadores e a reservar 4 palavras
de memória para este efeito. Nesta declaração, não é reservada memória para qualquer inteiro.
int k,j;
int *x[4];
onde, além da reserva das posições de memória para os vectores inteiros, se procedeu à sua
inicialização. A situação final pode ser representada pelo modelo da figura 3.6, situação B.
Listas dinâmicas
4.1 Introdução
Considere-se, por exemplo, um gestor de uma central telefónica que necessita de reservar
dinamicamente memória para cada chamada estabelecida (onde é armazenada toda a informação
sobre a ligação, como por exemplo os números de origem e destino, tempo de ligação, etc,) e
libertar essa mesma memória quando a chamada é terminada. Dado que o número de ligações
activas varia frequentemente ao longo do dia, uma solução baseada num vector dinâmico exi-
giria o seu redimensionamento frequente. Em particular, sempre que não fosse possı́vel encontrar
memória livre imediatamente a seguir ao fim do vector, seria necessário deslocar todo o vector (v.
função realloc, secção 3.4.2) para uma nova zona de memória, e a cópia exaustiva de todo o
seu conteúdo para a nova posição de memória. Este mecanismo, além de pouco eficiente, poderia
inviabilizar o funcionamento em tempo real do sistema, dado que uma percentagem significativa
do tempo disponı́vel seria dispendido na gestão da memória usada pelo vector.
Quando se pretende armazenar entidades individuais de memória que possam ser criadas e
libertadas individualmente com uma certa frequência, é normalmente mais adequado utilizar es-
truturas de dados criadas dinâmicas e organizadas em listas.
62 L ISTAS DIN ÂMICAS
Neste capı́tulo descrevem-se listas dinâmicas como uma forma de armazenar ou organizar
informação. Esta organização é, obviamente, independente do tipo de informação que se pretende
armazenar. Esta situação é semelhante à da construção de um armário com gavetas: a estrutura
do móvel é independente do conteúdo que se irá arrumar nas gavetas. Esta independência entre
móvel e conteúdo garante a generalidade do móvel, no sentido em que este será adaptável a várias
situações.
Uma forma de atingir este objectivo é, ao desenhar o programa, adoptar uma metodologia
designada por abstracção de dados(Martins, 1989). Esta metodologia baseia-se na definição e
distinção clara dos vários tipos de dados utilizados pelo programa (ou, seguindo o exemplo ante-
rior, distinguir tanto quanto possı́vel o móvel do conteúdo das gavetas). Segundo esta metodologia,
cada tipo de dados deve ter manipulado apenas por um conjunto de funções especı́ficas, designadas
métodos, conhecedoras dos detalhes internos do tipo de dados associado. Todos os outros blocos
do programa têm apenas que conhecer as propriedades abstractas ou genéricas deste tipo. A
manipulação do tipo só são acessı́veis de outros blocos de programa através dos métodos disponi-
bilizados pelo tipo de dados.
Esta metodologia de programação garante que, caso seja necessário alterar os detalhes inter-
nos de um determinado tipo, apenas é necessário alterar os métodos correspondentes a esse tipo.
Dado que todos os blocos de programas onde este tipo de dados é utilizado apenas acedem a ele
através dos métodos disponı́veis, requerendo apenas o conhecimento das suas propriedades gerais
(ou abstractas), é possı́vel minimizar ou eliminar totalmente as alterações necessárias aos out-
ros blocos de programa. Deste modo, a metodologia de abstracção de dados contribui para uma
relativa estanquecidade e independência dos diversos módulos constituintes.
Neste capı́tulo, utilizar-se-á nos diversos exemplos uma metodologia estrita de abstracção de
dados, chamando-se a atenção caso a caso para aspectos especı́ficos da implementação.
L ISTAS 63
dados
(pos 2)
dados
(pos 3)
dados
(pos 4)
NULL
Figura 4.1: Lista dinâmica. Todos os elementos são criados dinamicamente na zona de heap,
sendo suficiente uma variável local de tipo apontador (variável base, nas figura) para poder aceder
a todos os elementos da lista.
4.3 Listas
Uma lista dinâmica não é mais do que uma colecção de estruturas de dados, criadas dinamica-
mente, em que cada elemento dispõe de um apontador para o elemento seguinte (figura 4.1). Cada
elemento da lista é constituı́do por uma zona de armazenamento de dados e de um apontador para
o próximo elemento. Para ser possı́vel identificar o fim da lista, no apontador do último elemento
é colocado o valor NULL.
Para criar uma lista é necessário definir um tipo de dados e criar uma variável local (normal-
mente designada base ou raiz. Admita-se, por exemplo, que se pretendia que cada elemento da
lista guardasse uma string com um nome e um número inteiro. A criação de uma lista de elementos
deste tipo exigiria uma declaração de tipos e de variáveis como se segue:
int main(){
tipoLista *base;
/* ... */
é inválida porque tipoLista só é conhecido no final da declaração, e não pode ser utilizado
antes de completamente especificado.
Uma das vantagens deste tipo de estruturas é que cada um dos seus elementos pode ser reser-
vado e libertado individualmente, sem afectar os restantes elementos (exigindo apenas ajustar um
ou dois apontadores, de forma a garantir a consistência do conjunto). Adicionalmente, a ordem
dos elementos da lista é apenas definida pela organização dos apontadores. Deste modo, se for
necessário introduzir um elemento entre dois já existentes, não é necessário deslocar todos os el-
ementos de uma posição, como sucederia num vector. Basta de facto reservar espaço para um
elemento adicional e deslocar todos os outros elementos de uma posição. Na figura 4.2 apresenta-
se um exemplo em que se ilustra esta independência entre endereços de memória e sequência da
lista, e confere a esta uma flexibilidade superior à de um vector.
dados
(pos 1)
dados
(pos 4)
NULL
dados
(pos 3)
Figura 4.2: Lista dinâmica. A sequência dos elementos da lista é apenas definida pelos apontadores
de cada elemento, independentemente do endereço de memória ocupado.
base
Apesar das vantagens já referidas de uma lista, é necessário ter em atenção que o programa só
dispõe de uma variável para aceder a toda a lista, e que esta tem que ser percorrida elemento a ele-
mento até se atingir a posição pretendida. Num vector, dado que todos as posições são adjacentes,
para aceder a qualquer posição basta saber o endereço do primeiro elemento e o número de ordem
(ı́ndice) do elemento que se pretende aceder para ser possı́vel calcular o endereço do elemento que
se pretende. Ou seja, a flexibilidade acrescida da lista em termos de organização de memória é
conseguida à custa do tempo de acesso aos seus elementos.
Uma operação particularmente simples sobre uma lista, mas ilustrativa do mecanismos de
acesso geralmente adoptados, é a listagem de todos os seus elementos. Considere-se, por exemplo,
que se pretende listar no terminal o conteúdo de todos os números e nomes da lista do exemplo da
secção 4.3. Uma solução para este efeito seria a sequência de código seguinte:
tipoLista *base;
/* Listagem do conteÚdo */
lista(base);
O princı́pio essencial da operação de listagem está incluı́da na função lista e é muito sim-
ples. Inicializa-se um apontador aux para o inı́cio da lista. Seguidamente, enquanto este apon-
tador for diferente de NULL (ou seja, não atingir o fim da lista), o apontador é sucessivamente
avançado para o elemento seguinte.
Note-se como se adoptou neste bloco de código a metodologia de abstracção de dados. Ex-
iste neste exemplo uma separação funcional de tarefas entre as diversas entidades que partici-
pam o programa. A função lista manipula unicamente a estrutura de dados que suporta a
lista, percorrendo todos os seus elementos até ao fim da lista. No entanto, quando é necessário
escrever o conteúdo de cada nó no terminal, esta tarefa é delegada a uma função especı́fica (
escreveDados), responsável pela manipulação e processamento dos detalhes especı́ficos de
variáveis do tipo tipoDados. Neste sentido, tipoDados é, para a lista, um tipo abstracto
genérico, cuja representação interna ela desconhece, e da qual só tem que conhecer uma pro-
priedade muito genérica: uma variável deste tipo pode de alguma forma ser escrita no terminal,
havendo um método competente para o fazer.
Uma forma alternativa da função listar frequentemente utilizada passa pela utilização de um
ciclo for em vez do ciclo while:
4.5.1 Introdução
Uma pilha é frequentemente designada por uma estrutura LIFO, acrónimo derivado da ex-
pressão Last In First Out. A operação de armazenamento na pilha é geralmente designada de
operação de push, enquanto que a leitura é designada de pop.
As pilhas têm uma utilização frequente em informática sempre que se pretende inverter uma
sequência de dados. Embora a relação não seja óbvia, pode indicar-se, por exemplo, que o cálculo
de uma expressão aritmética em que vários operadores têm nı́veis de precedência diferentes é
realizada acumulando numa pilha resultados intermédios.
Considere-se, por exemplo, que se pretende inverter os caracteres da palavra Luı́s. A forma
como uma pilha pode contribuir para este resultado está graficamente sugerido na figura 4.4.
Uma pilha pode ser realizada em C através de uma lista dinâmica ligada. A colocação de
um novo elemento no topo da pilha corresponde a criar um novo elemento para a lista e inseri-lo
junto à base. De forma correspondente, a operação de remoção e leitura corresponde a retirar o
elemento da base, ficando esta a apontar para o elemento seguinte (v. figura 4.5)
L ISTAS : PILHAS 69
L u í s s í u L
s s
í í í í
u u u u u u
L L L L L L L L
Figura 4.4: Inversão de uma sequência de caracteres por meio de uma pilha.
’í’
Figura 4.5: Realização de uma pilha por uma lista ligada. No exemplo, considera-se uma pilha de
caracteres e a inserção do caracter ’ı́’ no topo da pilha. A tracejado indica-se a ligação existente
antes da inserção, a cheio após a inserção. A operação de remoção corresponde à realização
do mecanismo inverso (reposição da ligação a tracejado e libertação do elemento de memória
correspondente ao ’ı́’).
70 L ISTAS DIN ÂMICAS
4.5.2 Declaração
Tal como qualquer lista, a realização de uma pilha implica a declaração de um tipo de suporte
da lista e a utilização de uma variável para a base da pilha. A declaração da pilha pode ser feita,
por exemplo, pela declaração
onde se admite que tipoDados já foi definido e caracteriza o tipo de informação registada em
cada posição da pilha.
tipoPilha *pilha;
4.5.3 Inicialização
A primeira operação necessária para utilizar a pilha é inicializá-la ou criá-la. Uma pilha vazia
deve ter a sua base apontada para NULL. Embora seja possı́vel realizar directamente uma atribuição
à variável pilha, tratando-se de um detalhe interno de manipulação do tipo tipoPilha, esta
inicialização deverá ser delegada numa função especı́fica. Deste modo, para respeitar a metodolo-
gia de abstracção de dados, deve declarar-se uma pequena função
tipoPilha *inicializa(){
return NULL;
}
pilha = inicializa();
L ISTAS : PILHAS 71
4.5.4 Sobreposição
Para a criação da memória dinâmica, é conveniente criar uma função novoNo() que cria
espaço para um novo nó, verificando a simultaneamente a existência de erros na reserva de
memória. Esta função pode ser definida como
if(novo == NULL)
Erro("Erro na reserva de memória");
novo -> dados = x;
novo -> seg = NULL;
return novo;
}
onde Erro() é uma função genérica simples que imprime uma mensagem de erro e termina o
programa (ver secção 4.5.7).
novo = novoNo(x);
novo -> seg = pilha;
return novo;
}
Esta função, após a criação do novo elemento, limita-se a colocar o apontador seg a apon-
tar para o elemento que anteriormente se encontrava no topo, e indicar à base que o novo topo
corresponde ao novo elemento inserido.
72 L ISTAS DIN ÂMICAS
Note-se que esta função aceita como argumento o apontador original para o inı́cio da lista
(topo da pilha) e devolve a nova base alterada.
4.5.5 Remoção
if(pilhaVazia(pilha))
Erro("remoção de uma pilha vazia");
A operação de remoção é precedida de um teste para verificação de pilha vazia, para garantia
de integridade da memória (v. secção 4.5.6). Note-se que após a leitura da pilha, o bloco de
memória utilizado é libertado pela função free().
4.5.6 Teste
4.5.7 Exemplo
Neste caso, tipoDados é realizado por um simples caracter. Para respeitar e exemplificar
o princı́pio de abstracção, iremos escrever os métodos especı́ficos de leitura e escrita deste tipo.
De modo a explorar de forma eficaz a independência dos diversos tipos de dados, os métodos
correspondentes a cada tipo devem ser declarados em ficheiros separados. Deste modo, os métodos
de acesso a tipoDados são agrupados num ficheiro dados.c. Os protótipos correspondentes
são declarados no ficheiro dados.h.
Ficheiro dados.h
/*
* Ficheiro: dados.h
* Autor: Fernando M. Silva
* Data: 7/11/2002
* Conteúdo:
* Ficheiro com declaração de tipos e
* protótipos dos métodos para manipulação
* de um tipoDados, concretizados aqui
* por um tipo caracter simples.
*/
#ifndef _DADOS_H
#define _DADOS_H
#include <stdio.h>
#include <stdlib.h>
Ficheiro dados.c
/*
* Ficheiro: dados.c
* Autor: Fernando M. Silva
* Data: 12/11/2002
* Conteúdo:
* Métodos para manipulação de um
* tipoDados, concretizado por um
* caracter simples.
*/
#include "dados.h"
Ficheiro pilha.h
/*
* Ficheiro: pilha.h
* Autor: Fernando M. Silva
* Data: 7/11/2000
* Conteúdo:
* Ficheiro com declaração de tipos e
* protótipos dos métodos para manipulação
L ISTAS : PILHAS 75
#include "dados.h"
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údo:
* Métodos para manipulação de uma pilha suportada
* numa estrutura dinâmica ligada
*/
#include "pilha.h"
#include "util.h"
tipoPilha *inicializa(){
/*
* Cria uma nova pilha
* Retorna:
* Pilha inicializada
*/
return NULL;
}
*/
tipoPilha *aux;
if(pilhaVazia(pilha))
Erro("remoção de uma pilha vazia");
A função genérica de erro Erro() pode ser declarada num módulo genérico util.c, onde
são agrupadas funções utilitárias genéricas (neste exemplo, Erro() é única). O protótipos corre-
spondente deve ser declarado num módulo util.h.
Ficheiro util.h
/*
* Ficheiro: util.h
* Autor: Fernando M. Silva
* Data: 7/11/2002
* Conteúdo:
* Ficheiro com declaração de funções e
* protótipos genéricos
*/
#ifndef _UTIL_H
#define _UTIL_H
#endif /* _UTIL_H */
Ficheiro util.c
/*
* Ficheiro: util.c
* Autor: Fernando M. Silva
* Data: 1/11/2002
* Conteúdo:
* Funções genéricas
*/
78 L ISTAS DIN ÂMICAS
#include <stdio.h>
#include <stdlib.h>
#include "util.h"
Considere-se, por último, o programa principal. Este limita-se a inicializar a pilha e a efectuar
a leitura de um sequência de dados, acumulando-os sucessivamente na pilha. A leitura é efectuada
até que a função leDados() (método de tipoDados) identifique uma mudança de linha na
entrada. Seguidamente, os dados são sucessivamente removidos (desempilhados) e escritos no
dispositivo de saı́da, até 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údo:
* Programa principal simples para teste
* de uma pilha, usada para inversão 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ência de caracteres:\n");
while(!leDados(&x)){
pilha = sobrepoe(pilha,x);
}
L ISTAS : PILHAS 79
printf("Sequência invertida:\n");
while(!pilhaVazia(pilha)){
pilha = retira(pilha,&x);
escreveDados(x);
}
printf("\n");
exit(0);
}
A compilação automática de todos os módulos pode ser realizada por meio do utilitário make.
A Makefile correspondente poderia ser escrita como
#
# Ficheiro: Makefile
# Autor: Fernando M. Silva
# Data: 7/11/2000
# Conteúdo:
# Makefile para teste de estruturas dinâmicas
#
# A variável CFLAGS especifica as flags usadas
# por omissão nas regras de compilação
#
# A variável SOURCES, que define os ficheiro
# fonte em C, só e usada para permitir a
# evocação do utilitário "makedepend" (pelo
# comando ’make depend’), de modo a actualizar
# automaticamente as dependências dos ficheiros
# .o nos ficheiros .h
#
# A variável 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áveis
#
teste: $(OBJECTS)
gcc -o $@ $(OBJECTS)
#
# A regra ’make depend’ efectua uma actualização da
# makefile, actualizando as listas de dependências dos
80 L ISTAS DIN ÂMICAS
# 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.1 Introdução
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 é a vulgar fila de espera. Cada novo
elemento armazenado é colocado no final da fila, enquanto que cada elemento retirado é obtido do
inı́cio da fila (figura 4.6, A).
Uma fila pode ser realizada por meio de uma lista ligada, relativamente à qual são mantidos
não um, mas dois apontadores: um para a base ou inı́cio, por onde são retirados os elementos, e
outro para o fim ou último elemento da lista, que facilita a inserção de novos elementos na lista.1
4.6.2 Declaração
De forma a que seja possı́vel manipular uma única variável do tipoFila, esta é realizada
por uma estrutura de dados que agrupa os dois apontadores, inı́cio e fim. Deste modo, a declaração
da fila pode ser realizada por
1
De facto, o apontador para a base seria suficiente para realizar a fila; no entanto, cada operação de inserção exigiria
percorrer a totalidade da fila para realizar a inserção no final, método que seria pouco eficiente.
L ISTAS : FILAS 81
A
fim ’L’ ’u’ ’í’ ’s’
inicio
Figura 4.6: Realização de uma fila de caracteres com uma lista ligada. A - Estrutura da fila, B -
Fila em A após a inserção do caracter ’s’ (a tracejado, as ligações eliminadas), C - Fila em B após
a leitura de um caracter (’L’).
4.6.3 Inicialização
Note-se que na chamada à função o argumento deve ser efectuada por referência (passando o
endereço da variável tipoFila) de modo a que a variável seja alterável pela função.
82 L ISTAS DIN ÂMICAS
4.6.4 Inserção
A inserção de novos elementos corresponde à colocaçã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 criação da memória dinâmica, é conveniente criar uma
função auxiliar novoNo().
if(novo == NULL)
Erro("Erro na reserva de memória");
novo -> dados = x;
novo -> seg = NULL;
return novo;
}
novo = novoNo(x);
novo -> dados = x;
if(fila -> fim != NULL){
fila -> fim -> seg = novo;
}
else{
fila -> inicio = novo;
}
fila -> fim = novo;
}
Nesta função é necessário considerar o caso particular em que a fila está vazia e em que,
como tal, o apontador fim está a NULL. Neste caso particular, ambos os apontadores ( inicio
e fim) devem ser colocados ser inicializados com o endereço do novo elemento inserido. No
caso habitual (lista não vazia), basta adicionar o novo elemento ao último da lista e modificar o
apontador fim.
L ISTAS : FILAS 83
4.6.5 Remoção
if(filaVazia(fila))
Erro("Remoção de uma fila vazia");
A função de retira tem também uma estrutura simples. Tal como na função de inserção, é
necessário considerar o caso particular em que a lista tem apenas um elemento, situação em que o
apontador fim deve ser colocado a NULL depois da remoção.
Tal como no caso da pilha, é testada a condição de pilha vazia antes de efectuar a remoção
(ver secção 4.6.6).
4.6.6 Teste
Para manipulação da lista é indispensável dispor de uma função para testar se a fila se encontra
vazia. Para este teste basta verificar qualquer um dos apontadores se encontra a NULL. Este teste
pode ser efectuado pelo código
4.6.7 Exemplo
Para uma maior semelhança com o exemplo da pilha, utiliza-se também neste exemplo o caso
de uma fila de caracteres. Como seria de esperar, a fila não realiza neste caso uma inversão, mas
apenas um armazenamento temporário da informação, permitindo a sua reprodução pela mesma
ordem de entrada.
Tal como no exemplo da pilha, o código foi distribuı́do por três ficheiros: main.c, fila.c
e dados.c, tendo para os dois últimos sido desenvolvido um ficheiro de protótipos associado.
Ficheiro fila.h
/*
* Ficheiro: fila.h
* Autor: Fernando M. Silva
* Data: 7/11/2002
* Conteúdo:
* Ficheiro com declaração de tipos e
* protótipos dos métodos para manipulação
* de uma fila simples de elementos genéricos
* de "tipoDados".
*/
#ifndef _FILA_H
#define _FILA_H
#include "dados.h"
#endif /* _FILA_H */
Ficheiro fila.c
/*
* Ficheiro: fila.c
* Autor: Fernando M. Silva
* Data: 1/11/2002
* Conteúdo:
* Métodos para manipulação de uma fila suportada
* numa estrutura dinâmica ligada
*/
#include "fila.h"
#include "util.h"
tipoLista *aux;
if(filaVazia(fila))
Erro("Remoção de uma fila vazia");
Ficheiro main.c
/*
* Ficheiro: main.c
* Autor: Fernando M. Silva
* Data: 7/11/2000
* Conteúdo:
* 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ência de caracteres:\n");
while(!leDados(&x)){
adiciona(&fila,x);
}
printf("Sequência lida:\n");
while(!filaVazia(&fila)){
retira(&fila,&x);
escreveDados(x);
}
printf("\n");
exit(0);
}
4.7.1 Introdução
Um vector pode ser ordenado segundo uma qualquer relação de ordem utilizando um algo-
ritmo apropriado(Knuth, 1973) (selection sort, bubble-sort, quicksort ou qualquer outro). Embora
estes algoritmos estejam bem estudados e sejam frequentemente necessários, todos eles requerem
um esforço computacional significativo. É possı́vel provar que a quantidade de trabalho necessária
para esta tarefa não é apenas proporcional ao número de elementos a ordenar: a complexidade da
88 L ISTAS DIN ÂMICAS
tarefa aumenta significativamente mais que a dimensão do vector. Para compreender melhor a
quantidade de trabalho exigido, podemos comparar esta situação ao de arrumar alfabeticamente
uma biblioteca a partir de uma situação desorganizada. Se arrumar uma biblioteca com 5,000
livros demorar um dia, arrumar uma biblioteca com 10,000 livros não demorará apenas dois dias,
mas sim três ou quatro, já que cada tı́tulo individual terá que ser alfabeticamente comparado com
um número muito maior de outros livros.
No caso de uma biblioteca, se se pretender evitar a tarefa hercúlea de ordenar todos os livros,
o melhor é mantê-la sempre ordenada. Desta forma, cada novo livro adicionado terá apenas que
ser colocado no local certo, tarefa obviamente muito mais rápida.
As listas dinâmicas oferecem uma forma simples de criar e manter eficientemente uma
colecção de objectos ordenados. Ao contrário do vector, em que todas as posições se encon-
tram em endereços de memória contı́guos, a ordem dos elementos da lista não depende do seu
endereço de memória, mas apenas da ordem definida pela sequência de apontadores, tal como já
se mostrou 4.2.
A declaração e inicialização de uma lista segue os passos já vistos para o caso da pilha. A
declaração da lista pode ser realizada por
tipoDados dados;
struct _tipoLista *seg;
} tipoLista;
onde, tal como nos exemplos anteriores, tipoDados é um tipo abstracto que define a informação
armazenada em cada elemento.
tipoLista *inicializa(){
return NULL;
}
4.7.4 Procura
Para assentar ideias, admita-se temporariamente que se está a lidar com uma lista de inteiros.
Deste modo, presume-se que se definiu previamente
aux = base;
while((aux!=NULL) && aux -> dados != x)
aux = aux -> seg;
return aux;
}
A função anterior, embora funcione, apresenta o inconveniente de não explorar o facto da lista
estar ordenada. Por exemplo, no caso de uma lista ordenada de inteiros onde estejam todos os
valores pares entre 10 e 10000, se se procurar o número 15 será necessário ir até ao fim da lista
para concluir que o número não está presente. Como é óbvio, esta conclusão poderia ter sido tirada
muito mais cedo, logo que fosse atingido o número 16 na lista.
Deste modo, uma versão mais optimizada da função de procura pode ser escrita como
Neste caso, a lista é percorrida enquanto o elemento a procurar for inferior à posição actual
da lista. Quando o ciclo é interrompido, é testado se se se atingiu o fim da lista ou se se encontrou
o elemento procurado.
e verificar a forma como se toma partido do modo como o C avalia expressões lógicas. Repare-se
que o segundo operando da disjunção ( aux -> dados == x) só pode ser avaliado se aux
L ISTAS ORDENADAS 91
for diferente de NULL, já que de outra forma se poderia estar a gerar uma violação de memória
(tentativa de acesso através do endereço 0, o que se encontra fora do controlo do programador).
No entanto, o C garante que realiza a avaliação de expressões lógicas da esquerda para a direita e
que interrompe a sua avaliação assim que for possı́vel determinar univocamente o resultado final.
Neste exemplo, se aux for NULL, o primeiro operando da conjunção lógica ’&&’ é falso, o que
implica que o resultado global da expressão também o é. Assim, não sendo necessário o cálculo
do segundo operando, não há o risco de se produzir a violação de memória decorrente do acesso
através de um apontador NULL.
Refira-se, por último, que num problema prático podem coexistir várias funções de procura,
consoante o que se pretende encontrar. Por exemplo, se os elementos de uma lista são estruturas
que incluem, por exemplo, um número e um nome, podem ser escritas duas funções de procura,
uma para o número e outra para o nome. Claro que se a lista estiver ordenada por números, a
busca pelo número poderá ser optimizada (procura só até encontrar um elemento de número igual
ou superior ao procurado), mas a busca pelo nome terá obviamente que ser exaustiva se o nome
não existir, já que só no final da lista será possı́vel ter a certeza de que determinado nome não faz
parte da lista.
Nas duas funções de procura anteriores admitiu-se que o tipoDados era um inteiro. Esta
hipótese permitiu utilizar os operadores relacionais == e < de uma forma intuitiva.
No caso mais geral em que tipoDados é um tipo abstracto genérico esta comparação
não pode ser realizada directamente pelos operadores relacionais. Para demonstrar este facto,
considere-se que tipoDados corresponde a uma estrutura com um número e um nome de um
aluno. Neste caso, não faz sentido usar o operador relacional < para comparar dois alunos a e
b. De facto, o C desconhece o que se pretende comparar: é o nome dos alunos a e b ou os seus
números?
Para contornar esta dificuldade, seria possı́vel utilizar o operador ’.’ (membro de estrutura)
e aceder directamente ao campo numérico, utilizando o operador < para a comparação, ou aceder
ao campo com o nome e utilizar a função strcmp() de forma adequada. No entanto, qualquer
destas soluções estaria a violar o princı́pio de abstracção de dados, já que os métodos de procura
da lista, para quem tipoDados deveria ser um tipo abstracto genérico, estariam a aceder direc-
tamente a detalhes internos do tipo.
que devolve 1 se a for de algum modo anterior a b e 0 em caso contrário (as funções da lista não
precisam de conhecer os detalhes desta comparação) e um outro
Deste modo, uma solução mais geral da função de procura ordenada deveria ser escrita como
Para realizar uma inserção ordenada é suficiente percorrer a lista até encontrar um elemento
que seja superior ao que se pretende inserir. O novo elemento deverá ser colocado antes deste.
Apesar desta metodologia simples, a inserção ordenada requer algumas considerações suple-
mentares. Um dos pontos a ter em conta é que para posicionar o novo elemento antes do que foi
identificado é necessário alterar o apontador do elemento que se encontra antes deste. Ou seja, ao
L ISTAS ORDENADAS 93
antes depois
5 10
novo 7
Figura 4.7: Inserção entre dois elementos da lista. O apontador antes não aponta para o elemento
anterior da lista, mas sim para o campo apontador seg desse elemento.
percorrer a lista, é necessário manter uma referência não apenas para o elemento da lista que está
a ser comparado (elemento actual) mas também para o seu predecessor (elemento anterior).
De modo a melhor compreender esta operação, é conveniente começar por desenvolver uma
função auxiliar de inserção em que se admite que a posição de inserção já foi determinada e, como
tal, que já existem referências para o apontador do elemento anterior e para o elemento actual
(v. figura 4.7). Designaremos estas variáveis de referência por antes e depois. Neste caso o
código desta função pode ser escrito
*antes = novo;
novo -> seg = depois;
}
Uma vez definida esta função, a inserção ordenada apenas tem que ter em conta alguns casos
particulares, nomeadamente a inserção numa fila vazia ou a inserção antes do inı́cio. Nos outros
94 L ISTAS DIN ÂMICAS
casos, basta percorrer a lista com um apontador act, e manter presente que se deve manter um
apontador ant para o elemento anterior. A função insereOrdenado() pode então ser escrita
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 função recebe como um dos argumentos o valor da base e devolve o valor desta depois
da inserção. A base retornada é idêntica à base inicial, excepto quando a inserção é feita no inı́cio
da lista.
reg
1 4 9
Figura 4.8: Remoção. O apontador reg referencia o campo seg do registo anterior da lista.
Aqui, o primeiro if abrange simultaneamente a inserção numa lista vazia e antes do primeiro
elemento. Seguidamente, o ciclo while percorre a lista, mantendo apenas um apontador (equiv-
alente ao apontador ant do exemploanterior).
4.7.7 Remoção
Tal como a inserção, a remoção pode ser simplificada se for escrita uma função auxiliar
libertaReg que é executada quando já é conhecido o local de remoção. Esta função (v. figura
4.8) pode ser escrita
aux = reg;
reg = reg -> seg;
free(aux);
return reg;
}
Seguidamente, é necessário escrever uma função que procure o o elemento a apagar e chame
a função anterior. Esta função é dada por
if(base != NULL){
if(igual(base -> dados,x)){
base = libertaReg(base);
}
else{
96 L ISTAS DIN ÂMICAS
aux = base;
while((aux -> seg != NULL) &&
(menor(aux -> seg -> dados,x)))
aux = aux -> seg;
if((aux -> seg != NULL) &&
igual(aux -> seg -> dados,x))
aux -> seg = libertaReg(aux -> seg);
}
}
return base;
}
4.7.8 Exemplo
Como exemplo, apresenta-se aqui o código completo de um programa que manipula uma
lista ordenada de racionais. O tipo racional é representado por dois inteiros, que descrevem o seu
numerador e o denominador. O programa aceita uma sequência de racionais. Um racional positivo
é inserido na lista, enquanto que um racional negativo provoca uma tentativa de remoção do seu
simétrico, caso este exista (ou seja, a indicação de − 35 provoca a remoção do racional 53 ).
Omite-se aqui a listagem dos ficheiros util.h e util.c, idênticos ao introduzido no ex-
emplo da pilha (v. secção 4.5.7).
Ficheiro dados.h
/*
* Ficheiro: lista.h
* Autor: Fernando M. Silva
* Data: 7/11/2000
* Conteúdo:
* Definição do tipo genérico
* "tipoDados" usado nos exemplos de
* estruturas de dados dinâmicas.
*
* Para fins de exemplo, "tipoDados"
* é realizado por uma fracção 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;
Ficheiro dados.c
/*
* Ficheiro: dados .c
* Autor: Fernando M. Silva
* Data: 1/12/2000
* Conteúdo:
* Métodos de acesso exemplificativos
* da definição de um tipo abstracto "tipoDados"
*
* Neste exemplo, "tipoDados" implementa um numero
* racional, especificado por um numerador e um
* denominador
*/
#include "dados.h"
*/
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álido\n");
}
* 1 se x < y
* 0 em caso contrário
*/
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;
}
* x - racional
* Retorna
* simétrico 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údo:
* Ficheiro com declaração de tipos e
* protótipos dos métodos para manipulação
* de uma lista dinâmica simples
*
*/
#ifndef _LISTA_H
#define _LISTA_H
#include <stdio.h>
#include <stdlib.h>
/*
* Tipo dos dados da lista
*/
#include "dados.h"
/*
* Definição de tipoLista
*/
typedef struct _tipoLista {
tipoDados dados;
struct _tipoLista *seg;
} tipoLista;
/*
* Protótipos dos métodos de acesso
*/
L ISTAS ORDENADAS 101
/* Inicialização */
tipoLista *inicializa(void);
/*
* Procura x na lista iniciada por base, retornando um apontador
* para o registo que contem este valor (ou NULL se não 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ém 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 ÂMICAS
* Conteúdo:
* Métodos para manipulação
* de uma lista dinâmica
* simples (ordenada)
*
*/
/*
* Inclui ficheiro com tipo e protótipos
*/
#include "lista.h"
#include "util.h"
if(base == NULL){
insere(&base,x,NULL); /* Lista vazia */
104 L ISTAS DIN ÂMICAS
}
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 inserção
*/
ant = act; act = act -> seg;
}
insere(&(ant -> seg),x,act);
}
return base;
}
if(base != NULL){
if(igual(base -> dados,x)){
base = libertaReg(base);
}
else{
aux = base;
while((aux -> seg != NULL) &&
(menor(aux -> seg -> dados,x)))
aux = aux -> seg;
if((aux -> seg != NULL) &&
igual(aux -> seg -> dados,x))
aux -> seg = libertaReg(aux -> seg);
}
}
return base;
}
Ficheiro main.c
/*
* Ficheiro: main.c
* Autor: Fernando M. Silva
* Data: 7/11/2000
* Conteúdo:
* Programa principal simples para teste
* de estruturas dinâmicas ligadas.
*
* Neste teste, os dados armazenados na lista
* são fracções inteiras, implementadas
* por um tipo abstracto "tipoDados".
* Assim:
* 1. A especificação de um racional positivo,
106 L ISTAS DIN ÂMICAS
int main(){
tipoLista *base;
tipoDados x;
base = inicializa();
printf(" Programa para teste de uma lista "
"ordenada de racionais:\n");
printf(" - A introdução de um racional positivo "
"conduz à sua\n"
" inserção ordenada na lista.\n");
printf(" - A introdução de um racional "
"negativo procura o seu\n"
" simétrico na lista e, "
"caso exista, remove-o.\n");
base
1 4 9
Figura 4.9: Lista de inteiros com registo separado para a base. Nesta figura, a lista tem apenas três
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 Introdução
Embora a estrutura fundamental das listas dinâmicas seja no essencial a que se viu anterior-
mente, existem diversas variantes que têm como objectivo simplificar os mecanismos de acesso
ou ajustar a lista a objectivos especı́ficos.
Conforme se viu anteriormente, a manipulação de listas ordenadas exige que a base da lista
seja tratada como um caso particular. Uma forma de evitar estes testes adicionais é manter per-
manentemente um registo mudo no inı́cio da lista (registo separado para a base) que, embora não
seja utilizado de facto, permite simplificar o acesso aos restantes elementos. Adicionalmente, este
tipo de estrutura (v. figura 4.9 tem a vantagem do apontador para a base não ser modificado depois
da criação da lista.
As funções de manipulação da lista são no essencial semelhantes às da lista ordenada simples.
Embora sem listar aqui todas as funções de acesso, referem-se algumas das que são modificadas
pela existência de um registo permanente na base.
A inicialização da lista corresponde neste caso à criação do registo da base. Deste modo, a
108 L ISTAS DIN ÂMICAS
fim 1 5 8 9
inicio
Por outro lado, a função de inserção ordenada pode ser simplificada. Admitindo a utilização
da mesma função insere() já apresentado para a lista simples, a inserção pode ser feita por
onde se pode constatar a ausência do teste particular à base que se realiza na lista ordenada simples.
Um dos inconvenientes das listas simplesmente ligadas é serem unidireccionais. Por outras
palavras, ao aceder a um dado elemento da lista é fácil aceder ao elemento seguinte, mas o acesso
ao elemento anterior não é possı́vel.
Para evitar este inconveniente é possı́vel desenvolver uma lista duplamente ligada, em que
cada elemento dispõe de dois apontadores, um para o elemento seguinte e outro para o anterior.
Adicionalmente, tal como no caso da fila, o acesso à lista é geralmente mantido por dois apon-
tadores, um para o inı́cio e outro para o fim da lista (v. figura 4.10). A declaração de uma lista
duplamente ligada pode ser realizada simplesmente por
base
1 4 4 9
Figura 4.11: Lista em anel de inteiros. O último elemento aponta para o primeiro
base
5 8 9
4.8.4 Aneis
Numa lista simplesmente ligada, o último da elemento da lista é identificado pelo facto do
apontador para o elemento seguinte ser NULL. Uma alternativa é colocar o último elemento da
lista a apontar para a base (v. figura 4.11). A manipulação de uma estrutura deste tipo é muito
semelhante à de uma lista simples, substituindo-se apenas parte das comparações com o valor
NULL por comparações com o apontador da base.
Uma lista em anel apresenta a vantagem do último elemento apontar para o primeiro, o que
pode ser conveniente em aplicações em que seja necessário percorrer os elementos da lista de uma
forma cı́clica.
4.9.1 Introdução
Um anel duplo com registo separado para a base é uma estrutura dinâmica que combina as
diversas variantes abordadas anteriormente: registo separado para a base, ligação em anel e reg-
istos com apontadores bidireccionais (figura 4.12). Embora este tipo estrutura possa sugerir uma
manipulação à partida mais complexa, verifica-se na prática o inverso: a exploração correcta desta
estrutura origina, de modo geral, código mais simples.
110 L ISTAS DIN ÂMICAS
base
4.9.2 Declaração
4.9.3 Inicialização
tipoAnel *inicializa(){
tipoAnel *aux;
tipoDados regMudo;
aux = novoNo(regMudo);
aux -> seg = aux -> ant = aux;
return aux;
}
if(novo == NULL)
Erro("Erro na reserva de memória");
novo -> dados = x;
A NEL DUPLO COM REGISTO SEPARADO PARA A BASE 111
4.9.4 Listagem
A listagem de um anel pode ser feita por ordem directa ou inversa. Deste modo, podem ser
desenvolvidas duas funções para este efeito, de estrutura muito semelhante:
/* Listagem directa */
void listar(tipoAnel *base ){
tipoAnel *aux;
Repare-se que em qualquer das duas funções o inı́cio da listagem se efectua no elemento a seguir
à base (de modo a saltar o registo da base), enquanto que na condição de manutenção no ciclo se
testa o regresso à base.
4.9.5 Procura
Repare-se que, para simplificar a condição do ciclo, se iniciou o registo da base com o próprio
valor do elemento a procurar. Este artifı́cio garante que, mesmo que o elemento de procura não
se encontre no anel, a procura é interrompida quando se atinge a base. Claro que, deste modo,
é preciso testar à saı́da do ciclo se a interrupção se verificou por ter encontrado o local real de
interrupção e o elemento de procura, ou por se ter encontrado o elemento artificialmente colocado
na base. Neste último caso deverá retornar-se o apontador NULL.
4.9.6 Inserção
No caso de inserção ordenada no anel, é particularmente útil utilizar uma função só para a
inserção do registo, admitindo que já se conhece o local de inserção, e desenvolver posteriormente
a função de procura o local de inserção.
depois
1 8
Figura 4.14: Inserção num anel. Os números entre () correspondem às atribuições da listagem
apresentada no texto.
Repare-se que, dado que a lista é dupla, é suficiente o apontador para o elemento seguinte para
estabelecer todas as ligações.
insere(act,x);
}
4.9.7 Remoção
A operação de remoção pode ser realizada por duas funções complementares. A primeira
(removeReg()) é utilizada após a identificação do registo a eliminar e é responsável pela
libertação da memória ocupada e reconstrução das ligações. A segunda (apaga()) que corre-
sponde à função a utilizar externamente, procura o registo a eliminar e, caso o identifique, chama
114 L ISTAS DIN ÂMICAS
a primeira.
4.9.8 Exemplo
Utiliza-se neste caso o mesmo exemplo de listagem de racionais já apresentado anteriormente.
Neste caso, omite-se a listagem dos métodos do tipo tipoDados, e os ficheiros util.c e
util.h, obviamente idêntico ao anterior.
Ficheiro anel.h
/*
* Ficheiro: anel.h
* Autor: Fernando M. Silva
* Data: 7/11/2000
* Conteúdo:
* Ficheiro com declaração de tipos e
* protótipos dos métodos para manipulação
* de um anel com registo seoparado
* para a base
*
*/
#ifndef _ANEL_H
#define _ANEL_H
#include <stdio.h>
A NEL DUPLO COM REGISTO SEPARADO PARA A BASE 115
#include <stdlib.h>
/*
* Tipo dos dados da lista
*/
#include "dados.h"
/*
* Definição de tipoAnel
*/
typedef struct _tipoAnel {
tipoDados dados;
struct _tipoAnel *seg,*ant;
} tipoAnel;
/*
* Protótipos dos métodos de acesso
*/
/* Inicialização */
tipoAnel *inicializa(void);
/*
* Procura x na lista iniciada por base, retornando um apontador
* para o registo que contem este valor (ou NULL se não existe)
*/
tipoAnel *procura(tipoAnel *base,tipoDados x);
tipoAnel *procuraOrdenado(tipoAnel *base,tipoDados x);
/*
* Insere x antes do registo "depois"
*/
void insere(tipoAnel *depois,tipoDados x);
/*
* Insere x na lista ordenada iniciada por base
*/
void insereOrdenado(tipoAnel *base,tipoDados x);
/*
* Lista todos os elementos da estrutura.
*/
void listar(tipoAnel *base);
void listarInv(tipoAnel *base);
/*
* Remove da lista o elemento apontado por reg
116 L ISTAS DIN ÂMICAS
*/
void libertaReg(tipoAnel *reg);
/*
* Apaga da lista o registo que contém x
*/
void apaga(tipoAnel *base,tipoDados x);
#endif
Ficheiro anel.c
/*
* Ficheiro: anel.c
* Autor: Fernando M. Silva
* Data: 7/11/2000
* Conteúdo:
* Métodos para manipulação
* de um anel com registo separado
* para a base.
*
*/
/*
* Inclui ficheiro com tipo e protótipos
*/
#include "anel.h"
#include "util.h"
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ão 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;
/*
* Idêntico 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;
/*
* Insere x antes do registo "depois"
*/
void insere(tipoAnel *depois,tipoDados x){
tipoAnel *novo = novoNo(x);
118 L ISTAS DIN ÂMICAS
/*
* Insere x no anel ordenada iniciada por base
*/
void insereOrdenado(tipoAnel *base,tipoDados x){
tipoAnel *act;
/*
* Lista todos os elementos da estrutura.
*/
void listar(tipoAnel *base ){
tipoAnel *aux;
aux = base -> seg;
while(aux != base){
printf(" -> ");
escreveDados(aux -> dados);
printf("\n");
aux = aux -> seg;
}
}
/*
* Lista todos os elementos da estrutura
* por ordem inversa.
*/
void listarInv(tipoAnel *base ){
tipoAnel *aux;
aux = base -> ant;
while(aux != base){
printf(" -> ");
escreveDados(aux -> dados);
A NEL DUPLO COM REGISTO SEPARADO PARA A BASE 119
printf("\n");
aux = aux -> ant;
}
}
/*
* Remove do anel o elemento apontado por reg
* e liberta a memória associada.
*/
void libertaReg(tipoAnel *reg){
reg -> seg -> ant = reg -> ant;
reg -> ant -> seg = reg -> seg;
free(reg);
}
/*
* Apaga do anel o registo que contém x
* Retorna a base, eventualmente alterada
*/
void apaga(tipoAnel *base,tipoDados x){
tipoAnel *aux;
Ficheiro main.c
/*
* Ficheiro: main.c
* Autor: Fernando M. Silva
* Data: 7/11/2000
* Conteúdo:
* Programa principal simples para teste
* de estruturas dinâmicas ligadas.
*
* Neste teste, os dados armazenados na lista
* são fracções inteiras, implementadas
* por um tipo abstracto "tipoDados".
* Assim:
* 1. A especificação de um racional positivo,
120 L ISTAS DIN ÂMICAS
int main(){
tipoAnel *base;
tipoDados x;
base = inicializa();
printf(" Programa para teste de uma lista ordenada de racionais:\n");
printf(" - A introdução de um racional positivo conduz à sua\n"
" inserção ordenada na lista.\n");
printf(" - A introdução de um racional negativo procura o seu\n"
" simétrico na lista e, caso exista, remove-o.\n");
base
Uma lista de listas (ou um anel de anéis) nada tem de particular do ponto de vista de
programação. Cada lista por si só ú uma estrutura do tipo apontado anteriormente. No entanto,
é necessário ter em atenção que a lista e as suas sublistas têm tipos diferentes e, de acordo com
o modelo que temos vindo a desenvolver, cada uma delas precisará de um método especı́fico de
acesso. Por outras palavras, será necessário manter duas funções de listagem, duas funções de
inserção, etc, dado que cada tipo de lista exige um método especı́fico2 .
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 tipoListaDeLis-
tas, que suporta a lista principal.
A manipulação da lista faz-se pela combinação das funções (métodos) 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 é necessário inserir uma variável
x de tipoDados na sublista suportada no elemento da lista principal que tem o valor y. O código
para este efeito seria:
tipoListaDeListas *base,*aux;
tipoDadosSubLista x;
tipoDadosLista y;
L ISTAS DE LISTAS 123
/*
inicialização da lista e dos valores x e y
...
*/
aux = procuraOrdenadoListaDeListas(base,y);
if(aux == NULL)
fprintf(stderr,"Erro: valor não encontrado na lista principal");
else
insereDados(aux -> dados,x);
/*
...
*/
Conclusões
Martins, J. P. (1989). Introduction to Computer Science Using Pascal. Wadsworth Publishing Co.,
Belmont, California.
Ritchie, D. e Thmompson, K. (1974). The unix time sharing system. Commun ACM, páginas
365–375.
Apêndice A
Programa de teste que valida a consistência das atribuições da tabela 2.1, apresentada na
secção 2.6.4. Note-se que o facto de o programa compilar sem erros garante a consistência dos
tipos nas atribuições realizadas.
Nos processadores da famı́lia Intel, cada endereço de memória especifica apenas um byte,
apesar de, nos processadores 486 e seguintes, cada operação de acesso à memória se realizar em
palavras de 4 bytes (32 bits). De forma a obter-se valores equivalentes ao do modelo de memória
usado nos exemplos, realiza-se uma divisão por quatro e ajusta-se o endereço 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}};
int main(){
float (*pv3)[2];
float *pf;
float f;
pf = *(x+1); escreveEndereco((int)pf);
pf = *(x+2)+1; escreveEndereco((int)pf);
f = *(*(x+2)+1); printf("%4.1f\n",f);
pf = *x; escreveEndereco((int)pf);
f = **x; printf("%4.1f\n",f);
f = *(*x+1); printf("%4.1f\n",f);
pf = x[1]; escreveEndereco((int)pf);
return 0;
}