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

De IFSC
Revisão de 10h36min de 5 de dezembro de 2019 por imported>Fargoud (→‎Alocaçã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. 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 (1)
{
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 meuPonteiro;
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:

  1. Tratamento de exceções;
  2. 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:

Arquivos da Classe Aluno - Aluno.h:

class Aluno {  ...  
 public: 	
      Aluno(String^ N); 
      void LeIdade(int i); 
... };

Arquivos da Classe Aluno - Aluno.cpp:

Aluno::Aluno( )
{  
}
Aluno::Aluno(String ^N)
{ nome = N;
}
void Aluno::LeIdade (int i)
{
  idade = i;
}

Arquivos da Interface Gráfica - Form1.h:

NomeObjDin = textBox1->Text;
Aluno * PtrAluno = new Aluno(NomeObjDin) ; //cria objeto
PtrAluno->LeIdade(textBox2->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!

Exemplo 9:

Questões 1 a 3 da prova, agora usando alocação dinâmica do vetor de Objetos

#include <iostream>
#include <string>
using namespace std;
//QUESTÃO 1 DA PROVA ***********************
class Item     // Declaração da classe
{ public:
   string NomeItem;
   int Quantidade;
   float ValorUnitario;
   float TotalItem;
   Item();
   Item(string, int, float);
private:
   float CalculaTotal();
};
//implementação da classe - Construtor:
Item::Item()
{
   cout<< "\nEntre com o nome do produto: ";
   cin >> NomeItem;
   cout<<"\nEntre com a quantidade de itens deste produto: ";
   cin >> Quantidade;
   cout<<"\nEntre com o preco do produto: ";
   cin >> ValorUnitario;
   cout<< "\nValor total do Item:    R$" << CalculaTotal() ;
   cout <<"\n\n\n"<< endl  ;
}
//implementação da classe - Método privado:
float Item::CalculaTotal()
{
   TotalItem = ValorUnitario * Quantidade;
   return TotalItem;
}        // QUESTÃO 1 DA PROVA ***********************
//implementação da classe - Construtor sobrecarregado:
Item::Item(string Nome, int x, float y)      // QUESTÃO 2 DA PROVA ***********************
{
   NomeItem= Nome;
   Quantidade=x;
   ValorUnitario=y;
   cout << "\nItem: " << NomeItem;
   cout << "\nNumero de itens: "<< Quantidade;
   cout << "\nValor unitario: "<< ValorUnitario;
   cout<< "\nValor total do Item:    R$" << CalculaTotal() ;
   cout <<"\n\n\n"<< endl  ;
}    // QUESTÃO 2 DA PROVA ***********************
 //declaração da classe Estoque:
class Estoque    // QUESTÃO 3 DA PROVA ***********************
{   public:
    int NItens;
    Estoque();
    private:
   float CalculaTotalEstoque(Item []);
};
//implementação da classe - Construtor:
Estoque::Estoque()
{
   cout << "************************" <<  endl;
   cout << " Cadastro de Produtos "<< endl;
   cout<< "\nEntre com o numero de diferentes produtos no estoque (maximo 20): ";
   cin >> NItens;
   Item * ptrItem= new Item[NItens];  //USO DO OPERADOR new DE ALOCACAO DINAMICA
   //no lugar de: Item Itens[NItens];
   cout<< "\nValor Total em Estoque:    R$" << CalculaTotalEstoque(ptrItem) <<"\n\n\n"<< endl  ;
  //no lugar de CalculaTotalEstoque(Itens)
}
//implementação da classe - Método privado:
float Estoque::CalculaTotalEstoque(Item * pI)
//no lugar de CalculaTotalEstoque(Itens[])
{ float ValorTotalEstoque=0;
 int i;
 for(i=0;i<NItens;i++)
       ValorTotalEstoque += (pI++)->TotalItem; 
       //ACESSA OS ITENS DO VETOR POR MEIO DO PONTEIRO
      //no lugar de TempItens[i].TotalItem; 
    return ValorTotalEstoque;
     // QUESTÃO 3 DA PROVA ***********************
//CÓDIGO-CLIENTE - função main()
int main()
{
   cout << "************************" <<  endl;
   cout << "** PROGRAMA DO ESTOQUE ***"<< endl;
   cout << "************************\n" << endl;
   Item  TestaSobrecarga("Item teste sobrecarga", 10, 2.5);
   Estoque Est1;
   return 0;
}

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.




Objetos e Funções << AULA 9 - Criação dinâmica de objetos Herança>>

<<= Página do curso