Adding Pointers to Pointers

Expanding an allocated buffer is something you can do with a pointer, but not with an array. This flexibility is why I encourage all C language students to understand and use pointers, though it doesn’t make the concept nay less onery.

The goal of the program from last week’s Lesson is to build an array of allocated buffers, but it’s not an array. Instead, it’s an expanding buffer that contains pointers (memory locations) to integer storage. Each location holds the values to 10 integers. Figure 1 kinda illustrates how this contraption works.

Memory illustration

Figure 1. How storage is allocated and tracked in an expanding buffer.

I call each allocated buffer of 10 integers is a “stick.” The program keeps creating new sticks, tracking their addresses which are held in storage referenced by pointer tenint. Each time a new stick of 10 integers is allocated, its address is stored, as illustrated in the figure.

The program’s goal is to continue allocating new sticks for a total of ten. For today’s post, I improve the code from last week’s Lesson to show how ten such sticks can be allocated and tracked. No data is assigned to the sticks, as at this point my concern is managing the array of pointers.

2025_10_18-Lesson.c

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

/* allocate storage for ten integers */
int *ten(void)
{
    int *iptr = NULL;

    iptr = malloc( sizeof(int) * 10 );

    return(iptr);
}

int main()
{
    int **tenint = NULL;
    int y;

    /* seed the randomizer */
    srand( (unsigned)time(NULL) );

    /* allocate 10 sticks of 10 integers */
    for( y=0; y<10; y++ )
    {
        /* allocate first stick */
        if( tenint==NULL )
        {
            tenint = malloc( sizeof( int *) );
            if( tenint==NULL )
            {
                fprintf(stderr,"Unable to allocate storage\n");
                exit(1);
            }
        }
        else
        /* allocate additional sticks */
        {
            tenint = realloc(tenint, sizeof( int *) * (y+1) );
            if( tenint==NULL )
            {
                fprintf(stderr,"Unable to reallocate storage\n");
                exit(1);
            }
        }
        /* get the storage */
        *(tenint+y) = ten();
        if( *(tenint+y)==NULL )
        {
            fprintf(stderr,"Unable to allocate stick %d\n",y);
            exit(1);
        }
        printf("Stick %d allocated\n",y);
    }

    /* clean-up */
    for( y=0; y<10; y++ )
        free( *(tenint+y) );
    free(tenint);
    puts("All sticks freed");
    return 0;
}

In the main() function, int pointer tenint is declared as a double-pointer:

int **tenint = NULL;

This variable stores the location where various addresses are stored. Let me explain:

If the variable were declared as *tenint, it would reference the address of a buffer of integers, the location where integer values are stored. This variable was used in last week’s Lesson.

Because the variable is declared as **tenint, it holds the address where addresses are stored, specifically the addresses of integer buffers. (Look at Figure 1 again.)

In the code, the outer for loop uses variable y to allocate storage for the ten integer sticks. The first stick is handled differently as tenint isn’t yet allocated. An if test confirms this condition:

if( tenint==NULL )

If true, the malloc() function allocates storage for an integer pointer:

tenint = malloc( sizeof( int *) );

The sizeof operator uses expression int * to allocate storage for one integer pointer. This size is what’s needed, not storage for ten integers; what’s being stored is the address where ten integers are located in memory — a buffer.

When tenint is already initialized, the else condition handles its expansion. The realloc() function adds one integer-pointer-sized chunk to the storage at tenint. Variable y is determines how large to expand the buffer based on how many integer pointers (addresses) are already present:

tenint = realloc(tenint, sizeof( int *) * (y+1) );

For each spin of the loop, the tenint buffer is enlarged by one int-pointer-sized chunk.

Once new storage is allocated, an address is fetched from the ten() function. It’s assigned to the proper offset in the tenint buffer based on the value of variable y:

*(tenint+y) = ten();

The * (dereferencing) operator is used as it’s an address being stored in the buffer. This data type is what the tenint buffer is designed to store. If you instead use tenint+y (no asterisk), you get an address, not storage.

No values are assigned to the allocated storage returned from the ten() function; I’ll add this code in next week’s Lesson. But the loop continues, storing the addresses of the allocated buffers at specific offsets in the tenint buffer. Ten sticks are allocated.

For clean-up, each stick buffer is freed, then the tenint buffer:

for( y=0; y<10; y++ )
    free( *(tenint+y) );
free(tenint);

Here’s a sample run:

Stick 0 allocated
Stick 1 allocated
Stick 2 allocated
Stick 3 allocated
Stick 4 allocated
Stick 5 allocated
Stick 6 allocated
Stick 7 allocated
Stick 8 allocated
Stick 9 allocated
All sticks freed

Key takeaways from this code are:

  • The tenint list is a double pointer, the location of a buffer that stores addresses.
  • Pointer math allows each address to be accessed by using this format: *(tenint+y)
  • Each allocated stick must be freed first, before freeing the tenint buffer.

Crazy, but it works. For next week’s Lesson, I plug values into each stick.

Leave a Reply