DelphiBasics
  Home  |  A dying art - using pointers
 Documents
 Tutorials

 Writing your first program
 Writing your second program
 Amending this program

 Delphi data types
   Numbers
   Text (strings and chars)
   Sets and enumerations
   Arrays
   Records

 Programming logic
   Looping
   SubRoutines
   Exception handling

 Dates and times

 Files

 Pointers

 Printing text and graphics

 Object Orientation basics
   Memory leaks!
   Inheritance
   Abstraction
   Interfaces
   An example class

 References

 Standard components

 Articles

 A brief history of Delphi

 Usability : file handling

 Usability : reference books

 Author links

  A dying art - using pointers
Pointers in the World of Object Orientation
In the past, especially in languages, pointers were a critical aspect of the language. They allowed direct access to memory, enabling complex data structures to be built and navigated.
 
However, with the advent of Object Orientation, things have changed somewhat. For example, the TStringList class allows a list of strings to be built without the user needing to manage the storage. And hence, removing the need to use pointers to do this.
 
Additionally, the Pascal language has also evolved to avoid dynamic storage management by coders - for example with the advent of dynamic arrays. You can now simply code a SetLength call to set or increase the size of such an array as your program runs.
 
However, there are still some situations where pointers are valid in Delphi.
 

What are pointers?
Pointers are a special type of variable. Like a meta-variable. They can point to other variables, or to memory. You might use a record pointer, for example, to point to a block of memory where you have stored lots of record data. You would then use the pointer just as if it were a record variable. We'll see how below.
 
When calling Windows APIs (Application Programmer Interfaces), we are obliged to use pointers.
 

Types of pointers
Delphi provides a number of typed pointer types, such as PChar, and PExtended, along with a generic, 'point to anything' type - the Pointer type.
 
The nice thing about the typed pointers is that they work sensibly with the Inc and Dec functions. Incrementing an PInt64 pointer will add SizeOf(Int64) bytes to the pointer address so that it points to the next Int64 variable in memory.
 
The Pointer type is a dangerous one - it falls foul of Delphi's normally tight type handling. Use it with care, or you will end up addressing the wrong memory.
 

A simple example using PChar
The PChar type can be used to scan along a string :
 
 var
   myString  : string;
   myCharPtr : PChar;
   i : Integer;
 
 begin
  // Create a string of Char's
   myString  := 'Hello World';
 
  // Point to the first character in the string
   i := 1;
   myCharPtr := Addr(myString[i]);
 
  // Display all characters in the string
   while i <= Length(myString) do
   begin
     ShowMessage(myCharPtr^); // Display the string characters one by one
     Inc(i);
     Inc(myCharPtr);
   end;
 end;

There are two things to note here. First the use of Addr function to get the address of the string. You could equally use the @ operator. Pointers always work with addresses - the address of a variable here, or a block of acquired memory. Here we point the PChar value to the first character in the string.
 
Secondly, now that we have a pointer, we use the ^ character at the end of the pointer name to refer to what the pointer points to. In this case, a character.
 
A PChar^ will always give us a character. A PInt64^, for example, will give us an Int64 value. This is where the typing comes in.
 

Record pointers
You can define a pointer to any data type using a different technique:
 
 var
   myRecordPtr : ^TMyRecord;

Here, the ^ symbol is used to dereference the type - we are saying that we do not have a TMyRecord type, but a pointer to one. Note that this is a prefix use of ^.
 
Let us create a full record example :
 
 type
   TMyRecord = Record
     name : String[20];
     age  : Integer;
   end;
 
 var
   myRecord    : TMyRecord;
   myRecordPtr : ^TMyRecord;
 
 begin
   myRecord.name := 'Fred Bloggs';
   myRecord.age  := 23;
 
   myRecordPtr := @myRecord;
 
   ShowMessage(myRecordptr.name); // Displays 'Fred Bloggs'
 end;

When we simpy refer to the record field name, without a ^, Delphi is in fact adding one for us - it recognises what we are doing, and helps us make for more readable code.
 

A full memory handling example
In this example, we'll build a new class that is a limited number list equivalent to the TStringList class. This class will allow you to keep adding numbers to a list of numbers.
 
The class uses pointers to help store the numbers in a block of memory, reallocating this block when it is all used up.
 
First off, we will look at the constructor :
 
 var
   msCount  : Integer;  // Count of numbers in the list
   maxCount : Integer;  // Maximum numbers that can fit into current storage
   memStart : Pointer;  // Start of the memory holding the list
   nextSlot : PInt64;   // Points to the next free slot in memory
 
 const
   ALLOCATE_SIZE = 20;  // How many numbers to store in first memory block
 
 // Constructor - initialise everything
 constructor TNumberList.Create;
 begin
   msCount  := 0;   // No numbers in the list yet
 
  // Allocate space for a limited number of numbers
   GetMem(memStart, ALLOCATE_SIZE * SizeOf(Int64));
 
  // Indicate how many numbers that we can add before acquiring more memory
   maxCount := ALLOCATE_SIZE;
 
  // And point to the next free memory slot - the first!
   nextSlot := memStart;
 end;

The role of the constructor is to initialise the class. The key part of this is to allocate a block of memory that can hold 20 numbers. We'll use Int64 numbers (for some reason, Delphi does not provide an Integer pointer).
 
The GetMem call allocates storage of the desired size, setting the memStart generalised Pointer variable to the starting address of the memory allocated. Note that GetMem insists on a Pointer variable.
 
We'll add a routine to add a value to the memory :
 
 // Add a number to the list
 procedure TNumberList.Add(const number : Int64);
 begin
  // Store the number at the next slot in our memory block
   nextSlot^ := number;
 
  // And update things to suit
   Inc(msCount);
   Inc(nextSlot);
 end;

The passed number is stored in the next Int64 slot in our memory block, and this nextSlot pointer incremented. Note that this adds SizeOf(Int64) bytes to the address value in this pointer, because the Inc call knows the type of this pointer.
 
And here is a routine for retrieving a value :
 
 // Get the number at the index position (starting at 0)
 function TNumberList.GetValue(index : Integer): Int64;
 var
   numberPtr : PInt64;
 begin
  // Simply get the value at the given Int64 index position
   numberPtr := memStart;
   Inc(numberPtr, index);  // Point to the index'th Int64 number in storage
   Result := numberPtr^;   // And get the Int64 number it points to
 end;

Here we use Inc to add index Int64 size bytes to the start of memory to get to the slot of the required memory.
 
However, we have not yet covered the situation where the memory we allocate is all used up. We will extend the Add routine to do just this :
 
 // Add a number to the list
 procedure TNumberList.Add(const number : Int64);
 var
   newMemoryStart : Pointer;
   oldPtr, newPtr : PInt64;
   i : Integer;
 begin
  // If we do not have enough space to add the number, then get more space!
   if msCount = maxCount then
   begin
    // First allocate a bigger memory space
     GetMem(newMemoryStart, (maxCount + ALLOCATE_SIZE) * SizeOf(Int64));
 
    // Copy the data from the old memory here
     oldPtr := memStart;
     newPtr := newMemoryStart;
     for i := 1 to maxCount do
     begin
      // Copy one number at a time
       newPtr^ := oldPtr^;
       Inc(oldPtr);
       Inc(newPtr);
     end;
 
    // Free the old memory
     FreeMem(memStart);
 
    // And now refer to the new memory
     memStart := newMemoryStart;
     nextSlot := memStart;
     Inc(nextSlot, maxCount);
     Inc(maxCount, ALLOCATE_SIZE);
   end;
 
  // Now we can safely add the number to the list
   nextSlot^ := number;
 
  // And update things to suit
   Inc(msCount);
   Inc(nextSlot);
 end;

Here we abandon our old memory block (Delphi cannot let us extend the size of it), and create a bigger one. Having allocated it, we must copy the old memory contents to it. Here we see a new concept - assigning the value referred by one pointer to the contents of memory pointed to by another. Delphi knows to copy the whole Int64 value rather than just one byte because these are PInt64 pointers.
 
Below is the full code of the class :
 
 unit NumberList;
 
 interface
 
 type
   TNumberList = class
 
   private
     msCount  : Integer;  // Count of numbers in the list
     maxCount : Integer;  // Maximum numbers that can fit into current storage
     memStart : Pointer;  // Start of the memory holding the list
     nextSlot : PInt64;   // Points to the next free slot in memory
 
     function    GetValue(index : Integer) : Int64;
 
   public
     property    Items[index : Integer] : Int64
         read    GetValue; default;  // Default means we can use the list[i]
 
   published
     constructor Create;
     destructor  Destroy; override;
     procedure   Add(const number : Int64);
 
     property    Count : Integer
         read    msCount;
   end;
 
 implementation
 
 const
   ALLOCATE_SIZE = 20;  // How many numbers to store in first memory block
 
 // Constructor - initialise everything
 constructor TNumberList.Create;
 begin
   msCount  := 0;   // No numbers in the list yet
 
  // Allocate space for a limited number of numbers
   GetMem(memStart, ALLOCATE_SIZE * SizeOf(Int64));
 
  // Indicate how many numbers that we can add before acquiring more memory
   maxCount := ALLOCATE_SIZE;
 
  // And point to the next free memory slot - the first!
   nextSlot := memStart;
 end;
 
 // Destructor - release storage obtained
 destructor TNumberList.Destroy;
 begin
  // Free the allocated memory
   FreeMem(memStart);
 
  // Call TObject destructor
   inherited;
 end;
 
 // Add a number to the list
 procedure TNumberList.Add(const number : Int64);
 var
   newMemoryStart : Pointer;
   oldPtr, newPtr : PInt64;
   i : Integer;
 begin
  // If we do not have enough space to add the number, then get more space!
   if msCount = maxCount then
   begin
    // First allocate a bigger memory space
     GetMem(newMemoryStart, (maxCount + ALLOCATE_SIZE) * SizeOf(Int64));
 
    // Copy the data from the old memory here
     oldPtr := memStart;
     newPtr := newMemoryStart;
     for i := 1 to maxCount do
     begin
      // Copy one number at a time
       newPtr^ := oldPtr^;
       Inc(oldPtr);
       Inc(newPtr);
     end;
 
    // Free the old memory
     FreeMem(memStart);
 
    // And now refer to the new memory
     memStart := newMemoryStart;
     nextSlot := memStart;
     Inc(nextSlot, maxCount);
     Inc(maxCount, ALLOCATE_SIZE);
   end;
 
  // Now we can safely add the number to the list
   nextSlot^ := number;
 
  // And update things to suit
   Inc(msCount);
   Inc(nextSlot);
 end;
 
 // Get the number at the index position (starting at 0)
 function TNumberList.GetValue(index : Integer): Int64;
 var
   numberPtr : PInt64;
 begin
  // Simply get the value at the given Int64 index position
   numberPtr := memStart;
   Inc(numberPtr, index);  // Point to the index'th Int64 number in storage
   Result := numberPtr^;   // And get the Int64 number it points to
 end;
 
 end.

And here is how the code could be used :
 
 var
   list  : TNumberList;
   value : Int64;
   i     : Integer;
 begin
  // Create a number list object
   list := TNumberList.Create;
 
  // Add the first 30 even numbers to the list, each doubled in size
   for i := 0 to 29 do
     list.Add(i * 2);
 
  // Get the 22nd value = 44 (22 * 2)
   value := list[22];
   ShowMessage('22nd value = '+IntToStr(value));
 end;

 
 

Delphi Basics © Neil Moffatt All rights reserved.  |  Home Page