Programar em C++/Alocação dinâmica de memória

Alocação dinâmica de memória

editar

Refere-se ao recurso, existente nas linguagens de programação, onde podemos deixar de reservar memória quando o programa ainda está na fase de desenvolvimento, quando não sabemos ainda de quanta memória o programa irá precisar para executar algum procedimento, e o fazemos durante a execução. Podemos alocar, ou seja, reservar uma quantidade de bytes para serem usados, de duas maneiras diferentes: Em tempo de compilação ou em tempo de execução, aqui tratamos do segundo caso.

O compilador reserva espaço na memória para todos os dados declarados explicitamente, mas se usarmos ponteiros precisamos colocar no ponteiro um endereço de memória existente. Para isto, podemos usar o endereço de uma variável definida previamente ou reservar o espaço necessário no momento que precisemos. Este espaço que precisamos reservar em tempo de execução é chamada de memória alocada dinamicamente.

Reservar dinamicamente é o caso em que não sabemos, no momento da programação, a quantidade de dados que deverão ser inseridos quando o programa já está sendo executado. Em vez de tentarmos prever um limite superior para abarcar todas as situações de uso da memória, temos a possibilidade de reservar memória de modo dinâmico. Os exemplos típicos disto são os processadores de texto, nos quais não sabemos a quantidade de caracteres que o utilizador vai escrever quando o programa estiver sendo executado. Nestes casos podemos, por exemplo, receber a quantidade de caracteres que o usuário digita e depois alocamos a quantidade de memória que precisamos para guardá-lo e depois o armazenamos para uso posterior.

O modo mais comum de alocação de memória é o de alocar certa quantidade de bytes e atribuí-la a um ponteiro, provendo um "array", ou vetor. Nos tópicos a seguir abordaremos estes e outros casos de uso de memória alocada dinamicamente.

Operador new

editar

Existem duas maneira de alocar memória dinamicamente em tempo de execução: Em "C" e em "C++" podemos usar as funções de alocação dinâmica de memória existentes nas bibliotecas padrões, ou usamos uma palavra chave da linguagem C++ chamada new. Tratemos, portanto, do segundo caso pois é específico da linguagem "C++".

De fato esta palavra chave é considerada um operador, sua função é reservar memória em tempo de execução do programa. Ela funciona de modo análogo às funções de alocação de memória da linguagem "C", nas quais, indicamos a quantidade de bytes que queremos alocar e as mesmas reservam o referido espaço necessário. Porém, o operador new tem uma sintaxe a ser seguida, a qual será abordada mais adiante.

Sem usar o operador "new" teremos um erro primário, criado inicialmente pela alocação de maneira incorreta. Vamos ver um exemplo:

Exemplo de alocação dinâmica de forma incorreta:

 #include <iostream>
 using namespace std;
 int main ()
 {
   int numTests;
   cout << "digite o numero de testes : ";
   cin >> numTests;
   int testScore[numTests]; //Erro de declaração
   return 0;
 }

De fato, no exemplo acima, podemos ver que numTests não tem valor definido durante a compilação, neste caso o compilador reporta um erro, normalmente exigindo um valor ou uma constante declarada como valor literal. A razão da exigência de ter uma constante (ou literal) é que vamos alocar memória para o "array" no ato da compilação, e o compilador necessita saber exatamente a quantidade de memória que deve reservar… porém, se o número entre colchetes é uma variável, o compilador não sabe quanta memória deveria reservar para alocar o "array".

Reformulando o exemplo anterior agora com dados dinâmicos:

 #include <iostream>
 using namespace std;
 int main ()
 {
   int numTests;
   cout << "Enter the number of test scores:";
   cin >> numTests;        
   int * iPtr = new int[numTests];           //colocamos um ponteiro no inicio da memória dinâmica

   for (int i = 0; i < numTests; i++) //Podemos preecher o espaço de memória da forma que quisermos
   {
      cout << "Enter test score #" << i + 1 << " : ";
      cin >> iPtr[i];
   }

   for (int i = 0; i < numTests; i++)  //Mostramos o que foi preenchido ...
      cout << "Test score #" << i + 1 << " is "<< iPtr[i] << endl;
   delete iPtr;
   return 0;
 }

Agora conseguimos criar um "array" onde é o utilizador poderá definir o tamanho do "array" e depois colocar o valor para cada um dos elementos.

O operador "new" retorna o endereço onde começa o bloco de memória que foi reservado. Como retorna um endereço podemos colocá-lo num ponteiro. Assim, teremos um meio de manipular o conteúdo da memória alocada toda vez que mencionarmos o ponteiro.

Verificamos no exemplo o uso do ponteiro que deve ser do mesmo tipo que o tipo de variável que é alocado dinamicamente:

int * iPtr = new int[numTests];
  • Temos o termo new. Que é um operador cuja função é alocar dinamicamente memória;
  • Temos o tipo da variável alocada dinamicamente: "int";
  • Repare que NÃO temos o nome do "array";
  • Uma vez que o "array" fica sem nome, para nos referirmos a cada elemento do mesmo teremos de usar o ponteiro.

Podemos inicializar o ponteiro de duas maneiras:

 int *IDpt = new int;  // Alocamos um único elemento dinâmico do tipo "int"
 *IDpt = 5;            // Atribuímos o valor 5 dentro da memória dinâmica

ou

 int *IDpt = new int(5);        //Alocamos o objeto int e inicializamo-lo com 5.
 char *letter = new char('J');  //Alocamos o objeto char e inicializamo-lo com 'J'.

Por outro lado, se quisermos criar um "array" de objetos "char" podemos proceder da forma como foi dado anteriormente:

 int *AIDpt = new char[4];    //Aloca 4 objetos de caracteres.
 AIDpt[0] = 'A'; // Podemos preencher os valores de cada elemento
 AIDpt[1] = 'M';
 AIDpt[2] = 'O';
 AIDpt[3] = 'R';

Operador Delete

editar

Se realmente não necessitamos mais dos dados que estão num endereço de memória dinâmica, devemos apagá-la! Necessitamos liberar essa memória através do operador delete – este operador entrega ao sistema operacional ou sistema de gerenciamento de memória os espaços de memória reservados dinamicamente.

A sintaxe é

 delete iPtr;

O operador delete não apaga o ponteiro mas sim a memória para onde o ponteiro aponta. Na verdade, como já dissemos anteriormente, a memória não é de fato apagada, ela retorna ao estado de disponível como memória livre para ser alocada novamente quando for necessário.

No caso de alocação de memória utilizando ponteiros locais a ação de criar o espaço através do operador new deve ser seguida de um delete depois que o espaço alocado não for mais necessário. Da mesma forma, se o espaço alocado continuará a ser necessário no resto do programa o endereço deverá ser mantido em ponteiro global ou deverá ser retornado para ser usado depois da execução da função.

O tempo de vida de uma variável criada dinamicamente é o tempo de execução do programa. Se fizermos um delete e tivermos um ponteiro que aponta para o endereço que não esteja mais alocado, não conseguiremos acessar nenhuma memória específica e, neste caso, acessaremos uma posição de memória qualquer, geralmente a que está armazenada dentro do ponteiro. Esse valor de memória muitas vezes é zero ou qualquer outro sem sentido, um valor que em algum momento foi criado pelo processador em qualquer operação anteriormente executada.

Outro problema comum: Se alocamos memória dinamicamente dentro de uma função usando um ponteiro local, quando a função termina, o ponteiro será destruído, mas a memória mantém-se. Assim já não teríamos maneira de acessar essa memória porque nós não temos mais o seu endereço! Além disso não teremos mais como excluí-la.

Caso não for excluída a memória dinâmica alocada com o operador new através do operador delete, o programa irá acumular memória alocada (reservada), o que levará à parada inesperada do programa por falta de memória para alocação quando outra operação new for solicitada e não haver mais espaço para alocar memória.

Temos este exemplo:

 void myfunction()
 {
    int *pt;
    int av;
    pt = new int[1024];
    ....
    ....
    //Nenhum "delete" foi chamado...
 } 
 int main()
 {
    while (0) 
    {
        myfunction();
    }
    return 0;
 }

Quando a função “myfunction” é chamada a variável “av” é criada na pilha e quando a função acaba a variável é perdida. O mesmo acontece com o ponteiro pt, ele é uma variável local. ou seja quando a função acaba o ponteiro também termina e é perdido. Porém o espaço de memória alocado dinamicamente ainda existe. E agora não conseguimos apagar esse espaço porque não temos mais o ponteiro e a única maneira que tínhamos para saber onde ele estava era através do ponteiro que foi perdido quando a função foi finalizada.

Analisando o programa como um todo temos, à medida que o programa continua a executar, mais e mais memória que será perdida no espaço de alocação dinâmica controlado pelo sistema operacional. Se o programa continuar deixaremos de ter memória disponível e o programa deixará de operar. Nos sistemas operacionais atualmente em uso o programa será interrompido e uma mensagem de erro será retornada para o usuário.

Retornando um ponteiro para uma variável local

editar

Analisemos o código abaixo, no qual há um erro:

 #include <iostream>
 using namespace std;
 char * setName();
 int main (void)
 {
   char* str = setName(); 	//ponteiros para a função
   cout << str; 	        //imprimo o valor do ponteiro?
   return 0;
 }
 char* setName (void)
 {
   char name[80];             // vetor criado na pilha, o mesmo deixará de existir quando 
                              // a função acabar.
   cout << "Enter your name: ";
   cin.getline (name, 80);
   return name;              // E aqui temos o erro... o vetor name não poderá ser lido 
                             // quando a função retornar , logo pode gerar um erro em 
                             // tempo de execução.
 }


Neste código podemos ver como os ponteiros mal administrados podem causar falhas que podem levar o programa a abortar em tempo de execução. Como está escrito nos comentários do código, o conteúdo do vetor name deixará de ser utilizável, nem para operações de leitura ou, principalmente em operações de escrita. Neste segundo caso provocará um acesso a memória não autorizado, o programa poderá travar ou abortar.

A solução é estender o tempo de vida do ponteiro e do seu endereço destino. Uma solução possível seria tornar esse "array" global, mas existem alternativas melhores.

Retornando um Ponteiro a uma Variável Local Estática

editar

No código a seguir temos uma alternativa para o uso de vetores e retorno de seu conteúdo:

 #include <iostream>
 using namespace std;
 char * setName();
 int main (void)
 {
   char* str = setName();
   cout << str;
   return 0;
 }
 char* setName (void)
 {
   static char name[80]; 	//crio como static
   cout << "Enter your name: ";
   cin.getline (name, 80);
   return name;
 }

A diferença é que usamos a palavra static. Este modificador static, quando utilizado dentro de uma função para declarar variáveis, promove o vetor à categoria de permanente durante a execução do programa. Sendo assim, teremos como utilizar o endereço do ponteiro str tanto dentro como fora da função setName() e assim evitaremos de acessar uma memória não existente.

Retornando um Ponteiro com um valor de memória Criada Dinamicamente

editar

Outra alternativa, talvez melhor:

 #include <iostream>
 using namespace std;
 char * setName();
 int main (void)
 {
  char* str= setName();
  cout << str;
  delete str;            //faço o delete depois que o conteúdo do ponteiro não é mais necessário.
  return 0;
 }
 char* setName (void)
 {
  char* name = new char[80];     //crio ponteiro chamado de name e dou o valor do endereço da memoria dinâmica
  cout << "Enter your name: ";
  cin.getline (name, 80);
  return name;
 }

Isto funciona porque o ponteiro retornado da função setname() aponta para o "array" cujo tempo de vida persiste até que usemos o delete ou que o programa termine. O valor do ponteiro local name é atribuído na função main() a outro ponteiro str, desta forma podemos manipular o conteúdo da memória alocada até que não precisemos mais dele.

Este é um exemplo onde diferentes ponteiros apontam para o mesmo endereço. Na verdade podemos atribuir a qualquer ponteiro o endereço alocado, isso nos dá a possibilidade de manipular os dados armazenados na memória dinamicamente alocada em qualquer local onde seu endereço seja conhecido.

Para manter a segurança do código é sempre aconselhável que tenhamos um controle rígido sobre os ponteiros e seus valores (endereços armazenados). É imprescindível que os espaços de memória apontados por ponteiros sejam eliminados (liberados), quando não forem mais necessários, para evitar que o programa continue a reservar memória e não liberar, levando ao esgotamento da memória ou crescimento desnecessário do uso de memória por parte do programa.

Alocar dinamicamente Arrays (Vetores)

editar

Ora o que fizemos antes com variáveis, podemos fazer com "arrays", como já introduzimos brevemente anteriormente. Vejamos um exemplo:

int *pt = new int[1024];    //Aloca um Array (Vetor) de 1024 valores em int
 double *myBills = new double[10000];    
 /* Isso não significa que temos o valor 10000, mas sim que alocamos 10000 valores em double para guardar o monte de milhares de contas que recebemos mensalmente. */

Notar a diferença:

int *pt = new int[1024];    //Aloca um vetor que pode ter 1024 valores em int diferentes
int *pt = new int(1024);    //Aloca um único int com valor de 1024 (uma variável inicializada)

Para utilizar o delete em arrays usamos o ponteiro com o valor da primeira célula do array:

 delete pt;
 delete myBills;

A melhor maneira para alocar um "array" dinamicamente e inicializá-lo com valores é usar o loop:

int *buff = new int[1024];
 for (i = 0; i < 1024; i++) 
 {
    *buff = 52; //Assimila o valor 52 para cada elemento
     buff++;
 }
 ...
 ...
 buff -= 1024;
 delete buff;

	//ou se quisermos desta maneira:
 int *buff = new int[1024];
 for (i = 0; i < 1024; i++) 
 {
    buff[i] = 52; //Assimila o valor 52 para cada elemento
 }
 ...
 ...
 delete buff;

Note que a segunda forma, além de mais elegante, não altera o valor do vetor e assim facilita a remoção da alocação usando o delete.

Dangling Pointers

editar

Quando temos um ponteiro que não tem em seu conteúdo um valor de memória válido, o qual pertence ao programa sendo executado, temos um problema grave, principalmente se isso ocorreu por engano. Isso pode ser um problema difícil de identificar, principalmente se o programa já está com um certo número de linhas considerável. Vejamos um exemplo:

int *myPointer;
 myPointer = new int(10);
 cout << "O valor de myPointer e " << *myPointer << endl;
 delete myPointer;
 *myPointer = 5; //Acesso indevido a uma área não identificada.(Lembre-se que o ponteiro perdeu o valor válido de memória que apontava antes do '''delete'''. Agora a área de memória não pertence mais ao programa.).
 cout << "O valor de myPointer e " << *myPointer << endl;

Neste exemplo liberamos a memória dinâmica, mas o ponteiro continua a existir. Isto é um "bug", e muito difícil de detectar. Acontece que se essa memória for acessada ou escrita será corrompida. A melhor maneira de evitar isso é, depois do delete, fazer o ponteiro apontar para zero, fazê-lo um ponteiro nulo. Depois disso se tentarem usar o ponteiro iremos ter a "run time exception" e o "bug" poderá ser identificado

Assim, corrigindo o código anterior ficaríamos com:

int *myPointer;
 myPointer = new int(10);
 cout << "The value of myPointer is " << *myPointer << endl;
 delete myPointer;
 myPointer = 0;
 *myPointer = 5;    //Essa instrução vai causar uma "run-time exception", agora.
 cout << "The value of myPointer is " << *myPointer << endl;

Verificar a existência de memória para dinâmica

editar

Na alocação dinâmica temos de nos certificar de que a alocação no heap foi feita com sucesso e podemos ver isso de duas maneiras:

  • Uma são as exceções (este é o método defaut)
 bobby = new int [5];  // se isso falhar, é lançada uma "bad_alloc" (exception)

vamos ver este caso quase no ultimo capitulo- isto é uma capítulo avançado

  • nothrow, aqui no caso de não se conseguir a memória retorna um ponteiro nulo, e o programa continua.
 bobby = new (nothrow) int [5];

supostamente este método pode ser tedioso para grandes projetos


Vamos ver um exemplo com o caso de nothrow

 // rememb-o-matic
 #include <iostream>
 using namespace std;
 int main ()
 {
  int i,n,*p;
  cout << "Quantos números você deseja digitar? ";
  cin >> i;
  p= new (nothrow) int[i];            //criamos I variaveis na execução
  if (p == 0)
    cout << "Erro: a memória não pode ser alocada."<<endl;
  else
  {
    for (n=0; n<i; n++)
    {
      cout << "Digite o número: ";
      cin >> p[n];
    }
    cout << "Você digitou os seguintes números: ";
    for (n=0; n<i; n++)
      { cout << p[n];
        if (n<i-1) 
            cout << ", ";
      }
    cout<<endl;
    delete p;
  }
  return 0;
 }

Neste simples exemplo implementamos uma forma de alocar posições de memória para um vetor de 'n' elementos, de forma que o usuário possa digitar a quantidade de elementos que ele vai digitar e depois proceda com a entrada dos valores. Veja que o número de elementos é determinado pelo próprio usuário no início do programa, logo depois de indicar a quantidade de números a ser digitada o programa pede que seja alocada a quantidade de memória necessária para guardar os números a serem digitados. Neste momento, caso algum erro de alocação ocorrer, o sistema retorna o ponteiro nulo e podemos então, abortar o prosseguimento do programa, mostrando uma mensagem de erro em seguida para que o usuário fique ciente que ocorreu um erro.