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: | *-----------------------------------> ^
+----------------------------+
char arr[] = "hello";creates a 6-byte array on the stack and copies the literal's characters into it. You own it and may change it.const char *ptr = "hello";creates an 8-byte pointer holding the address of the literal, which lives in a read-only section of the program, alongside the machine code. Nothing is copied.
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:
| function | returns |
|---|---|
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
- Modifying a literal through
char *: segmentation fault. Useconst char *for literals, arrays for text you will change. - Copying the pointer and thinking you copied the string: after
char *t = s;both point to the same characters; changingt[0]changess[0]. To copy the text, use an array andsnprintf(orstrcpywith a size check). - Comparing with
==:p == "yes"compares addresses. Usestrcmp. - Using a search result without checking for
NULL:strchr(s, ':') + 1when there is no colon isNULL + 1, and printing it crashes. - Returning a pointer to a local array:
char buf[64]; ... return buf;dangles (gcc warnsfunction returns address of local variable). Literals are fine to return; local arrays are not.
Recap
char arr[] = "..."is a writable copy;const char *p = "..."points at a read-only literal that lives for the whole program.- Write
const char *for pointers to strings you will not modify. - Walk strings with
for (p = s; *p; p++); pointer differences give lengths and indexes. - A pointer into a string is its suffix;
strchr,strrchrandstrstrreturn such pointers (orNULL). - An array of
const char *is a table of strings, the same shape asargv.
Going further: read man 3 strchr and man 3 strstr. Then write starts_with(const char *s, const char *prefix) using strncmp and strlen.