A função free() - Liberar e evitar vazamento de memória

No artigo passado de nossa apostila de C, ensinamos o como alocar memória de uma maneira dinâmica, através da função malloc() da biblioteca stdlib.h

Agora vamos ensinar como liberar essa memória que foi previamente alocada, que é um bom hábito que evita um famoso problema, o vazamento de memória. E ao término do tutorial, vamos resolver um exercício que foi proposto no artigo passado, onde iremos mostrar o uso das funções malloc() e free().


Entre no mercado de trabalho! Clique aqui e obtenha seu certificado de programação C!

Memory leak (vazamento de memória) em linguagem C

No artigo inicial desta seção sobre alocação dinâmica de memória, citamos vários motivos pela qual o uso desta técnicas é imprescindível para qualquer programador C, e é um verdadeiro diferencial em relação à grande maioria das outras linguagens de programação, pois poucas irão lhe propiciar o poder de controlar cada byte da memória de seu computador.

Vimos que ao usar a função malloc() estamos, na verdade, reservando um espaço em memória. Isto se chama alocar, e uma vez feito isso, aquela região da memória estará protegida e não será possível usar ela para outro propósito.
Porém, por mais que tenha uma memória grande, ela será sempre limitada, e há certas aplicações (como um sistema operacional ou um jogo de alta performance) que irão exigir, e muito, de sua memória.

E é aí que surge o problema que embasa este artigo de nossa apostila: se você alocar muita memória, chegará uma hora que não vai ter mais nenhum byte disponível para uso e seu software vai simplesmente parar (as vezes seu computador simplesmente trava e não volta nem com reza brava).

Geralmente isso ocorre por uma falha de programação, pois o programador simplesmente deixou muita memória 'vazar'.
Uma maneira simples de acontecer isso é ficar alocando memória dentro de um looping e não liberar ela antes do término desse looping.

Como você deve ter estudado funções em C, quando criamos uma variável dentro do escopo da função, é como se ela não existisse fora dela. Então podemos criar um ponteiro, alocar memória e fazer o dito cujo ponteiro armazenar o endereço da memória alocada. Se não liberarmos a memória antes da função acabar, ela vai ficar eternamente alocada e inacessível, pois 'perdemos' o ponteiro quando a função terminou.

Vamos mostrar como é isso na prática.
Criamos uma função chamada aloca(), que não recebe nem retorna nada.
Dentro dela criamos um ponteiro para um inteiro e em seguida alocamos 100 bytes, através da malloc().
Obviamente, quando a função termina, esse ponteiro deixa de existir, mas a memória reservada continua lá, e inacessível.

Na nossa main() fazemos infinitas chamadas da função aloca(), que aos poucos vai reservando de 100 em 100 bytes a memória de seu computador. E aos poucos você vai notar sua máquina ficando lenta, pois vai haver cada vez menos memória, vai ficando cada vez mais 'difícil' de achar espaços para alocar até...sua máquina travar completamente.

Veja, é um código bem simples, usa coisas básicas que aprendemos aqui em nosso curso e mostra bem o que é o vazamento de memória:

#include <stdio.h>
#include <stdlib.h>

void aloca()
{
 int *ptr;
 ptr = (int *) malloc(100);
}

int main(void)
{
 while(1)
  aloca();

 return 0;
}

Se estiver no Linux, abra uma janela de seu terminal e digite: free -m -s 1
Esse comando irá mostrar, a cada 1 segundo, o estado das memórias livres em sua máquina.

free(): A função que libera memória

A solução para este tipo de problema é simples, basta usar a função free(), que vai liberar o espaço de memória que foi previamente alocado. Assim como outras funções de alocação dinâmica de memória, esta função também está na biblioteca stdlib.h

Ela recebe um ponteiro, o que foi usado para receber o endereço do bloco de memória alocada, e não retorna nada.
Ou seja, sua sintaxe é bem simples:

free(ponteiro);

O grande problema reside na pergunta 'Onde liberar memória?'
Se seu projeto for mais simples, provavelmente só vai precisar liberar ao final de sua aplicação (embora a memória seja geralmente liberada após terminar um programa, é uma boa prática de programação sempre liberar o que foi alocado antes de sua aplicação terminar).

Mas, basicamente, devemos liberar a memória sempre que não formos mais usar o que foi alocado.
Isso geralmente acontece ao final das funções ou loopings que fazem pedidos de alocação de memória.

Como exemplo, podemos 'consertar' o exemplo de código passado, simplesmente colocando free(ptr) ao final da função.
Veja que agora ela pode rodar infinitamente, pois a memória alocada é liberada após seu 'uso'.

#include <stdio.h>
#include <stdlib.h>

void aloca()
{
 int *ptr;
 ptr = (int *) malloc(100);
 
 free(ptr);
}

int main(void)
{
 while(1)
  aloca();

 return 0;
}






A importância do bom gerenciamento de memória

Como havíamos dito, usar alocação de memória fará sempre seus programas serem mais robustos e seguros, por isso indicamos que use sempre em seus projetos, mesmo nos mais simples. E obviamente, sempre libere a memória alocada após seu uso, pois o vazamento de memória é um problema muito comum, que certamente você irá se deparar, caso siga a profissão.

É um problema tão habitual, que atormenta tanto os programadores, que existem até ferramentas para detectar os 'leaks memory'. Muitas vezes isto ocorre não por um erro, mas sim por um ataque.
Os anti-vírus, por exemplo, estão sempre gerenciando o uso da memória dos computadores que protegem, pois é um tipo de ataque comum, consumir toda a memória de um sistema, para travá-lo e tirá-lo do ar.

Se isso pode ocorrer em máquinas boas e modernas, imagine para quem trabalha com microcontroladores, por exemplo, que muitas vezes possuem apenas alguns poucos kilobytes de memória. Não é à toa que engenheiros de computação que trabalham com hardware estão sempre preocupados com a maneira na qual usam a memória de seus projetos.

Linguagens de programação de alto nível, como o Java, geralmente fazem de maneira 'automática' esse gerenciamento de memória. Geralmente funciona de maneira razoável, raramente funciona de maneira perfeita.
O ideal, para garantir que seu aplicativo seja sempre o mais confiável, robusto, seguro e rápido, só mesmo gerenciando 'na mão'.

Como diz o ditado: Se quer algo bem feito, faça-o você mesmo.

Alocação de memória, free() e segurança

Embora o ensino da alocação e liberação de memória seja comum em vários livros e cursos, algumas coisas passam batido, principalmente no que se refere à segurança de uma aplicação.

Vamos mostrar agora uma maneira bem comum de explorar falhas através dos ponteiros.
Vamos criar um programa simples, que irá pedir uma senha e armazenar num local que foi previamente alocado.
Então você usa essa senha como quiser, e como é um bom programador, irá usar a free() para liberar a memória que foi usada, até mesmo por questões de segurança.

Porém, ao contrário do que muitos pensam (e aí que mora o perigo), ao liberar a memória você não vai apagar os dados existentes nela, você vai apenas dizer ao seu computador "Hey, esse bloco de bytes aqui, já usei, então você pode pegar para fazer outra coisa".

Mas as informações ainda estão lá. E como obter o que tem lá?
Através do ponteiro que ainda aponta para lá. Ou seja, a free() não vai mudar o endereço armazenado no ponteiro, ele ainda vai continuar apontando para sua senha mesmo após aquele bloco de memória ter sido liberado.

Veja o código do programa:

#include <stdio.h>
#include <stdlib.h>


int main(void)
{
 char *senha;
 
 senha = (char *) malloc(21*sizeof(char));
 printf("Digite sua senha [ate 20 caracteres]: ");
 scanf("%[^\n]s", senha);
 
 printf("Senha: %s\n", senha);
 printf("Endereço antes da free(): %d\n", &senha);
 
 free(senha);
 
 printf("Endereço depois da free(): %d\n", &senha);

 return 0;
}

O resultado:

Ou seja, o ponteiro maroto continua apontando pro local da memória que está minha senha, e alguém poderá usar de uma maneira bem maléfica caso eu não tenha gravado outra coisa por cima desse bloco de memória. Como nos proteger, então?

Uma boa prática de segurança é que sempre que for 'liberar' seus ponteiros, fazer eles apontarem para NULL.
Ou seja, faça:
ptr=NULL;

Sempre que usar a free(), pois embora tenha liberado a memória para outro uso, o ponteiro continuará apontando para aquele endereço de memória.
Assim, o ponteiro não vai mais te dedurar.

Exercício resolvido: Usando malloc() e free()

No tutorial passado de nosso curso, passamos o seguinte exercício:
Crie um programa que calcula a média de uma quantidade qualquer (informada pelo usuário) de números.
O programa deve armazenas esses números em um vetor. Depois, use esse vetor para mostrar todos os números e mostrar a média dele.
Use alocação dinâmica de memória para colocar os números no vetor. Não desperdice memória.

Vamos resolvê-lo agora para ilustrar o uso da malloc() e da free().
Na main(), o programa inicia um looping, que só para se o usuário digitar 0.

Neste looping é pedido um número inteiro, que será o tanto de números que o usuário vai digitar.
Após ele fornecer essa informação, passamos ela para a função aloca() que vai alocar dinamicamente um vetor de inteiros, com o número de elementos exato que o usuário digitou, e retornar o endereço desse espaço alocado.

Esse endereço é armazenado no ponteiro *numeros, da main().
Em seguida, mandamos esse vetor e o número de elementos para a função media(), que irá calcular a média de todos os elementos deste vetor e retornar esse float (a média de inteiros pode ser um número decimal).
Também mandamos os mesmos argumentos para a função exibe(), que irá mostrar os números digitados.

Após cada iteração do laço while, devemos liberar a memória que está apontado pelo ponteiro *numeros, senão fizermos isso a função aloca() vai alocar um espaço diferente de memória a cada iteração, consumindo a memória aos poucos.

#include <stdio.h>
#include <stdlib.h>

int *aloca(int num)
{
 int count,
  *numbers;
 
 numbers = (int *)malloc(num*sizeof(int));
 
 for(count=0 ; count < num ; count++)
 {
  printf("Numero [%d]: ", count+1);
  scanf("%d", &numbers[count]);
 }
 
 return numbers;
}

float media(int *numbers, int num)
{
 float media=0.0;
 int count;
 
 for(count=0 ; count<num ; count++)
  media += numbers[count];
 
 return media/num;
}

void exibe(int *numbers, int num)
{
 int count;
 
 for(count=0 ; count < num ; count++)
  printf("%3d", numbers[count]);
}

int main(void)
{
 int num=1,
  *numeros;
 
 while(num)
 {
  printf("Media de quantos numeros [0 para sair]: ");
  scanf("%d", &num);
  
  if(num > 0)
  {
   numeros = aloca(num);
   exibe(numeros,num);
   printf("\nA media destes numeros eh: %.2f\n", media(numeros, num) );
   free(numeros);
  }
 }

 return 0;
}


Esse programa calcula a média de 1, 2, 10, mil ou 1 milhão de números.
E o melhor, só aloca 1, 2, 10, mil ou exatos 1 milhão de bytes, nem um a mais. Extramente econômico, não deixa seu computador 'lerdo' por consumir memória demais, o que é um grande problema ocasionado por péssimos programadores.

A Wikipedia possui um excelente artigo sobre Memory Leak.

5 comentários:

João Lucas disse...

Olá C Progressivo,

Copiei o código do programa que nos pede uma senha, adicionei apenas mais uma linha exatamente depois do comando "free(senha);".

Logo abaixo de comando coloquei: "senha = NULL;"

Quis saber se o ponteiro realmente para outro endereço de memória senão o alocado anteriormente.

Para minha surpresa, após digitar a senha, o endereço do ponteiro senha continuo absolutamente o mesmo.

Revisei o código e aparentemente não encontrei nada de errado, enfim, testei em outro código teste que fiz, o resultado não foi diferente.

Gostaria de saber o que pode estar acarretando esse não apontamento para NULL.

PS: Como alternativa, já tentei apontar o ponteiro para um inteiro 0 e o resultado foi o mesmo "senha = 0;".

Anônimo disse...

Não muda, pq ele colocou null o endereço em que senha recebe. Mas no programa ele mostra o endereço de senha não o endereço que senha guarda. Se vc tirar & do printf que mostra senha, verá que limpou o endereço armazenado em senha, ou seja, quaisquer que sejam os dados apontado pelo ponteiro virou lixo.

Anônimo disse...

Se voce imprimir a senha após a chamada do free(), o conteúdo do ponteiro também estará limpo, então não entendi exatamente o porque de setar para NULL após o free(). De qualquer forma você não tem mais acesso ao conteúdo.

Anônimo disse...

Galera
O conceito de uso do NULL esta certo, porém, pra tirar a prova real se o conteúdo foi apagado vocês estão retornado o endereço de memoria do ponteiro e não do endereço de memoria que deve se apagado do ponteiro.

O CORRETO SERIA:

char *senha;

senha = (char *) malloc(21*sizeof(char));
printf("Digite sua senha [ate 20 caracteres]: ");
scanf("%[^\n]s", senha);

printf("Senha: %s\n", senha);
printf("Endereço antes da free(): %d\n", senha);

free(senha);

printf("Endereço depois da free(): %d\n", senha);

return 0;
----------------------------------------------------


E DEPOIS DE USAR O NULL:


char *senha;

senha = (char *) malloc(21*sizeof(char));
printf("Digite sua senha [ate 20 caracteres]: ");
scanf("%[^\n]s", senha);

printf("Senha: %s\n", senha);
printf("Endereço antes da free(): %d\n", senha);

free(senha);
senha = NULL;


printf("Endereço depois da free(): %d\n", senha);

return 0;

por: m.menezes.costa2014@bol.com.br

Anônimo disse...

O cara aí em cima está certo: "vocês estão retornado o endereço de memoria do ponteiro e não do endereço de memoria que deve se apagado do ponteiro."

Gostou desse tutorial de C?
Sabia que o acervo do portal C Progressivo é o mesmo, ou maior que, de um livro ou curso presencial?
E o melhor: totalmente gratuito.

Mas para nosso projeto se manter é preciso divulgação.
Para isso, basta curtir nossa página no Facebook e/ou clicar no botão +1 do Google.
Contamos e precisamos de seu apoio.