1. Disciplina: Programação de Sistemas
Informáticos
Ano Letivo 2011/2012
Aida Meira
Curso Profissional de Técnico de Gestão e
Programação de Sistemas Informáticos
Módulo V
Estrutura de Dados Composta
2. Estruturas de Dados Compostas
M"
2 Professora Aida Meira
INDICE
1. Apontadores..................................................................................................3
A memória ........................................................................................................3
Apontadores .....................................................................................................5
Declaração .....................................................................................................10
Operadores ....................................................................................................11
Expressões.....................................................................................................12
Comparação...................................................................................................13
Ponteiros e vetores ........................................................................................13
Ponteiros como parametros de funções.........................................................16
Ponteiros para funções ..................................................................................20
2. Tipos Estruturados......................................................................................23
O tipo estrutura...............................................................................................23
Ponteiro para estruturas.................................................................................25
Passagem de estruturas para funções...........................................................26
Definição de “novos” tipos..............................................................................28
Vetores de estruturas .....................................................................................30
Vetores de ponteiros para estruturas.............................................................32
3. Estruturas de Dados Compostas
M"
3 Professora Aida Meira
1. Apontadores
A MEMÓRIA
A memória de um computador encontra-se organizada em células ou palavras
de memória individuais. Cada palavra de memória é referenciada por um
endereço e armazena um dado conteúdo. Designa-se por “número de bits da
arquitetura” o número máximo de bits que um processador ´e capaz de ler num
único acesso à memória. Cada palavra de memória de referência de um
processador tem em geral um número de bits idêntico ao número de bits da
arquitetura.
DOUBLE
FLOAT
i= 2450
k=113
j=11331
F=225.345 22.5E+14
5
INT
4. Estruturas de Dados Compostas
M"
4 Professora Aida Meira
Admite-se aqui que a palavra de memória e o tipo inteiro são representados por
32 bits.
De acordo com a figura, durante a compilação foram atribuídos as variáveis i, j,
k, f os endereços 1002,1005, 1006 e 1004, respetivamente, enquanto a
variável d foi atribuído o endereço 1007.
Note-se que, com exceção do tipo double, cada variável tem uma
representação interna de 32 bits, ocupando por isso apenas um endereço de
memória. A variável d, de tipo double, tem uma representação interna de 64
bits, exigindo por isso duas palavras de memória: os endereços 1007 e 1008.
Na figura encontram-se representados outras posições de memória,
provavelmente ocupadas por outras variáveis, e cujo conteúdo é desconhecido
do programador.
5. Estruturas de Dados Compostas
M"
5 Professora Aida Meira
APONTADORES
Um ponteiro é uma variável que contém um endereço
de memória, normalmente referenciando a posição de
uma outra variável.
(Schildt-1996)
Um apontador é uma variável cujo conteúdo é um endereço de outra posição
de memória. A declaração de variáveis do tipo apontador pode ser construída a
partir de qualquer tipo definido anteriormente, e deve especificar o tipo de
variável referenciada pelo apontador.
A declaração de uma variável do tipo apontador é feita colocando um * antes
do nome da variável. Assim, na declaração:
As variáveis i, j, k, m são do tipo int, f é do tipo float e d, d2 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 pd e pd2 são do tipo apontador para
double.
7. Estruturas de Dados Compostas
M"
7 Professora Aida Meira
Exemplo: Função Troca
Função para trocar o valor de duas variáveis inteiras:
– Void troca (int a, int b)
Essa função tem o código:
int aux;
aux = a;
a = b;
b = aux;
• Vamos agora chamar a função troca no main, onde a=5 e b=10.
• Qual é o valor de cada variável depois de chamares a função?
a = 10 e b=5 , Não! O valor fica igual.
O que a função recebeu, na realidade, não foi a variável A nem a variável B.
Ela recebeu os seus valores.
Portanto, dentro da função, realmente houve uma troca mas as variáveis locais
de cada função "morrem" quando a função termina.
Sendo assim, de volta no main, não houve alteração de valores.
Em vez de receberes o valor de cada variável, recebes o local onde esse valor
está armazenado!
Assim podes alterar directamente o seu valor e assim, se mudas o valor no sitio
onde ele está guardado, esse valor é alterado tanto dentro das funções como
fora.
Portanto mudas o cabeçalho da função para:
void swap( int * a, int * b);
8. Estruturas de Dados Compostas
M"
8 Professora Aida Meira
e o código para:
int aux;
aux = *a;
*a = *b;
*b = aux;
Assim sendo, estás mesmo a alterar a "raiz" da variável e assim, no main, a
alteração mantém-se.
Tal como pode ser observado, a solução adotada 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
copia destes valores), o endereço permite à função o conhecimento da
9. Estruturas de Dados Compostas
M"
9 Professora Aida Meira
posição das variáveis a e b em memoria e, deste modo, permite a
manipulação do seu conteúdo por meio de um endereçamento indireto.
Vamos observar o mapa de memória das diferentes fases que passa no
programa anterior:
Imagem 1 - Antes da chamada da função Imagem 2 - No inicio da troca
10. Imagem 3 - No final da troca Imagem 4 - Após o regresso ao main
DECLARAÇÃO
A declaração de uma variável do tipo ponteiro (ou apontador) consiste do tipo
base (aquele para o qual o ponteiro vai apontar), um * e o nome da variável.
A forma geral é:
tipo *nome;
ou
tipo* nome;
Exemplos:
int *contador; ponteiro para um inteiro
char *meuString; ponteiro para caracteres
float *raizQuadrada; ponteiro para real.
11. Estruturas de Dados Compostas
M"
11 Professora Aida Meira
Caso especial:
void *simplesPonteiro; ponteiro genérico.
OPERADORES
Existem dois operadores especiais para ponteiros:
* indireção.
– Devolve o valor apontado pelo ponteiro.
& operador de endereço.
– Devolve o endereço na memória de seu operando.
Exemplo:
main ()
{
int *aponta;
int valor1, valor2;
valor1 = 5; // inicializa valor1 com 5
aponta = &valor1; // aponta recebe o endereço de valor1, ou seja:
passa a apontar para valor1
valor2 = *aponta; // valor2 recebe o valor apontado por aponta,
nesse caso 5, pois aponta possui como valor
o endereço de valor1
}
12. Estruturas de Dados Compostas
M"
12 Professora Aida Meira
Precedência: Tanto o & como o * possuem precedência maior do que todos os
outros operadores, com exceção do menos unário, que possuem a mesma.
– int valor; int *aponta; valor = *aponta++
EXPRESSÕES
ATRIBUIÇÃO
Atribuição direta entre ponteiros passa o endereço de memória apontado por
um para o outro.
int *p1, *p2, x;
x = 4;
p1 = &x; /* p1 passa a apontar para x */
p2 = p1; /* p2 recebeu o valor de p1, que é */
/* o endereço de x, ou seja: p2 */
/* também aponta para x. */
printf ("%p", p2 ); /* imprime o endereço de x */
printf ("%i", *p2 ); /* imprime o valor apontado por */
/* p2, seja: o valor de x. */
O operador de endereço &, quando usado como operador sobre um ponteiro,
devolve o endereço ocupado por este ponteiro, não o endereço apontado por
ele.
Duas operações aritméticas são válidas com ponteiros: adição e subtração.
Estas são muito úteis com vetores.
A expressão abaixo é válida em "C":
int *p1, *p2, *p3, *p4, x=0;
p1 = &x;
p2 = p1++;
p3 = p2 + 4;
p4 = p3 - 5; /* p4 acaba tendo o mesmo valor que p1 */
/* no começo. */
13. Estruturas de Dados Compostas
M"
13 Professora Aida Meira
/* Note que p1 foi incrementado e */
/* agora tem o valor (&x + 1). */
Observe que aqui as expressões *p2 e *p3 vão resultar em um erro, já que
esses ponteiros estarão apontando para áreas de memória que não estão
associadas com nenhuma variável. O único endereço de memória acessável é
o de x.
COMPARAÇÃO
Podes comparar ponteiros, para saber se um ponteiro aponta para um
endereço de memória mais alto do que outro.
Exemplo:
int *p, *q;
....
if (p < q)
printf("p aponta para um endereço menor que o de q");
É um trecho de programa perfeitamente válido em "C".
– Isto pode ser útil em testes em matrizes e vetores.
PONTEIROS E VETORES
Ponteiros, Vetores e Matrizes possuem uma relação muito estreita em "C”
A qual podemos aproveitar de muitas formas para escrever programas que
ninguém entende...
Em C, os elementos de um vetor são sempre guardados sequencialmente, a
uma distância fixa um do outro. Com isso, é possível facilmente passar de um
elemento a outro, percorrendo sempre uma mesma distância para frente ou
14. Estruturas de Dados Compostas
M"
14 Professora Aida Meira
para trás na memória. Dessa maneira, podemos usar ponteiros e a aritmética
de ponteiros para percorrer vetores. Na verdade, vetores são ponteiros ― um
uso particular dos ponteiros.
Exemplo:
char nome[30] = "José da Silva";
char *p1, *p2;
char car;
int i;
p1 = nome; // nome sozinho é um ponteiro
// para o 1º elemento de nome[].
car = nome[3]; // Atribui 'é' a car.
car = p1[0]; // Atribui 'J' a car. Válido.
p2 = &nome[5]; // Atribui a p2 o endereço da 6ª
// posição de nome, no caso 'd'.
printf( "%s", p2); // Imprime "da Silva"...
p2 = p1; // Evidentemente válido.
p2 = p1 + 5; // Equivalente a p2 = &nome[5]
printf( "%s",(p1 + 5)); // Imprime "da Silva"...
printf( "%s",(p1 + 20)); // Cuidado: Imprime lixo!!
for (i=0; strlen(nome)- 1; i++)
{
printf ("%c", nome[i]); // Imprime 'J','o','s',etc
p2 = p1 + i;
printf ("%c", *p2); // Imprime 'J','o','s',etc
}
Acompanhe o exemplo a seguir.
15. Estruturas de Dados Compostas
M"
15 Professora Aida Meira
Começamos declarando um vetor com três elementos; depois, criamos um
ponteiro para esse vetor. Mas repare que não colocamos o operador de
endereço em vetorTeste; fazemos isso porque um vetor já representa um
endereço, como você pode verificar pelo resultado da primeira chamada a
printf().
Podemos usar a sintaxe *(ptr + 1) para aceder o inteiro seguinte ao
apontado pelo ponteiro ptr. Mas, se o ponteiro aponta para o vetor, o próximo
inteiro na memória será o próximo elemento do vetor!
De fato, em C as duas formas *(ptr + n) e ptr[n] são equivalentes.
Não é necessário criar um ponteiro para usar essa sintaxe; o vetor em si já é
um ponteiro, de modo que qualquer operação com ptr será feita igualmente
com vetorTeste. Todas as formas abaixo de aceder o segundo elemento do
vetor são equivalentes:
vetorTeste[1];
*(vetorTeste + 1);
ptr[1];
*(ptr + 1)
16. Estruturas de Dados Compostas
M"
16 Professora Aida Meira
Exemplo:
PONTEIROS COMO PARAMETROS DE FUNÇÕES
Comecemos por uma situação-problema: eu tenho 2 variáveis e quero trocar o
valor delas. Vamos começar com um algoritmo simples, dentro da função
main():
17. Estruturas de Dados Compostas
M"
17 Professora Aida Meira
Esse exemplo funcionará exatamente como esperado: primeiramente ele
imprimirá "5 10" e depois ele imprimirá "10 5". Mas e se quisermos trocar várias
vezes o valor de duas variáveis? É muito mais conveniente criar uma função
que faça isso. Vamos fazer uma tentativa de implementação da função swap
(troca, em inglês):
18. Estruturas de Dados Compostas
M"
18 Professora Aida Meira
No entanto, o que queremos não irá acontecer.
Verá que o programa imprime duas vezes "5 10". Por que isso acontece?
Lembre-se do escopo das variáveis: as variáveis a e b são locais à função
main(), e quando as passamos como argumentos para swap(), os valores são
copiados e passam a ser chamados de i e j; a troca ocorre entre i e j, de modo
que quando voltamos à função main() nada mudou.
Então como poderíamos fazer isso? Como são retornados dois valores, não
podemos usar o valor de retorno de uma função. Mas existe uma alternativa: os
ponteiros.
19. Estruturas de Dados Compostas
M"
19 Professora Aida Meira
Neste exemplo, definimos a função swap() como uma função que toma como
argumentos dois ponteiros para inteiros; a função faz a troca entre os valores
apontados pelos ponteiros. Já na função main(), passamos os endereços das
variáveis para a função swap(), de modo que a função swap() possa modificar
variáveis locais de outra função. O único possível inconveniente é que, quando
usarmos a função, teremos de lembrar de colocar um & na frente das variáveis
que estivermos passando para a função.
Se pensar bem, já vimos uma função em que passamos os argumentos
precedidos de &: é a função scanf()!
Por que fazemos isso?
É simples: chamamos a função scanf() para que ela ponha nas nossas
variáveis valores digitados pelo usuário.
Essas variáveis são locais, e portanto só podem ser alteradas por outras
funções através de ponteiros!
Quando uma função recebe como parâmetros os endereços e não os valores
das variáveis, dizemos que estamos a fazer uma chamada por referência; é o
caso desse último exemplo. Quando passamos diretamente os valores das
20. Estruturas de Dados Compostas
M"
20 Professora Aida Meira
variáveis para uma função, dizemos que é uma chamada por valor; foi o caso
do segundo exemplo.
PONTEIROS PARA FUNÇÕES
Os ponteiros para funções servem, geralmente, para passar uma função como
argumento de uma outra função.
Neste exemplo:
Veja que criamos uma função que retorna a soma dos dois inteiros a ela
fornecidos; no entanto, ela não é chamada diretamente.
Ela é chamada pela função operação, através de um ponteiro. A função main
passa a função soma como argumento para operação, e a função operação
chama essa função que lhe foi dada como argumento.
21. Estruturas de Dados Compostas
M"
21 Professora Aida Meira
Note bem o terceiro argumento da função operação: ele é um ponteiro para
uma função.
Neste caso, ele foi declarado como um ponteiro para uma função que toma
dois inteiros como argumentos e retorna outro inteiro.
O * indica que estamos declarando um ponteiro, e não uma função. Os
parênteses em torno de *func são essenciais, pois sem eles o compilador
entenderia o argumento como uma função que retorna um ponteiro para um
inteiro.
A forma geral para declarar um ponteiro para uma função é:
tipo_retorno (*nome_do_ponteiro)(lista de argumentos)
Para chamar a função apontada pelo ponteiro, há duas sintaxes. A sintaxe
original é:
(*nome_do_ponteiro)(argumentos);
Se ptr é um ponteiro para uma função, faz bastante sentido que a função em si
seja chamada por *ptr. No entanto, a sintaxe mais moderna permite que
ponteiros para funções sejam chamados exatamente da mesma maneira que
funções:
nome_do_ponteiro(argumentos);
Por fim, para inicializar um ponteiro para função, não precisamos usar o
operador de endereço (ele já está implícito). Por isso, quando chamamos a
função operação, não precisamos escrever &soma.
Veja mais um exemplo — na verdade, uma extensão do exemplo
anterior:
22. Estruturas de Dados Compostas
M"
22 Professora Aida Meira
Aqui, criamos mais uma função, subtracao, além de criar um outro ponteiro
para ela (uma espécie de "atalho"), menos. Na função main, referimo-nos à
função de subtração através desse atalho.
Veja também que aqui usamos a sintaxe moderna para a chamada de
ponteiros de funções, ao contrário do exemplo anterior.
23. Estruturas de Dados Compostas
M"
23 Professora Aida Meira
2. Tipos Estruturados
Na linguagem C, existem os tipos básicos (char, int, float, etc.) e seus
respetivos ponteiros que podem ser usados na declaração de variáveis. Para
estruturar dados complexos, nos quais as informações são compostas por
diversos campos, necessitamos de mecanismos que nos permitam agrupar
tipos distintos.
O TIPO ESTRUTURA
Em C, podemos definir um tipo de dado cujos campos são compostos de vários
valores de tipos mais simples. Para ilustrar, vamos considerar o
desenvolvimento de programas que manipulam pontos no plano cartesiano.
Cada ponto pode ser representado por suas coordenadas x e y, ambas dadas
por valores reais.
Sem um mecanismo para agrupar as duas componentes, teríamos que
representar cada ponto por duas variáveis independentes.
float x;
float y;
No entanto, deste modo, os dois valores ficam dissociados e, no caso do
programa manipular vários pontos, cabe ao programador não misturar a
coordenada x de um ponto com a coordenada y de outro.
Para facilitar este trabalho, a linguagem C oferece recursos para agruparmos
dados. Uma estrutura, em C, serve basicamente para agrupar diversas
variáveis dentro de um único contexto.
No nosso exemplo, podemos definir uma estrutura ponto que contenha as duas
variáveis.
A sintaxe para a definição de uma estrutura é mostrada abaixo:
24. Estruturas de Dados Compostas
M"
24 Professora Aida Meira
Desta forma, a estrutura ponto passa a ser um tipo e podemos então declarar
variáveis deste tipo.
struct ponto p;
Esta linha de código declara p como sendo uma variável do tipo struct ponto.
Os elementos de uma estrutura podem ser acedidos através do operador de
acesso “ponto” (.). Assim, é válido escrever:
Manipulamos os elementos de uma estrutura da mesma forma que variáveis
simples.
Podemos aceder seus valores, atribuir-lhes novos valores, aceder seus
endereços, etc.
Exemplo: Capturar e imprimir as coordenadas de um ponto.
Para exemplificar o uso de estruturas em programas, vamos considerar um
exemplo simples em que capturamos e imprimimos as coordenadas de um
ponto qualquer.
25. Estruturas de Dados Compostas
M"
25 Professora Aida Meira
A variável p, definida dentro de main, é uma variável local como outra qualquer.
Quando a declaração é encontrada, aloca-se, na pilha de execução, um
espaço para seu armazenamento, isto é, um espaço suficiente para armazenar
todos os campos da estrutura (no caso, dois números reais).
Atenção que o acesso ao endereço de um campo da estrutura é feito da
mesma forma que com variáveis simples: basta escrever &(p.x), ou
simplesmente &p.x, pois o operador de acesso ao campo da estrutura tem
precedência sobre o operador “endereço de”.
PONTEIRO PARA ESTRUTURAS
Da mesma forma que podemos declarar variáveis do tipo estrutura:
struct ponto p;
Podemos também declarar variáveis do tipo ponteiro para estrutura:
struct ponto *pp;
Se a variável pp armazenar o endereço de uma estrutura, podemos aceder os
campos dessa estrutura indiretamente, através de seu ponteiro:
(*pp).x = 12.0;
Neste caso, os parênteses são indispensáveis, pois o operador “conteúdo de”
tem precedência menor que o operador de acesso. O acesso de campos de
estruturas é tão comum em programas C que a linguagem oferece outro
26. Estruturas de Dados Compostas
M"
26 Professora Aida Meira
operador de acesso, que permite aceder campos a partir do ponteiro da
estrutura. Este operador é composto por um traço seguido de um sinal de
maior, formando uma seta (->).
Portanto, podemos reescrever a atribuição anterior fazendo:
pp->x = 12.0;
Em resumo, se temos uma variável estrutura e queremos aceder seus campos,
usamos o operador de acesso ponto (p.x); se temos uma variável ponteiro
para estrutura, usamos o operador de acesso seta (pp->x).
Seguindo o raciocínio, se temos o ponteiro e queremos aceder o endereço de
um campo, fazemos &pp->x
PASSAGEM DE ESTRUTURAS PARA FUNÇÕES
Para exemplificar a passagem de variáveis do tipo estrutura para funções,
podemos reescrever o programa simples, mostrado anteriormente, que captura
e imprime as coordenadas de um ponto qualquer. Inicialmente, podemos
pensar em escrever uma função que imprima as coordenadas do ponto. Esta
função poderia ser dada por:
void imprime (struct ponto p)
{
printf("O ponto fornecido foi: (%.2f,%.2f)n", p.x,
p.y);
}
A passagem de estruturas para funções processa-se de forma análoga à
passagem de variáveis simples, porém exige uma análise mais detalhada. Da
forma como está escrita no código acima, a função recebe uma estrutura inteira
como parâmetro.
Portanto, faz-se uma cópia de toda a estrutura e a função acede aos dados
desta cópia.
27. Estruturas de Dados Compostas
M"
27 Professora Aida Meira
Existem dois pontos a serem ressaltados. Primeiro, como em toda passagem
por valor, a função não tem como alterar os valores dos elementos da estrutura
original (na função imprime isso realmente não é necessário, mas seria numa
função de leitura).
O segundo ponto diz respeito à eficiência, visto que copiar uma estrutura
inteira pode ser uma operação custosa (principalmente se a estrutura for muito
grande).
É mais conveniente passar apenas o ponteiro da estrutura, mesmo que não
seja necessário alterar os valores dos elementos dentro da função.
Desta forma, uma segunda (e mais adequada) alternativa para escrevermos a
função imprime é:
void imprime (struct ponto* pp)
{
printf("O ponto fornecido foi: (%.2f,%.2f)n", pp->x,
pp->y);
}
Podemos ainda pensar numa função para ler a hora do evento. Observamos
que, neste caso, obrigatoriamente devemos passar o ponteiro da estrutura,
caso contrário não seria possível passar ao programa principal os dados lidos:
void captura (struct ponto* pp) {
printf("Digite as coordenadas do ponto(x y): ");
scanf("%f %f", &p->x, &p->y);
}
Com estas funções, nossa função main ficaria como mostrado abaixo.
int main (void) {
struct ponto p;
captura(&p);
imprime(&p);
28. Estruturas de Dados Compostas
M"
28 Professora Aida Meira
return 0;
}
Exercício:
Função para determinar a distância entre dois pontos.
Considere a implementação de uma função que tenha como valor de retorno a
distância entre dois pontos. O protótipo da função pode ser dado por:
DEFINIÇÃO DE “NOVOS” TIPOS
A linguagem C permite criar nomes de tipos. Por exemplo, se escrevermos:
typedef float Real;
Podemos usar o nome Real como um mnemônico para o tipo float. O uso de
typedef é muito útil para abreviarmos nomes de tipos e para tratarmos tipos
complexos. Alguns exemplos válidos de typedef:
typedef unsigned char UChar;
typedef int* PInt;
typedef float Vetor[4];
Neste fragmento de código, definimos UChar como sendo o tipo char sem
sinal, PInt como um tipo ponteiro para int, e Vetor como um tipo que
representa um vetor de quatro elementos. A partir dessas definições, podemos
declarar variáveis usando estes mnemônicos:
Vetor v;
29. Estruturas de Dados Compostas
M"
29 Professora Aida Meira
...
v[0] = 3;
...
Em geral, definimos nomes de tipos para as estruturas com as quais nossos
programas trabalham. Por exemplo, podemos escrever:
struct ponto {
float x;
float y;
};
typedef struct ponto Ponto;
Neste caso, Ponto passa a representar nossa estrutura de ponto. Também
podemos definir um nome para o tipo ponteiro para a estrutura.
typedef struct ponto *PPonto;
Podemos ainda definir mais de um nome num mesmo typedef. Os dois
typedef
anteriores poderiam ser escritos por:
typedef struct ponto Ponto, *PPonto;
A sintaxe de um typedef pode parecer confusa, mas é equivalente à da
declaração de variáveis. Por exemplo, na definição abaixo:
typedef float Vector[4];
Se omitíssemos a palavra typedef, estaríamos a declarar a variável Vector
como sendo um vetor de 4 elementos do tipo float. Com typedef, estamos a
definir um nome que representa o tipo vetor de 4 elementos float.
De maneira análoga, na definição:
30. Estruturas de Dados Compostas
M"
30 Professora Aida Meira
typedef struct ponto Ponto, *PPonto;
Se omitíssemos a palavra typedef, estaríamos a declarar a variável Ponto
como sendo do tipo struct ponto e a variável PPonto como sendo do tipo
ponteiro para
struct ponto.
Por fim, salienta-se que podemos definir a estrutura e associar mnemônicos
para elas em um mesmo comando:
typedef struct ponto {
float x;
float y;
} Ponto, *PPonto;
É comum os programadores de C usarem nomes com as primeiras letras
maiúsculas na definição de tipos. Isso não é uma obrigatoriedade, apenas um
estilo de codificação.
VETORES DE ESTRUTURAS
Já discutimos o uso de vetores para agrupar elementos dos tipos básicos
(vetores de inteiros, por exemplo).
Nesta seção, vamos discutir o uso de vetores de estruturas, isto é, vetores
cujos elementos são estruturas. Para ilustrar a discussão, vamos considerar o
cálculo da área de um polígono plano qualquer delimitado por uma sequência
de n pontos. A área desse polígono pode ser calculada somando-se as áreas
dos trapézios formados pelos lados do polígono e o eixo x, conforme ilustra a
Figura
31. Estruturas de Dados Compostas
M"
31 Professora Aida Meira
FIGURA: Cálculo da área de um polígono
Na figura, ressaltamos a área do trapézio definido pela aresta que vai do ponto
pi ao ponto pi+1. A área desse trapézio é dada por: ( )( ) / 2 i 1 i i 1 i a # x ! x y "
y " " . Somando-se as “áreas” (algumas delas negativas) dos trapézios
definidos por todas as arestas chega-se a área do polígono (as áreas externas
ao polígono são anuladas). Se a sequência de pontos que define o polígono for
dada em sentido anti-horário, chega-se a uma “área” de valor negativo.
Neste caso, a área do polígono é o valor absoluto do resultado da soma.
Um vetor de estruturas pode ser usado para definir um polígono. O polígono
passa a ser representado por uma sequência de pontos. Podemos, então,
escrever uma função para calcular a área de um polígono, dados o número de
pontos e o vetor de pontos que o representa. Uma implementação dessa
função é mostrada abaixo.
Um exemplo de uso dessa função é mostrado no código abaixo:
32. Estruturas de Dados Compostas
M"
32 Professora Aida Meira
int main (void){
Ponto p[3] = {{1.0,1.0},{5.0,1.0},{4.0,3.0}};
printf("area = %fn",area (3,p));
return 0;
}
Exercício:
Altere o programa acima para capturar do teclado o número de pontos que
delimitam o polígono. O programa deve, então, alocar dinamicamente o vetor
de pontos, capturar as coordenadas dos pontos e, chamando a função área,
exibir o valor da área.
VETORES DE PONTEIROS PARA ESTRUTURAS
Da mesma forma que podemos declarar vetores de estruturas, podemos
também declarar vetores de ponteiros para estruturas. O uso de vetores de
ponteiros é útil quando temos que tratar um conjunto elementos complexos.
Para ilustrar o uso de estruturas complexas, consideremos um exemplo em que
desejamos armazenar uma tabela com dados de alunos.
Podemos organizar os dados dos alunos em um vetor. Para cada aluno, vamos
supor que sejam necessárias as seguintes informações:
• nome: cadeia com até 80 caracteres
• matricula: número inteiro
• endereço: cadeia com até 120 caracteres
• telefone: cadeia com até 20 caracteres
Para estruturar esses dados, podemos definir um tipo que representa os dados
de um aluno:
struct aluno {
char nome[81];
33. Estruturas de Dados Compostas
M"
33 Professora Aida Meira
int mat;
char end[121];
char tel[21];
};
typedef struct aluno Aluno;
Vamos montar a tabela de alunos usando um vetor global com um número
máximo de alunos. Uma primeira opção é declarar um vetor de estruturas:
#define MAX 100
Aluno tab[MAX];
Desta forma, podemos armazenar nos elementos do vetor os dados dos alunos
que queremos organizar. Seria válido, por exemplo, uma atribuição do tipo:
...
tab[i].mat = 9912222;
...
No entanto, o uso de vetores de estruturas tem, neste caso, uma grande
desvantagem.
O tipo Aluno definido acima ocupa pelo menos 227 (=81+4+121+21) bytes1. A
declaração de um vetor desta estrutura representa um desperdício significativo
de memória, pois provavelmente estamos a armazenar um número de alunos
bem inferior ao máximo estimado. Para contornar este problema, podemos
trabalhar com um vetor de ponteiros.
typedef struct aluno *PAluno;
#define MAX 100
PAluno tab[MAX];
Assim, cada elemento do vetor ocupa apenas o espaço necessário para
armazenar um ponteiro. Quando precisarmos alocar os dados de um aluno
34. Estruturas de Dados Compostas
M"
34 Professora Aida Meira
numa determinada posição do vetor, alocamos dinamicamente a estrutura
Aluno e guardamos seu endereço no vetor de ponteiros.
Considerando o vetor de ponteiros declarado acima como uma variável global,
podemos ilustrar a implementação de algumas funcionalidades para manipular
nossa tabela de alunos. Inicialmente, vamos considerar uma função de
inicialização. Uma posição do vetor estará vazia, isto é, disponível para
armazenar informações de um novo aluno, se o valor do seu elemento for o
ponteiro nulo. Portanto, numa função de inicialização, podemos atribuir NULL a
todos os elementos da tabela, significando que temos, a princípio, uma tabela
vazia.
void inicializa (void)
{
int i;
for (i=0; i<MAX; i++)
tab[i] = NULL;
}
Uma segunda funcionalidade que podemos prever armazena os dados de um
novo aluno numa posição do vetor. Vamos considerar que os dados serão
fornecidos via teclado e que uma posição onde os dados serão armazenados
será passada para a função. Se a posição da tabela estiver vazia, devemos
alocar uma nova estrutura; caso contrário, atualizamos a estrutura já apontada
pelo ponteiro.
void preenche (int i)
{
if (tab[i]==NULL)
tab[i] = (PAluno)malloc(sizeof(Aluno));
printf("Entre com o nome:");
scanf(" %80[^n]", tab[i]->nome);
35. Estruturas de Dados Compostas
M"
35 Professora Aida Meira
printf("Entre com a matricula:");
scanf("%d", &tab[i]->mat);
printf("Entre com o endereco:");
scanf(" %120[^n]", tab[i]->end);
printf("Entre com o telefone:");
scanf(" %20[^n]", tab[i]->tel);
Podemos também prever uma função para remover os dados de um aluno da
tabela. Vamos considerar que a posição da tabela a ser liberada será passada
para a função:
void remove (int i) {
if (tab[i] != NULL) {
free(tab[i]);
tab[i] = NULL;
}
}
Para consultarmos os dados, vamos considerar uma função que imprime os
dados armazenados numa determinada posição do vetor:
void imprime (int i) {
if (tab[i] != NULL) {
printf("Nome: %sn”, tab[i]->nome);
printf("Matrícula: %dn”, tab[i]->mat);
printf("Endereço: %sn”, tab[i]->end);
printf("Telefone: %sn”, tab[i]->tel);
}
}
36. Estruturas de Dados Compostas
M"
36 Professora Aida Meira
Por fim, podemos implementar uma função que imprima os dados de todos os
alunos da tabela:
void imprime_tudo (void) {
int i;
for (i=0; i<MAX; i++)
imprime(i);
}
Exercício:
Faça um programa que utilize as funções da tabela de alunos escritas acima.
Exercício:
Re-escreva as funções acima sem usar uma variável global.
Sugestão: Crie um tipo Tabela e faça as funções receberem este tipo como
primeiro parâmetro.