The code if (input == "admin") compiles in C without a single warning. Worse, it sometimes works. In the test below, comparing two pointers to identical string literals returned true — not because the contents matched, but because the compiler stored one copy of "hello" and handed back the same address twice. Change one of them to a variable holding the same characters and the identical-looking test returns false.
That is the shape of almost every string-comparison bug in C: it passes its first test. This guide covers the comparison functions that actually work — strcmp, strncmp, memcmp and the case-insensitive variants — how to read their return values correctly, why == fails on char *, and the input handling that has to be right before any comparison matters. Every program below was compiled and run for this article on Ubuntu 24.04 with GCC 13.3, using gcc -std=c11 -Wall -Wextra and g++ -std=c++17 -Wall -Wextra, including runs under AddressSanitizer and UndefinedBehaviorSanitizer. Every output block — including the sanitizer reports and the memory addresses — is captured verbatim.
Table of Contents
- The Short Answer
- What strcmp() Does
- Which Function for Which Job
- Why == Doesn't Work on char *
- Reading the Return Value Correctly
- strncmp: Bounded and Prefix Comparison
- The Other Comparison Functions
- The Comparison Is Not the Dangerous Part
- Comparing Strings in C++
- Key Takeaways
- Frequently Asked Questions
- Conclusion
The Short Answer
How do I compare two strings in C? Call strcmp(a, b) and test the result against zero. strcmp(a, b) == 0 means the strings are equal. Include <string.h>.
Why doesn’t == work? Because a and b are pointers. == compares the two addresses, not the characters they point at. It is a valid comparison of the wrong thing, which is why the compiler stays silent.
Which function should I use? strcmp for whole strings, strncmp when you need a length limit or a prefix test, memcmp for fixed-size buffers, strcasecmp when case should be ignored. The table below gives the gotcha for each.
What strcmp() Does
strcmp() compares two null-terminated strings one byte at a time and returns an integer describing their order: zero if the strings are identical, a negative value if the first sorts before the second, and a positive value if it sorts after. It stops at the first differing byte or at the terminating \0, whichever comes first. It is declared in <string.h> and takes two const char * arguments.
The signature is:
int strcmp(const char *str1, const char *str2);
The comparison is lexicographical, using the byte values of the characters interpreted as unsigned char, and it is case-sensitive and not locale-aware. In ASCII, "Zebra" sorts before "apple" because uppercase Z has a lower byte value than lowercase a.
The unsigned char detail is not pedantry. Plain char is signed on most platforms, so a hand-written comparison loop that subtracts *a - *b without casting gets the opposite sign for any byte above 127. Comparing a UTF-8 accented character against "z" returned 73 from strcmp and −183 from an uncast loop in a test for this article — one says the accented character sorts after z, the other says before. Any comparison touching non-ASCII text needs the cast. The cppreference entry for strcmp gives the normative wording.
Which Function for Which Job
| Function | What it compares | Use it when | The gotcha |
|---|---|---|---|
strcmp | Two strings, to their terminators | Comparing whole strings for equality or order | Undefined behaviour if either argument is not a valid null-terminated string |
strncmp | At most n bytes, stopping early at a terminator | Prefix tests, or bounding a comparison | n limits the comparison, not the buffer — it does not make an unterminated string safe |
memcmp | Exactly n bytes, terminators included | Fixed-size buffers, binary data | Compares padding and leftover bytes too, so equal strings can compare unequal |
strcasecmp | Two strings, ignoring case | Case-insensitive matching | POSIX, not standard C. MSVC spells it _stricmp |
strcoll | Two strings, using the current locale | Sorting text for human display | Ordering depends on the active locale; typically more expensive than strcmp |
== | Two pointers | Never, for string contents | Compiles silently and is intermittently correct |
Why == Doesn’t Work on char *
This is one of the most common string bugs in C, and the reason it survives code review is that it is not reliably wrong.
#include <stdio.h>
#include <string.h>
int main(void) {
const char *lit1 = "hello"; /* two pointers to identical literals */
const char *lit2 = "hello";
char arr1[] = "hello"; /* two arrays with identical contents */
char arr2[] = "hello";
char built[6]; /* a copy made at run time */
strcpy(built, "hello");
printf("lit1 == lit2 -> %s (addresses %p vs %p)\n",
(lit1 == lit2) ? "TRUE " : "FALSE", (const void*)lit1, (const void*)lit2);
printf("lit1 == built -> %s (addresses %p vs %p)\n",
(lit1 == built) ? "TRUE " : "FALSE", (const void*)lit1, (void*)built);
printf("\nstrcmp(lit1, lit2) == 0 -> %s\n", strcmp(lit1, lit2) == 0 ? "TRUE" : "FALSE");
printf("strcmp(lit1, built) == 0 -> %s\n", strcmp(lit1, built) == 0 ? "TRUE" : "FALSE");
return 0;
}
Output:
lit1 == lit2 -> TRUE (addresses 0x563c4540b008 vs 0x563c4540b008)
arr1 == arr2 -> FALSE (addresses 0x7fffb6728356 vs 0x7fffb672835c)
lit1 == built -> FALSE (addresses 0x563c4540b008 vs 0x7fffb6728362)
strcmp(lit1, lit2) == 0 -> TRUE
strcmp(arr1, arr2) == 0 -> TRUE
strcmp(lit1, built) == 0 -> TRUE
Read the addresses. lit1 and lit2 hold the same address — the compiler noticed two identical literals and stored one copy, which it can do because a literal is a shared read-only object rather than a copy you own. What each form actually allocates is covered in char array vs string in C and C++. So == returns true, and a test written against string literals passes. The moment the data comes from anywhere else — user input, a copy, a different array — the addresses differ and the same expression returns false while the contents are still identical.
The two arrays behave differently again: arr1 and arr2 are distinct objects, so their addresses differ and == is false, while their contents are identical. (For how arrays are laid out generally, see our guide to arrays as a data structure.)
There is one more detail worth knowing. GCC 13.3 does warn about comparing two arrays:
warning: comparison between two arrays [-Warray-compare]
23 | (arr1 == arr2) ? "TRUE " : "FALSE", ...
| ^~
note: use '&arr1[0] == &arr2[0]' to compare the addresses
But it issues no warning at all for lit1 == lit2 or lit1 == built, because comparing two pointers is a legitimate operation. The compiler catches the case that is obviously wrong and stays silent on the case that actually appears in real code, where at least one side is a char * parameter.
Reading the Return Value Correctly
Another common mistake is testing the wrong thing. The C standard guarantees only the sign of the result — zero, negative, or positive. It does not guarantee any particular magnitude.
On this implementation, strcmp happens to return the difference between the first differing characters, matching a hand-written byte loop exactly:
a b strcmp naive
----------------------------------------------------------------------------------------
apple banana -1 -1
a z -25 -25
Apple apple -32 -32
abc abcdef -100 -100
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaX aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaZ -2 -2
xyzzyABCDEFGHIJKLMNOPQRSTUVWXYZ012 xyzzyABCDEFGHIJKLMNOPQRSTUVWXYZ0!2 16 16
That agreement is a property of this library, not of the language, and code must not depend on it. Here is what happens when it does:
The bug: testing strcmp(a,b) == 1 instead of strcmp(a,b) > 0
a b strcmp == 1 > 0
----------------------------------------------------
b a 1 TRUE TRUE
z a 25 FALSE TRUE
banana apple 1 TRUE TRUE
strcmp("b", "a") returns 1 and strcmp("banana", "apple") returns 1, so == 1 looks correct on two cases out of three. strcmp("z", "a") returns 25 and the test collapses. Always compare the result against zero with <, > or ==, never against 1 or −1.
strncmp: Bounded and Prefix Comparison
strncmp compares at most n bytes. Its main honest use is prefix matching:
const char *path = "/usr/local/bin";
if (strncmp(path, "/usr", 4) == 0) {
/* path begins with /usr */
}
Output:
strncmp("/usr/local/bin", "/usr", 4) == 0 -> TRUE (prefix match)
strncmp("ab", "abcdef", 6) -> -99 (stops at the NUL in "ab")
The second line is the detail people miss. Asking for 6 bytes does not mean 6 bytes are read: strncmp still stops at a terminator, so comparing "ab" against "abcdef" stops after three bytes and reports a difference.
The dangerous misreading is the opposite one. strncmp bounds the comparison, not the buffer. If a string is genuinely unterminated, strncmp(a, b, n) with an n larger than the data is undefined behaviour, exactly as strcmp would be. The length limit protects you from a long string, not from a malformed one.
The Other Comparison Functions
memcmp compares exactly n bytes and does not stop at a terminator. That makes it right for fixed-size records and binary data, and wrong for strings held in oversized buffers:
buf1 = "hi" + zero fill, buf2 = "hi" + leftover 'X's
strcmp(buf1, buf2) -> 0 (stops at the NUL: equal)
memcmp(buf1, buf2, 8) -> -88 (compares all 8 bytes: NOT equal)
Both buffers hold the string "hi". strcmp agrees they are equal. memcmp compares all eight bytes and disagrees, because one buffer was zero-filled and the other still contains whatever was in it before. Neither answer is wrong — they are answering different questions.
Case-insensitive comparison is not in standard C. strcasecmp is POSIX, declared in <strings.h> (note the s), and returns 0 for a case-insensitive match:
strcasecmp("Hello", "hELLO") -> 0 (POSIX; MSVC calls it _stricmp)
On MSVC the function is _stricmp from <string.h>. Portable code usually needs a small #ifdef, or a hand-rolled loop using tolower on unsigned char values.
strcoll compares using the current locale, which is what you want when sorting names for display to a human and not what you want when checking whether two identifiers match. Its ordering depends on the locale set by setlocale, and it is typically more expensive than strcmp — though no timing was measured for this article.
The Comparison Is Not the Dangerous Part
The version of this article that stood here for years opened with a password checker. Its comparison was fine. Everything around it was not:
char password[] = "123xyz";
char input[10];
do {
printf ("What is your password? ");
fflush (stdout);
scanf ("%s",input);
}
while (strcmp (password,input)!=0);
That code compiles under gcc -std=c11 -Wall -Wextra with zero warnings. It has two serious defects.
First, scanf("%s", input) has no length limit. It writes whatever it is given into a ten-byte buffer. Compiled with AddressSanitizer and fed thirty characters:
==500==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7fc2f6c0004a
WRITE of size 31 at 0x7fc2f6c0004a thread T0
#0 scanf_common
#3 0x56447bd4d437 in main /home/claude/str/old_article.c:13
Address 0x7fc2f6c0004a is located in stack of thread T0 at offset 74 in frame
#0 in main /home/claude/str/old_article.c:5
This frame has 2 object(s):
[32, 39) 'password' (line 6)
[64, 74) 'input' (line 8) <== Memory access at offset 74 overflows this variable
A write of size 31 into a ten-byte object. One honest note on this build: the sanitizer’s frame map shows password at offset 32 and input at offset 64, so input sits above password in the frame and the overflow runs away from it rather than into it. Overwriting the stored password by typing a long enough one does not work here. That is a property of this compiler’s stack layout, not a safety guarantee.
Second, the return value of scanf is never checked. When input runs out, scanf returns EOF without touching input, the loop condition never changes, and the program spins:
$ echo "wrong" | timeout 2 ./old_article | grep -c "What is your password?"
2402287
Two point four million prompts in two seconds, and it would have continued indefinitely. A password checker that never terminates on end-of-input is a CPU-spinning failure mode for anything that can control its input stream — a script, a pipe, a closed terminal.
The safe version
#include <stdio.h>
#include <string.h>
#define MAXLINE 64
/* Reads one line safely. Returns 1 on success, 0 on EOF or error. */
static int read_line(char *buf, int size) {
if (fgets(buf, size, stdin) == NULL) return 0; /* EOF or error */
buf[strcspn(buf, "\n")] = '\0'; /* strip the newline */
return 1;
}
int main(void) {
const char *expected = "123xyz";
char input[MAXLINE];
int attempts = 0;
while (attempts < 3) {
printf("What is your password? ");
fflush(stdout);
if (!read_line(input, sizeof input)) { /* EOF: give up, do not loop */
printf("\nNo more input.\n");
return 1;
}
attempts++;
if (strcmp(expected, input) == 0) { /* compare the SIGN, not == 1 */
puts("Password is correct!");
return 0;
}
puts("Incorrect.");
}
puts("Too many attempts.");
return 1;
}
Four changes carry the weight. fgets takes the buffer size, so it cannot overrun. strcspn(buf, "\n") strips the trailing newline that fgets keeps — miss this and every comparison fails, because "123xyz\n" is not "123xyz". The NULL return is checked, so end-of-input terminates the program instead of spinning it. And the attempt count is bounded.
Compiled with -fsanitize=address,undefined and given the two inputs that broke the original:
=== 200 characters of input ===
What is your password? Incorrect.
What is your password? Incorrect.
What is your password? Incorrect.
Too many attempts.
=== immediate EOF ===
What is your password?
No more input.
[exit 1]
No sanitizer reports on either run.
Comparing Strings in C++
If you are writing C++, most of this disappears. std::string overloads the comparison operators to compare contents:
#include <iostream>
#include <string>
int main() {
std::string a = "hello";
std::string b = "hello";
std::cout << "a == b -> " << std::boolalpha << (a == b) << '\n';
std::cout << "a.compare(b) -> " << a.compare(b) << " (same sign rule as strcmp)\n";
std::string apple = "apple", banana = "banana";
std::cout << "apple < banana -> " << (apple < banana) << '\n';
const char *p1 = "hello";
const char *p2 = "hello";
std::cout << "\nstd::string == const char* -> " << (a == p1) << " (compares contents)\n";
std::cout << "const char* == const char* -> " << (p1 == p2)
<< " (compares ADDRESSES - same trap as C)\n";
return 0;
}
Output:
a == b -> true
a.compare(b) -> 0 (same sign rule as strcmp)
apple < banana -> true
std::string == const char* -> true (compares contents)
const char* == const char* -> true (compares ADDRESSES - same trap as C)
Note the last line. The trap does not go away in C++ — it just gets smaller. As soon as both sides are raw pointers, == is comparing addresses again, and the literal-pooling coincidence makes it print true here exactly as it did in C. std::string::compare follows the same sign convention as strcmp, so everything said above about testing against zero applies to it too.
Key Takeaways
==onchar *compares addresses, not characters. It compiled without a warning and returnedtruefor two identical literals only because the compiler pooled them to one address.- Test
strcmpagainst zero, never against 1. The standard guarantees the sign only.strcmp("z", "a")returned 25 on this build, so== 1failed while> 0succeeded. strncmpbounds the comparison, not the buffer. It is the right tool for prefix tests and gives no protection against an unterminated string.memcmpdoes not stop at the terminator. Two buffers holding"hi"compared equal withstrcmpand unequal withmemcmp, because one still held leftover bytes.- Case-insensitive comparison is not standard C —
strcasecmpis POSIX,_stricmpis MSVC. - The input is more dangerous than the comparison.
scanf("%s", buf)on a ten-byte buffer produced an AddressSanitizer stack-buffer-overflow, WRITE of size 31, and the unchecked return value spun the program 2.4 million times in two seconds at end-of-input. fgetsplusstrcspnis the safe pattern, and stripping the trailing newline is the step that is easiest to forget.
Frequently Asked Questions
Conclusion
The pattern running through all of this is that C will let you compare the wrong thing without complaint. == on two pointers is a legitimate expression, strcmp(a, b) == 1 is a legitimate test, and memcmp on an oversized buffer is a legitimate call. Each is correct C and each answers a question you did not ask, and the compiler has no way to know the difference. The only one GCC warned about here was comparing two arrays — the form that rarely appears in real code, while the char * version that does appear drew no warning at all.
That makes string comparison a good place to build the habit of asking what a line actually compares rather than what it looks like it compares. The same question applies to the input handling around it, which is where the real damage was: a comparison cannot be safer than the buffer it reads. The rest of the C programming section covers the neighbouring ground.



