Constant Pointers in C: Reading Right to Left

The const keyword in the C programming language can be quite confusing to read when it’s used with pointer variables. Consider the following pile of const modifiers on an array of strings:

const char *const *const p;

What part of this array of strings is rendered constant by each of the three const keywords?

In today’s blog post, we’ll discuss the meaning of multiple const keywords in a single variable declaration. We’ll demonstrate a trick for reading and understanding multiple const keywords, like in this example — the trick is to read the const modifiers in a variable declaration from right to left.

A Bare Two-Dimensional Array

Let’s begin by building an array of strings with no const modifiers. A string in C can be represented as a pointer to the first character in a NULL-terminated array of characters:

char *aString;

So, an array of strings can be represented as an array of pointers, each of which points to the first character in a NULL-terminated array:

char **arrayOfStrings;

We’ll use the following code throughout this blog post, which creates an array of strings, like the one above, in the create() function:

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

static char **create();

int main()
{
    char **arr = create();
    printf("%s\n%s\n", arr[0], arr[1]);
    return 0;
}

static char **create()
{
    char **arr = calloc(2, sizeof(char *));
    arr[0] = calloc(6, sizeof(char));
    arr[1] = calloc(7, sizeof(char));
    strcpy(arr[0], "Hello");
    strcpy(arr[1], "World!");
    return arr;
}

The array created by the above code contains the string "Hello" at index 0 and the string "World!" at index 1. It then prints those two words, one per line, in the main() function:

Hello
World!

Modifying the Bare Array

Because nothing is declared constant in the bare char ** array, we’re free to modify individual characters in the strings, the first-character pointer representing each string, and the overall char ** pointer variable itself.

Let’s introduce a function called modify(char **) that’ll modify our two-dimensional array in these three different ways:

static void modify(char **p)
{
    p[0][0] = 'h';
    p[1] = "Mars";
    p = NULL;
}

int main()
{
    char **arr = create();
    modify(arr);
    printf("%s\n%s\n", arr[0], arr[1]);
    return 0;
}

This code changes the first character in "Hello" to a lowercase 'h', changes the second string in the array (i.e., its first-character pointer) to "Mars", and then changes the local variable p to point to nothing. (Changing the local variable p to NULL has no effect on the overall behaviour of the program, but it’s allowed.)

The code now outputs:

hello
Mars

The Leftmost const

Let’s start by exploring the const modifier that we’ll see most often as C programmers: a const in the leftmost position in the variable declaration. Let’s change the signature of the modify() function to the following:

static void modify(const char **p)

We’re going to read this declaration for p from right to left.

Reading right to left, p is: a pointer to a pointer to characters that are constant. Critically, it is the “characters that are constant”. This declaration means that the characters in each string cannot be modified.

Going back to the definition of the modify() function:

static void modify(const char **p)
{
    p[0][0] = 'h';
    p[1] = "Mars";
    p = NULL;
}

The struck-out line, p[0][0] = 'h', violates the const modifier by modifying a character in a string, and will cause a compiler error.

Casting and Memory Leaks

As a quick note about the above example, each time we change the signature of modify(), we’ll cast the argument in the call to modify() that occurs in the main() function:

int main()
{
    ...
    modify((const char **)arr);
    ...
}

From now on, I’ll skip explicitly writing the cast in the main() function, and leave it to students to make the necessary cast each time we change the signature of modify().

For the purpose of this blog post, we’ll also ignore the memory leaks in this code. The three calls to calloc(3) have no corresponding calls to free(3) (and aren’t even checked for NULL returns), but that’s not important for this discussion.

The Middle const

With the note about casting and memory leaks aside, let’s try placing the const modifier in a different position. This time, let’s place it between the two pointer symbols. We’ll modify the signature of modify() to the following:

static void modify(char *const *p)

Reading from right to left, p is: a pointer to constant pointers to characters. Specifically, each pointer to characters (i.e., each pointer representing a string) is constant. That means that which string is pointed to at each array index in p cannot be modified.

static void modify(char *const *p)
{
    p[0][0] = 'h';
    p[1] = "Mars";
    p = NULL;
}

In this case, the struck-out line p[1] = "Mars" violates the const modifier by changing the value of the pointer at p[1] (i.e., the pointer representing the string at p[1]). This assignment will cause a compiler error.

The Rightmost const

Finally, let’s try placing the const modifier in the rightmost position — after both of the pointer symbols. This time we’ll modify the signature as such:

static void modify(char **const p)

Reading from right to left: p is a constant pointer to pointers to characters. That is, p is a constant pointer to an array of strings. This time, it’s the value of the variable p itself that can’t be modified.

static void modify(char **const p)
{
    p[0][0] = 'h';
    p[1] = "Mars";
    p = NULL;
}

In this case, the struck-out line p = NULL violates the const modifier by changing the value of p itself, and will cause a compiler error.

Combining const Modifiers

We’re able to combine any of these const modifiers to enforce, at compiler time, as many restrictions as we like. Let’s consider the following example:

static void modify(const char *const *p)

In this case, reading from right to left, p is a pointer to constant pointers to characters that are constant. Put another way, all of the characters in the strings are constant, and the pointer to each string is constant.

static void modify(const char *const *p)
{
    p[0][0] = 'h';
    p[1] = "Mars";
    p = NULL;
}

This time, the top two operations in modify() will be caught as errors at compile time.

I encourage students to experiment with all the possible combinations of these const modifiers in the signature of modify(), to get a feel for what data is protected with each choice of modifiers.

Summary

The const keyword in C can seem pretty overwhelming at first when it is applied to pointers, or even pointers to pointers (such as with an array of strings).

By using a const modifier, we can protect the contents of a local highest-level pointer variable, the value of the pointers inside an array of pointers, or even the deepest-level values (i.e., the individual values inside a two-dimensional array).

The trick to understanding the different const modifiers in a variable declaration is to read them from the right to the left. Doing so with an array of strings allows us to determine if it’s the characters that are constant (the deepest-level char values in the char **), the individual string pointers in the array that are constant (the char * pointers inside the char ** array), or the top-level variable that’s constant (the value of the char ** pointer itself).

For more tips, and to arrange for personalized tutoring for yourself or your study group, check out Vancouver Computer Science Tutoring.