Arrays & Strings · beginner · ~8 min
## What you will learn - Compare two C strings by content using `strcmp`, and read its three-way return value correctly (negative / zero / positive). - Write the idiomatic equality test `strcmp(a, b) == 0` and explain why a bare `if (strcmp(a, b))` does the opposite of what most beginners expect. - Explain why the `==` operator compares *pointers*, not characters, and avoid that classic trap. - Choose between `strcmp`, `strncmp` (bounded / prefix comparison), and `strcasecmp` (case-insensitive, POSIX) for the right job. - Use string comparison safely: never pass `NULL`, never compare against non-terminated buffers, and understand lexicographic ordering.
When you write C programs you constantly need to ask "are these two pieces of text the same?" or "which one comes first alphabetically?" That is what string comparison is for: deciding the relationship between two strings of text.
In the prerequisite lesson C strings you learned that a C string is just an array of char ending in a special zero byte called the null terminator ('\0'). There is no built-in string type and no built-in == for text. That has a surprising consequence: the natural-looking code if (name == "admin") does not compare the letters. It compares two memory addresses. To compare the actual characters you must walk both arrays byte by byte until they differ or until you hit the terminator — and the standard library already does this for you in one function: strcmp.
strcmp ("string compare") lives in <string.h>. You hand it two strings and it returns a single int describing their order. Crucially, that return value is not a true/false answer. It is a three-way signal: zero means equal, a negative number means the first string sorts before the second, and a positive number means it sorts after. Once that one idea clicks, most strcmp bugs disappear.
This lesson builds directly on what you know about character arrays and null termination, then adds the comparison family: strcmp for full comparison, strncmp for comparing a limited number of characters (great for prefix checks), and strcasecmp for ignoring upper/lower case. These functions are everywhere — command parsers, menus, sorting, configuration files, and login checks all rely on them.
String comparison is one of the most frequently used operations in real software. A command-line tool reading git commit vs git status, a parser deciding whether a token is the keyword return, a menu matching the user's choice, a qsort call ordering a list of names alphabetically — all of these are string comparisons under the hood.
Getting it wrong has real consequences. The two most common beginner mistakes — using == on strings, and forgetting == 0 after strcmp — both compile cleanly and often appear to work in quick tests, then fail in production. == on string literals can even seem to work because the compiler sometimes stores identical literals at the same address, which makes the bug intermittent and maddening to track down.
String comparison also sits at the heart of correctness in security-sensitive code such as checking a username or comparing a configuration value. Knowing exactly what strcmp reads (everything up to and including the terminator) and what it requires (two valid, null-terminated strings) is what separates code that quietly corrupts data or reads out of bounds from code that behaves predictably.
Before comparing strings, recall their shape. A C string is a contiguous block of char values ending in '\0' (the null terminator, byte value 0). Comparison functions rely entirely on that terminator to know where the string stops.
char a[] = "cat";
index: 0 1 2 3
+----+----+----+----+
a -> | 'c'| 'a'| 't'|'\0'|
+----+----+----+----+
0x63 0x61 0x74 0x00 <- ASCII codes
strcmp walks this array byte by byte, comparing ASCII codes, and stops at the first difference or at the '\0'.
Knowledge check: If a buffer holds the characters 'h', 'i' but has no '\0' after them, what could strcmp do when reading it? (Answer: it keeps reading past the buffer into unknown memory — undefined behavior.)
strcmp: the three-way comparisonDefinition. int strcmp(const char *a, const char *b) compares the two null-terminated strings a and b and returns:
| Return value | Meaning |
|---|---|
0 |
the strings are exactly equal |
negative (< 0) |
a is lexicographically less than b (sorts first) |
positive (> 0) |
a is lexicographically greater than b (sorts last) |
How it works internally. It compares the first pair of characters. If they are equal and not '\0', it moves to the next pair, and so on. At the first position where the characters differ, it returns the difference of those two char values (interpreted as unsigned char). If it reaches the end of both strings simultaneously (both '\0'), they are equal and it returns 0.
strcmp("car", "cat")
position 0: 'c' == 'c' -> continue
position 1: 'a' == 'a' -> continue
position 2: 'r' vs 't' -> differ! 'r'(114) < 't'(116)
result: negative ("car" sorts before "cat")
Important: the exact magnitude (e.g. -2 vs -1) is not guaranteed by the standard. Only the sign is meaningful. Never write if (strcmp(a,b) == -1) — write if (strcmp(a,b) < 0).
When to use it: equality tests and sorting. When NOT to use it: when either argument might be NULL, or when a buffer might not be null-terminated — both are undefined behavior.
Common pitfall: treating the result as a boolean. strcmp returns 0 for equal, and 0 is "falsey" in C, so if (strcmp(a, b)) runs the body when the strings differ.
Knowledge check (predict the output): What does strcmp("apple", "applesauce") return — negative, zero, or positive? (Answer: negative — "apple" is a prefix, and the shorter string ends first, so its '\0' (0) compares less than 's'.)
== compares pointers, not textWhen a and b are char * (or arrays that decay to pointers), a == b asks "do these point to the same address?" It does not look at any characters.
char x[] = "yes";
char y[] = "yes";
x -> [address 0x1000] 'y''e''s''\0'
y -> [address 0x2000] 'y''e''s''\0'
x == y -> false (0x1000 != 0x2000)
strcmp(x, y) == 0 -> true (same characters)
This is the single most common string bug for beginners. To compare contents, always use strcmp.
Knowledge check (explain in your own words): Why might if ("hi" == "hi") sometimes appear true even though comparing strings with == is wrong? (Answer: the compiler may store identical string literals at one shared address, so the pointers happen to match — but you must never rely on this.)
strncmp and strcasecmpint strncmp(const char *a, const char *b, size_t n) compares at most the first n characters. It stops early if it hits a difference or a '\0'. This is the standard, portable way to do a prefix check: strncmp(s, "http://", 7) == 0 tests whether s begins with http://.
int strcasecmp(const char *a, const char *b) is like strcmp but ignores upper/lower case ("YES" equals "yes"). It is a POSIX/BSD extension declared in <strings.h> (note the plural), not in standard C11. There is also strncasecmp for a bounded, case-insensitive comparison.
| Function | Header | Compares | Case-sensitive | Bounded |
|---|---|---|---|---|
strcmp |
<string.h> |
full strings | yes | no |
strncmp |
<string.h> |
first n chars |
yes | yes |
strcasecmp |
<strings.h> (POSIX) |
full strings | no | no |
strncasecmp |
<strings.h> (POSIX) |
first n chars |
no | yes |
#include <string.h> // strcmp, strncmp
#include <strings.h> // strcasecmp (POSIX only)
int r = strcmp(a, b); // r < 0, r == 0, or r > 0
if (strcmp(a, b) == 0) { ... } // EQUAL
if (strcmp(a, b) != 0) { ... } // NOT equal
if (strcmp(a, b) < 0) { ... } // a sorts before b
if (strcmp(a, b) > 0) { ... } // a sorts after b
if (strncmp(s, "GET ", 4) == 0) { ... } // prefix check (first 4 chars)
Key points:
const char * — the strings are read, never modified.strncmp is a size_t count of characters, not bytes-of-buffer; it is an upper bound, and the function still stops at a '\0'.0 with a relational operator. Never compare against a specific number like -1 or 1.strcmpstrcmp(a, b) compares two strings character by character. Its return value tells you their order:
a sorts before b.a sorts after b.Note what strcmp does not return: a boolean (true/false). To test for equality, compare the result to 0 explicitly:
if (strcmp(a, b) == 0) {
// a and b are equal
}
strncmp(a, b, n) — compares only the first n characters. Use it for fixed-prefix comparisons.strcasecmp — compares strings ignoring upper/lower case. This is a POSIX function; standard C has no equivalent.#include <stdio.h>
#include <string.h>
/* Classify the relationship between two strings using strcmp. */
static void compare(const char *a, const char *b) {
int r = strcmp(a, b);
if (r == 0) {
printf("\"%s\" == \"%s\" (equal)\n", a, b);
} else if (r < 0) {
printf("\"%s\" < \"%s\" (a sorts first)\n", a, b);
} else {
printf("\"%s\" > \"%s\" (a sorts last)\n", a, b);
}
}
int main(void) {
compare("hello", "hello"); /* equal */
compare("car", "cat"); /* a before b */
compare("cat", "car"); /* a after b */
compare("apple", "applesauce"); /* prefix: shorter sorts first */
/* Prefix check with strncmp: does the request start with "GET "? */
const char *request = "GET /index.html";
if (strncmp(request, "GET ", 4) == 0) {
printf("This is a GET request.\n");
}
/* Equality test: the idiomatic command dispatch pattern. */
const char *command = "quit";
if (strcmp(command, "quit") == 0) {
printf("Quitting.\n");
}
return 0;
}
What it does. The helper compare prints whether the first string is equal to, before, or after the second according to strcmp. main runs four comparisons, then demonstrates a prefix check with strncmp and the canonical equality-based command dispatch.
Expected output:
"hello" == "hello" (equal)
"car" < "cat" (a sorts first)
"cat" > "car" (a sorts last)
"apple" < "applesauce" (a sorts first)
This is a GET request.
Quitting.
Edge cases to keep in mind: comparing a string to a prefix of itself returns nonzero (the shorter one sorts first); strncmp(a, b, 0) always returns 0 (zero characters compared); and passing NULL to any of these functions is undefined behavior, so guard inputs that might be null before calling.
#include <string.h> brings in the declarations for strcmp and strncmp. Without it the compiler may assume a wrong return type and produce a warning or subtle bug.compare takes both strings as const char *, signaling they are read-only, and stores strcmp's result in r exactly once so the value is computed a single time.if (r == 0) ... else if (r < 0) ... else covers all three outcomes. Note we test < 0, not == -1, because only the sign is guaranteed.compare("hello", "hello"): every character matches and both strings end at the same '\0', so strcmp returns 0 → "equal".compare("car", "cat"): positions 0 and 1 match; at position 2, 'r' (114) differs from 't' (116). strcmp returns a negative value → "a sorts first".compare("apple", "applesauce"): the first five characters match; then "apple" reaches its '\0' (value 0) while "applesauce" has 's' (115). 0 is less than 115, so the result is negative.strncmp(request, "GET ", 4) call compares only "GET " (4 characters) against the start of request. They match, the result is 0, and the message prints — that is how you detect a prefix.strcmp(command, "quit") == 0 is the everyday dispatch idiom: take an action only when the strings are exactly equal.strcmp("car", "cat")| Position | char in a | char in b | Equal? | Action |
|---|---|---|---|---|
| 0 | 'c' (99) | 'c' (99) | yes | continue |
| 1 | 'a' (97) | 'a' (97) | yes | continue |
| 2 | 'r' (114) | 't' (116) | no | return 114 - 116 (negative) |
== 0// WRONG: runs the body when the strings are DIFFERENT
if (strcmp(name, "admin")) {
grant_access();
}
Because strcmp returns 0 when equal, and 0 is falsey, this grants access to everyone except admin — the exact opposite of the intent.
// CORRECT
if (strcmp(name, "admin") == 0) {
grant_access();
}
How to recognize it: any if (strcmp(...)) without a comparison operator is a red flag. Make == 0 / != 0 a habit.
== on strings// WRONG: compares addresses, not characters
if (input == "yes") { ... }
This tests whether input points to the same memory as the literal "yes", which it almost never does.
// CORRECT
if (strcmp(input, "yes") == 0) { ... }
How to recognize it: any ==, !=, <, > directly between two char * values. Those compare pointers. For text, route through strcmp.
// WRONG: relies on a magnitude the standard does not promise
if (strcmp(a, b) == -1) { ... }
Many implementations return -1, but some return other negative values. The test can silently fail when you change compiler or platform.
// CORRECT
if (strcmp(a, b) < 0) { ... }
// WRONG: buf is filled but never null-terminated
char buf[3] = { 'y', 'e', 's' };
if (strcmp(buf, "yes") == 0) { ... } // reads past buf
strcmp keeps reading until it finds a '\0'; here there isn't one inside buf, so it reads out of bounds.
// CORRECT: leave room for and write the terminator
char buf[4] = { 'y', 'e', 's', '\0' };
if (strcmp(buf, "yes") == 0) { ... }
Compiler warnings to enable. Build with -Wall -Wextra. Comparing a char * to a string literal with == triggers a warning on many compilers (comparison with string literal results in unspecified behavior). Treat that warning as a real bug.
Symptom: the equality branch never (or always) runs.
== 0. Print the raw result: printf("r=%d\n", strcmp(a, b));. If it prints 0 yet your branch is skipped, your condition logic is inverted.Symptom: comparison gives random / inconsistent results, or the program crashes.
NULL argument. Print the strings first with %s; if the output contains garbage or the program crashes on the printf, the string is malformed before strcmp ever runs.gcc -fsanitize=address,undefined file.c then run the program. AddressSanitizer reports out-of-bounds reads caused by missing terminators; UBSan flags other undefined behavior.Symptom: strcasecmp is "undeclared."
#include <strings.h> and compile with a POSIX-aware setting (e.g. -D_GNU_SOURCE on Linux) or use it only on platforms that provide it.Questions to ask when it doesn't work:
== 0 (or != 0) after strcmp?== between two pointers?NULL strings?< 0 / > 0) rather than a specific number?strcmp, strncmp, and strcasecmp are read-only — they never write — but they can still cause undefined behavior through what they read:
strcmp and strcasecmp keep reading until they encounter '\0'. If a buffer is not terminated within its bounds, they read past the end into adjacent memory. This is an out-of-bounds read and undefined behavior. Always ensure strings are terminated; when building a buffer manually, reserve one byte for '\0'.NULL arguments. Passing NULL to any of these dereferences a null pointer — a crash at best. Validate pointers that may be null before comparing.strncmp bounds the read but not the terminator requirement of its peers. strncmp(a, b, n) will read at most n characters from each string (and stop early at a '\0'), so it is the safer choice when one operand may not be terminated within a known length — for example comparing the start of a fixed-size buffer.== -1) is not a memory-safety bug but a portability/correctness bug; combine the habit of < 0 / > 0 with bounds awareness.-fsanitize=address,undefined to catch out-of-bounds reads and other UB during development; these are the fastest way to surface a missing terminator before it reaches users.Because this is not a security-category lesson, the key takeaway is robustness: validate inputs (non-null, terminated, expected length) before comparing, and prefer strncmp when working with raw, fixed-size buffers.
Where string comparison shows up:
git, apt, and almost every CLI dispatch on the subcommand using strcmp(argv[1], "status") == 0 style checks.if, while, return) and matching tokens.qsort of an array of strings uses a comparator that simply returns strcmp(*a, *b), because strcmp's three-way result is exactly what qsort expects.strncmp(line, "GET ", 4) == 0 or matching a header name.strcasecmp.Best practices (beginner):
== 0 / != 0 after strcmp; never leave it bare.== between two strings to compare text.< 0, > 0), never a specific number.Best practices (advanced):
strncmp for prefix matching and whenever an operand might not be terminated within a known length.strcasecmp is POSIX, so guard it or provide a fallback if you target non-POSIX systems.qsort comparators, return strcmp directly rather than reducing to -1/0/1 by hand — it is correct and clearer.if/else chains for maintainability.Write a function int str_eq(const char *a, const char *b) that returns 1 when the two strings are equal and 0 otherwise.
strcmp and the == 0 idiom.str_eq("cat", "cat") → 1; str_eq("cat", "car") → 0.return statement.strcmp, equality test.Write a program that reads two words and prints FIRST, SECOND, or EQUAL depending on which sorts earlier (or whether they are equal).
strcmp, not a literal.car cat → FIRST; input dog dog → EQUAL.if (r == 0) ... else if (r < 0) ... else ....Implement int has_prefix(const char *s, const char *pre) that returns 1 if s begins with pre, else 0.
strncmp with the length of pre (strlen(pre)).has_prefix("GET /x", "GET") → 1; has_prefix("POST", "GET") → 0.1.strncmp(s, pre, strlen(pre)) == 0.strncmp, strlen.Write a command dispatcher that accepts quit, help, or list regardless of letter case (so QUIT and Quit both work).
strcasecmp; include <strings.h>; print which command matched, or unknown command.HELP → help selected.strcasecmp(cmd, "quit") == 0 checks.strcasecmp, case-insensitive comparison.Given an array of strings, sort it alphabetically and print the result.
qsort with a comparator that calls strcmp. Remember each element of a char *[] is a char *, so the comparator receives const void * pointing to a char * (a char **).{"pear", "apple", "fig"} → output apple fig pear.return strcmp(*(const char *const *)x, *(const char *const *)y);.strcmp as a comparator, qsort, pointer-to-pointer.strcmp(a, b) is a three-way comparison, not a boolean: 0 = equal, < 0 = a sorts before b, > 0 = a sorts after b. Only the sign is guaranteed.strcmp(a, b) == 0. A bare if (strcmp(a, b)) runs when the strings differ — the opposite of what beginners expect.== on strings compares addresses, not characters. Always route text comparison through strcmp.strncmp(a, b, n) compares at most n characters — the portable way to do prefix checks. strcasecmp ignores case but is POSIX (<strings.h>), not standard C11.NULL, null-terminated strings; otherwise strcmp reads out of bounds. Use strncmp when an operand may not be terminated within a known length, and test with -fsanitize=address,undefined.-1 or 1; compare to 0 with <, >, ==, or !=.