AED – Ponteiros e alocação dinâmica Prof. João Luis Assirati 1

Propaganda
AED – Ponteiros e alocação dinâmica
Prof. João Luis Assirati
1 Ponteiros
Na linguagem C,
• Ponteiros são variáveis especializadas em guardar endereços de memória.
• Ponteiros têm tipo. O trecho de programa abaixo exemplifica estas duas propriedades:
int n; /* Declara um int */ 2 int *p; /* Declara um ponteiro do tipo int */ 3 float x; /* Declara um float */ 4 float *q; /*
Declara um ponteiro do tipo float */ 5 p = &n; /* p guarda o endereço de n */ 6 q = &x; /* q guarda o endereço de x */
7 p = &x; /* Erro: p e x têm tipos incompatíveis */ 8 q = &n; /* Erro: q e n têm tipos incompatíveis */
1
Na linha 1, declaramos um variável int e na linha 2 declaramos um ponteiro do tipo int. A diferença entre as duas
declarações é que para declarar um poiteiro colocamos um * antes do nome da variável. Da mesma forma, na linha 3
declaramos um float e na linha 4 declaramos um ponteiro do tipo float.O fato do ponteiro p ser do tipo int significa que ele
deve ser usado para guardar o endereço de variáveis do tipo int. Em outras palavras, um ponteiro do tipo int deve guardar
o endereço de memória onde está localizado um int. Assim, na linha 5 p passa a guardar o endereço da variável n. Da
mesma forma, na linha 6 q guarda o endereço da variável x, o que é consistente, pois x é uma variável float e q é um
ponteiro do tipo float. Nas linhas 7 e 8 temos duas atribuições incorretas, pois os ponteiros estão guardando os endereços
de variáveis de tipos diferentes. Em geral, compiladores C permitirão as atribuições das linhas 7 e 8, que são úteis em
algumas aplicações avançadas de baixo nível, mas emitirão um aviso de que tipos incompatíveis estão sendo atribuídos.
Quando um ponteiro guarda o endereço de uma variável, dizemos que o ponteiro aponta para a variável
e simbolizamos com um diagrama de flechas:
Figura 1: Diagrama de flechas para ponteiros.
Neste diagrama, a caixa representa uma área de memória (um conjunto de bytes) onde está a variável
n. Nesta área de memória está guardado o número 10. Assim, podemos formular a seguinte regra prática:
o tipo de um ponteiro indica o tipo da cariável para a qual ele aponta.
Assimcomoasvariáveiscomunsquejáestudamos,osponteirostêmvalor.
conteúdo. Quando um ponteiro aponta para uma variável, dizemos que
•
•
Porém,somenteosponteiros
têm
O valor do ponteiro é o endereço que ele guarda
O conteúdo de um ponteiro é o valor da variável apontada por ele
Na Figura 1, o valor de x é 10, portanto o conteúdo de p é 10. O valor de p é o endereço da variável x, que não
está representado na figura. Em última instância, ponteiros são variáveis como outras quaisquer, portanto também
ocupam espaço na memória. Sendo assim, ponteiros têm tamanho e endereço:
•
O tamanho (sizeof) de um ponteiro é independente do seu tipo. Nas atuais arquiteturas Intel de 32 bits,
ocupam 4 bytes; na arquitetura X86_64 ocupam 8 bytes.
•
O endereço de um ponteiro é a posição da memória em que aquele ponteiro se localiza.
Para ilustrar estes conceitos, vamos considerar um exemplo de organização hipotética das variáveis do programa
abaixo na memória:
1
int
n,
*p;Figura
double
*q;
x,
2:
3
10;
n
=
4
3.14;
x
=
5
&n;
p
=
6
&x;
q
=
Abaixo, mostramos alocações dinâmicas de memória equivalentes às alocações estáticas da listagem 6
dadas no início desta seção:
Diagrama de setas
Listagem 8: Alocação dinâmica de vetores equivalente à listagem 6
para o programa ao
1
int
*v;
lado
2
double
*x;
3
char
*t;
4
malloc (10
*
sizeof(int ));
v
=
5 1 declaramos de uma forma compacta uma variável inteira n e um ponteiro p do tipo int e na linha 2
Na linha
malloc (200
*
sizeof(double ));
x
=
6
uma variável
do tipo double. Ao lado mostramos o diagrama de setas após as
t double x e =um ponteiro
mallocq (64);
atribuições das
linhas
a 6. Abaixo,
mostramos
como
variáveis
se edistribuiriam
memória,
considerando
Note
que 3nestas
alocações
dinâmicas,
v, xestas
e t são
ponteiros
não vetores,na
mas
após suas
inicializações com
endereços hipotéticos:
malloc() podem ser usados como se fossem vetores. Note também que na linha 6 devemos alocar espaço
para 64 caracteres, mas não é necessário escrever 64 * sizeof(char) porque sizeof(char) é sempre 1.
Figuraeste
3: Organização
hipotética
variáveis
e ponteiros
memóriaem que:
Vamos ilustrar
método através
de umde
exemplo.
Queremos
umna
programa
1. O usuário informe quantos números vai digitar
Esta figura
que o tamanho
p etamanho
q são iguais,
embora
ponteiros
tipo diferente.
2. mostra
Faça alocação
dinâmicados
de ponteiros
um vetor de
suficiente
parasejam
guardar
todos osdenúmeros
digitados
Segundo
a
3. Preencha o vetor com números digitados pelo usuário.
figura, &p vale
4. Calcule e imprima a soma de todos os elementos do vetor.
0x1b2 e &q vale
Este programa, em uma versão simples, é mostrado abaixo. O vetor alocado será de números inteiros,
0x1b9. O valor
portanto o usuário deve digitar números inteiros:
de p é 0x1a3Listagem
e
9: Soma dos elementos de um vetor dinamicamente alocado
o valor de1 q é
#include <stdio.h>
0x1a9.
2
#include <stdlib.h>
Finalmente,3
o
conteúdo de
p
é
10 e o conteúdo de q é 3.14.
4
int
main () {
int
n;
/*
Tamanho
d vetor
5
o
6
int
i; /*
índice
e contador
7
int
*p; /*
Ponteiro
para a
int
s;
/*
dos
elementos
8
soma
2
9
1
0
1
printf (" Quantos
scanf ("%d", &n);
numeros
deseja digitar? "
É possível acessar o valor de uma variável através de um ponteiro que aponta para ela com o operador *,
chamado de operador de desreferenciação ou operador de indireção ou ainda operador de conteúdo. Nas
figuras 2 e 3, temos *p == 10 e *q == 3.14. O programa abaixo ilustra o uso do operador *:
1
int n; 2 int *p; 3 p = &n; 4 *p = 10; 5 printf("%d\n", n); 6 n = 20; 7 printf("%d\n", *p);
Nas linhas 1 e 2, declaramos o inteiro n e o ponteiro p, não inicializados. Um ponteiro não inicializado guarda
um endereço aleatório da memória, isto é, aponta algum ligar desconhecido. Na linha 3, inicializamos p,
fazendo com que ele guarde o endereço de n, isto é, aponte para n. Na linha 4, acessamos a variável n através
do ponteiro p com o operador de conteúdo. Como resultado, guardamos 10 na variável
n. Assim, na linha 5, o printf() imprime 10. Na linha 6, o valor de n é modificado para 20. Na linha 7, acessamos
o valor de n através de p, novamente com o operador de conteúdo *, imprimindo 20. Podemos representar o
funcionamento deste programa através de um “teste de mesa com setas”, na figura abaixo:
Figura 4: Teste de mesa com setas
Um caso importante ocorre quando um ponteiro aponta para o primeiro elemento de um vetor. Considere o
exemplo abaixo:
int v[5] = {2, 3, 5, 7, 11}; 2 int *p; 3 p = v; /* p aponta para v[0] */ 4 printf("%d\n", *p); /* imprime v[0] (2) */ 5 /* Imprime
todo o vetor v através de p */ 6 printf("%d, %d, %d, %d, %d\n", p[0], p[1], p[2], p[3], p[4]);
1
Na linha 1, declaramos um vetor de 5 inteiros inicializado e na linha 2 um vetor p do tipo int. Na linha 3, p
recebe o endereço do primeiro elemento do vetor v (lembramos que o identificador v vale &v[0]). Esta
atribuição é consistente, pois o primeiro elemento do vetor v é um int (assim como todos os elementos do vetor
v). É possível acessar o elemento v[0], que vale 2, através de *p. Portanto, a linha 4 imprime 2. Naturalmente,
gostaríamos também de acessar os demais elementos v[1], v[2], v[3] e v[4] através de p. Felizmente, a
linguagem C permite fazer isto aplicando o operador de índice [] à frente do ponteiro, como se ele fosse o
próprio vetor. Assim, p[0] é sinônimo de *p. p[1] acessa o próximo inteiro à frente do endereço p, p[2] acessa
segundo inteiro à frente do endereço p e assim por diante. Deste modo, a linha 5 imprime 2, 3, 5, 7, 11. Todos
estes fenômenos podem ser compreendidos através do diagrama abaixo:
Figura 5: Ponteiro
um vetor
apontando para
Nas seções 3, 4 e
expor três
importantes de
2 Conceitos de
5 abaixo, vamos
aplicações
vetores.
funções
Nesta seção, faremos
uma revisão de
conceitos de funções
em C que serão
importantes para as
seções 3 e
4.
Funções são módulos de execução que permitem separar um programa em partes mais simples. A
interface de uma função é definida por seus parâmetros, que informam quais são os dados necessários para
a execução da função, e seu tipo de retorno, que informa o “resultado” da função quando ela termina. A
interface de uma função é definida pela sua primeira linha, que deve ter a forma
<tipo de retorno> <nome da função>(<lista de parâmetros>)
Quando uma função é chamada, ela “recebe” argumentos, que são copiados em ordem nos seus
parâmetros.
Parâmetros são sempre variáveis locais, inicializados no momento em que a função é chamada com os
valores dos argumentos.
Tomemos como exemplo uma função que recebe dois números e imprime a sua média, sem retornar
nada, definida no código abaixo:
Listagem 1: Cálculo da média com uma função que imprime a média
#include <stdio.h> 2 #include <stdlib.h> 3 4 /* Protótipos das funções */ 5 void media1(float a, float b); 6 7 /*
Implementação das funções */ 8 void media1(float a, float b) { 9 float m;
10 m = (a + b) / 2; 11 printf("A media vale %f.\n", m); 12 } 13 14 int main() { 15 float x, y; 16 printf(" Digite dois numeros: ");
1
17
scanf("%f", &x); 18 scanf("%f", &y); 19 media1(x, y); 20 system(" pause "); 21 return 0; 22 }
Na linha 8, vemos que a função media1() tem dois parâmetros a e b. Seu tipo de retorno é void, o que quer dizer
que ela não retorna nenhum valor. Na linha 19, a função media1() é chamada e recebe os parâmetros x e y. Neste
momento, x e y são copiados em a e b. A função media1() calcula então a média entre a e b na linha 10. Note que a
media entre a e b é igual à média entre x e y. Na linha 11 esta média é impressa e a função acaba. A partir daí, a
execução volta para a função main() na linha 20.
Este programa pode ser implementado de forma diferente, tal que toda a entrada e saída seja feita na
função main(). Neste caso, definimos uma função que recebe dois argumentos, calcula sua média e a retorna,
de acordo com o código abaixo:
Listagem 2: Cálculo da média com uma função que retorna a média
#include <stdio.h> 2 #include <stdlib.h> 3 4 /* Protótipos das funções */ 5 float media2(float a, float b); 6 7 /*
Implementação das funções */ 8 float media2(float a, float b) { 9 float m;
10 m = (a + b) / 2;
11 return m;
12 }
1
13
int main() {
float x, y, z;
16 printf(" Digite dois numeros: ");
17 scanf("%f", &x);
18 scanf("%f", &y);
19 z = media2(x, y);
20 printf("A media vale %f.\n", z);
21 system(" pause ");
22 return 0;
23 }
14
15
Ela funciona como a função media1(), só que ao invés de imprimir o resultado, dá o comando return m; na linha 11.
O que este comando faz é copiar o valor de m (que contém a média) na variável z, que está recebendo o valor de
retorno da função na linha 19. Por isto, seu tipo de retorno é definido como float ao invés de void na linha 8. Quando
a função termina, a execução volta para a função main() na linha 20, que imprime z.
As funções media1() e media2() definidas acima realizam passagem de argumentos por valor, pois é o
valor do argumento que é copiado no parâmetro. Na próxima seção estudaremos uma modalidade diferente
de passagem de argumentos.
Observação: em geral, é preferível implementar funções especializadas, como media2(), que não misturam
cálculo com entrada e saída (a função media1() realiza não só o cálculo da média mas também o imprime).
Suponha, por exemplo, que não queremos imprimir o valor da média, mas simplesmente queremos imprimir
“aprovado” ou “reprovado”, conforme a média for maior ou menor do que 5. Neste caso, a função media1()
não nos ajudaria, mas poderíamos facilmente usar a função media2() como no trecho de código abaixo:
if(media2(x, y) >= 5) { printf("
Aprovado\n"); }
3 Passagem de argumentos por referência
Uma função realiza passagem de um argumento por referência quando recebe o endereço, e não
o valor, da variável do argumento. Neste caso, o parâmetro correspondente deve ser um ponteiro. Através
de passagem por referência, uma função pode alterar o seu argumento, pois conhece o endereço seu
argumento. Vamos considerar um exemplo simples:
void incrementa_valor(int a) { 2 a = a + 1; 3 } 4 5 void incrementa_referencia(int *p) { 6 *p = *p + 1; 7 } 8 9 int main() {
int x = 0;
11 incrementa_valor(x); /* Passa x por valor */
12 printf("%d\n", x); /* Imprime 0 */
13 incrementa_referencia(&x); /* Passa x por referência */
14 printf("%d\n", x); /* Imprime 1 */
15 }
1
10
Na linha 11, a função incrementa_valor() recebe o valor da variável inteira x, ou seja, realiza a passagem do
argumento por valor. Assim, o parâmetro desta função deve ser um inteiro, como mostrado na linha 1
(parâmetro a). Já na linha 13, a função incrementa_referencia() recebe o endereço da variável x, portanto
realiza passagem por referência. O parâmetro que recebe o endereço &x deve ser, portanto, um ponteiro do
tipo int, como se vê na linha 5 (parâmetro p). Vamos mostrar que a linha 11 não altera o valor de x mas a linha
13 o altera:
•
Na chamada da função incrementa_valor(), na linha 11, o valor de x, que é 0, é copiado na variável a.
Quando a função incrementa_valor() executa, na linha 2, é a variável local a que é incrementada, portanto após a
linha 2 a vale 1. A variável x, que pertence à função main(), continua com valor 0. Portanto, a linha 12 imprime 0.
•
Na chamada da função incrementa_referencia(), na linha 13, o endereço de x é copiado no ponteiro p.
Quando esta função executa, p aponta para x. Na linha 6, estamos incrementando o conteúdo de p, obtido com o
operador *. Como p aponta para x, *p é a própria variável x. Assim,
o comando *p = *p + 1 é equivalente a x=x+1, com a diferença que o comando x=x+1 é proibido na
função incrementa_referencia() porque x é variável da função main(), mas *p = *p
+1 é permitido, pois p é variável da função incrementa_referencia(). Portanto, a variável x é modificada
na chamada da linha 13, e a função printf() da linha 14 imprime 1.
Podemos simbolizar o funcionamento do programa com o seguinte diagrama:
Figura 6: Efeito das funções incrementa_valor() e incrementa_referencia()
Um exemplo importantíssimo bastante usado é a implementação da função troca(), que troca o valor dos
seus argumentos. Ela recebe dois argumentos passados por referência e troca os seus valores. O código é
dado abaixo:
Listagem 3: Função troca()
void troca(int *a, int *b) { 2 int aux; 3 aux = *a; 4 *a = *b; 5 *b = aux; 6 } 7 8 int main() { 9 int x = 10, y = 20;
troca(&x, &y);
11 printf("%d, %d", x, y); /* imprime 20, 10 */
12 }
1
10
O diagrama abaixo mostra o funcionamento da função troca():
Figura 7: Efeito da função troca()
Uma função pode ser definida com argumentos mistos por valor e por referência. Vamos dar o exemplo da
função raizquad() abaixo, que recebe um argumento por referência e o outro por valor:
Listagem 4: Cálculo da raiz quadrada com verificação do argumento
#include <stdio.h> 2 #include <stdlib.h> 3 #include <math.h> /* necessário para a função sqrt() */ 4 5 /* Protótipos
das funções: */ 6 int raizquad (double *a, double b); 7 8 /* Implementação das funções */ 9 int raizquad (double *a,
double b) {
10 if(b < 0) return 1;
11 *a = sqrt(b);
12 return 0;
13 }
1
14
int main() {
double x, y;
17 printf(" Digite um numero: ");
18 scanf("%lf", &x);
19 if(raizquad(&y, x) == 0) {
20 printf("A raiz quadrada vale %f\n", y);
21 } else {
22 printf(" Erro: valor invalido .\n");
15
16
23
} 24 system(" pause "); 25 return 0; 26 }
Esta função tenta calcular a raiz quadrada de b e guardar em *a. Se b < 0, não existe a raiz quadrada e a
função retorna 1, que representa erro. Se b >= 0, ela calcula a raiz quadrada e a guarda no primeiro
argumento, retornando 0, que representa sucesso. Na linha 9, a raiz quadrada de x é calculada (se possível) e
seu valor é guardado em y. x é passado por valor e y é passado por referência.
4 Argumento vetor
Outro caso importante do uso de ponteiros é quando precisamos passar um vetor a uma função. Em C,
fazemos isso passando o endereço do primeiro elemento eo número de elementos do vetor à função. Ou
seja, vetores são passados por referência ao seu primeiro elemento. No exemplo abaixo, criamos uma função
chamada maxvet() que recebe um vetor de floats e retorna o maior número contido neste vetor:
Listagem 5: Função que encontra o elemento máximo de um vetor
float maxvet(float *p, int n) { 2 int i; 3 float max = p[0]; 4 for(i = 1; i < n; i++) { 5 if(p[i] > max) { 6 max = p[i]; 7 } 8 } 9
return max;
10 }
1
11
int main() {
/* Vetor inicializado com 5 elmentos */
14 float v[5] = {2.3, -1.1, 7.2, 8.5, -3.4};
15 /* Imprime 8.5 */
16 printf("%f\n", maxvet(v, 5));
17 }
12
13
Neste exemplo, na linha 12 passamos o endereço do primeiro elemento v[0] para o ponteiro p e o número de
elementos 5 para o inteiro n. Quando a função maxvet() começa a rodar, temos a situação descrita na figura 5.
Assim, os elementos do vetor v são acessíveis através do ponteiro p com o operador de índice, p[i], e a soma
é acumulada na variável s.
Deve ser notado que a expressão v somente carrega o endereço &v[0], e não o número de elementos do
vetor, que é 5. Por isso, é necessário passar não somente v mas também o número de elementos do vetor
para que a função maxvet() possa percorrê-lo, calculando a sua soma.
Este método de passar um vetor a uma função informando o endereço do primeiro elemento é conveniente
também porque vetores podem ser muito grandes. Se fôssemos passar uma cópia completa do vetor à função,
perderíamos muito tempo. É muito mais rápido em termos de processamento passar apenas o endereço do
primeiro elemento. O problema desta abordagem é que não podemos modificar o vetor na função sem modificar o
vetor no argumento (precisamos ser cuidadosos na programação para não alterar
o vetor original se isto não for desejado).
5 Alocação dinâmica
A declaração de vetores nas formas
Listagem 6: Alocação estática de vetores
int v[10]; 2 double x[200]; 3
char t[64];
1
é conhecida como alocação estática. Sua característica principal é que o número de elementos é fixo, definido no
código fonte, e só pode ser mudado editando e recompilando o programa. Dizemos que o tamanho do vetor é
“hardcoded”. Existe uma possibilidade mais flexível para criar vetores de tal modo queseu tamanho possa
serdefinido durantea execuçãodoprograma atravésdarequisiçãodemaismemória para o programa, conhecida como
alocação dinâmica de vetores.
A memória, assimcomo todos osoutros recursos do computador, é controladapelo sistema operacional. Quando um
programa precisa de mais memória, ele não pode escolher e usar aleatoriamente um bloco de memória, mas deve
requisitar memória ao sistema operacional, que conhece quais pedaços de memória não estão sendo usado por outros
programas. A interface de comunicação entre o programa e o sistema operacional, específica para o pedido de mais
memória, é a função malloc(), definida no arquivo de cabeçalho stdlib.h, que deve ser incluído. Quando um programa
executa o comando malloc(<número de bytes>), o sistema operacional reserva um bloco contíguo de memória de
tamanho <número de bytes> para ele. O programa precisa apenas conhecer o endereço na memória do primeiro byte do
bloco reservado, o que é feito através do valor de retorno da função malloc(). Este endereço deve, naturalmente, ser
guardado em um ponteiro.
Tipicamente, um programa que faz alocação dinâmica declara um ponteiro e chama a função malloc()
como no exemplo abaixo:
Listagem 7: Alocação dinâmica de um vetor de 3 elementos float
1
float *p; 2 p = malloc(3 * sizeof(float)); 3 p[0] = 10.0; 4 p[1] = 20.0; 5 p[2] = 30.0;
Neste trecho de programa, queremos alocar um vetor de 3 floats. Precisamos portanto pedir 12 bytes ao
systema operacional, que o programa escreve como a multiplicação 3 * sizeof(float) == 3 * 4 == 12. O efeito
deste trecho de programa é descrito no diagrama abaixo:
Figura 8: Uso da
função malloc()
Os
três
momentos
do
processo podem
ser
descritos
como:
1. O programa X
declara um
ponteiro p não
inicializado. A
memória está
sendo usada
pelos
programas A,
B, C e D e há
espaços não
utilizados. O
sistema
operacional
guarda uma
lista de
ponteiros para
as áreas de
memória não
utilizadas.
2. O programa
chama a
função
malloc(), que
se comunica
com o sistema
operacional,
requisitando
memória para
guardar 3
floats. O
sistema
operacional
escolhe uma área livre com tamanho suficiente para guardar o vetor.
3. O valor de p passa a ser o endereço de memória devolvido pelo sistema operacional. O programa X entende
esta área de 12 bytes como 3 floats, que ele acessa como p[0], p[1] e p[2]. O sistema operacional atualiza a
lista de blocos não utilizados.
1
int
n,
*p;
double
*q;
x,
3
10;
n
=
4
3.14;
x
=
5
&n;
p
=
6
&x;
q
=
Abaixo, mostramos alocações dinâmicas de memória equivalentes às alocações estáticas da listagem 6
dadas no início desta seção:
Listagem 8: Alocação dinâmica de vetores equivalente à listagem 6
1
int
*v;
2
double
*x;
3
char
*t;
4
malloc (10
*
sizeof(int ));
v
=
5
malloc
(200
*
sizeof(double ));
x
=
6
t
=
malloc (64);
Note que nestas alocações dinâmicas, v, x e t são ponteiros e não vetores, mas após suas inicializações com
malloc() podem ser usados como se fossem vetores. Note também que na linha 6 devemos alocar espaço
para 64 caracteres, mas não é necessário escrever 64 * sizeof(char) porque sizeof(char) é sempre 1.
Vamos ilustrar este método através de um exemplo. Queremos um programa em que:
1. O usuário informe quantos números vai digitar
2. Faça alocação dinâmica de um vetor de tamanho suficiente para guardar todos os números digitados
3. Preencha o vetor com números digitados pelo usuário.
4. Calcule e imprima a soma de todos os elementos do vetor.
Este programa, em uma versão simples, é mostrado abaixo. O vetor alocado será de números inteiros,
portanto o usuário deve digitar números inteiros:
Listagem 9: Soma dos elementos de um vetor dinamicamente alocado
1
#include <stdio.h>
2
#include <stdlib.h>
2
3
4
int
main () {
int
5
6
7
8
9
1
0
1
1
1
2
n;
int
int
int
/*
i; /*
*p; /*
/*
s;
/*
1
5
for(i
contador
para a
elementos
e
dos
soma
Realiza
p
1
4
alocação
a
malloc(n
=
Lê
= 0;
i
<
os
}
1
8
/*
s
for(i
=
Acumul
a
0;
a som
a
número
d
o
n; i++) {
= 0;
i
s
=
do
s
elementos
< n; i++) {
s
+ p[i];
dinâmica
números :\n");
scanf ("%d", &p[i]);
1
7
deseja digitar? ");
* sizeof(int ));
o
s
1
6
2
1
índice
Ponteiro
vetor
numeros
/*
printf (" Digite
2
0
d
o
printf (" Quantos
scanf ("%d", &n);
1
3
1
9
Tamanho
d
o
vetor
e
m
s */
teclado
Na linha 12, realizamos uma alocação dinâmica. O número de bytes necessários para guardar n números
inteirosécalculadocomooprodutoden (digitadopelousuário)pelotamanhodeuminteiro(sizeof(int)). Note que o usuário
deve conhecer com antecedência a quantidade de números que serão digitados. A partir da linha 12, p pode ser
usado como se fosse um vetor, pois aponta para o primeiro byte de um bloco contíguo de memória cedido pelo
sistema operacional. Nas linhas 15, 16 e 17 lemos o vetor do teclado e nas linhas 19, 20, 21 e 22 calculamos a
soma dos seus elementos exatamente da mesma maneira como faríamos com um vetor comum.
Podemos melhorar o programa acima separando as linhas 15, 16 e 17 da listagem 9 em uma função
dedicada a ler os valores digitados e guardá-los no vetor, que chamaremos le_vetor(). Separaremos também
as linhas 19, 20, 21 e 22 da listagem 9 em uma função dedicada ao cálculo da soma dos elementos do vetor,
chamada soma_vetor(). A listagem do programa melhorado é dada abaixo:
Listagem 10: Soma dos elementos de um vetor dinamicamente alocado, versão com funções
#include <stdio.h> 2 #include <stdlib.h> 3 4 /* Protótipos das funções */ 5 void le_vetor(int *pont, int num); 6 int
soma_vetor(int *pont, int num); 7 8 /* Implementação das funções */ 9
10 void le_vetor(int *pont, int num) {
11 int i;
12 printf(" Digite os numeros:\n");
13 for(i = 0; i < num; i++) {
14 scanf("%d", &pont[i]);
15 }
16 }
1
17
int soma_vetor(int *pont, int num) {
int i;
20 int soma = 0;
21 for(i = 0; i < num; i++) {
22 soma = soma + pont[i];
23 }
24 return soma;
25 }
18
19
26
int main() {
int n; /* Tamanho do vetor a ser alocado */
29 int *p; /* Ponteiro para a alocação dinâmica */
30 printf(" Quantos numeros deseja digitar? ");
31 scanf("%d", &n);
32 p = malloc(n * sizeof(int));
33 le_vetor(p, n);
34 printf(" Soma: %d\n", soma_vetor(p, n));
35 system(" pause ");
36 return 0;
37 }
27
28
As funções le_vetor() e soma_vetor() recebem um vetor como argumento, neste caso, um vetor dinamicamente
alocado apontado por p. Por isto, devem receber o endereço do primeiro elemento do vetor. Como vimos, na
alocação dinâmica que ocorre na linha 32 da listagem 10 o ponteiro p guarda justamente este endereço. Deve ser
notado que a função soma_vetor() não altera o vetor, mas a função le_vetor() altera. Isto é possível porque, como
vimos na seção anterior, a passagem de vetores como argumentos de funções se dá por referência.
Finalmente, é interessante notar que a alocação dinâmica oferece uma flexibilidade maior do que a
alocação estática de vetores, pois o usuário pode controlar o tamanho do vetor durante o funcionamento do
programa. No entanto, o usuário deve conhecer o tamanho do vetor com antecedência, como mostra
o programa acima para a soma dos elementos do vetor. Se precisamos lidar com um fluxo de dados cujo
tamanho não conhecemos até que o último dado chegue, é necessário um tipo de estrutura de dados mais
flexível do que vetores dinamicamente alocados chamado lista ligada, que estudaremos no futuro.
Observação: é possível encontrar programas que realizam alocação dinâmica com um molde entre a saída
da função malloc() e o ponteiro. Isto é feito na forma apresentada abaixo:
Listagem 11: Alocação dinâmica com moldes, atualmente desaconselhada
int *v; 2 double *x; 3 char *t; 4 v = (int *) malloc(10 * sizeof(int)); 5 x = (double *) malloc(200 * sizeof(double)); 6 t =
(char *) malloc(64);
1
Esta prática era necessária em versões antigas da função malloc() e atualmente é desaconselhada. No
entanto, ela é necessária em programas escritos na linguagem C++, que tem regras mais estritas na atribuição
de ponteiros do que a linguagem C, o que faz com que muitos programadores ainda a conservem.
6 Devolução de memória
Assim como um programa pode requisitar memória ao sistema operacional através da função malloc(), esta memória
também pode ser devolvida, quando não for mais necessária, através da função free(), definida em stdlib.h. Devolver
memória é importante quando um programa realiza muitas alocações de memória durante a sua vida. Se um
programa realiza muitas alocações e nunca devolve memória, ele pode acabar reservando toda a memória do
computador e o sistema pode terminar sem memória.
Usamos a função free() do seguinte modo:
int *p; 2 p = malloc(100 * sizeof(int)); 3 <o programa usa o vetor alocado p[i]> 4 free(p); 5 <o programa continua,
mas não pode mais usar o vetor p[i]>
1
Após a alocação de memória na linha 2, usamos o vetor em algum algoritmo representado na linha 3. Quando
a memória alocada não é mais necessária, pode ser devolvida, com o comando da linha 4. A partir deste
momento, a área de memória apontada por p não pertence mais ao programa, e da linha 5 em diante não
podemos mais acessá-la, isto é, a operação p[i] ou *p é ilegal.
Quando um programa termina, toda a memória alocada por ele é automaticamente devolvida ao sistema.
Por isto, nenhum dos programas acima precisa devolver devolver memória, pois quando vetor
alocadonãoémaisnecessáriooprogramatambémtermina. Noentanto,sequiséssemosdevolveramemória antes do
fim dos programas das listagens 9 e 10, bastaria escrever free(p); antes de system("pause"); (quando o vetor
não é mais necessário).
Download