Apontadores (Ponteiros)


Um apontador é uma variável que contém o endereço de um dado ou de um código de programa. Podemos obter o endereço de uma variável usando a função Addr ou o operador @.

Exemplo 1: Obtendo o endereço de uma variável integer:

var N : integer;
begin
   writeln('Addr(N)=', longint(Addr(N)));
   writeln('@N=', longint(@N))
end.

 

Variável tipo Pointer


Usa-se uma variável tipo Pointer para armazenar um endereço de memória.

var <nome-da-variável> : Pointer;

Este tipo de apontador pode ser usado para guardar o endereço de qualquer variável, independente do tipo da mesma.

Exemplo 2: Atribuindo endereços de variáveis a um apontador:

var
   N : integer;
   R : real;
   S : String[50];
   P : Pointer;
begin
   P := Addr(N);   {1}
   writeln('Addr(N)=', longint(P));

   P := Addr(R);   {2}
   writeln('Addr(R)=', longint(P));

   P := @S;        {3}
   writeln('Addr(S)=', longint(P));

   P := @P;        {4}
   writeln('Addr(P)=', longint(P));
end.

 

Supondo que N, R, S e P estão juntos na memória, teríamos uma organização semelhante à abaixo:

 

Porém, as variáveis dinâmicas no Free Pascal são normalizadas para ocupar pedaços de memória de 16, 32 ou 64 bytes (o menor possível). Sendo assim, teríamos o resultado abaixo:

 

Apontadores Tipados


São apontadores que podem ser usados para referenciar um tipo específico. São declarados da seguinte forma:

var <nome-da-variável> : ^<tipo>;

Exemplo 3: Crie um apontador para um Item que possui um nome e um preço:

type
   TItem = record
      Nome : String;
      Preco : real;
   end;
var
   PItem : ^TItem;
   Item : TItem;
begin
   Item.Nome := 'Livro de Ed1';
   Item.Preco := 25.0;
   PItem := Addr(Item);
end.

 

Desreferenciando um Apontador


Desreferenciar um apontador faz com que obtenhamos o valor contido no endereço apontado.

<identificador>^

A atribuição de apontadores é igual a uma variável normal.

Exemplo 4: A atribuição de apontadores é igual a uma variável normal.

var
   X : integer;
   P1, P2 : ^integer;
begin
   X := 10;
   P1 := @X;
   writeln(X);
   writeln(P1^);
   P2 := P1;
   P2^ := 100;
   writeln(X)
end.

 

 

A Constante Nil


É uma constante que representa um endereço de memória inválido, ou seja, um apontador com esse valor não aponta para nada.

 

Variáveis Dinâmicas


São variáveis que são criadas e destruídas durante a execução do programa (dinamicamente e sob demanda). Não são associados a um identificador. Não são declaradas na seção de declaração de variáveis. São sempre referenciadas através de apontadores.

Permitem melhor gerenciamento da memória, já que podem ser alocadas sob demanda e liberadas assim que não forem mais necessárias.

Suportam a criação de estruturas de dados complexas de forma eficiente, como listas encadeadas e árvores.

Em certas situações, são mais eficientes do que estruturas estáticas, permitindo que o programador decida qual é melhor em cada situação.

Porém, o uso de variáveis dinâmicas está associada a mais erros de programação, já que a alocação e desalocação de memória fica a cargo do programador.

 

Alocação e Desalocação de Variáveis Dinâmicas


Há uma função chamada new que dado um apontador cria uma variável dinâmica do tipo do apontador e faz com que o apontador tipado passado aponte para a variável criada.

new (<apontador-tipado>)

Para liberar a memória alocada pela função new, usa-se o procedimento dispose.

dispose (<apontador-tipado>)

Exemplo 5: Usando new e dispose para trabalhar com variáveis dinâmicas.

type
   TItem = record
      Nome : String[20];
      Preco : real
   end;
var
   PItem, PAnterior : ^TItem;
begin
   New(PItem);
   PItem^.Nome := 'Livro de ED1';
   PItem^.Preco := 30.0;
   PAnterior := PItem;

   New(PItem);
   PItem^.Nome := 'Outro livro';
   PItem^.Preco := 10.0;

   writeln(PAnterior^.Nome);
   writeln(PAnterior^.Preco);

   writeln(PItem^.Nome);
   writeln(PItem^.Preco);

   Dispose(PAnterior);
   Dispose(PItem);

   PItem := nil;
   PAnterior := nil
end.

 

 

Exemplo 6: Usando variáveis dinâmicas para ler e guardar na memória uma lista de itens.

type
   PItem = ^TItem;
   TItem = record
      Nome : string[20];
      Preco : real;
      Proximo : PItem
   end;
var
   Head, PNew, PAtual : PItem;
   Resp : char;
begin
   Head := nil;

   repeat
      New(PNew);
      writeln('Digite o nome e o preco do Item:');
      readln(PNew^.Nome, PNew^.Preco);
      PNew^.Proximo := Head;
      Head := PNew;
      write('Outro Item (S/N)?');
      readln(Resp);
   until UpCase(Resp) = 'N';

   writeln('Produtos Lidos:');
   PAtual := Head;
   while PAtual <> nil do
      begin
         writeln(PAtual^.Nome, ' - ', PAtual^.Preco);
         PAtual := PAtual^.Proximo;
      end
end.

 

Alocando e Desalocando Memória usando GetMem e FreeMem


GetMem(<apontador>, <número-de-bytes>)

FreeMem(<apontador>, <número-de-bytes>)

 

A Lista de Blocos Livres


Ao desalocar uma variável dinâmica, normalmente cria-se um espaço vazio na memória. O endereço e o tamanho desta variável são incluídos na lista de blocos livres. As funções New e GetMem verificam se há um bloco livre na lista com o tamanho desejado.

O uso desta lista de blocos livres permite que áreas de memória fornecidas pelo sistema operacional sejam reaproveitadas para outras variáveis dinâmicas sem que seja necessário alocar mais memória.

 

As funções MaxAvail e MemAvail


MaxAvail retorna o tamanho do maior bloco de memória livre, incluindo o bloco que começa no topo do heap.

MemAvail retorna a soma dos tamanhos de todos os blocos livres.

Exemplo 7: Usando GetMem, FreeMem, MaxAvail e MemAvail.

var
   P1, P2 : Pointer;
begin
   writeln('MaxAvail:', MaxAvail, ', MemAvail:', MemAvail);
   GetMem(P1, 5000);
   GetMem(P2, 1000);
   writeln('MaxAvail:', MaxAvail, ', MemAvail:', MemAvail);
   FreeMem(P1, 5000);
   writeln('MaxAvail:', MaxAvail, ', MemAvail:', MemAvail);
   FreeMem(P2, 1000);
   writeln('MaxAvail:', MaxAvail, ', MemAvail:', MemAvail);
end.

As funções MaxAvail e MemAvail só funcionam no Turbo Pascal. Isto ocorre porque o Free Pascal é um compilador para sistemas operacionais modernos e que permitem vários processos em paralelo e não limita a quantidade de memória disponível para um programa, como ocorre com o DOS. Sendo assim, mesmo que as funções retornariam uma quantidade de memória que muito provavelmente já não seria válida porque outros processos estão alocando e desalocando memória em paralelo.

Desta forma, com o Free Pascal devemos tentar alocar a quantidade de memória e testar se tivemos sucesso, ou simplesmente não testar e deixar o programa encerrar por falta de memória.

 

Cuidados com o Dispose


Após executar Dispose o apontador continua apontado para o mesmo endereço

 

Erros Comuns com Apontadores


Exemplo 8: O endereço da para a variável é perdido, impedindo sua liberação da memória.

var
   P1, P2, P3 : ^integer;
begin
   {Situação 1}
  
new(P1); {Cria uma variável dinâmica e faz P1 apontar para ela}
  
new(P2); {Cria outra variável dinâmica}
   P1 := P2; {P1 e P2 passam a apontar para a segunda variável criada. A primeira fica perdida na memória}

   {Situação 2}
   new(P3); {Cria a terceira variável dinâmica e P3 aponta para ela}
   new(P3); {Cria a quarta variável dinâmica e P3 aponta para ela. Perde-se o endereço da terceira variável}
end.

 

A figura abaixo mostra o que acontece com a pilha e o heap durante a execução do exemplo acima. Observe que as variáveis "Var 1" e "Var 3" estão perdidas na memória, já que não se tem mais referências para ela a partir da pilha, seja diretamente ou indiretamente:

 
Exemplo 9: Uma variável dinâmica desalocada continua sendo usada.

var
   P1, P2 : ^integer;
begin
   new(P1); {Cria uma variável dinâmica e faz P1 apontar para ela}
   readln(P1^);
   writeln(P1^);
   dispose(P1); {P1 já foi liberada da memória e entrou na lista de blocos livres}
  
   new(P2); {Cria outra variável dinâmica, podendo até reusar o espaço desalocado de P1}

   readln(P1^); {Continua usando P1 como se não tivesse desalocado, podendo gerar erros de execução}
   writeln(P1^);
end.

A figura abaixo mostra o que acontece com a pilha e o heap durante a execução do exemplo acima. Observe que após o dispose(P1), a variável dinâmica "Var 1" é liberada e passa a fazer parte do espaço livre. Porém, P1 continua apontando para esta área da memória, o que permite inclusive continuar acessando os dados lá armazenados. Logo sem seguinda, é o comando new(P2) aloca uma variável dinâmica "Var 2" que pode ser alocada no espaço antes destinado a "Var 1". Caso isso aconteça, P1 e P2 estarão apontando para a mesma variável, sendo possível alterar o valor da mesma a partir de 2 apontadores distintos. O pior é que estes apontadores podem ser de tipos diferentes, o que quebra o faz com que o Pascal não seja uma linguagem fortemente tipada.

 

Coleta Automática de Lixo


Deixar o gerenciamento de memória a cargo do programador, apesar de ser mais flexível e potencialmente mais eficiente (caso seja um ótimo programador), normalmente é associado a erros, que só ocorrem em determinadas situações e em tempo de execução.

Além disso, o vazamento de memória não provoca erros visívies ao usuário. Porém, faz com que o programa use mais memória do que o necessário, requerendo frequentes reinicializações do mesmo para liberá-la. Na maioria dos programas isto não é grave, mas em Sistemas Operacionais e Servidores (Web, Aplicação, Arquivos, por exemplo) é crítico porque demandaria muita memória ou reinicializações freqüentes.

Diante deste problema, as linguagens de programação mais modernas têm adotado a liberação automática de memória, conhecida como Coleta Automática de Lixo (Automatic Garbage Collection). Recebe este nome porque o programador não se preocupa em desalocar as variáveis dinâmicas usada, o que faz com que lixo (garbage) seja acumulado na memória. Mas nestas linguagens o ambiente (Máquina Virtual Java, por exemplo) varre a memória em busca deste lixo e o libera automaticamente.

É importante lembrar que mesmo linguagens que usam Coleta Automática de Lixo precisamos fazer com que os objetos deixem de ser referenciados (atribuindo null a uma referência Java). Caso contrário, não serão liberados. Por exemplo, ao utilizar um array para armazenar os objetos de uma lista seqüencial, podemos controlar através do tamanho quantos objetos estão armazenados no array. O restante é considerado lixo. Porém, para o coletor de lixo, como temos a referência para o array e este para os objetos, ele não é capaz de desalocá-los.

Existem vários algoritmos para detecar lixo (garbage), ou seja, objetos para os quais não temos mais referência. De forma bastante simplificada, a idéia é partir das referências estáticas e das contidas na pilha, seguindo as referências contidas em cada objeto atingido e incluindo na lista dos objetos atingíveis. Os que não foram atingidos, podem ser desalocados porque não temos como acessá-los.

Vantagens


Desvantagens