Alocação dinâmica de memória
Quando vos falei da representação das variáveis na memória do computador, disse-vos que cada variável ficava guardada numa posição de memória. Por exemplo, se fizéssemos um programa que contivesse a seguinte declaração,
int x = 9;
o x poderia ir parar à posição de memória E2 quando o programa fosse executado. Na realidade, as coisas não são bem assim. Cada posição de memória corresponde apenas a um byte e uma variável inteira ocupa geralmente 4 bytes. Ou seja, o número 9 não vai estar representado numa única posição de memória mas sim em 4 posições de memória (ex: E2, E3, E4 e E5). Aliás, aquilo que está nessas 4 posições de memória é a representação em binário do número 9. A figura que se segue dá esta visão mais detalhada da memória.
Apesar da variável x estar localizada nos endereços E2, E3, E4 e E5, costuma dizer-se que o endereço da variável x é E2. Isto é, o endereço de uma variável é o endereço do primeiro byte que a variável ocupa.
Se declararmos um array a de 10 inteiros, o compilador vai reservar um bloco de memória consecutivo que permita guardar esses 10 inteiros. Se um inteiro ocupar 4 bytes, o compilador terá de reservar um bloco de 40 bytes (por exemplo, do endereço E100 até ao endereço E139).
a[0] vai ocupar as posições E100, E101, E102, E103 a[1] vai ocupar as posições E104, E105, E106, E107 a[2] vai ocupar as posições E108, E109, E110, E111 ... ... a[9] vai ocupar as posições E136, E137, E138, E139
Do mesmo modo que dizemos que o endereço da variável x é E2, podemos dizer que o endereço do array a é E100. Isto é, o endereço do array é o endereço do primeiro byte que o array ocupa. De facto, quando declararmos:
int a[10];
O nome a é o endereço da primeira posição do array. Por outras palavras,
a é sinónimo de &a[0]
Como tal, é perfeitamente válido fazer coisas deste estilo,
int a[10]; int *p; p = a; *p = 4; /* equivalente a dizer a[0] = 4 */
Quando demos os arrays, disse-vos que tinham de declarar os arrays com uma dimensão máxima. Por exemplo, se quisessem fazer um programa para ordenar uma lista de n números, em que n seria um número a ser introduzido pelo utilizador, teriam de fazer qualquer coisa deste estilo:
#include <stdio.h> #define N 10000 main() { int a[N]; int n; printf("Quantos números quer ordenar? "); scanf("%d", &n); if( n > N ) printf("erro: a dimensão máxima do array é %d\n", N); else /* código para introduzir um array de n números e ordená-los */ ... }
O programa acima tem a limitação de só funcionar quando n<=N. O truque que vos ensinei foi o de definir N como sendo um número muito grande e depois só usar parte do array. Esta solução tem a desvantagem de desperdiçar memória. Estamos a reservar um array de 10000 inteiros e muito provavelmente o número n introduzido pelo utilizador vai ser só 10 ou 20!
A alternativa a esta solução é requisitar a memória ao computador durante a própria execução do programa. A ideia é perguntar ao utilizador a dimensão do array. Se o utilizador introduzir 10, pedimos ao computador para nos dar um bloco de memória que permita guardar 10 inteiros. Deste modo, o array ocupa apenas o espaço que é estritamente necessário.
Na biblioteca stdlib.h existe uma função chamada malloc que permite requisitar ao computador n bytes de memória (malloc é uma abreviatura de memory alocation). Aqui vai o programa:
#include <stdio.h> #include <stdlib.h> main() { int *a; int n; printf("Quantos números quer ordenar? "); scanf("%d", &n); a = (int *) malloc( n * sizeof(int) ); if( a == NULL ) { printf("Erro: não há memória\n"); exit(1); } /* código para introduzir um array de n números e ordená-los */ ... free( a ); }
Em vez de declaramos a variável a como sendo um array de inteiros, declaramos a como sendo um apontador para um inteiro. A ideia é que a vai ser o endereço do primeiro elemento do array.
Concentrem-se na linha do programa que tem o malloc. A linha tem um aspecto feio mas não é nada de complicado.
a = (int *) malloc( n * sizeof(int) );
A função malloc tem um argumento que é o número de bytes de memória que pretendemos utilizar. Se quisermos guardar n inteiros temos de pedir n vezes o número de bytes que cada inteiro ocupa.
O número de bytes que cada inteiro ocupa é dado por sizeof(int) No caso geral, sizeof(T) dá o número de bytes que uma variável do tipo T ocupa.
Aquilo que o malloc retorna é o endereço do primeiro byte do bloco de memória que foi pedido. Caso não haja memória, o malloc retorna NULL.
O (int *) que aparece antes do malloc é necessário porque a variável a é um apontador para inteiro. No caso geral, se quisermos alocar memória para um array de n elementos de um tipo T temos de fazer assim:
T *a; a = (T *) malloc( n * sizeof(T) );
Importante: Quando reservamos memória com malloc, devemos sempre libertar essa memória quando esta já não for mais necessária. Isso faz-se utilizando a função free. Uma vez feito o free, a memória pode ser reutilizada pelo sistema operativo.
#include <stdio.h> #include <stdlib.h> main() { int *a; int i, k, m, min, temp, n; /* Alocar memória para guardar o array */ printf("Quantos números quer ordenar? "); scanf("%d", &n); a = (int *) malloc( n * sizeof(int) ); if( a == NULL ) { printf("ERRO: nao ha memoria.\n"); exit(1); } /* Receber os valores a ordenar */ for (i=0; i<n; i++) { printf("%d.º numero -> ",i+1); scanf("%d",&a[i]); } /* Ordenar o array */ for( k=0; k<=n-1; k++ ) { /* descobre o índice do mínimo em a[k], a[k+1], ..., a[n-1] */ min = a[k]; m = k; for( i=k; i<=n-1; i++ ) if( a[i] < min ) { min = a[i]; m = i; } /* troca a[k] com a[m] */ temp = a[k]; a[k] = a[m]; a[m] = temp; } /* Escrever os elementos do array ordenados */ for( i=0; i<n; i++ ) printf("%d ", a[i]); printf("\n"); /* libertar a memória */ free( a ); }
O programa ainda podia ser melhorado mas vou deixar isso para a próxima aula.
Quando um programa é executado, o sistema operativo coloca o código do programa (as instruções em linguagem máquina) na memória do computador. Além das instruções, o sistema operativo também reserva memória para as variáveis que o vosso programa vai utilizar.
O mais interessante de tudo é que o vosso programa não é o única coisa que está na memória do computador. Enquanto o vosso programa está a ser executado, também podem estar a correr um editor de texto (ex: emacs) e um browser (ex: Netscape) para aceder à Internet. O emacs e o Netscape também são programas (ver curiosidade no final da página) e também ocupam memória. E isto não fica por aqui. Pode haver vários utilizadores, cada qual a correr vários programas no mesmo computador.
Resumindo, a memória do computador é um recurso que é partilhado pelos vários programas dos vários utilizadores. Quem se encarrega de fazer a gestão da memória é o sistema operativo (o Linux no nosso caso).
O próprio sistema operativo também é um programa (mais correctamente, é um conjunto de programas) e ele próprio também está a ocupar parte da memória do computador.
Quando a memória do computador começa a ficar cheia, o sistema operativo pode utilizar parte do disco para servir de memória RAM. A desvantagem disso é que o tempo de acesso ao disco é muito menor do que o tempo de acesso à memória RAM. É por essa razão que por vezes os computadores que vocês utilizam nas aulas práticas ficam lentos.
Curiosidades:
O primeiro browser gráfico da Web chamava-se Mosaic e foi feito em 1993 por Marc Andreesen, na altura tinha 22 anos e era estudante na Universidade de Illinois. Passados uns meses, Marc Andreesen terminou o curso e formou uma empresa chamada Netscape.
Aquilo que vocês aprendem nesta disciplina é apenas o aieou da programação. Ainda vos resta muito para aprender. Se estudarem muito e forem dedicados, um dia poderão ser capazes de escrever programas como o Netscape e o Linux.