C / STRINGS AND BUFFER SAFETY
Comparing and searching with strcmp and strchr
Use strcmp's sign correctly, test equality with == 0, and turn strchr's returned pointer into an index or substring without dereferencing NULL.
What you will learn
- Compare content with strcmp(a,b) == 0, not with a == b, which compares addresses.
- Read only the sign of strcmp; the magnitude is unspecified and varies by library.
- Use strncmp(s, prefix, n) == 0 for prefix checks so the scan stops at n bytes.
- Check strchr's result for NULL before dereferencing it or subtracting for an index.
Understanding Comparing and searching with strcmp and strchr
strcmp does not answer "are these the same", it answers "which one sorts first". It walks both strings byte by byte and, at the first position where they differ, returns a negative value if the byte in the first string is smaller and a positive value if it is larger. Only the sign is specified: an implementation may return -1, -32 or -4096 for the same pair, so the only portable tests are == 0, < 0 and > 0. The bytes are compared as unsigned char, which is why 'Z' (90) sorts before 'a' (97) and why a lead byte like 0xC3 from a UTF-8 sequence sorts after every ASCII letter.
Both functions inherit the NUL contract. strcmp stops at the first differing byte, or at a terminator both strings reach together, so on well-formed strings it never reads past the end, but on a buffer you forgot to terminate it keeps walking into whatever memory follows. strncmp caps the scan at n bytes and still stops early at a NUL, which makes strncmp(line, "GET ", 4) == 0 the right way to ask "does it start with this": it cannot touch the fifth byte even if line only holds three. The trap is thinking of strncmp as a safer strcmp; it asks a different question, and a too-small n silently accepts every string sharing that prefix.
strchr returns a pointer, not an index and not a boolean: the address of the first matching byte inside your own string, or NULL when the byte is absent. Treat that pointer as a cursor, since printing it prints the rest of the string and p - s gives the offset, but both only make sense after NULL is ruled out, because subtracting from a null pointer is undefined and in practice yields a huge offset that a later memcpy will happily believe. The terminator counts as part of the string for searching, so strchr(s, '\0') deliberately returns the address of the terminator, a cheap way to land on the end of a string you are already scanning. The siblings cover the rest: strrchr scans backwards, strstr matches a multi-byte substring, and memchr takes an explicit length so it works on buffers with no terminator at all.
Both functions take const char * arguments, but strchr and strrchr hand back a plain char *, which quietly gives you a writable pointer into data that may be a read-only string literal.
<stdio.h>
<string.h>
static const char *rel(int r)
{
if (r < 0) return "before";
if (r > 0) return "after";
return "equals";
}
int main(void)
{
const char *a = "apple";
const char *b = "apricot";
const char *z = "Zebra";
const char *req = "GET /index.html";
char *slash;
printf("%-7s %-6s %s\n", a, rel(strcmp(a, b)), b);
printf("%-7s %-6s %s\n", z, rel(strcmp(z, a)), a);
printf("%-7s %-6s %s\n", a, rel(strcmp(a, "apple")), "apple");
printf("starts with GET: %d\n", strncmp(req, "GET ", 4) == 0);
slash = strchr(req, '/');
if (slash != NULL)
printf("first / at index %td, tail is %s\n", slash - req, slash);
if (strchr(req, '?') == NULL)
printf("no ? in the line\n");
printf("terminator at offset %td, strlen %zu\n",
strchr(req, '\0') - req, strlen(req));
return 0;
}
Neither function returns a boolean: strcmp returns a sign describing byte order, and strchr returns a pointer into your own string or NULL.
Worked examples
The two comparisons beginners mix up
Shows that == compares addresses while strcmp compares bytes, and that a bare strcmp condition is true when the strings differ.
<stdio.h>
<string.h>
int main(void)
{
char buf[8] = "yes";
const char *lit = "yes";
printf("pointer equality: %d\n", buf == lit);
printf("strcmp equality: %d\n", strcmp(buf, lit) == 0);
if (strcmp(buf, "yes") == 0)
puts("explicit == 0 says: equal");
if (strcmp(buf, "no"))
puts("non-zero says: different, even though it reads as true");
return 0;
}
Example explained
Line 1char buf[8] = "yes" copies the bytes into a new array, so buf and lit are different addresses and buf == lit is 0 even though the text matches.
Line 2strcmp returns 0 only when both strings reach their terminator at the same position, so == 0 is the equality test.
Line 3if (strcmp(buf, "no")) is entered because 'y' (121) differs from 'n' (110); the line reads like a match test and means the opposite.
strchr forwards, strrchr backwards
Splits a path with the same needle searched from both ends, and turns a found pointer into an owned substring.
<stdio.h>
<string.h>
int main(void)
{
const char *path = "src/net/http.parser.c";
const char *first = strchr(path, '.');
const char *last = strrchr(path, '.');
const char *slash = strrchr(path, '/');
printf("from first dot: %s\n", first ? first : "(none)");
printf("from last dot: %s\n", last ? last : "(none)");
printf("basename: %s\n", slash ? slash + 1 : path);
if (last != NULL) {
char stem[32];
size_t n = (size_t)(last - path);
if (n < sizeof stem) {
memcpy(stem, path, n);
stem[n] = '\0';
printf("stem: %s\n", stem);
}
}
return 0;
}
Example explained
Line 1strchr stops at the first '.' and strrchr at the last one, so the same needle gives different answers whenever it occurs more than once.
Line 2Printing the returned pointer prints the tail of path because the pointer aliases into the original string; nothing was copied.
Line 3last - path is computed only inside the NULL check, since subtracting path from a null pointer is undefined rather than zero.
Line 4memcpy of n bytes plus an explicit stem[n] = '\0' is what converts a found pointer into a real, terminated string you own.
Important notes
strcmp returns as soon as two bytes differ, so its running time leaks how long the common prefix was; never compare passwords, session tokens or MACs with it, loop over every byte and OR the differences together instead.
strcmp orders raw bytes, not words: it is unaffected by locale, so "Zebra" precedes "apple" and accented letters land after 'z'. Use strcoll if you need locale collation.
Common mistakes
Writing if (strcmp(name, "admin")) to mean "is admin": the body runs for every name except admin, so an authorization check accepts exactly the wrong set of users.
Testing strcmp(a, b) == -1: many implementations return the difference of the first differing bytes, such as 32 between 'a' and 'A', so the test is false even when a sorts first.
Using strchr(line, '=') + 1 with no NULL check: when the line has no '=', the code adds 1 to a null pointer and reads address 1, crashing on a hosted system and corrupting data elsewhere.
Try it yourself
Change, predict, then run
In a browser editor, store user=42 in a char array, find the '=' with strchr, and print the key and the value as two separate strings. Then change the array to a line with no '=' and confirm your program reports an error instead of crashing.
Open the C workspaceCheck your understanding
A program contains if (strcmp(a, b) == -1) puts("a first"); it behaves correctly on one machine but silently stops working after a C library upgrade. What is the real cause?
- Only the sign of strcmp's return value is specified, so an implementation may legitimately return any negative value, such as -32
- strcmp was changed to return an unsigned value, so a negative result is now impossible
- The new library made strcmp locale-aware, so the ordering of the two strings changed
- == binds more tightly than the function call, so the comparison applies to the wrong operand
Show answer
The standard only guarantees the sign, and a common implementation returns the difference of the first differing unsigned char values, so comparing 'a' with 'A' can yield -32 or 32 and never exactly -1; the fix is to test < 0. Option 3 is tempting because collation really is locale-dependent, but that applies to strcoll, whereas strcmp is defined purely on byte values and ignores the locale.