Demystifying pointers in Delphi

Having a general understanding of how memory and addressing works, helps you understand pointers in Delphi (and C/C++). Learning assembler also gives you that knowledge, but there are simpler ways to think about it.

A pointer is a memory location that contains the address to the actual data you want.

Think of a street with houses. Make a list of the houses and their addresses.
This is a list of pointers. Each pointer in the list leads to the actual house it refers to.

As you move through the list and follow each pointer, you can visit each house.

Street of houses (i.e. your blocks of data, 1Kb each)
10k          11k          12k          13k         14K
|Apple       |Pear        |            |Banana      |Orange      |
|            |            |            |            |            |
|         H1 |         H2 |            |         H3 |         H4 |

Your list of addresses (aka 4 byte pointers) is stored at memory address 100k
  ptrlist: array[0..3] of pointer;

assuming the list has been initialized with the correct addresses  
ptrlist[0] 100k		contains 10k
ptrlist[1] 100k+4	contains 11k
ptrlist[2] 100k+8	contains 13k
ptrlist[3] 100k+12	contains 14k

for var ix := 0 to Length(ptrlist) - 1
do begin
  here, ptrlist[ix] = 10k,11k,13k,14k, 
  and ptrlist[ix]^ = whatever value that is stored in the respective house that the pointer addresses

f.x. ptrlist[1] contains 11k (and that value is stored at 100k+4), 
and ptrlist[1]^ points to 'Pear', i.e. what is stored from address 11k

Why the empty house?
To exemplify that your list of pointers may be a consecutive array or linked list, but the data each pointer points to does not necessarily need to be consecutive.

Now, if you address ptrlist[4] – you are out of bounds on the pointer list, and if you are so “lucky” that the address @ptrlist[4] (which is 100k+16) is not inaccesible, then ptrlist[4]^ will point you to whatever random value that pointer contains,, and most likely give you an access violation, or for sure – point you to data you are not meant to visit.

Pointer Operators

@ = Get the address of the reference.
@Variable gives you the pointer memory address of the content of Variable

  v: integer;
  p: pointer;
  v := 5;
  p := @v; // @ finds the address of v

p now contains the memory address of v

^ = Look at that address
p^ = v = 5

The ^ also is used when declaring a type pointer – i.e. a type safe reference to the memory we are going to access. A typed pointer is just the same size as the basic pointer type, since it also is just a memory reference, but we now also know the size of what the pointer refers to.

Use of pointers in APIs

APIs come in many flavours, and one frequently used model for Windows APIs is that you allocate a structure and then pass the address of that structure to the API. The API call will then fill the structure with data which you can use.

  TData = Array of char;
  PData = ^TData; // Defines a typed pointer
  TQueryNameData = record
   Length: Integer;
   Name: PData;

So, what is the size of TQueryNameData?
SizeOf( TQueryNameData) = SizeOf(Length) + SizeOf(Name) = 4 + 4 = 8

Name: PData is only a pointer and hence needs to be allocated separately.

Hence in this example, we manage the memory ourselves

  MaxLen = 100;
  GetName: TQueryNameData;
  // first we prepare a memory location to hold the data 
  // This API allows us to define a max size that it will copy
  // so that we can avoid a buffer overrun
  GetName.Length := MaxLen;
  GetName.Name := AllocMem(GetName.Length * SizeOf(char));

  Then we pass the address of our variable to the API
  if QueryNameAPI(@GetName)
  then begin
    // GetName.Name^ should now contain data filled from the API
    // NB: Remember to call FreeMem for each AllocMem

Another API model is to have a Open/Process/Close use pattern, i.e. you first query the API to get a handle and f.x. a list of pointers. These pointers will then be accessible until you call the related closing API. In these cases, the memory is often managed by the API, and if you want to keep the values, you have to copy them during the process phase.

  TWho = record
    Name: PAnsiString;
    Address: PAnsiString;
  TWhoList = Array of TWho;
  PWhoList = ^TWhoList;
  TQueryWhoListData = record
    Count: Integer;
    Who: PWhoList;

  Names: TQueryWhoListData;

  var h: handle := AcquireWhoList(@Names);  
  if h > 0 // The API gave us a list
  then try
    for var ix := 0 to Names.Count - 1
    do Writeln(Names.Who^[ix].Name^, ' ', Names.Who^[ix].Address^);
    // or copy the data to your own structure
    ReleaseWhoList(h);  // Let go of the API

Look at Names.Who^[ix].Name^:
The first ^ points to the address where the array of TWho records is located.
The second ^ points to the address where the Name Ansistring is located.
The same goes for Names.Who^[ix].Address^ as well.

In modern Delphi you can actually do away with the ptr^ notation for typed pointers, and simply type ptr, and the compiler understands that it is what you refer to that you want, and not the address of what you refer to.

This means that for typed pointers, Names.Who^[ix].Address^
can be written as Names.Who[ix].Address instead.

Personally I sometimes like to use the old-school notation for clarity.

So, why can’t you keep referencing the data you received after letting go of the API? The simple answer is that you no longer can trust the referred memory to be available and unchanged. Typically, the API opening may allocate a block of memory that it fills and passes you, and it will release that block after you close the API.

For more in-depth information, please visit – which delves deeper into the topic of pointers.

Leave a Reply