C String Pointer Idioms

Eight habits for working with strings through pointers. Each one takes a few minutes to read.

In C, a string is just a row of characters in memory, with a special '\0' character at the end to mark where it stops. A pointer is a number that says where one of those characters is. That is all you need to start, but a few patterns come up again and again in real C code, and once you know them, most string code becomes easy to read.

The first four come from a tiny regular-expression matcher by Rob Pike, from the book The Practice of Programming, which is only about 30 lines long because it uses all four. Most sections have a diagram: press Next to watch the pointers move.

1.A pointer is the rest of the string

Say p points at the first character of "abc". Then p + 1 points at the 'b', and if you print the string starting there, you get "bc". So any pointer into a string is a string too: the rest of the original, from that character to the end. Nothing is copied, since both pointers look at the same characters in memory.

This makes it easy to work through a string one character at a time: deal with the first character, then do the same thing again on p + 1, and stop when you reach '\0'. Here it is counting how many times a character appears:

int count_char(const char *s, int c)
{
    if (*s == '\0')                             /* nothing left: no more to count */
        return 0;
    return (*s == c) + count_char(s + 1, c);    /* this one, plus the rest */
}

*s means "the character that s points at", and (*s == c) is 1 if it matches and 0 if not, so it can be added straight to the count. (Each call uses a little memory, so for very long strings a loop is safer.)

2.'\0' marks the end

Every C string ends with '\0', so you never need to pass the length around. To ask "is there anything left?", you just look at the character the pointer is on:

return *text == '\0';   /* true if we have used up the whole text */

Rule: always check that you are not on the '\0' before you use the character or move past it.

Past the '\0' is memory that is not part of the string, and reading it can crash the program or quietly give wrong answers. In the matcher, . means "any character", so without the first check below it would match the '\0' too, and text+1 would point outside the string:

if (*text != '\0' && (regexp[0] == '.' || regexp[0] == *text))
    return matchhere(regexp+1, text+1);

Looking at the next character

p[0] is the character p points at, and p[1] is the one after it. Peeking ahead is often all you need: in a pattern like "a*", you only know that a is special once you see the * after it. The same rule applies one step further: only look at p[1] once you know p[0] is not '\0', so the worst p[1] can be is the '\0' itself.

while (p[0] != '\0') {          /* from here on, p[1] is safe to look at */
    if (p[1] == '*')                     { token(p[0], STAR); p += 2; }
    else if (p[0] == '$' && p[1] == '\0') { token('$', END);  p += 1; }
    else                                 { token(p[0], ONE);  p += 1; }
}
Splitting "a.*b$" into pieces

3.*p++: read, then move on

*p++ does two things at once: it gives you the character p is pointing at, and then moves p forward by one. Copying a string is just this, over and over, until the '\0' has been copied:

void copy(char *to, const char *from)
{
    while ((*to++ = *from++) != '\0')
        ;
}

The matcher uses it to try every possible starting point in the text. A do loop runs its body before checking the condition, so the empty string at the very end gets tried too:

do {
    if (matchhere(regexp, text))
        return 1;
} while (*text++ != '\0');
Every starting point in "ab"

4.Let one test protect another

With && (and) and || (or), C checks the left side first and stops as soon as it knows the answer, so if the left side of && is false, the right side never runs. That lets the left side protect the right. This loop counts how many characters at the start of the text match c:

while (*text != '\0' && (*text++ == c || c == '.'))
    n++;
Counting the as at the start of "aaab"

The order matters. Swap the two sides to c == '.' || *text++ == c, and for . the left side is always true, so || never runs the right side, text never moves, and the loop runs forever. Whenever one side of && or || changes something, ask: can the other side stop it from running?

5.const: a promise not to change the string

If a function only reads a string, write the parameter as const char *. This is a promise that the compiler holds you to, so if you change the string by mistake, the program will not compile:

int count_vowels(const char *s)
{
    ...
    *s = 'x';   /* error: the compiler refuses this */
}

A function that does change its string, such as one that turns it into capitals, has to take a plain char *.

char s[] and char *s are different

char buf[] = "hello"char *lit = "hello"
What it isyour own copy: a row of characters (an array)a pointer to the original text in the program
sizeof6: the five letters plus the '\0'8 on most computers: the size of a pointer
Change a letterfinenot allowed: the program may crash
Point it somewhere elsenot allowedfine

When you pass an array to a function, C quietly passes a pointer to its first character instead. That is why both can go to the same const char * parameter, and also why sizeof inside a function gives the size of a pointer, never the length of the string.

6.Give back a pointer, and subtract pointers

A function that searches a string can give back a pointer to what it found, or NULL ("points at nothing") if there is nothing. This is how the standard strchr works:

const char *find_char(const char *s, int c)
{
    for (; *s != '\0'; s++)
        if (*s == c)
            return s;
    return NULL;
}

What you get back is the rest of the string from that point (idiom 1), so you can search again from just after it. If you want a position number, subtract: taking one pointer away from another tells you how many characters apart they are, so p - s is the position of p, counting from 0.

for (p = find_char(s, 'a'); p != NULL; p = find_char(p + 1, 'a'))
    printf(" %d", (int)(p - s));     /* prints 1 3 5 for "banana" */
Finding every 'a' in "banana"

The length of a string works the same way: walk a second pointer to the '\0', and how far it went is the length.

size_t length(const char *s)
{
    const char *p = s;
    while (*p != '\0')
        p++;
    return p - s;          /* 6 for "banana" */
}

Rule: only subtract two pointers into the same string, and check a search result for NULL before you use it.

7.Two pointers to edit a string in place

To remove every 'a' from a string without making a new copy, use two pointers that walk along the same string: r (for read) looks at every character, and w (for write) only moves when a character is kept. w is never ahead of r, so it never writes over a character that has not been read yet.

void remove_char(char *s, int c)
{
    char *w = s;
    for (const char *r = s; *r != '\0'; r++)
        if (*r != c)
            *w++ = *r;
    *w = '\0';             /* the line people forget: end the shorter string */
}
remove_char("banana", 'a')

Change the test, and the same loop does other jobs. This one squeezes runs of spaces down to a single space, using w[-1], the last character written. w > s comes first so that w[-1] is only looked at once something has been written (idiom 4 again).

if (!(*r == ' ' && w > s && w[-1] == ' '))
    *w++ = *r;

8.A pointer to a pointer

When you pass a pointer to a function, the function gets its own copy of it, so if the function moves its copy, yours stays where it was. To let a function move your pointer, for example to say "I read a number, and here is where I stopped", give it the address of your pointer. Its type is then const char **, a pointer to a pointer, and this is how the standard strtol works:

int read_int(const char **pp, int *out)
{
    const char *p = *pp;               /* work on a copy, write it back at the end */
    while (*p == ' ')
        p++;
    if (*p < '0' || *p > '9')
        return 0;
    int n = 0;
    while (*p >= '0' && *p <= '9')
        n = n * 10 + (*p++ - '0');
    *out = n;
    *pp = p;
    return 1;
}

The caller passes &cursor, the address of their pointer, and keeps reading until there are no numbers left:

const char *cursor = "12 7  300";
int n;
while (read_int(&cursor, &n))
    printf("%d\n", n);                /* 12, then 7, then 300 */
Reading the numbers in "12 7 300"

The same type walks a list of strings, like the command-line arguments argv. The list ends with NULL, just as a string ends with '\0':

const char *words[] = {"one", "two", "three", NULL};
for (const char **w = words; *w != NULL; w++)
    printf("%s\n", *w);

One more thing: <ctype.h>

When you pass a character to isspace, isdigit or the other <ctype.h> functions, write isspace((unsigned char)*p). Some characters, such as accented letters, are stored as negative numbers, and those functions do not accept them.