Ponteiros e alocação dinâmica Tem um ditado que diz: Eu não sei, mas tenho o telefone de quem sabe. Trabalhar com ponteiros é muito parecido com isto, mas o ditado fica assim: Eu não sei, mas tenho o endereço de quem sabe. Alguns tipos de variáveis são chamadas de Ponteiro porque elas apontam para algum endereço de memória. Isto aparentemente complica a vida do programador, pois além de tratar da informação é necessário tratar também do endereço da informação. E não é só isto: basta uma variável ponteiro fazendo referência a uma área que não lhe diz respeito, e na melhor das hipóteses a aplicação emitirá uma Violação de acesso à memória. O lado bom é que sistemas operacionais modernos (para não dizer decentes) seguram o tranco e não deixam você destruir outras aplicações ou ele próprio. Nos tempos do DOS era muito comum ter que recorrer ao gabinete e disparar um reset via hardware. Os novos tipos de dados do Object Pascal visam transformar ponteiros em algo mais simples, mais fácil de usar, ainda mais útil, e por vezes são tratados de forma tão transparente que muitas pessoas passam anos trabalhando com eles sem saber. Quer um bom exemplo? Ansi string (a string padrão do compilador Delphi, do FPC em modo delphi ou com a diretiva $H habilitada) é um ponteiro para string que é alocada e desalocada sozinha pelo compilador. Segue alguns conceitos sobre o tema. VARIÁVEIS ESTÁTICAS São as variáveis da forma tradicional, que guardam a própria informação, e o compilador faz o tratamento de onde esta informação será guardada (pode ser um registrador do processador ou parte da pilha de chamadas). Não há preocupação com alocação nem liberação deste espaço por parte do programador. var A: Longint; { A é uma variável do tipo Longint, ocupará 4 bytes, e a alocação deste espaço é tratada pelo compilador } ALOCAÇÃO DINÂMICA Entende-se por alocação dinâmica a criação e destruição de espaço na memória, em tempo de execução, para armazenamento e manipulação de informação. É de responsabilidade do programador desalocar toda memória que vier a alocar durante a execução do seu aplicativo, a menos que o próprio compilador faça isto. Também é dever do programador saber quando o compilador fará isto. PONTEIROS Entende-se por ponteiros os tipos de variáveis que apontam para uma área da memória. Ponteiros podem apontar para: • Áreas com informação, tal como uma variável; • Uma rotina (procedure, function ou método); • Um endereço nulo. Ponteiros - Áreas com informação O ponteiro pode ser declarado como: • tipado - ele sabe qual o tipo de informação para o qual está apontando. • não tipado - ele não sabe para que tipo de dado está apontando. É necessário fazer um typecast para recuperar a informação. Typecast será abordado adiante. O operador @ (arroba) pode ser usado para atribuir o endereço de uma variável para um ponteiro. Ex.: A := @B; { A recebe o endereço da variável B } Ou ainda, um ponteiro pode receber o endereço de uma área de memória alocada especialmente para ele: { A - aponta para uma variável do tipo Longint } new(A); { É alocada uma área na memória, com um total de 4 bytes, e A está apontando para esta nova área } Neste último caso o programador será responsável por esta área alocada, e deverá desalocar antes de encerrar a execução do seu programa. No primeiro caso isto não é necessário, e sequer deverá ser feito, pois o endereço de memória pertence à variável B que é de responsabilidade do compilador. Ponteiros - Uma rotina É possível criar ponteiros especialmente preparados para receber o endereço de uma rotina. Neste caso, um exemplo fala mais sobre a sua utilidade: na programação por eventos, o disparo de um click em um botão gera um evento que deve ser interpretado por uma rotina. Uma variável OnClick possui o endereço da rotina que fará o tratamento deste evento. Ponteiros - Um endereço nulo No Pascal é conhecido como nil. Utiliza-se associar um ponteiro à constante nil (A := nil) para que fique claro que este ponteiro não está apontando para informação alguma. Desta forma, torna-se possível a comparação: if A = nil then writeln('Não há informação') else writeln('A está apontando para alguma informação'); DECLARAÇÃO Declara-se um ponteiro tipado informando o tipo da variável para o qual ele apontará, precedido pelo operador ^ (circunflexo). var A: ^Longint; { A é uma variável ponteiro, e aponta para um Longint } B: ^string; { B é uma variável ponteiro e aponta para uma string } É possível também declarar um ponteiro não tipado, ou seja, que não se sabe exatamente para qual tipo de informação ele irá apontar. var C: Pointer; { C é uma variável ponteiro, e pode apontar para qualquer tipo de dado } Pode-se declarar também um ponteiro à uma procedure ou função. var D: procedure(Arg: Byte); procedure rotina1(Arg: Byte); begin writeln('Rotina 1 recebeu ', Arg); end; procedure rotina2(Arg: Byte); begin writeln('Rotina 2 recebeu ', Arg); end; begin D := @rotina2; D(10); { Irá imprimir: 'Rotina 2 recebeu 10' } end. RECUPERAÇÃO DOS DADOS Toda variável ponteiro (inclusive classes) possuem duas formas de se trabalhar - uma é com o endereço para o qual ela aponta, e outra é para a informação que está no respectivo endereço. Exemplo: var A: ^Longint; B: Longint; begin { A linha abaixo faz com que A aponte para o endereço de B, é lida assim: "A recebe o endereço de B" } A := @B; { primeira forma - utilizando o endereço para o qual A está apontando } { A linha abaixo guarda o valor 5 na área de memória apontada por A, é lida assim: "O endereço apontado por A recebe 5" } A^ := 5; { segunda forma - utilizando a informação apontada por A } writeln(B); end. Note a diferença no uso da variável. Sem circunflexo, é endereço. Com circunflexo, é a informação apontada por aquele endereço. TYPECAST O typecast informa ao compilador o tipo de dado para o qual a variável ponteiro está apontado. Há situações em que não é possível determinar o tipo exato de um ponteiro em tempo de projeto. Isto é bastante comum em programação orientada a objetos, aonde o ponteiro (a variável de uma determinada classe) pode apontar para uma série de objetos semelhantes, mas não iguais entre si. Apenas para fins de esclarecimento, segue uma utilização do Typecast. Não é o exemplo mais comum, mas é o mais simples de apresentar, explicar e entender: var A: Pointer; B: Longint; begin A := @B; B := 10; writeln(Longint(A^)); end; Recordando o exemplo anterior: colocar o circunflexo após a variável ponteiro significa trabalhar com uma variável comum, que em seu conteúdo possui um número, uma string, etc. Isto é possível graças a forma com que o ponteiro é declarado: var A: ^Longint; { Ponteiro para Longint } begin writeln(A^); { Imprime um Longint } Mas ao criar ponteiros não tipados com o Pointer, o compilador fica sem saber com o que está trabalhando quando o circunflexo é utilizado após a variável. DESALOCANDO O QUE FOI ALOCADO Tudo o que foi alocado pelo programador, e que não é controlado pelo compilador, deverá ser desalocado logo que possível. var A: ^Longint; begin New(A); { Aloca uma área na memória para armazenar um Longint } A^ := 10; { Coloca o valor 10 nessa nova área da memória } Dispose(A); { Desaloca o espaço reservado pela procedure New } end; CONCLUSÃO Estes tópicos abordam tudo o que diz respeito a ponteiros. Conforme colocado, existe mais assunto relacionado devido aos novos tipos de dados do Object Pascal, tal como arrays dinâmicos, classes e interfaces, ansi strings, entre outros. No entanto todos estes tipos trabalham sobre estes conceitos recém abordados, o que muda é a interface do uso e a forma de alocar, acessar e desalocar esta área de memória.