Arrays & Strings · beginner · ~8 min
## What you will learn - Explain what `strlen` measures and why C strings need a terminator to have a length at all. - Write the canonical pointer-walking loop `while (*p) p++;` and a beginner-friendly index version, and know they are equivalent. - Compute a length from two pointers using pointer subtraction (`p - s`) and understand the `ptrdiff_t` / `size_t` types involved. - Choose the correct return type (`size_t`) and parameter type (`const char *`) for a read-only string function. - Identify and avoid the two classic strlen hazards: a missing NUL terminator and a `NULL` pointer. - Reason about why `strlen` is O(n) and what that means for performance in loops.
Almost every C program eventually has to answer a simple question about text: how long is this string? You need the length to allocate a buffer, to copy bytes, to loop over characters, to compare two pieces of text, or to print something neatly. The standard-library function that answers it is strlen, declared in <string.h>.
In most languages a string "knows" its own length — it is stored next to the characters. C does not work that way. As you saw in C strings, a C string is just a plain array of char with one rule: the real text ends at the first byte whose value is zero, written '\0' and called the NUL terminator. Nothing records the length separately. So to find the length, strlen has to start at the beginning and count characters until it hits that zero byte. That single idea — walk a NUL-terminated string — is the foundation of nearly every string routine in C.
This lesson rebuilds strlen from scratch. Doing so connects two things you already know: the C strings memory model (characters followed by a '\0') and the looping tools from for / while / do-while. Once you can write my_strlen confidently, the same walking pattern lets you implement copying, searching, and counting functions later in this category. We will start with plain language and a memory picture, then introduce the precise terminology (pointers, pointer arithmetic, size_t) as we go.
Length handling is where a huge fraction of real C bugs and security holes live. If a program miscounts a string's length — or asks for the length of data that was never terminated — it can read past the end of an array, corrupt memory, crash, or leak data. Buffer overflows, one of the most exploited classes of vulnerability in history, almost always involve a length that was wrong or unchecked.
Understanding strlen deeply gives you three practical wins. First, you stop treating string functions as magic: you know exactly what work they do and what they assume about your data. Second, you learn that strlen is O(n) — it re-scans the whole string every time — so calling it inside a loop condition can quietly turn an O(n) loop into O(n²). Third, you internalize the safety habit that the rest of the standard library depends on: a buffer is only a valid C string if it actually contains a '\0'. That habit carries directly into strcpy, strcat, and every networking, parsing, or file-handling task you will ever write in C.
A C string is an array of bytes ending in '\0' (the integer value 0). The string "cat" is stored as four bytes, not three:
index: 0 1 2 3
char: 'c' 'a' 't' '\0'
value: 99 97 116 0
<-- text --> ^ terminator
strlen = 3 (the '\0' is NOT counted)
strlen returns the number of bytes before the terminator. The terminator itself is never included in the length, even though it occupies a real byte in memory. This is why a buffer that must hold an N-character string needs room for N + 1 bytes.
When to rely on this: any time you receive data that is guaranteed NUL-terminated (string literals, output of well-behaved library functions). When NOT to: raw bytes from a file, socket, or read() call are not automatically terminated — calling strlen on them is a bug.
Knowledge check: How many bytes does the literal "" (empty string) occupy in memory, and what does strlen("") return?
A pointer is a variable that holds the address of a byte. const char *p = s; makes p point at the first character. The expression *p (dereference) reads the byte p currently points to. p++ advances p to the next byte. By repeatedly testing *p and advancing, you walk through the string one character at a time.
s ---> [ 'c' ][ 'a' ][ 't' ][ '\0' ]
^p (start)
^p
^p
^p -> *p == '\0', stop
The loop while (*p) p++; reads as: "while the current character is not zero, move forward." Because '\0' equals 0 and C treats 0 as false, the loop stops exactly at the terminator. There is no separate comparison *p != '\0' needed — though writing it that way is clearer for beginners and compiles to the same thing.
Knowledge check (predict the output): If p and s both point at the start and the loop runs until *p == '\0', what is the value of p - s for the string "hi"?
When two pointers point into the same array, subtracting them yields the number of elements between them. After the walk, p sits on the '\0' and s is still at the start, so p - s is the count of characters scanned — the length. The result of pointer subtraction has type ptrdiff_t (a signed type); we cast it to size_t for the return value because a length is never negative.
s ---------------------> p
[ 'c' ][ 'a' ][ 't' ][ '\0' ]
\__________________/
p - s == 3
Pitfall: pointer subtraction is only defined when both pointers are in the same array (or one-past-the-end of it). Subtracting unrelated pointers is undefined behavior.
size_t and const char *strlen returns size_t, an unsigned integer type defined in <stddef.h> / <string.h> that is big enough to hold the size of any object. Using int instead is a real bug: on very long strings an int can overflow, and comparing a signed result against unsigned values causes confusing conversions.
The parameter is const char * because the function only reads the string. const documents that intent and lets the compiler reject any accidental write. This matches the real prototype: size_t strlen(const char *s);.
Knowledge check (explain in your own words): Why is const on the parameter useful even though strlen would compile without it?
strlen must touch every character once, so its time grows linearly with the length — O(n). This is fine on its own, but dangerous when hidden inside a loop condition like for (size_t i = 0; i < strlen(s); i++), which recomputes the length on every iteration and becomes O(n²). Compute the length once and store it.
The standard prototype lives in <string.h>:
size_t strlen(const char *s);
// ^^^^^^ ^^^^^ ^^^^^^
// return type const: read-only the string to measure
A minimal, annotated reimplementation:
#include <stddef.h> // for size_t
size_t my_strlen(const char *s) {
const char *p = s; // p walks the string; s stays anchored at the start
while (*p != '\0') { // stop at the NUL terminator (*p == 0)
p++; // advance one character
}
return (size_t)(p - s); // characters between start and terminator = length
}
The ultra-compact idiom while (*p) p++; is identical in behavior to while (*p != '\0') p++; — prefer the explicit form while learning.
strlen?strlen is one of the smallest and most widely used functions in the C standard library. It returns the length of a string.
Implementing it yourself teaches the classic C pattern for processing text: walk a NUL-terminated string.
A NUL-terminated string is an array of characters that ends with a special marker byte, '\0' (value zero). That byte tells you where the string stops.
size_t my_strlen(const char *s) {
const char *p = s;
while (*p) p++;
return (size_t)(p - s);
}
Here is what each part does:
p starts at the beginning of the string.while (*p) keeps looping as long as the current character is not '\0'. The NUL byte is zero, which is treated as false, so the loop stops there.p++ moves to the next character.p - s is the number of characters between the start and the terminator. That is the length.const for read-only parametersDeclare the parameter as const char * when the function only reads the string.
#include <stdio.h>
#include <stddef.h> // size_t
#include <string.h> // real strlen, for comparison
/* Reimplementation of strlen using a pointer walk.
* Precondition: s must point to a NUL-terminated string (not NULL). */
size_t my_strlen(const char *s) {
const char *p = s; // p moves; s remembers where we started
while (*p != '\0') { // loop until the terminator
p++; // step to the next character
}
return (size_t)(p - s); // distance walked = number of characters
}
int main(void) {
const char *samples[] = { "cat", "", "hello, world", "a\tb" };
size_t count = sizeof(samples) / sizeof(samples[0]);
for (size_t i = 0; i < count; i++) {
size_t mine = my_strlen(samples[i]);
size_t real = strlen(samples[i]); // sanity-check against the library
printf("\"%s\" -> my_strlen=%zu, strlen=%zu %s\n",
samples[i], mine, real,
(mine == real) ? "(match)" : "(MISMATCH!)");
}
return 0;
}
What it does: my_strlen walks each sample string to its '\0' and returns the count, then main prints that count alongside the library strlen so you can verify they agree. The %zu format specifier is the correct one for size_t.
Expected output:
"cat" -> my_strlen=3, strlen=3 (match)
"" -> my_strlen=0, strlen=0 (match)
"hello, world" -> my_strlen=12, strlen=12 (match)
"a b" -> my_strlen=3, strlen=3 (match)
Edge cases shown: the empty string "" has length 0 because the very first byte is '\0'. The string "a\tb" contains an embedded tab character (one byte), so its length is 3 — escape sequences like \t are a single byte, not two. Note there is no free/fclose needed here: the strings are string literals with static storage and no heap or file resources are acquired.
We trace my_strlen("cat"). Memory holds c a t \0 at addresses we will call 100, 101, 102, 103.
const char *p = s; — p and s both hold address 100, pointing at 'c'.while (*p != '\0') — *p is 'c' (not 0), condition true, enter the loop.p++ — p becomes 101 ('a').*p = 'a' → true; p++ → 102 ('t').*p = 't' → true; p++ → 103 ('\0').*p = '\0' → false; exit the loop.return (size_t)(p - s) → 103 - 100 = 3.| step | p (addr) |
*p |
condition | action |
|---|---|---|---|---|
| init | 100 | 'c' | — | p = s |
| 1 | 100 | 'c' | true | p++ |
| 2 | 101 | 'a' | true | p++ |
| 3 | 102 | 't' | true | p++ |
| 4 | 103 | '\0' | false | exit |
| return | 103 | — | — | p - s = 3 |
Key insight: s never moves, so it acts as a fixed reference point. The loop's only job is to push p to the terminator; the subtraction at the end converts "where we stopped" into "how far we walked," which is the length. For the empty string "", step 1's condition is immediately false, so the loop body never runs and p - s is 0.
int instead of size_tint my_strlen(const char *s) { // WRONG return type
int n = 0;
while (s[n]) n++;
return n;
}
Why it is wrong: lengths are never negative and can exceed INT_MAX on large buffers, causing signed overflow (undefined behavior). It also clashes when compared against the unsigned values the rest of the library uses. Fix: return size_t and count with a size_t. Recognize it: compiler warnings like "comparison of integer expressions of different signedness."
char buf[3] = { 'a', 'b', 'c' }; // no room for '\0'
size_t n = my_strlen(buf); // walks past the end of buf!
Why it is wrong: buf has no terminator, so the loop reads beyond the array until it happens to hit a zero byte somewhere in memory — a buffer over-read and undefined behavior. Fix: ensure the buffer holds a '\0', e.g. char buf[4] = "abc"; or set buf[3] = '\0'. Prevent it: never call strlen on bytes read from files/sockets without first terminating them.
for (size_t i = 0; i < strlen(s); i++) { /* ... */ } // O(n^2)
Why it is wrong: strlen(s) runs on every iteration, re-scanning the whole string each time. Fix: compute once: size_t len = strlen(s); for (size_t i = 0; i < len; i++) .... Recognize it: programs that get dramatically slower as input strings grow.
char *copy = malloc(strlen(src)); // WRONG: forgot the +1
strcpy(copy, src); // writes one byte too many
Why it is wrong: strlen does not count '\0', so you must allocate strlen(src) + 1 bytes for a copy. Fix: malloc(strlen(src) + 1). This single off-by-one is one of the most common C string bugs.
Compiler errors / warnings
int and size_t. Make your counters size_t.%zu to print a size_t, not %d.#include <string.h>.Runtime errors
NULL/dangling pointer. Run under a sanitizer: cc -fsanitize=address,undefined -g file.c && ./a.out. AddressSanitizer will point at the exact over-read.x/8xb s shows eight raw bytes from s so you can confirm a 00 terminator is present.Logic errors
printf("[%c=%d]", *p, *p); inside the loop.Questions to ask when it doesn't work: Is the pointer non-NULL? Is there a '\0' somewhere in the buffer? Are my counter and return types size_t? Did I reserve len + 1 bytes when allocating a copy?
strlen is a textbook source of buffer over-reads when its precondition is violated, so treat the precondition as load-bearing:
strlen keeps reading until it finds a zero byte. If the buffer has none, it reads past the array end — undefined behavior that can crash, leak adjacent memory, or be exploited. Always guarantee a '\0' before measuring. Functions like read(), recv(), and fread() do not add one.strlen(NULL) dereferences address 0 — undefined behavior, typically a segfault. If a pointer might be NULL, check it first: if (s == NULL) return 0; (or handle the error explicitly).strlen on a pointer to memory that has been freed or to a local array that has gone out of scope. The bytes may have been reused.strlen(s) + 1 to include the terminator. Forgetting the + 1 is a one-byte heap overflow.strnlen(s, max) (POSIX) which stops after max bytes and cannot run off a non-terminated buffer.To verify safety, compile with -fsanitize=address,undefined and feed edge cases (empty string, no terminator, NULL) in a local test — the sanitizers will flag the over-read or the NULL deref immediately.
Where it shows up: strlen (or an inlined equivalent) runs constantly — every time a shell prints a prompt, a web server formats a header, a database stores a text column, or printf lays out %s. Parsers measure tokens with it; networking code uses it to size payloads; embedded firmware uses it on fixed command strings. Compilers often replace strlen("literal") with a compile-time constant and may use SIMD-optimized versions for runtime strings, but the contract is the same as the simple loop you wrote.
Professional best-practice habits
Beginner rules:
size_t; print with %zu.const char *.strlen(s) + 1 for copies.Advanced habits:
strnlen, snprintf, memcpy with an explicit count) over those that scan to a terminator.Write size_t my_strlen(const char *s) using array indexing (s[i]) instead of a pointer walk. Loop until s[i] == '\0' and return i. Test: "" → 0, "hello" → 5. Hint: declare i as size_t. Concepts: terminator, loops, size_t.
Reimplement the same function with the pointer idiom: anchor s, walk a copy p with while (*p) p++;, return (size_t)(p - s). Verify it matches your Beginner 1 result on the same inputs. Concepts: pointers, dereference, pointer subtraction.
Write size_t safe_strlen(const char *s) that returns 0 when s is NULL and the normal length otherwise. Input/Output: safe_strlen(NULL) → 0, safe_strlen("abc") → 3. Constraint: do not call the library strlen. Hint: one if guard before the loop. Concepts: NULL safety, defensive checks.
Write size_t my_strnlen(const char *s, size_t maxlen) that returns the length but never scans more than maxlen bytes (so it is safe on data that might lack a terminator). I/O: my_strnlen("hello", 3) → 3, my_strnlen("hi", 10) → 2. Constraint: stop as soon as you hit '\0' or reach maxlen. Hint: loop while (i < maxlen && s[i] != '\0'). Concepts: bounded scanning, over-read prevention.
Write size_t word_count(const char *s) that returns the number of whitespace-separated words in s. Walk the string once (O(n)); a word starts at each non-space character that follows a space or the start of the string. I/O: " hello world " → 2, "" → 0, "one" → 1. Constraint: single pass, no extra allocation, treat space/tab/newline as separators. Hint: track whether you are currently "inside a word." Concepts: single-pass string walk, state tracking, the same terminator-driven loop as strlen.
strlen finds it by walking from the start to the '\0' NUL terminator and counting the bytes before it (the terminator is not counted).const char *p = s; while (*p) p++; return (size_t)(p - s); — anchor the start, advance a pointer to the terminator, and subtract.size_t, accept const char *, print with %zu.strnlen for untrusted input.strlen is O(n): compute a length once instead of inside a loop condition, and always allocate strlen(s) + 1 for a copy.