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.
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.)
'\0' marks the endEvery 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);
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; }
}
"a.*b$" into pieces*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');
"ab"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++;
*text != '\0': is there anything left? If not, the rest never runs, so
text never moves past the end.*text++ == c: does this character match? This also moves text on, even
when the answer is no.|| c == '.': or, if c is ., accept any character.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?
const: a promise not to change the stringIf 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 differentchar buf[] = "hello" | char *lit = "hello" | |
|---|---|---|
| What it is | your own copy: a row of characters (an array) | a pointer to the original text in the program |
sizeof | 6: the five letters plus the '\0' | 8 on most computers: the size of a pointer |
| Change a letter | fine | not allowed: the program may crash |
| Point it somewhere else | not allowed | fine |
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.
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" */
'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.
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;
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;
}
pp is where the caller keeps their pointer.*pp is the caller's pointer itself.**pp is the character the caller's pointer is on.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 */
"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);
<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.