Qual a diferença entre New, GetMem e AllocMem #AQuemPossaInteressar

Categoria: Artigos
Categoria Pai: Addicted 2 Delphi!
Acessos: 1322
Imagem meramente ilustrativa

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:


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ê).



1 A partir do Delphi 2009 (Delphi 12) todas as strings são Unicode (UTF-16), com 2 bytes por caractere. Delphis até o Delphi 2007 (Delphi 11) utilizam por padrão AnsiStrings que possuem 1 byte por caractere