Strings
char array plus a promise: the first '\0' (NUL) byte marks the end. There is no length field anywhere — every standard function walks the bytes until it finds the NUL. That design is the source of C strings' speed and of their most famous disasters. This page makes the model, and the safe idioms, second nature.
The NUL-Terminated Model
"Hello" occupies six bytes, not five: five letters plus the terminating '\0'. Array sizes must always leave room for it, and every string function trusts it to exist — overwrite or omit the NUL and functions read past your buffer looking for the end that never comes.
Figure 1 — "Hi" occupies 3 bytes; strlen counts until the NUL. No length is stored anywhere.
#include <stdio.h>
#include <string.h> // strlen
int main(void) {
char word[6] = {'H', 'e', 'l', 'l', 'o', '\0'}; // explicit NUL
char easy[] = "Hello"; // compiler adds the NUL
printf("strlen = %zu, bytes = %zu\n",
strlen(easy), sizeof(easy)); // 5 and 6 — the NUL counts
return 0;
}
The pair strlen (logical length) and sizeof (array capacity) look alike and are not — the first measures until NUL, the second the whole array. Confusing them is a classic off-by-one bug.
Literals, Arrays, and Pointers
There are three distinct things that all "look like strings", and only one of them is writable:
| Declaration | Storage | Writable? |
|---|---|---|
char *s = "Hi" | string literal (typically read-only .rodata) | No — modifying it is UB |
char s[] = "Hi" | local array, copied at init | Yes |
char s[3]; s[0]='H'; ... | explicit array built by hand | Yes |
#include <stdio.h>
int main(void) {
char *lit = "Hi"; // points into immutable storage
char arr[] = "Hi"; // own 3-byte copy on the stack
// lit[0] = 'X'; // UB — may crash on modern OSes
arr[0] = 'X'; // fine: arr is ours
printf("%s\n", arr); // "Xi"
return 0;
}
Reassigning the pointer is fine (lit = "bye") — the confusion is that char * invites writing through it. When a function takes const char *, it promises to treat the data as read-only; respect that contract on both sides.
The string.h Toolbox
The standard library ships a complete toolkit — and a set of traps. The key functions, with their safety notes:
| Function | Does | Safety note |
|---|---|---|
strlen(s) | length until NUL | Runs forever if not NUL-terminated |
strcpy(dst, src) | copy, incl. NUL | Writes unbounded — overflow risk |
strncpy(d, s, n) | copy up to n bytes | Does NOT always add NUL — pad it yourself |
strcat/snprintf | append / format | Use snprintf for safe appending |
strcmp(a, b) | lexicographic compare | Return 0 means equal |
strstr(hay, needle) | find substring | Returns pointer or NULL |
strchr/strrchr | find a character | First / last occurrence |
#include <stdio.h>
#include <string.h>
int main(void) {
char name[32];
snprintf(name, sizeof(name), "%-15s | %3d", "ada", 7); // safe formatting
printf("'%s'\n", name);
// find a substring and report which side it starts at
const char *text = "the quick brown fox";
const char *hit = strstr(text, "brown");
if (hit != NULL) {
printf("found at offset %td\n", hit - text); // 10
}
return 0;
}
The Safe Copy Idiom
Copying text into a fixed buffer is the most common string operation — and the most common exploit. The disciplined pattern: bound every write, and always enforce a final NUL yourself, because the library may not.
#include <stdio.h>
#include <string.h>
int main(void) {
const char *src = "a very long title that overflows small buffers";
char dst[16];
// SAFE: copy at most 15 bytes, then force the terminating NUL
strncpy(dst, src, sizeof(dst) - 1); // reserves one byte for '\0'
dst[sizeof(dst) - 1] = '\0'; // strncpy may have omitted it
printf("'%s'\n", dst); // truncated but well-formed
return 0;
}
Classic String Bugs
Every string exploit in history is one of these three. Learn the shapes and your review eye will catch them on sight:
- Buffer overflow — copying an unchecked source into a fixed buffer. The
gets()function could not even be given a bound and was removed from the standard for exactly this; never mourn it. - Missing NUL —
strncpyof a source as long as the buffer leaves no terminator; laterstrlen/printfwalk into foreign memory. - Off-by-one — declaring
char s[5]for"Hello": five letters plus NUL is six bytes.
char bad[5];
strcpy(bad, "Hello"); // BUG: writes 6 bytes into 5 — 'o' lands outside
char worse[3];
strncpy(worse, "abc", 3); // no room for NUL was reserved
printf("%s", worse); // BUG: reads past the array hunting for '\0'
Fix both with the safe idiom above — reserve the terminator slot and set it explicitly.
Dynamic Strings
When text must grow beyond a guessable size, build it on the heap with realloc. A minimal, correct grow-by-doubling loop is the seed of a real string library — the study projects page grows it into one.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void) {
size_t cap = 8, len = 0;
char *buf = malloc(cap);
if (buf == NULL) return 1;
const char *words[] = {"one", "two", "three"};
for (size_t i = 0; i < 3; i++) {
size_t need = len + strlen(words[i]) + 1; // +1 for space or NUL
if (need > cap) { // grow by doubling
cap *= 2;
char *grown = realloc(buf, cap);
if (grown == NULL) { free(buf); return 1; }
buf = grown;
}
len += snprintf(buf + len, cap - len, "%s%s", i ? " " : "", words[i]);
}
printf("joined: '%s'\n", buf);
free(buf);
return 0;
}
Next: data structures — linked lists, vectors, and hash tables built from these tools.