Introduction to Pointers

Introduction

Understanding pointers is crucial for programs that need both speed and flexibility. This article introduces the pointer data type and how to use a single pointer variable and memory buffers.

Pointing the Way

You are probably familiar with the standard data types such as Integer, String and Double. If you dimension a variable such as Dim a as Integer = 10, then you know that a will be equal to the value of 10. When you reference a standard data type, you are referencing the value of the variable. What you may not know, is that the variable a is just a handle for a section of memory that contains the actual data.

When you dimension a variable you are telling the compiler to create a memory segment equal to the size of the data type. If you dimension an Integer variable, the compiler will allocate 4 bytes of memory to hold the data. How does the compiler know where in memory these 4 bytes are? It records the address of the memory segment in a symbol table indexed to the dimensioned variable name.

When you Dim a as Integer, the symbol a is allocated a memory segment of 4 bytes and the address of a is stored in the symbol table. When you reference a in your program, the compiler looks up the address of a and updates the memory segment. Rather than letting the compiler do all the work, you can create your own pointer and memory segment and handle all the updates yourself. While this may seem to be a lot of work, you can get rid of those look-ups the compiler must do and boost the performance of your program.

Another advantage of pointers is that they offer you dynamic memory allocation. Suppose you need to manage a list of integers, but you don’t know ahead of time how big that list will be. By using pointers you can allocate as many integers as you need during the runtime process, avoiding a lot of the overhead associated with dynamic arrays and get a performance boost in the process.

What is a Pointer?

A pointer is a variable that holds a memory address. A pointer points to a memory location that contains data. The pointer doesn’t contain the actual data; it contains the address of the data. This may seem confusing at first, but just remember that a pointer points to data. This data resides in a memory location somewhere in your RAM, and the address of that memory location is what is stored in your pointer variable.

You create a pointer variable much like a standard variable with the addition of the ptr qualifier. You can create pointers to any of the supported data types, user defined types, arrays, subroutines and functions. The following code snippet illustrates creating an integer pointer.

Dim aptr as Integer Ptr

When you first dimension a pointer variable, what does it point to? The answer is: nothing. A newly dimensioned pointer variable doesn’t point to anything because it hasn’t been initialized with an address. A pointer that hasn’t been initialized is called a NULL pointer. Before you can use a pointer, you must initialize the pointer with an address. If you happen to reference a NULL pointer in your program, the program will probably crash. You must always initialize a pointer with an address before you can safely use that pointer.

Getting the Address

There are two methods of getting an address of a memory segment. You can use the addressof operator @ or use the built-in functions Allocate and Callocate. In this tutorial you will see how to use the Callocate and Allocate functions to initialize a pointer. The addressof operator will be covered in follow-up tutorial explaining how to use subroutine and function pointers.

The Callocate and Allocate functions both return the address of a memory segment, but Callocate clears the memory segment to its default values while Allocate returns the address of a raw memory segment. Allocate is slightly faster than Callocate since it doesn’t have the extra step of clearing the memory segment, but in most cases, you probably wouldn’t notice the difference. The following short program shows how to create and initialize an integer pointer using Callocate.

#define NULL 0 

Dim aptr As Integer Ptr 

aptr = Callocate(1, Len(Integer)) 
If aptr <> NULL Then 
    *aptr = 10 

    Print "Address stored in aptr is "; aptr 
    Print "Value of aptr is"; *aptr 

    Deallocate aptr 
Else 
    Print "Could not create pointer." 
End If 

Sleep 
End

In this example, the Callocate function is used to create a memory segment and return the starting address, which is stored in the variable aptr. Callocate takes two parameters: the first parameter is the number of elements you want to create and the second parameter is the size of each element. The Len function, when used with a data type identifier such as Integer, will return the length of the data type. Since an integer is 4 bytes long, Callocate will reserve a memory segment of 4 bytes and return the starting address. If you had wanted two integer elements, Callocate would have set aside a memory segment 8 bytes long, or 2 * 4.

After the pointer has been initialized, the indirection (also called the value of) operator * is used to set the value of aptr to 10. The indirection operator tells the compiler that you want to work with the value of the pointer, not the address stored in the pointer. * aptr = 10 is the same as myInt = 10 if myInt was Dimmed as an Integer. Both methods work with the values of the variable.

The Deallocate function is used to release the memory segment pointed to by aptr back to the operating system. After using Deallocate, aptr is once again a NULL pointer and must be initialized before it can be used again.

Strictly speaking, it isn’t necessary to use Deallocate in this instance since the pointer has global scope. The OS will automatically deallocate aptr when the program terminates. However, it is a good habit to always deallocate your pointer variables when you have finished working with them. If this code had been in a subroutine, you would need to use Deallocate before you left the sub, or the memory segment would have been lost to the program. This is called a memory leak, and can cause quite a number of problems, most of which lead to program crashes. By always explicitly Deallocating your pointer variables, you won’t forget to do so when it is necessary.

If you run the program you should see something similar to the following.

Address stored in aptr is 3089520
Value of aptr is 10

The address of aptr will be different on your machine and will probably be different if you run the program several times. The OS determines where to get the memory from and it may not be the same location on each run. The address isn’t important and you should never use the value of the address in your program; it is just shown here to confirm that Callocate did indeed return a valid memory address.

The Allocate function works in a similar manner.

#define NULL 0

Dim aptr As Integer Ptr

aptr = Allocate(1 * Sizeof(Integer))
If aptr <> NULL Then
    *aptr = 10

    Print "Address stored in aptr is "; aptr
    Print "Value of aptr is"; *aptr

    Deallocate aptr
Else
    Print "Could not create pointer."
End If

Sleep
End

In this example, the Allocate syntax is slightly different than the Callocate syntax, but works in same way. Rather than two parameters you pass one parameter, the calculated value of the number of bytes you want to create. The Sizeof operator returns the length of the passed data type just like the Len function, and is multiplied by the number of data type elements you wish to create. In this case we are creating 1 integer, so Allocate creates a memory segment 4 bytes long. The output is similar to the previous example.

You will see in both examples that an If statement is used to check the value of aptr before it is used. If the value is NULL, or 0, then the pointer was not initialized and is not safe to use. Checking the value of your newly created pointer variable to make sure it was properly initialized will ensure that your program doesn’t crash in those rare instances when a user’s computer may have limited memory.

Creating Dynamic Memory Buffers

The real power of using pointers comes into play when you need to create a memory segment that contains more than one value. You can create a memory segment that behaves much like an array, without the overhead of an array. This is useful in situations where you do not know the number of elements needed before the program is run, or you need to have the functionality of a dynamic array within a type definition. FreeBasic doesn’t support dynamic arrays within a type, so using a pointer allows you to get the functionality of a dynamic array without the array syntax. (Creating dynamic arrays within a type will be covered in another tutorial.)

The following short program illustrates how to create, initialize and use a memory segment of 10 integer values.

#define NULL 0
#define numelements 10

Dim aptr As Integer Ptr
dim i as integer

'Create the memory segment
aptr = Callocate(numelements, Len(Integer))
If aptr <> NULL Then

    'Initialize the memory segment
    for i = 0 to numelements - 1
        *(aptr + i) = i + 1
    next

    'Print out the values of the memory segment
    for i = 0 to numelements - 1
        Print "Value of element";i;" is"; *(aptr + i)
    next

    Deallocate aptr
Else
    Print "Could not create pointer."
End If

Sleep
End

First, you will see that a new define has been added, #define numelements 10. This is so we know how many elements we will have in our memory segment. There are no functions in FreeBasic that will tell you the size of a memory segment; you need to keep track of this information yourself. Once the memory segment is successfully created using Callocate, you can use the memory segment.

Following the Callocate function, two For-Next loops are used, one to load values into the memory segment and one to read the values of the memory segment. Notice the use of the parentheses around the pointer variable. The parentheses are needed since the indirection operator has higher precedence than the addition operator. Without the parentheses, the compiler would think that you wanted to put the value of the variable i into the memory location rather than using i to increment the address of the pointer variable.

When you run the program you should see the following output.

Value of element 0 is 1
Value of element 1 is 2
Value of element 2 is 3
Value of element 3 is 4
Value of element 4 is 5
Value of element 5 is 6
Value of element 6 is 7
Value of element 7 is 8
Value of element 8 is 9
Value of element 9 is 10

Notice that the elements of the memory segment start at 0 and end at number-of-elements – 1. When aptr is initialized, it is pointing to the start of the memory segment so you begin counting at 0. You may be wondering how the compiler knows to move the address the correct number of bytes? Since the program Dimmed an integer pointer, and the compiler knows that an integer is 4 bytes long, when you add an offset to the pointer address, the compiler will calculate the correct number of bytes based on the type. In other words, aptr is a typed pointer, a pointer of a particular type. In this case an integer type.

One last note on this example. Notice that the actual starting address contained in aptr was never incremented. That is, the code isn’t aptr += i, but aptr + i. This is so you do not lose the starting address of the pointer variable, which is needed by both For-Next loops and Deallocate. You should never increment the actual starting address of your pointer. Once you lose the starting address of your memory segment, it is difficult, and sometimes impossible, to recover that information. Once you lose the starting address, you run the risk of accessing a memory location outside of the range of the pointer which generally leads to the (in)famous Memory Access Error.

Pointer Indexing

As you can see from the previous example, the memory segment behaves like an array. FreeBasic offers an alternative method to access the memory elements, Pointer Indexing. The following program illustrates how to use pointer indexing.

#define NULL 0
#define numelements 10

Dim aptr As Integer Ptr
dim i as integer

'Create the memory segment
aptr = Callocate(numelements, Len(Integer))
If aptr <> NULL Then

    'Initialize the memory segment
    for i = 0 to numelements - 1
        aptr[i] = i + 1
    next

    'Print out the values of the memory segment
    for i = 0 to numelements - 1
        Print "Value of element";i;" is"; aptr[i]
    next

    Deallocate aptr
Else
    Print "Could not create pointer."
End If

Sleep
End

Notice that in this example, square brackets are used as a suffix to aptr with the For-Next variable i contained within the brackets. As you can see this is visually a much cleaner implementation and looks quite similar to using a zero-based array.

Dynamic Dynamic Memory Buffers

Since pointers are dynamic not only can you create them at runtime, you change the size of the buffer at runtime as well. The following program illustrates using a memory segment to load an unknown number of integers at runtime.

#define NULL 0

Dim aptr As Integer Ptr
Dim As Integer i, numelements  

'Create the memory segment
aptr = Callocate(1, Len(Integer))
If aptr <> NULL Then

    'Read in the first value
    Restore datavalues
    Read i

    'Make sure we are not at the end of the list
    If i <> -1 Then
        numelements = 1
        aptr[numelements - 1] = i
        'Read in the rest of the values
        Do 
            Read i
            If i <> -1 Then
                'Increment the number of elements
                numelements += 1
                aptr = Reallocate(aptr, numelements)
                'Save the value in memory segment
                aptr[numelements - 1] = i
            End If
        Loop Until i = -1 

        'Print out the values of the memory segment
        For i = 0 To numelements - 1
            Print "Value of element";i;" is"; aptr[i]
        Next
    Else
        Print "No data values present."
    End If

    Deallocate aptr
Else
    Print "Could not create pointer."
End If

Sleep
End

datavalues:
data 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, -1

In this example, the data statements represent an unknown number of values. In a real program these values may be in a file or input by the user. Numelements has been changed from a #define to a variable so that the program can update the number of elements as needed. The pointer is initialized to a starting address using Callocate to be sure we have a valid pointer. The first Read statement loads the initial value from the list. The program then enters a DO LOOP and reads in the remaining values until a –1 is read which signals the end of the list.

The Reallocate function is used to grow the memory segment. Reallocate takes two parameters, the pointer to resize, and the number of needed elements in the memory segment. The number of elements is the total size of the buffer, not just the newly created portion. If the buffer contains data, as in this example, the data already in the buffer is copied over to the new buffer. Of course you can shrink the buffer as well, and any data contained within the memory segment that is outside the new size will be discarded.