Even today, the C programming language is everywhere. It powers operating systems, embedded devices, and countless applications. For many, it's the first language they learn, a solid foundation in the world of code.
But C, despite its age and widespread use, holds some truly strange secrets. It has peculiar behaviors and surprising rules that can trip up even experienced programmers. These aren't bugs, but rather features of its design, often leading to head-scratching moments.
Characters, Numbers, and the Surprising sizeof
One of the first things you learn in C is about characters. A single character like 'A' is stored differently than the number 65, even though they might represent the same thing. This difference becomes clear when you compare a zero with the null character.
For example, 0 == '\0' is true. This makes sense because \0 is the null terminator, which has a numeric value of zero. But 0 == '0' is false, which can confuse newcomers. The character '0' actually has a numeric value different from zero, often 48 in ASCII systems. It's a character, not the number zero itself.
Another interesting point is how C handles the size of a character literal. You might expect sizeof('0') to be 1, because a char type usually takes up one byte. However, in C, character literals are treated as int types. This means sizeof('0') will often be 4 or even 8 bytes, depending on your system, instead of the expected 1 byte. It's a small detail, but it shows C's unique way of working.
The
Mystery of NULL and Pointers
Pointers are a core part of C. They let you work directly with memory addresses. A very important concept for pointers is NULL, which means a pointer isn't pointing to any valid memory location. But how C defines and uses NULL can be a bit confusing.
Many programmers learn that NULL is essentially 0. And it's true that NULL == 0 usually evaluates to true. You can also compare a pointer to (void*)0, which is a null pointer cast to a generic pointer type, and it will also be true. This shows how flexible C is with its definition of “nothing” for pointers.
What's even more surprising is how macros interact with this. If you define a macro like #define FOO 0, then a comparison like (FOO == NULL) will also evaluate to true. This happens because the preprocessor replaces FOO with 0 before the compiler sees it, making it effectively (0 == NULL). This flexibility, while powerful, requires careful understanding.
Strings: More Than Just Letters
Strings in C are not a basic type like numbers or characters. They are arrays of characters, always ending with a special null character (\0). This null terminator is what tells C where a string ends. This detail has some interesting effects.
When you calculate the size of a string literal, like sizeof("hello"), you might expect it to be 5, for the five letters. But C includes the hidden null terminator in its size calculation. So, sizeof("hello") actually gives you
- Always remember that extra byte for the terminator when working with string lengths.
There's also a difference between how you declare strings. Consider char str[] = "hello"; and char *str = "hello";. The first creates an array on the stack, and you can change its contents (like str[0] = 'H';). The second creates a pointer to a string literal, which is often stored in read-only memory. Trying to change str[0] in the second case will likely cause your program to crash. It's a subtle but crucial difference in memory handling.
Arrays and Pointers: A Strange Connection
C treats arrays and pointers in very similar ways, especially when passing them to functions. This similarity leads to some truly bizarre, yet valid, syntax.
If you have an array int a[] = {1,2,3}; and a pointer int *b = a;, you can access elements using b[1]. This is standard. But here’s the quirk: 1[b] also works and gives you the same result. This is because array indexing b[1] is actually interpreted as *(b + 1). Since addition is commutative (b + 1 is the same as 1 + b), *(1 + b) is also valid, which means 1[b] works too. It's a mind-bending piece of C's design.
However, don't confuse this with comparing entire arrays. If you declare char str[] = "hello"; and char str2[] = "hello";, then str == str2 will be false. This is because str and str2 are actually pointers to the *starting memory addresses
- of two different arrays, even if their contents are identical. Comparing them compares their addresses, not their values.
The Comma
Operator and Increment Tricks
C has an operator that often goes unnoticed: the comma operator. It allows you to put multiple expressions where C expects a single one. The expressions are evaluated from left to right, and the value of the entire comma expression is the value of the rightmost expression.
For example, if you write int a = 5; int b = 6; a,b;, the expression a,b evaluates to b (which is 6), but the result isn't used. More interesting is int c = (a,b);. In this case, c would be assigned the value of b, which is