AULA 9 - Programação II - Graduação
CRIAÇÃO DINÂMICA DE OBJETOS
Criar, declarar variáveis e objetos consiste, basicamente, em alocar memória. Porém, enquanto se está programando um código, muitas vezes é difícil, ou mesmo impossível, prever quanto de memória será necessário para alocar algum dado.
Por exemplo: em um programa que cadastra clientes, QUANTOS clientes vão solicitar o cadastro? Qual o tamanho do vetor que deve ser criado?? E se for criado um vetor suficientemente grande para armazenar todos os possíveis futuros clientes, o que fazer com a memória que ficará absolutamente bloqueada e ociosa, até os cadastros serem finalizados??
A solução, então, é não alocar memória enquanto o programa está sendo desenvolvido, isto é, em tempo de projeto, mas sim apenas quando for necessário, à medida que cada cadastro seja efetivado, isto é, em tempo de execução do programa.
Isto se chama Alocação dinâmica de memória!
Reservar memória dinamicamente é o caso em que não se sabe, no momento da programação, a quantidade de dados que deverão ser inseridos quando o programa já está sendo executado.
Em vez de se tentar prever um limite superior para abarcar todas as situações de uso da memória, tem-se a possibilidade de reservar memória de modo dinâmico. Outro exemplo típico 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.
Para alocar memória para um número indefinido de objetos em C++, durante a execução do programa, utiliza-se os operadores new e delete.
Com estes operadores, são usados ponteiros para reservar o espaço necessário, no momento que for necessário. Em outras palavras, o modo mais comum de alocação dinâmica 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 serão abordados estes e outros casos de uso de memória alocada dinamicamente.
Sintaxes:
NomeClasse * NomePtr = new NomeClasse ;
cria um novo objeto da classe NomeClasse, chama o construtor (se houver) e devolve para NomePtr um ponteiro para o objeto. 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.
delete NomePtr;
libera o espaço utilizado pelo objeto apontado por NomePtr e chama o destrutor do objeto (se houver).
As palavras reservadas new e delete funcionam de modo análogo às funções de alocação de memória da linguagem "C" (malloc e free), 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.
O operador new
Sem usar o operador "new" teremos um erro primário, criado inicialmente pela alocação de maneira incorreta.
Contra-Exemplo 1
Alocação dinâmica de forma incorreta:
#include <iostream>
using namespace std;
int main ()
{
int numTests;
cout << "Digite o numero de notas: ";
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 projeto e compilação.
Neste caso, alguns compiladores reportam 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".
Exemplo 1:
Reformulando o exemplo anterior agora com dados dinâmicos.
#include <iostream>
using namespace std;
int main ()
{
int numTests;
cout << "Digite o numero de notas:";
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 << "Entre nota #" << i + 1 << " : ";
cin >> iPtr[i];
}
for (int i = 0; i < numTests; i++) //Mostra o que foi preenchido ...
cout << "Nota da prova #" << 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.
Verifica-se no exemplo 1, que o uso do ponteiro que deve ser do mesmo tipo que o tipo de variável que é alocada dinamicamente:
int * iPtr = new int[numTests];
Note que não foi dado nenhum NOME ao vetor!!!
Uma vez que o vetor fica sem nome, o ponteiro deverá ser usado para acessar indiretamente cada dado.
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 vetor 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';
O operador delete
Quando o dado não for mais necessário, é importante liberar a memória. O operador "delete" entrega ao sistema operacional, ou sistema de gerenciamento de memória, os espaços de memória reservados dinamicamente.
O operador "delete" não apaga o ponteiro, mas sim a memória para onde o ponteiro aponta.
Na verdade, a memória também não é de fato apagada, mas sim 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.
Um 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 há maneira de acessar essa memória, porque não temos mais o seu endereço! Além disso não tem mais como excluí-la, também.
Caso não seja 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.
Contra-Exemplo 2:
void minhafuncao()
{ int *pt;
int av;
pt = new int[1024];
....
....
//Nenhum "delete" foi chamado...
}
int main()
{ ...
while (0)
{
minhafuncao();
}
return 0;
}
Quando a função “minhafuncao” é 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", pois é uma variável local.
Porém o espaço de memória alocado dinamicamente ainda existe. E agora não é possível mais apagar esse espaço, porque o ponteiro que o identificava foi destruído.
Analisando o programa como um todo, à 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, muita memória será perdida e o programa provavelmente vai travar.
Variáveis locais criadas dinamicamente
Analisemos agora o código abaixo, no qual há um erro:
Contra-Exemplo 3:
Retornando um ponteiro para uma variável local:
#include <iostream>
using namespace std;
char * pegaNome();
int main (void)
{
char* str = pegaNome(); //ponteiros para a função
cout << str; //???
return 0;
}
char* pegaNome(void)
{
char nome[80]; // vetor que deixará de existir quando
// a função acabar.
cout << "Digite seu nome: ";
cin.getline (nome, 80);
return nome; // o vetor nome 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.
O conteúdo do vetor "nome" deixará de ser utilizável ao final da função "pegaNome()", para operações de leitura e, 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 vetor global, mas existem alternativas melhores.
Modificador static
Exemplo 2:
Retornando um Ponteiro a uma Variável Local Estática 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 * pegaNome();
int main (void)
{
char* str = pegaNome();
cout << str;
return 0;
}
char* pegaNome(void)
{
static char nome[80]; //crio como static
cout << "Digite seu nome: ";
cin.getline (nome, 80);
return nome;
}
A diferença aqui é a palavra static.
Este modificador, quando utilizado dentro de uma função para declarar variáveis, promove o vetor à categoria de permanente, durante toda 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.
Exemplo 3:
Retornando um Ponteiro com um valor de memória Criada Dinamicamente Outra alternativa, talvez melhor:
#include <iostream>
using namespace std;
char * pegaNome();
int main (void)
{
char* str= pegaNome();
cout << str;
delete str; //faço o delete depois que o conteúdo do ponteiro não é mais necessário.
return 0;
}
char* pegaNome(void)
{
char* nome = new char[80]; //crio ponteiro chamado de name e dou o valor do endereço da memoria dinâmica
cout << "Digite seu nome: ";
cin.getline (nome, 80);
return nome;
}
Isto funciona porque o ponteiro retornado da função pegaNome() aponta para o vetor cujo tempo de vida persiste até que usemos o delete ou que o programa termine. O valor do ponteiro local "nome" é 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.
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, ou para evitar quebra de encapsulamento.
Alocação dinâmica de vetores
Ora, o que foi feito com variáveis, pode ser feito agora com vetores:
Exemplo 4:
int *pt = new int[1024]; //Aloca um Array (Vetor) de 1024 valores em int double *minhasContas = new double[10000]; /* Isso não significa que temos o valor 10000, mas sim que alocamos 10000 valores reais em formato double, para guardar o monte de milhares de contas que recebemos mensalmente. */
IMPORTANTE!!!!!!
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 vetores, usamos o ponteiro com o valor do nome do vetor:
delete pt; delete minhasContas;
A melhor maneira para inicializar um vetor dinamicamente valores é usar o loop:
Exemplo 5:
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 ponteiro, e assim facilita a remoção da alocação usando o delete.
Ponteiros pendentes (Dangling Pointers)
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 ocrreu 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.
Contra-Exemplo 4:
int *meuPonteiro;
meuPonteiro= new int(10);
cout << "O valor de meu ponteiro \202 " << *meuPonteiro<< endl;
delete meuPonteiro;
*meuPonteiro= 5; //Acesso indevido a uma área não identificada
cout << "O valor de meu ponteiro \202 " << *meuPonteiro<< 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, torná-lo NULO.
Depois disso se houver tentativa de usar o ponteiro, o compilador gerará um "run time exception" e o "bug" poderá ser identificado
Assim, corrigindo o código anterior:
Exemplo 6:
int *meuPonteiro; meuPonteiro= new int(10); cout << "O valor de meu ponteiro \202 " << *meuPonteiro<< endl; delete myPointer; meuPonteiro= 0; *meuPonteiro= 5; //Essa instrução vai causar uma "run-time exception", agora. cout << "O valor de meu ponteiro \202 " << *meuPonteiro<< endl;
Erro de alocação
Na alocação dinâmica, é necessário se certificar de que a alocação foi feita com sucesso.
Existem duas maneiras de se fazer isto:
- Tratamento de exceções;
- Uso do operador nothrow
O tratamento de exceções é o método default. Por exemplo:
bobby = new int [5]; // se isso falhar, é lançada uma "bad_alloc" (exception)
O modificador nothrow é usado da forma:
bobby = new (nothrow) int [5];
Caso o operador "new" não consiga fazer a alocação, a memória vai retornar um ponteiro nulo, e o programa continua. O problema é que método pode ser tedioso para grandes projetos
Exemplo 7:
// 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.
Alocação dinâmica de objetos
Os operadores "new" e "delete" também são usados para alocação dinâmica de objetos:
Exemplo 8:
class Aluno { ...
public:
Aluno(String^ N);
void LeIdade(int i);
... };
...
NomeObjDin = Edit1->Text;
Aluno * PtrAluno = new Aluno(NomeObjDin) ; //cria objeto
PtrAluno->LeIdade(Edit2->Text); //acessa método do objeto
...
delete PtrAluno; ... //destrói o objeto apontado por PtrAluno
DICA: você pode criar um atributo estático (static int) na própria classe (e seu método público de atualização, também static), para manter uma contagem do número de objetos criados.
OBS: inicialização na declaração!!!
OBS2: Note que é esta forma que a maioria das IDEs C++, principalmente as visuais, utiliza!
EXERCÍCIO:
Crie a classe Pessoa, contendo os atributos Nome, Endereco e RG. A classe tem um construtor e um método privado EntraDados( ). No código cliente, crie a classe CadastraPessoa e utilize-a para gerar uma lista dinâmica de objetos do tipo Pessoa, com funções de inclusão, remoção e atualização de dados dos elementos da lista.