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).