Qual a diferença entre New, GetMem e AllocMem #AQuemPossaInteressar
Escrito por Carlos B. Feitoza Filho | |
Categoria: Artigos | |
Categoria Pai: Addicted 2 Delphi! | |
Acessos: 5398 |
Se você é um programador Delphi que passou toda sua vida desenvolvendo apenas aplicações voltadas a área comercial ou administrativa, muito dificilmente você terá tido contato com ponteiros e até os evita, e isso é totalmente natural.
Para ser um bom programador você não precisa, necessariamente, saber tudo sobre ponteiros. O Delphi consegue abstrair boa parte daquilo que só seria acessível com ponteiros e consegue facilitar até mesmo o trabalho com ponteiros quando você precisa usá-los. Dito isso, eu acredito que conhecimento nunca é de mais, então, mesmo que você não precise usar ponteiros é muito desejável que você conheça alguns conceitos básicos.
Um dos conceitos mais básicos, na minha opinião, sobre ponteiros é que antes de usá-los você precisa alocar sua memória e para isso o Delphi fornece três subrotinas: New, GetMem e AllocMem. Use cada uma delas de acordo com a sua necessidade. Muitas vezes, mais de uma delas pode ser usada em uma situação. Cabe a você escolher baseado em critérios específicos. Abaixo eu vou explicar o que cada uma delas faz e assim te ajudar a escolher com sabedoria:
- procedure New(var P: Pointer) - Segundo a ajuda do próprio Delphi, o procedure New, cria (aloca espaço para) uma nova variável dinâmica e faz com que P aponte para ela. P é uma variável de qualquer tipo que seja um ponteiro (por isso o parâmetro é do tipo genérico "Pointer"). Ao final da execução, o tamanho do bloco de memória apontado por P será definido pelo tamanho daquilo que a variável P aponta. Para acessar o valor apontado por P, usa-se o operador de deferência (^) do Pascal. Suponha que você tem uma variável A do tipo PInteger, que por definição seria um ponteiro para um integer, que tem 32bits (4 Bytes). Ao fazer New(A), A conterá um ponteiro para um bloco de memória que tem um tamanho fixo de 4 bytes e para atribuir um novo valor, basta fazer, por exemplo, A^ := 123456.
Como se pode observar, o procedure New detecta o tamanho do bloco de memória a ser alocado, por isso este procedure só pode ser usado com ponteiros para tipos ou estruturas de tamanho fixo, que o compilador conhece ou consegue deduzir. O procedure sabe que um PInteger é um ponteiro para um Integer e sabe que um Integer precisa de 4 Bytes, logo, ele consegue alocar a memória precisamente (4 Bytes). Estruturas, como records, também tem tamanho conhecido e podem tirar proveito das deduções que o procedure New faz. Conclui-se, portanto, que o procedure New não pode ser usado nem com ponteiros sem tipo (Pointer) e nem com ponteiros do tipo PChar.
Para desalocar a memória alocada pelo procedure New, e assim não causar vazamentos de memória, é mandatório que se use o procedure Dispose, que tem a assinatura procedure Dispose(var P: Pointer). No nosso exemplo, para desalocar nossa variável A, do tipo PInteger, basta fazer Dispose(A). Neste momento a variável A ficará indefinida, ou mais especificamente, ela continuará apontando para o mesmo local de memória, porém, como este local foi desalocado ele poderá ser utilizado por outro ponteiro a qualquer momento. Após o uso do procedure Dispose não haverá vazamento de memória, porque ao finalizar o programa não haverá um bloco de memória alocada, porém após o uso do procedure Dispose, o comando Assigned(A) ainda vai retornar true, visto que a função Assigned (function Assigned(var P): Boolean) testa apenas se a variável está nula (nil), o que não é verdade, ela não é nula, mas aponta pra lixo, portanto muito cuidado. Após o procedure Dispose, caso a variável ainda possa estar sendo referenciada no código, é boa prática atribuir nil a ela e assim ficar tranquilo para usar Assigned para verificar este estado. - procedure GetMem(var P: Pointer; Size: Integer) - O procedure GetMem aloca um bloco de memória do tamanho daquilo que for especificado em seu parâmetro Size e retorna o endereço do bloco de memória alocado na variável informada no parâmetro P. Este procedure deve ser utilizado para alocar memória para estruturas ou tipos dos quais o compilador não conhece o tamanho ou não é capaz de deduzir, por exemplo, variáveis do tipo Pointer ou PChar, podem ser alocadas com o procedure GetMem porque você será obrigado a informar o tamanho do bloco de memória a alocar. Seria a alternativa ao uso do procedure New, que só pode ser usado com tipos e estruturas de tamanho conhecido.
Nada impede que se possa usar o procedure GetMem com tipos de tamanho conhecido. Por exemplo, suponha que você tem a variável A do tipo PInteger (veja a explicação para o procedure New). Ao fazer GetMem(A,4), A conterá um ponteiro para um bloco de memória que tem um tamanho fixo de 4 bytes. Como se pode ver, neste caso, é muito mais simples fazer New(A) do que GetMem(A,4). Uma característica do procedure GetMem (e também da function AllocMem), é que você pode alocar mais (ou menos) memória do que o tipo realmente precisa, e isso pode causar problemas, portanto, só use o procedure GetMem se for estritamente necessário.
Tal como o procedure New, tudo que for alocado com o procedure GetMem precisa ser desalocado. Para isso utilize o procedure FreeMem, que tem a assinatura procedure FreeMem(var P: Pointer). Ele funciona exatamente do mesmo modo que o procedure Dispose e possui uma versão sobrecarregada que possui um segundo parâmetro Size, porém, segundo a ajuda do Delphi, a RTL atualmente não utiliza este parâmetro para validar o bloco de memória e assim a primeira versão, apenas com o parâmetro P é suficiente. - function AllocMem(Size: NativeInt): Pointer - A function AllocMem funciona de forma idêntica ao procedure GetMem, porém ao invés de retornar o endereço do bloco de memória alocado em um parâmetro, ela de fato retorna este endereço. Tal como ocorre com o procedure GetMem, o parâmetro Size indica o tamanho do bloco de memória a alocar, neste caso, para alocar uma variável A do tipo Pointer com 18 Bytes devemos executar o comando A := AllocMem(18).
É claro que esta não é a principal diferença entre GetMem e AllocMem. A function AllocMem executa uma ação que nem GetMem e nem New fazem, ela preenche com zeros todo o bloco de memória que ela aloca! Como se sabe, o ato de alocar memória não implica que aquele local de memória esteja vazio, ele pode ter lixo, restos de outros blocos de memória que foram desalocados previamente, por exemplo (veja a explicação para o procedure New). Assim, podemos dizer que a function AllocMem é a forma mais "segura" de alocar um bloco de memória, pois ele garante que após a alocação, todos os bytes sejam iguais a zero.
Tal como os procedures New e GetMem, aquilo que for alocado com AllocMem precisa ser desalocado e para isso usa-se também o procedure FreeMem.
A verdade sobre o tipo PChar
Se você é um leitor atento e se você tem um conhecimento básico a respeito de ponteiros, você deve estar se perguntando agora porque o PChar não pode ser utilizado com o procedure New se um PChar é simplesmente um ponteiro para um caractere (Char), que tem 2 Bytes[1] fixos. Bom, a verdade (prepare-se) é que o PChar não é simplesmente um ponteiro para um Char. Um PChar não é o mesmo que ^Char e tem muito mais semelhanças com uma String, aliás, segundo Rudy Velthuis, uma String é um PChar e eu concordo plenamente com ele!
Como PChar e String são "a mesma coisa" (note as aspas, por favor), com certeza eu não posso usar o procedure New para alocar memória para um PChar e esperar que ele se comporte como um PChar qualquer, aliás, utilizar um PChar mesmo com o procedure GetMem ou a function AllocMem é totalmente fora do normal e eu pretendo aqui demonstrar. Observe o programa a seguir:
program Exemplo;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils, Windows;
type
PMyPChar = ^Char;
var
P1: PChar;
P2: PMyPChar;
begin
P1 := 'Carlos'; // OK!
P1^ := 'Carlos'; // E2010 Incompatible types: 'Char' and 'string'
P1^ := 'C'; // Access Violation
P2 := 'Carlos'; // E2010 Incompatible types: 'PMyPChar' and 'string'
P2^ := 'Carlos'; // E2010 Incompatible types: 'Char' and 'string'
P2^ := 'C'; // Access Violation
New(P2); // OK!
P2^ := 'C'; // OK!
Dispose(P2); // OK!
New(P1); // OK!
P1 := 'Carlos'; // OK!
Dispose(P1); // Access Violation
New(P1); // OK!
P1 := 'Carlos'; // OK!
P1^ := 'C'; // Access Violation
Dispose(P1); // OK!
New(P1); // OK!
P1^ := 'C'; // OK!
Dispose(P1); // OK!
GetMem(P1,14); // OK!
P1 := 'Carlos'; // OK!
FreeMem(P1); // Access Violation
GetMem(P1,14); // OK!
P1^ := 'C'; // OK!
StrPCopy(P1,'Carlos'); // OK!
FreeMem(P1); // OK!
end.
As linhas 17 a 19 mostram que o P1 é diretamente compatível com uma atribuição direta de uma string (P1 := 'Carlos') e que não é possível atribuir uma string àquilo que ela aponta, por motivos óbvios, já que se trata de um PChar e não é possível atribuir uma String a um Char. (P1^ := 'Carlos'). Curiosamente, ao executar o programa, não foi possível realizar a atribuição P1^ := 'C', a qual gerou um Access Violation, contradizendo duas coisas: primeiro o Delphi não parece ter alocado memória para um ponteiro para um caractere, pelo menos ele não fez isso de maneira usual pois P1 consegue receber uma string sem ter sido explicitamente inicializado, mas não consegue receber um caractere usando a notação usual (P1^), o que me leva a concluir que o P1, não é um ponteiro para um caractere, ou pelo menos não é um ponteiro para um caractere como normalmente poderíamos imaginar.
As linhas 21 a 23 mostram que P2, nossa variável que é de fato um ponteiro para um caractere (^Char), não é capaz de receber diretamente uma string (P2 := 'Carlos'), que por motivos óbvios não consegue receber uma string naquilo que ela aponta (P2^ := 'Carlos') e que ao executar o programa, a tentativa de atribuir um caractere a P2 por meio da deferência (P2^ := 'C') falha com Access Violation porque a memória para P2 não foi alocada. Ao contrário de P1, cuja memória parece ter sido alocada de forma bem peculiar pelo Delphi, P2 exige que a alocação seja feita explicitamente.
As linhas 25 a 27 mostram o que é necessário para usar P2 de forma correta, alocando sua memória (New(P2)), realizando a atribuição (P2^ := 'C') e desalocando sua memória (Dispose(P2)). Note aqui que foi perfeitamente válido usar New numa variável que é um ponteiro real para um caractere (^Char), justamente por conta daquilo que muitos de vocês devem ter pensado: um ponteiro para um caractere, aponta para um Char, que tem tamanho fixo em 2 Bytes.
As linhas 29 a 40 mostram o que acontece quando tentamos alocar espaço para P1 usando o procedure New. A alocação parece funcionar normalmente nos três casos. Nos dois primeiros casos é feita uma tentativa de atribuir uma string a P1, justamente como fizemos na linha 17 e aparentemente isso ocorre sem problemas, porém, as linhas que seguem a atribuição da string a P1, falham e levantam um Access Violation, isto é, no primeiro caso o Dispose levanta a exceção e no segundo caso a atribuição àquilo que a variável aponta levanta a exceção. Fica claro que alocar memória com o procedure New para uma variável do tipo PChar parece destruir sua "essência", fazendo-a se comportar como uma variável ordinária (^Char), porém que permite, em tempo de projeto, receber atribuição de uma string, o que invariavelmente vai provocar problemas como vimos. O terceiro caso não tenta atribuir uma string a P1, e por conta disso as 3 linha compilam e rodam perfeitamente.
O procedure New não deve ser usado para alocar espaço para uma variável do tipo PChar, como demonstramos acima, mas e quanto ao procedure GetMem? As linhas 42 a 44 utilizam o procedure GetMem para alocar 14 Bytes necessários para escrever a palavra "Carlos" dentro dela (os dois bytes adicionais são o terminador duplo NULL NULL). Novamente, após tentar atribuir a palavra "Carlos" a P1, que parece ocorrer sem problemas, o FreeMem levanta um Access Violation, exatamente como ocorreu com o procedure New.
Neste ponto você pode chegar a conclusão de que os procedures New e GetMem têm os mesmos problemas quando tentam alocar memória para um PChar e isso é uma conclusão correta, porém ainda é possível extrair uma pequena vantagem do procedure GetMem. Como com ele é possível alocar quanto espaço se deseja, também é possível atribuir uma string inteira dentro de um PChar alocado com o procedure GetMem, e obviamente não é fazendo atribuição direta, já que isso falha.
Nss linhas 46 a 49 há a alocação de memória para a variável P1 (GetMem(P1,14)), uma atribuição àquilo que a variável aponta (P1^ := 'C') e, para atribuir uma string inteira, utilizamos o procedure StrPCopy para copiar todos os bytes da string "Carlos" no local de memória apontado por P1. Usar StrPCopy equivale a usar a notação P1^ := 'C' em cada posição da memória alocada usando aritmética de ponteiros, mas é evidente que ninguém quer fazer isso :) Usar StrPCopy é muito mais fácil e, mais fácil ainda é usar o PChar como o Delphi o disponibiliza, sem precisar de alocação!
Conclusão: se você pretende usar PChar em seus programas, pense muitas vezes antes alocar espaço para variáveis deste tipo. Elas foram concebidas para serem usadas sem alocação (O Delphi faz isso para você).