AULA 9 - Programação II - Graduação

De IFSC
Revisão de 09h42min de 11 de abril de 2018 por imported>Fargoud (→‎CRIAÇÃO DINÂMICA DE OBJETOS)
Ir para navegação Ir para pesquisar

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.

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.

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

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 "

Exemplo:

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!!!



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 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 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 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 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) 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 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. 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 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.

Programar em C++Sobrecarga de operadoresExceções Categoria: Livro/Programar em C++ Menu de navegação Não autenticadoDiscussãoContribuiçõesCriar uma contaEntrarMóduloDiscussãoLerAlterações pendentesEditarEditar código-fonteVer históricoPesquisa

Pesquisar na wiki Wikilivros Página principal Biblioteca Página aleatória Livro aleatório Ajuda Donativos Projecto Portal comunitário Diálogos comunitários Mudanças recentes Wikijúnior Imprimir/exportar Criar uma coleção Descarregar como PDF Versão para impressão Ferramentas Páginas afluentes Alterações relacionadas Carregar ficheiro Páginas especiais Hiperligação permanente Informações da página Citar esta página

Línguas Adicionar hiperligações Esta página foi editada pela última vez à(s) 21h51min de 1 de outubro de 2017. Este texto é disponibilizado nos termos da licença Creative Commons Atribuição-Compartilhamento pela mesma Licença 3.0 Unported; pode estar sujeito a condições adicionais. Consulte as Condições de Uso para mais detalhes. Política de privacidadeSobre o WikilivrosExoneração de responsabilidadeProgramadoresDeclaração sobre cookiesVersão móvel

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.