Ir para conteúdo
Faça parte da equipe! (2024) ×

C++ Trabalhando com Pointers


~~Surfistinha~~
 Compartilhar

Posts Recomendados

Quando uma classe contém dados membro que são pointers, há uma série de cuidados adicionais para assegurar que essa classe realmente funcione como esperado. Por exemplo, quando uma instância de uma classe é destruída, o construtor deve certificar-se que todos os blocos de memória dentro da classe sejam liberados. Um outro cuidado envolve o operador de atribuição: o comportamento padrão para o operador = - cópia de todos os dados membro - como vimos até agora, não funciona para dados membro pointers.

 

Para tornar mais perceptível essa diferença, vamos construir uma classe Stack com matriz e com pointers. Aqui está a versão com matriz e uma função main contendo o código para teste. (Esse código é idêntico ao visto no Tutorial 4).

 


class Stack
{
int stk[100];
int top;
public:
Stack(): top(0) {}
~Stack() {}
void Clear() {top=0;}



void Push(int i) {if (top < 100) stk[top++]=i;}
int Pop()
{
if (top > 0) return stk[--top];
else return 0;
}
int Size() {return top;}
};

void main()
{
Stack stack1, stack2;

stack1.Push(10);
stack1.Push(20);
stack1.Push(30);
cout << stack1.Pop() << endl;
stack2=stack1;
cout << stack1.Size() << endl;
cout << stack2.Size() << endl;
cout << stack2.Pop() << endl;
cout << stack2.Pop() << endl;
É necessário se cadastrar para acessar o conteúdo.

Essa é uma implementação completa da classe. Ela realiza os procedimentos de liberação de memória dentro de seu destrutor, e funciona da mesma maneira que versão anterior da classe Stack. Contudo, essa implementação não funciona conforme o esperado após um comando de atribuição como

 

stack1 = stack2;

 

O diagrama seguinte demonstra o que acontece. A operação de atribuição, nesse caso, apenas copia os dados membro de stack2 para stack1, deixando o mesmo conjunto de dados em memória sendo apontado por dois pointers.

 

pointersuu.jpg

 

 

Após a atribuição, os pointers stack1.pop e stack2.pop apontam ambos para a mesma cadeia de blocos de memória. Se uma das pilhas for excluída, ou se uma delas executar a função Pop, a outra pilha vai apontar para um endereço de memória que não mais será válido.

 

Em algumas máquinas esse código será compilado sem erros e tudo parecerá estar correto por um certo tempo durante a execução do programa. Mas tão logo o sistema comece a apontar para endereços de memória que não mais sejam válidos, a execução começará a ter um comportamento errático sem razão aparente até que o programa finalmente falhe.

 

O que precisamos é de uma maneira de reformular a operação de atribuição para criar uma cópia dos blocos de memória apontados pelos pointers. Mas de onde vem esse operador de atribuição, e como podemos modificá-lo?

 

Funções default

 

Quando você cria uma classe, quatro funções default são criadas automaticamente e serão utilizadas, a menos que você as sobrescreva. Essas funções default são:

 

* O construtor default

* O construtor de cópia default

* O operador de atribuição default

* O destrutor default

 

O construtor default é invocado quando você declara uma instância da classe sem passar qualquer parâmetro. Por exemplo, se você criar uma classe Sample sem a definição explícita de um construtor, então o comando seguinte invoca o construtor default para s:

 

Sample s;

 

A seguinte declaração com inicialização de s2 invoca o construtor de cópia:

 

Sample s1;

 

Sample s2 = s1;

 

O destrutor default é chamado quando se encerra o escopo dentro do qual a variável foi criada, e o construtor de atribuição é chamado quando ocorre uma operação de atribuição normal.

 

Você pode sobrescrever qualquer um desses construtores, definindo as suas próprias funções. Por exemplo, se você define explicitamente um construtor para a classe, o construtor default não será criado pelo compilador.

 

O código seguinte vai nos ajudar a ter uma melhor compreensão do que fazem o construtor e o destrutor default:

 

É necessário se cadastrar para acessar o conteúdo.

 

A classe Clas2 não tem construtor nem destrutor definidos explicitamente, mas esse código produz a seguinte saída:

 

class1 constructor

class0 constructor

class0 destructor

class1 destructor

 

O que aconteceu é que o compilador criou automaticamente tanto o construtor quanto o destrutor default para Clas2. O comportamento do default construtor é chamar o construtor da classe base, bem como o construtor default para cada um dos dados membro que são classes. O destrutor default chama o destrutor da classe base e dos dados membro que são classes.

 

Digamos que você crie um novo construtor para Clas2 que aceite um inteiro. O compilador ainda assim vai chamar os necessários construtores da classe base e dos dados membro que são classes.

 

O código seguinte demonstra esse processo:

 

É necessário se cadastrar para acessar o conteúdo.

 

sso também funciona e produz a seguinte saída:

 

class1 constructor

class0 constructor

class2 constructor

class0 destructor

class1 destructor

 

Mas agora você não pode mais declarar uma variável não inicializada do tipo Clas2 porque não há mais um construtor default. O código seguinte demonstra:

Class2 c(1); // OK

Class2 e; // not OK--no default constructor

É também possível declarar uma matriz de uma classe, a menos que não haja um construtor default definido.

 

Contudo, você pode recriar o construtor default, criando explicitamente um construtor com uma lista de parâmetros vazia, da mesma maneira que cria outros construtores para a classe.

 

O operador de atribuição e o construtor de cópia também são criados automaticamente. Ambos apenas copiam os dados membro da instância à direita do sinal = para a instância à esquerda. No caso de nossa classe Stack, nós queremos eliminar essas funções default e usar funções próprias, para que a operação de atribuição funcione corretamente. A seguir estão as duas novas funções para a classe Stack, e a função Copy compartilhada por ambas:

 

É necessário se cadastrar para acessar o conteúdo.

 

A função de atribuição se inicia verificando o caso de auto-atribuição, como em

 

s = s;

 

Se verifica tratar-se de auto-atribuição, a função não faz nada, ou seja, não efetua a auto-atribuição. Não sendo auto-atribuição, a função limpa a instância recipiente e copia a lista ligada existente na memória, de modo que a instância à esquerda do operador de atribuição tenha sua própria cópia da pilha.

 

O construtor de cópia é basicamente o mesmo que qualquer outro construtor. É usado para manejar os seguintes casos:

Stack s1;

s1.Push(10);

s1.Push(20);

Stack s2(s1); // copy constructor invoked

Stack s3 = s1; // copy constructor invoked

Uma vez implementados o operador de atribuição e o construtor de cópia, a classe Stack está completa. Pode manejar qualquer condição e funcionar corretamente.

 

Conclusão

 

Tudo isso talvez lhe pareça muito trabalho a fazer, mas geralmente esses cuidados adicionais são necessários apenas quando se trabalha com pointers. O que acontece é que você tem que realmente proteger suas estruturas baseadas em pointers contra contingências que invalidem os dados.

 

Em alguns programa em C, os programadores poderão fazer pressuposições como eu posso apontar o mesmo bloco de memória com vários pointers sem problemas, porque nessa parte do código nada modifica os blocos apontados. Contudo, se um outro programador viola essa pressuposição, ainda que acidentalmente, o programa pode falhar, e falhas decorrentes de problemas com pointers são difíceis de seguir e de localizar.

 

Tais problemas não vão ocorrer em uma classe C++ definida com segurança, porque todas essas contingências estão previamente consideradas e cobertas.

 

Você pode verificar que a implementação mostrada acima é ainda ineficiente. O que acontecerá se você quiser ter apenas uma cópia dos blocos de memória que formam a pilha? Por exemplo, o que acontecerá se os dados da pilha ocuparem alguns megabytes de memória, e você não tiver memória suficiente para fazer uma cópia?

 

O que você faz nesse caso é usar uma técnica como um contador de referências - cada instância da classe incrementa uma variável global estática que contém o número de instâncias usando a mesma cópia dos dados. Cada destrutor decrementa esse mesmo contador. Somente quando um construtor, após decrementar o contador, detecta que não há mais qualquer instância da classe usando os dados, é que realmente se libera a memória usada para conter os dados.

Link para o comentário
Compartilhar em outros sites

Este tópico está impedido de receber novos posts.
 Compartilhar

  • Quem Está Navegando   0 membros estão online

    • Nenhum usuário registrado visualizando esta página.
×
×
  • Criar Novo...

Informação Importante

Nós fazemos uso de cookies no seu dispositivo para ajudar a tornar este site melhor. Você pode ajustar suas configurações de cookies , caso contrário, vamos supor que você está bem para continuar.