quinta-feira, 4 de agosto de 2011

Os problemas de não alocar e desalocar corretamente

Você já aprendeu na escola:

"Se alocou com malloc, desaloque com free
Se alocou com new, desaloque com delete
Se alocou com new[], desaloque com delete[]"

Tudo bem, você tenta usar certo, mas é um cara esquecido e no meio do seu código você coloca:

char* dados = new char[50];
...
delete dados;

Depois de anos, você percebe o erro. Mas este código já rodou há muito tempo e nunca deu problema. Para comprovar, você cria um programinha novo e faz o seguinte teste:

char* dados;

while ( 1 )
{
    dados = new char[50];
    delete dados;
}

Fica observando o tamanho do programa em memória e percebe que ele nunca aumenta! O delete está desalocando corretamente, mesmo sem os []!

Abismado, você faz mais um teste:

while ( 1 )
{
    dados = new char[50];
    free( dados );
}

E tudo funciona corretamente! Será que o compilador é inteligente e sabe o que faz? Ou será simplesmente frescura o que dizem sobre desalocar da mesma forma que alocou?

Meu rapaz, estes exemplos acima podem funcionar perfeitamente, mas somente para os tipos básicos (int, char, double...). Os problemas de verdade vão aparecer quando você estiver manipulando objetos em C++. E não espere que seu compilador vá te ajudar nessas horas.

Vamos aos exemplos de onde você terá problemas:

malloc x new / free x delete


As funções malloc/free são funções de C que simplesmente reservam e liberam espaço na memória. O new e o delete são funções de C++ que tem função semelhante, porém também chamam o construtor/destrutor de seus objetos.
Se você estiver programando em C++, é uma boa prática esquecer o malloc/free e utilizar sempre new/delete (mesmo que você esteja alocando um tipo básico que não possui construtor/destrutor).

Vamos a um exemplo prático em que você poderia ter problema de utilizar incorretamente estas funções:

Temos a seguinte classe definida:

class X
{
public:
    X()
    {
        printf("Entrou no construtor\n");
        mpDados = new char[20];
    }
    ~X()
    {
        printf("Destruindo objeto\n" );
        delete[] mpDados; //Que bonito! Alocou com new[]
                          //e desalocou com delete[]!
    }

private:
    char* mpDados;
};

A alocação/desalocação de dados está beleza, mas você, em seu código, faz o seguinte:

void func()
{
    X* obj = new X();
    free( obj );
}

Seu código compila e a função func é executada sem travar, mas você percebe que o output na tela será simplesmente:


Entrou no construtor

Não foi impresso nenhum "Destruindo o objeto"! Isso significa que o delete[] mpDados jamais foi chamado, e os 20 bytes alocados no construtor jamais serão liberados. Se você fizer um

while ( 1 )
{
    func();
}

notará que a memória utilizada pelo seu programa crescerá infinitamente.

Agora vamos supor que você fez o contrário:

X* obj = (X*)malloc( sizeof( X ) );
delete obj;

Notará que o construtor de X não é chamado. Quando o delete chamar o destrutor, seu código provavelmente irá travar no delete[] mpDados, pois o mpDados estará apontando para uma posição inválida da memória.


E se fizer:

X* obj = (X*)malloc( sizeof( X ) );
free( obj ); 

"Aloquei com malloc e liberei com free! Está correto, nao está??"

Este código irá funcionar corretamente, mas usá-lo assim é suicídio.
O que ele está fazendo é simplesmente alocar uma região na memória com o tamanho do objeto X e liberá-la depois. Mas X não foi inicializado. Qualquer outra função da classe que você tentar utilizar vai dar problema.

Vamos supor que X possuísse uma função para setar o mpDados:

void X::SetDados( const char* dados )
{
    assert( strlen( dados ) < 20 );
    strcpy( mpDados, dados );
}

E você faz o seguinte:

X* obj = (X*)malloc( sizeof( X ) );
obj->SetDados( "abcde" );
free( obj ); 

Já imagina o que vai acontecer, né?

Por isso, em c++, esqueça malloc e free. Deixe essas funções para os códigos de C.


delete x delete[]


Ok, entendi o problema com malloc e free, mas qual o problema de alocar com new[] e desalocar com delete?

Continuando com o exemplo da classe X. Faça o seguinte:

X* meuArrayX = new X[5];
delete[] meuArrayX;

Qual será o output na tela?
Bom, o new irá chamar o construtor para cada um dos 5 objetos criados, e o delete[] irá fazer a mesma coisa com os destrutores.

O output será:

Entrou no construtor
Entrou no construtor
Entrou no construtor
Entrou no construtor
Entrou no construtor
Destruindo objeto
Destruindo objeto
Destruindo objeto
Destruindo objeto
Destruindo objeto

Agora experimente:

X* meuArrayX = new X[5];
delete meuArrayX;

Isso é o que a ISO chama de "undefined behavior". Na maioria dos compiladores, você terá uma saída do tipo:

Entrou no construtor
Entrou no construtor
Entrou no construtor
Entrou no construtor
Entrou no construtor
Destruindo objeto

Ou seja, somente o primeiro objeto será desalocado. Em outros compiladores, é possível que o código trave no delete.

Já deu para entender, não é?

3 comentários:

  1. valeu cara ajudou muito. Estou começando em c++, mas já deu uma clareada.

    ResponderExcluir
  2. Olá, gostaria de saber como faço um teste simples de desalocação de memória?
    no exemplo a seguir faço o teste de alocação ex:

    float *array = new float[10];


    if(array == NULL){
    cout << "erro de alocação de memoria!" << endl;
    }else{
    cout << "memoria alocada com sucesso!" << endl;
    }

    //porém após o "delete[] array;" não sei como testar se realmente desalocou..

    ResponderExcluir
    Respostas
    1. Fala rapaz,
      Não há como saber se sua memória foi desalocada ou não. Porém, salvo se você tenha um problema físico de memória, não há porque ele não desalocar corretamente. Se tiver problema com alguma invasão de memória, provavelmente vai dar algum erro e sair do programa no momento de desalocar.
      Se você quiser conferir somente para não correr o risco de deletar duas vezes, uma boa prática é apontar para NULL depois de deletar.

      Ex:

      {
      char* array = new char[10];
      ...
      delete[] array;
      ...
      delete[] array; //vai dar erro de execução
      }

      agora se fizer:

      {
      char* array = new char[10];
      ...
      delete[] array;
      array =NULL;
      ...
      delete[] array; //não vai dar erro, a linha vai ser ignorada

      }

      Espero ter aclarado suas dúvidas! Qualquer coisa é só perguntar!

      Excluir