Arrays & Strings · beginner · ~8 min

strcmp and string comparison

## 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.

Overview

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.

Why it matters

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.

Core concepts

1. A C string is bytes plus a terminator

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.)

2. strcmp: the three-way comparison

Definition. 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'.)

3. == compares pointers, not text

When 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.)

4. strncmp and strcasecmp

int 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

Syntax notes

Syntax and structure

#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:

  • Both arguments are const char * — the strings are read, never modified.
  • The third argument to 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'.
  • Compare the result against 0 with a relational operator. Never compare against a specific number like -1 or 1.

Lesson

Comparing strings with strcmp

strcmp(a, b) compares two strings character by character. Its return value tells you their order:

  • 0 — the strings are equal.
  • negativea sorts before b.
  • positivea 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
}

Related functions

  • 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.

Code examples

#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.

Line by line

Walkthrough of the key example

  1. #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.
  2. 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.
  3. The chain if (r == 0) ... else if (r < 0) ... else covers all three outcomes. Note we test < 0, not == -1, because only the sign is guaranteed.
  4. compare("hello", "hello"): every character matches and both strings end at the same '\0', so strcmp returns 0 → "equal".
  5. 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".
  6. 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.
  7. The 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.
  8. The final strcmp(command, "quit") == 0 is the everyday dispatch idiom: take an action only when the strings are exactly equal.

Trace table for 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)

Common mistakes

Realistic mistakes and fixes

Mistake 1: forgetting == 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.

Mistake 2: using == 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.

Mistake 3: comparing the result against a literal

// 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) { ... }

Mistake 4: passing a non-terminated buffer

// 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) { ... }

Debugging tips

Debugging string comparisons

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.

  • Check for a missing == 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.

  • Likely a missing null terminator or a 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.
  • Run under a sanitizer: 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."

  • It is POSIX, not C11. Add #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:

  1. Did I write == 0 (or != 0) after strcmp?
  2. Am I accidentally using == between two pointers?
  3. Are both arguments valid, null-terminated, non-NULL strings?
  4. Am I testing the sign (< 0 / > 0) rather than a specific number?

Memory safety

Memory safety and undefined behavior

strcmp, strncmp, and strcasecmp are read-only — they never write — but they can still cause undefined behavior through what they read:

  • Null terminator required. 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'.
  • No 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.
  • Sign, not magnitude. Relying on the exact integer (e.g. == -1) is not a memory-safety bug but a portability/correctness bug; combine the habit of < 0 / > 0 with bounds awareness.
  • Tooling. Compile tests with -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.

Real-world uses

Real-world uses and best practices

Where string comparison shows up:

  • Command-line tools and shells. git, apt, and almost every CLI dispatch on the subcommand using strcmp(argv[1], "status") == 0 style checks.
  • Parsers and interpreters. Recognizing keywords (if, while, return) and matching tokens.
  • Sorting. 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.
  • Networking. Detecting the HTTP method with strncmp(line, "GET ", 4) == 0 or matching a header name.
  • Configuration. Comparing a config key against known option names, often case-insensitively with strcasecmp.

Best practices (beginner):

  • Always write == 0 / != 0 after strcmp; never leave it bare.
  • Never use == between two strings to compare text.
  • Test only the sign of the result (< 0, > 0), never a specific number.
  • Make sure both arguments are valid, null-terminated strings.

Best practices (advanced):

  • Reach for strncmp for prefix matching and whenever an operand might not be terminated within a known length.
  • Keep case-insensitive comparison portable: strcasecmp is POSIX, so guard it or provide a fallback if you target non-POSIX systems.
  • For qsort comparators, return strcmp directly rather than reducing to -1/0/1 by hand — it is correct and clearer.
  • Centralize repeated comparisons (e.g. a command table mapping names to handlers) instead of long if/else chains for maintainability.

Practice tasks

Practice tasks

Beginner 1 — Equal or not

Write a function int str_eq(const char *a, const char *b) that returns 1 when the two strings are equal and 0 otherwise.

  • Requirements: use strcmp and the == 0 idiom.
  • Example: str_eq("cat", "cat")1; str_eq("cat", "car")0.
  • Hint: the body can be a single return statement.
  • Concepts: strcmp, equality test.

Beginner 2 — Order report

Write a program that reads two words and prints FIRST, SECOND, or EQUAL depending on which sorts earlier (or whether they are equal).

  • Requirements: branch on the sign of strcmp, not a literal.
  • Example: input car catFIRST; input dog dogEQUAL.
  • Hint: if (r == 0) ... else if (r < 0) ... else ....
  • Concepts: three-way comparison.

Intermediate 1 — Prefix check

Implement int has_prefix(const char *s, const char *pre) that returns 1 if s begins with pre, else 0.

  • Requirements: use strncmp with the length of pre (strlen(pre)).
  • Example: has_prefix("GET /x", "GET")1; has_prefix("POST", "GET")0.
  • Constraint: an empty prefix should return 1.
  • Hint: strncmp(s, pre, strlen(pre)) == 0.
  • Concepts: strncmp, strlen.

Intermediate 2 — Case-insensitive menu

Write a command dispatcher that accepts quit, help, or list regardless of letter case (so QUIT and Quit both work).

  • Requirements: use strcasecmp; include <strings.h>; print which command matched, or unknown command.
  • Example: input HELPhelp selected.
  • Hint: chain strcasecmp(cmd, "quit") == 0 checks.
  • Concepts: strcasecmp, case-insensitive comparison.

Challenge — Sort names alphabetically

Given an array of strings, sort it alphabetically and print the result.

  • Requirements: use 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 **).
  • Example: input {"pear", "apple", "fig"} → output apple fig pear.
  • Constraint: do not modify the strings themselves, only their order.
  • Hint: the comparator looks like return strcmp(*(const char *const *)x, *(const char *const *)y);.
  • Concepts: strcmp as a comparator, qsort, pointer-to-pointer.

Summary

Summary

  • 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.
  • Test equality with 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.
  • Safety: both arguments must be valid, non-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.
  • Remember: never compare the result to -1 or 1; compare to 0 with <, >, ==, or !=.

Practice with these exercises