Pointers · about 50 min

Strings through pointers

Why this matters

You now know that a string is a char array with a terminator, and that an array decays to a pointer to its first element. Put those together and you get the way C code actually handles text: as char * and const char *. Every string function in the library takes and returns pointers; argv is an array of them; tables of names are arrays of them. Pointers also let you refer to part of a string, such as the file name at the end of a path, without copying a single byte. This lesson connects strings and pointers and clears up the confusing difference between char s[] and char *s.

Two ways to name "hello"

#include <stdio.h>

int main(void)
{
    char arr[] = "hello";       /* an array: a writable copy */
    const char *ptr = "hello";  /* a pointer to the literal itself */

    arr[0] = 'j';
    printf("arr = %s, ptr = %s\n", arr, ptr);
    printf("sizeof arr = %zu, sizeof ptr = %zu\n", sizeof arr, sizeof ptr);
    printf("ptr + 2 = %s\n", ptr + 2);
    return 0;
}
arr = jello, ptr = hello
sizeof arr = 6, sizeof ptr = 8
ptr + 2 = llo

They look alike but are different things:

  stack (main's frame)                         read-only data (part of the executable)
 +----------------------------+               +---+---+---+---+---+----+
 | arr: | j | e | l | l | o |\0|              | h | e | l | l | o | \0 |   the literal
 +----------------------------+               +---+---+---+---+---+----+
 | ptr: |  *-----------------------------------> ^
 +----------------------------+

String literals live for the whole run of the program, so returning one from a function is fine (return "error";), unlike returning a pointer to a local array.

Literals are read-only

char *p = "hello";
p[0] = 'j';

This compiles without a single warning, yet:

$ ./prog
Segmentation fault (core dumped)

Modifying a string literal is undefined behavior; Linux maps that memory read-only, so the CPU faults. C allows char *p = "..." (without const) only because pre-1989 code did it. Always write const char * for pointers to literals; then the mistake becomes a compile error: assignment of read-only location '*p'. (gcc's -Wwrite-strings option warns about the non-const form, but it is not part of -Wall -Wextra.)

Walking a string with a pointer

The pointer version of the "visit each character" loop:

for (const char *p = s; *p != '\0'; p++) {
    /* *p is the current character */
}

*p != '\0' is often shortened to *p, since the terminator is the only char that counts as false. Here are two library functions written this way:

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

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

void copy(char *dst, const char *src)
{
    while ((*dst++ = *src++) != '\0') {
        /* everything happens in the condition */
    }
}

int main(void)
{
    char buf[32];
    const char *path = "/usr/include/stdio.h";

    copy(buf, "pointers");
    printf("%s has length %zu\n", buf, length(buf));

    const char *slash = strrchr(path, '/');   /* last '/' in path */
    if (slash != NULL) {
        printf("file name: %s\n", slash + 1);
        printf("directory: %.*s\n", (int)(slash - path), path);
    }

    const char *hit = strstr(path, "include");
    printf("strstr found \"include\" at index %td\n", hit - path);
    return 0;
}
pointers has length 8
file name: stdio.h
directory: /usr/include
strstr found "include" at index 5

length moves p to the terminator; the pointer difference is the length. copy is the famous one-liner from the original C book. Read *dst++ = *src++ as: copy the char at src to dst, then advance both (postfix ++ applies to the pointers, after their old values are used). The assignment's value is the char copied, so the loop stops right after copying the terminator. It is compact, and exactly as unsafe as strcpy: it has no idea how big dst is.

Pointers into the middle: suffixes for free

Any pointer into a string, up to its terminator, is a valid string: the suffix from that point on. slash + 1 above is the file name; no copy was made. The library functions that search return such pointers:

functionreturns
strchr(s, c)pointer to the first c in s, or NULL
strrchr(s, c)pointer to the last c, or NULL
strstr(s, sub)pointer to the first occurrence of the string sub, or NULL

Check for NULL before using the result. Subtracting the original pointer gives an index.

A prefix is harder, because it would need its own terminator. Either copy it (snprintf), or print it with a precision: %.*s takes an int length argument followed by the string, and prints at most that many characters, which is how "directory" was printed above. Or, if the buffer is yours, write a '\0' into it, which is exactly what trim in this lesson's exercise does.

Arrays of strings

An array of const char * is the standard way to keep a table of strings:

#include <stdio.h>

int main(void)
{
    const char *days[] = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"};
    int n = sizeof days / sizeof days[0];

    for (int i = 0; i < n; i++) {
        printf("%d: %s (first letter %c)\n", i, days[i], days[i][0]);
    }
    printf("sizeof days = %zu\n", sizeof days);
    return 0;
}
0: Mon (first letter M)
1: Tue (first letter T)
...
6: Sun (first letter S)
sizeof days = 56
  days (7 pointers, 56 bytes)        literals (read-only)
  +-----+
  |  *--+------------------------->  "Mon\0"
  +-----+
  |  *--+------------------------->  "Tue\0"
  +-----+
  | ... |
  +-----+
  |  *--+------------------------->  "Sun\0"
  +-----+

days[i] is a const char *, and days[i][0] indexes into the string it points to. This is exactly the shape of argv from the arrays module: char *argv[] is an array of pointers to strings. Compare with a 2D char array, char days2[7][4], which stores the characters themselves in 28 bytes, every row the same width. The pointer table allows strings of different lengths and is cheap to sort: you swap pointers, not characters.

const char * in function parameters

A function that only reads a string should take const char *. It documents the intent, lets callers pass literals safely, and the compiler enforces it. strlen, strcmp, strchr: all take const char *. A function that modifies the string, like trim below, takes char *, and must only be given writable memory (an array), never a literal.

Common mistakes

Recap

Going further: read man 3 strchr and man 3 strstr. Then write starts_with(const char *s, const char *prefix) using strncmp and strlen.

Exercises

Exercise 1 · quiz

Writing to a literal

What happens when this runs on Linux?

char *p = "hello";
p[0] = 'j';
printf("%s\n", p);
Answer
  • Undefined behavior: string literals are read-only, and on Linux the write crashes with a segmentation fault

p points directly at the literal's characters, which the compiler places in a read-only section of the executable. Modifying a string literal is undefined behavior; on Linux the page is mapped read-only, so the write faults. It compiles without a warning (C keeps char *p = "..." legal for historical reasons), which is why you should write const char *p = "hello";: then the compiler rejects p[0] = 'j'. If you need a modifiable string, use an array: char s[] = "hello";.

Exercise 2 · quiz

Predict the output

What does this print? (strchr(s, c) returns a pointer to the first c in s.)

const char *s = "programming";
printf("%s %c %s\n", s + 3, *(s + 1), strchr(s, 'm'));
Answer
  • gramming r mming

A pointer into the middle of a string is itself a string: it runs from there to the same terminator. s + 3 skips "pro", giving "gramming". *(s + 1) is the char at index 1, 'r'. strchr finds the first 'm' (index 6) and returns a pointer to it, so %s prints from there: "mming". No characters were copied for any of this.

Exercise 3 · write code

Count a character

Write

int count_char(const char *s, char c)

that returns how many times c occurs in the string s. Walk the string with a pointer (while (*s != '\0') and s++) instead of an index. For example count_char("banana", 'a') is 3 and count_char("", 'x') is 0.

File: main.c

Starting point

#include <stdio.h>

int count_char(const char *s, char c)
{
    /* your code here */
    return 0;
}

int main(void)
{
    printf("%d\n", count_char("banana", 'a'));
    return 0;
}

Example

expected output

3
Hint 1

s is your own copy of the pointer, so you may move it with s++ without affecting the caller.

Hint 2

Inside the loop compare *s == c (the character), then advance with s++.

Reference solution
#include <stdio.h>

int count_char(const char *s, char c)
{
    int n = 0;
    while (*s != '\0') {
        if (*s == c) {
            n++;
        }
        s++;
    }
    return n;
}

int main(void)
{
    printf("%d\n", count_char("banana", 'a'));
    return 0;
}

Exercise 4 · write code

Trim without copying

Write

char *trim(char *s)

that removes leading and trailing spaces from s without copying the text: write a '\0' just after the last non-space character, and return a pointer to the first non-space character (a pointer into the same buffer). If s is all spaces or empty, return a pointer to an empty string (for example the terminator itself). Only the space character ' ' counts as space.

The starter's main reads lines and prints each trimmed line between square brackets, for example hi there becomes [hi there]; keep it as it is.

File: main.c

Starting point

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

char *trim(char *s)
{
    /* your code here */
    return s;
}

int main(void)
{
    char line[256];
    while (fgets(line, sizeof line, stdin) != NULL) {
        line[strcspn(line, "\n")] = '\0';
        printf("[%s]\n", trim(line));
    }
    return 0;
}

Example

input

   hi there

expected output

[hi there]

Example

input

clean

expected output

[clean]
Hint 1

First move s forward while *s == ' '. Now s points at the first non-space character (or the terminator).

Hint 2

Then make char *end = s + strlen(s); (pointing at the terminator) and move it back while the character before it, end[-1], is a space. Stop at s so you never move before the start.

Hint 3

Finally *end = '\0'; and return s;.

Reference solution
#include <stdio.h>
#include <string.h>

char *trim(char *s)
{
    while (*s == ' ') {
        s++;                      /* skip leading spaces */
    }
    char *end = s + strlen(s);    /* points at the terminator */
    while (end > s && end[-1] == ' ') {
        end--;                    /* back up over trailing spaces */
    }
    *end = '\0';
    return s;
}

int main(void)
{
    char line[256];
    while (fgets(line, sizeof line, stdin) != NULL) {
        line[strcspn(line, "\n")] = '\0';
        printf("[%s]\n", trim(line));
    }
    return 0;
}