Posts
Search
Contact
Cookies
About
RSS

Pointers - newbie hell for the C coder ?

Added 28 Oct 2020, 3:28 p.m. edited 18 Jun 2023, 1:12 a.m.

Pointers can be a confusing topic for the new C programmer, often explanations get bogged down in too much technical detail, or are just too simplistic. I'm going to attempt to show in both a simple and comprehensive manner just what pointers are and how to use them.

First let's consider the design of a library, there are a number of aspects that affect the API (Application Programming Interface) often a function will need a particular piece of information, this can sometimes be a large structure - something you wouldn't want to copy wholesale to a function, additionally if the function needs to alter the structure passing it a copy won't help, the function changes the copy but what about the original ?

C always uses "pass by value" for parameters when calling a function, so we have a problem! Fortunately this is where pointers come in.

A pointer is at heart just an integer value, it can have a "type" but we'll get to that later. This value is nothing special, it's just a memory address. Lets look at a fragment of code.

my_ptr = &my_big_structure;
a_function_call(my_ptr); 

Oh ho, I've snuck in a bit of ascii soup! I have a distaste for random ascii characters in languages, fortunately C doesn't do this anywhere as badly as some other languages, but I digress! What is this offending character? Did you spot the ampersand (&) ? The easiest way to "read" this is as "the address of"

So the first line of code reads my_ptr equals the address of my_big_structure. Passing by value has some important implications, if we pass a structure to a function, the whole value (contents) of the structure is copied to the function, could be a big copy, and the function only has access to this copy, that ceases to exist when the function returns. However in the code above, although we are still passing by value (the value being the address of the structure), the function knows exactly which structure it is and if it modifies it, it's not modifying a copy. Additionally we haven't had to copy the whole of the structure but instead just a single integer value (the value of the pointer). Incidentally you could return a structure you're modifying, at the cost of another copy, this is okay for one or two small structures, but you do need to keep a weather eye on just what you're copying to functions.

Sending structures using pointers is a two way street, often when you initialise a library you will be passed back a pointer, (sometimes called a "context") that is used when calling further functions from that library. Often for libraries that are stateful they will hold various global values in their "context".

So far we can see that a pointer is just that, a way to redirect us to some other value, a pointer to something else. Lets also look at how related pointers and arrays are with a somewhat contrived code example

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

void addem( int* v, int n) 
{
    int t = 0;
    for (int i=0; i<n; i++) {  t += v[i];  }
    printf("total = %i\n", t);
}

void main() 
{    
    int* a = malloc(sizeof(int)*3);
    a[0] = 3;
    a[1] = 2;
    a[2] = 1;
    
    int b[] = { 3, 2, 1 };
    
    addem( a, 3 );
    addem( b, 3 ); 

    free(a);
}

Looking at the start of the main function we can see some more ascii soup this time the asterisk (*) which is often used to avoid confusion with x ( x and ✕ (multiply) are just too similar!) In this case however its being used to signify that a is a pointer, specifically its an integer pointer (int*). So how does a pointer have a type? Surely its just a memory location, well the type refers to the type of value the pointer is pointing to. So you know that an integer pointer (int*) is pointing to integer values, and this is why there is a void pointer (void*) too, sometimes its not appropriate to give a type what we're pointing to.

Getting back to the code the variable a is being assigned a value from the malloc function, this simply allocates some space for us and passes back the address (a pointer) to that block, its single parameter is the amount of memory we want, in this case the size of 3 integers. Even for basic types like int you're always wise to use sizeof. Who knows at some point in the future you could be compiling this code on a system with different sized integers, while the C standards do specify a minimum size for an integer its does NOT specify a maximum.

Having set aside some space for some integers in memory, we are using the pointer as if its an array to store some values, let's look at this in a little more detail.

a[1] = 2;         // these lines are equivalent 
*( a + 1 ) = 2;

At first glance this isn't particularly readable code and is probably best avoided! breaking it down, first we are dereferencing the a pointer + 1 or unpacking the pointer so we're talking about the integer that the pointer is pointing to, but hang on if we just add one to a pointer, its not referencing the correct location is it? Well fortunately when doing pointer arithmetic (oh the horror!) the compiler is bright enough to check what type the pointer is, so it can multiply the addition by the size of the type.

So that line of code is saying find the value at pointer a plus the size of an integer and set the value at that memory location to 2. Phew! isn't it much more readable when you treat a pointer as if its an array!!! As an aside this is why you can't do pointer arithmetic with a void pointer (void*)

The other similarity with arrays comes when we call a function that is expecting a pointer, if you supply an array variable (with out an index) you are in effect saying...

        addem( &b[0], 3 );  // these lines are equivalent
        addem( b, 3 );

So you can see that there is a tight correlation between pointers and arrays, and in fact pointers are often used when manipulating arrays.

Lets look at some potentially even more confusing code!

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

struct blob {
    int member;
    // more stuff here making it unfeasibly large!
};

int main()
{
    struct blob* blobs = malloc(sizeof(struct blob)*8);
    blobs[0].member=1;
    blobs[1].member=2;
    //...
    blobs[7].member=7;

    struct blob* pnta = blobs;
    struct blob* pntb = blobs + 7;
    
    struct blob* tmp;
    tmp = pnta;
    pnta = pntb;
    pntb = tmp;
    
    printf("first value %i, second value %i\n", pnta->member, pntb->member);
}

We're assuming here that our structure is something large that we don't want to copy around any more than we have to. First of all we're allocating some memory as before, again using the pointer as if it were an array. This means we can use the dot notation to access structure members as if blobs wasn't a pointer but an actual instance of a structure, allowing us to assign values to each of the structures in the block of memory we've allocated.

Next we're creating two pointers, and as before we're using pointer arithmetic in this case to reference the first and eighth item.

Now when we swap the values of the pointers, no "blob" structures have been copied, however what do you think are the values that get printed out. If you're not sure, compile the code and see what happens, is this what you expected ?

Notice in the printf statement there is more ascii soup (->) the allows us to access structure members from a pointer. At the risk of confusing! these two lines are equivalent.

    printf("first value %i, second value %i\n", pnta->member, pntb->member);
    printf("first value %i, second value %i\n", (*pnta).member, (*pntb).member);

While the above two lines are equivalent I hope you'll agree using the arrow operator (->) is much more readable. By dereferencing the pointer we get to use it just as if it was an instance of the structure (or other type) that its pointing to.

Imagine if you needed to view an array sometimes ordered by some member value and sometimes ordered by another member. You could reorder the list every time, potentially causing many copies of structures. A much easier approach however is to maintain two additional arrays of pointers, you swap around the pointers in these arrays to represent the different ordering of the actual data.

Hopefully I haven't confused you too much! I think especially for the new(ish) coder there's a lot of detail here, which I hope I've illustrated with at least some clarity. Do reread anything that you find difficult and check the various links too...

For the more advanced amongst you, you might want to try a challenge !

typedef struct _node {
    void* data
    struct node* next;
} node;

Given this minimal structure we can build simple lists. Given a number of "node" structures with each "next" member pointing to the next node, and each data pointer pointing to some other structures we want to maintain a list of, can you make a set of functions that will let you do the following:

Okay this is advanced stuff so don't get disheartened if you get stuck, start off by manually creating and linking a few nodes together, can you iterate through this list ?

Here's something to get you started...

#include <stdio.h>

typedef struct _node node;

struct _node {
    void* data;
    struct _node* next;
};


int main()
{
    char* str[] = { "One", "Two", "Three" };

    node list, n2, n3;
    
    list.data = str[0];
    list.next = &n2;
    
    n2.data = str[1];
    n2.next = &n3;
    
    n3.data = str[2];
    n3.next = NULL;
    
    node* current = &list;
    
    while (current != NULL) {  // iteration ain't I nice to you!
        printf("%s\n", (char*)current->data);
        current = current->next;
    }

    return 0;
}

Good luck, and don't forget you're supposed to be enjoying this!