A forma principal de abstração para a memória da linguagem C são as variáveis. A criação de uma variável é uma forma organizada de se dizer ao compilador que se quer um tanto de memória, que esse tanto vai ser usado para armazenar dados de um determinado tipo, que vai passar a ser referenciado por tal nome dentro do programa. O compilador vai então verificar que todo o uso dessa memória é realizado de acordo com esse "contrato", e vai tentar otimizar a quantidade de memória necessária para esse uso (por exemplo, quando uma função começa sua execução, suas variáveis precisam de memória, mas quando uma função termina de executar, essas variáveis não são mais necessárias, e a memória que elas estão utilizando pode ser reutilizada para outra coisa -- isso é feito automaticamente pelo compilador).
Mas essa forma de usar memória por vezes tem limitações, e em algumas situações surge a necessidade de se ter um controle maior sobre o uso da memória. Por exemplo, para poder usar memória além da pré-definida pelas variáveis presentes no programa (pense em um programa que só vai poder definir quanto de memória vai precisar depois que já está executando, porque lê dados de um arquivo, por exemplo), ou para organizar o uso da memória de uma forma diferente da imposta pela alocação e liberação ligada automaticamente à ordem de execução das funções (uma função que cria uma variável e gostaria que ela pudesse ser usada pela função que a chamou, por exemplo).
Para esses casos, tem-se a alocação explícita de memória (mais conhecida como alocação dinâmica, que é um nome pior, porque a alocação automática feita pelo mecanismo de execução das funções também é dinâmica). Nessa forma de alocação de memória, é o programador quem realiza a alocação e a liberação da memória, no momento que considerar mais adequado. Essa memória é por vezes chamada de anônima, porque não é vinculada a uma variável com nome pré-definido.
Como essa memória não é associada a variáveis, a forma que se tem para usar esse tipo de memória é através de ponteiros.
Existem duas operações principais de manipulação desse tipo de memória: a operação de alocação e a operação de liberação de memória. Quando se aloca memória, se diz quanto de memória se quer (quantos bytes), e se recebe do sistema esse tanto de memória, na forma de um ponteiro para a primeira posição do bloco de memória alocado. As demais posições seguem essa primeira, de forma contígua, como em um vetor. Para se liberar a memória, passa-se um ponteiro para essa mesma posição, o sistema sabe quanto de memória foi alocada e faz o necessário para disponibilizar essa memória para outros usos. Depois de liberado, o bloco de memória não pode mais ser utilizado.
Essas operações estão disponibilizadas em C na forma de funções, acessíveis incluindo-se stdlib.h
.
Essas funções são malloc
e free
.
A função malloc
recebe um único argumento, que é a quantidade de bytes que se deseja, e retorna um ponteiro para a região alocada, que tem esse número de bytes disponíveis. Caso a alocação não seja possível, o ponteiro retornado tem um valor especial, chamado NULL
. Sempre deve-se testar o valor retornado por malloc
para verificar se a alocação de memória foi bem sucedida.
A função free
recebe um único argumento, que é um ponteiro para a primeira posição de memória do bloco a ser liberado, obrigatoriamente o mesmo valor retornado por um pedido de alocação de memória anterior.
Não existe limitação no tamanho de um bloco a alocar, nem na quantidade de blocos alocados, a não ser a quantidade de memória disponível.
Para facilitar o cálculo da quantidade de memória, existe o operador sizeof
, que dá o número de bytes usado por qualquer tipo de dados. Por exemplo, sizeof(double)
diz quantos bytes de memória são necessário para se armazenar um valor do tipo double
.
Como a memória alocada é contígua, uma forma usual de se usar a memória alocada é como um vetor. Como vimos anteriormente, o uso de um vetor através de um ponteiro é muito semelhante (pra não dizer igual) ao uso de um vetor diretamente. O fato de o ponteiro estar apontando para memória alocada explicitamente ou estar apontando para memória que pertence a um vetor de verdade não muda a forma de uso.
Por exemplo, para se alocar memória para se usar como um vetor de tamanho definido pelos dados, pode-se usar algo como:
#include <stdio.h>
#include <stdlib.h>
float calcula(int n, float v[n])
{
// faz um cálculo complicado sobre os elementos do vetor
float t=0;
for (int i=0; i<n; i++) {
t += v[i];
}
return t/n;
}
int main()
{
float *vet;
int n;
printf("Quantos dados? ");
scanf("%d" , &n);
vet = malloc(n * sizeof(float));
if (vet == NULL) {
printf("Me recuso a ser explorado dessa forma vil!\n");
return 5;
}
// a partir daqui, vet pode ser usado como se fosse um vetor de tamanho n
for (int i=0; i<n; i++) {
printf("digite o dado %d ", i);
scanf("%f", &vet[i]);
}
float resultado = calcula(n, vet);
printf("O resultado do cálculo é: %f\n", resultado);
free(vet);
// a partir daqui, a região apontada por vet não pode mais ser usada.
return 0;
}