Programar em C++/Alocação dinâmica de memória
Alocação dinâmica de memória
[editar | editar código-fonte]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 | editar código-fonte]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 | editar código-fonte]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 | editar código-fonte]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 | editar código-fonte]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 | editar código-fonte]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 | editar código-fonte]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 | editar código-fonte]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 | editar código-fonte]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.