Most segfaults you'll run into happen because a null pointer sneaked right past a guard check that was messy or ordered poorly. Getting a solid grip on how C looks at each operator - and the exact order it tackles them in - is basically what keeps your code running cleanly instead of dying out of nowhere in production.
Quick Info:
Core Categories: Arithmetic, Relational, Logical, Bitwise, Assignment, Memory.
Most Bug-Prone: Bitwise AND (
&) vs. Logical AND (&&).Evaluation Flow: Left-to-right for most things, but right-to-left for assignments and unary operators.
Memory Operators: Address-of (
&), Dereference (*), Sizeof (sizeof).
Arithmetic and Assignment Operators
Your basic math operators (+, -, *, /, %) work just like you'd expect on paper. That said, things get tricky fast once you mix in assignments and increment operations under the hood.
Increment/Decrement Quirks (Pre vs. Post)
Where you place that ++ actually matters a ton. Prefix (++x) bumps the value up right away before the rest of the expression gets evaluated. Postfix (x++), on the other hand, grabs the current value for the expression first, and only bumps it up afterward.
Mix these up inside a loop condition, and you've got an instant off-by-one bug. I'd recommend sticking to prefix notation in your daily loops unless you genuinely need that old snapshot value. In older compilers and non-optimized builds, it also skips creating unnecessary temporary copies.
C
int x = 5;
int a = ++x; // x becomes 6, a = 6
int b = x++; // b = 6, then x becomes 7Right-to-Left Associativity in Chain Assignments
Almost every operator you work with parses left to right, but assignment operators (=, +=, -=) throw that out the window.
When you write something like a = b = 5, the compiler actually resolves b = 5 first, and then hands that resulting value off to a. This right-to-left flow makes initializing a cluster of variables look tidy, but watch out: chaining them into compound logic checks on the same line gets messy fast.
C
int a, b;
a = b = 5; // b = 5 first, then a = 5Relational and Logical Operators
Relational operators (==, !=, >, <, >=, <=) inspect your values and hand back either a plain 1 for true or 0 for false. From there, your logical operators (&&, ||, !) let you stitch those conditions together.
| Operator | Description | Example | Result |
|---|---|---|---|
== |
Equal to | 5 == 5 |
1 (true) |
!= |
Not equal to | 5 != 3 |
1 (true) |
> |
Greater than | 10 > 5 |
1 (true) |
< |
Less than | 3 < 8 |
1 (true) |
>= |
Greater than or equal | 5 >= 5 |
1 (true) |
<= |
Less than or equal | 4 <= 6 |
1 (true) |
&& |
Logical AND | 1 && 0 |
0 (false) |
|| |
Logical OR | 1 || 0 |
1 (true) |
! |
Logical NOT | !1 |
0 (false) |
How Short-Circuit Evaluation Prevents Null Pointer Crashes
Logical operators come with short-circuit evaluation baked in. What that means in practice is simple: if the left side of a logical AND (&&) turns out false, the compiler doesn't even bother looking at the right side.
Think of it as your first line of defense against crashes. Take this pattern:
C
if (ptr != NULL && ptr->value == 1) {
// safe to access ptr->value
}If that pointer is null, that first test immediately fails and execution bails out. The program never touches ptr->value.
If C forced both sides to run every single time, your code would blow up on a null dereference right then and there. Short-circuiting isn't just a clever optimization trick; it's the standard way to write safe pointer checks in C.
Bitwise Operators in Embedded Systems
You won't see bitwise operators (&, |, ^, ~, <<, >>) pop up every day in typical desktop software. Down in embedded systems or OS dev, though, you can't live without them - they let you flip individual hardware register flags without trampling the bits sitting right next to them.
| Operator | Description | Example | Result |
|---|---|---|---|
& |
Bitwise AND | 5 & 3 |
1 (0101 & 0011 = 0001) |
| |
Bitwise OR | 5 | 3 |
7 (0101 | 0011 = 0111) |
^ |
Bitwise XOR | 5 ^ 3 |
6 (0101 ^ 0011 = 0110) |
~ |
Bitwise NOT | ~5 |
-6 (inverts all bits) |
<< |
Left shift | 5 << 1 |
10 (multiplies by 2) |
>> |
Right shift | 5 >> 1 |
2 (divides by 2) |
The Critical Difference Between & and &&
One of the easiest typos to make is dropping a single & when you meant &&. Logical && only cares whether both expressions are non-zero (treating them as true or false). Bitwise &, meanwhile, lines up the numbers bit by bit, runs an AND on each pair, and spits out a new raw number.
C
// Bitwise AND: checks shared bits
if (5 & 2) { } // 0101 & 0010 = 0000 → evaluates to 0 (false)
// Logical AND: checks if both are non-zero
if (5 && 2) { } // both non-zero → evaluates to 1 (true)Accidentally using & instead of && creates nasty bugs that compile totally clean without a single warning. The compiler thinks everything is fine on paper, but your program silently takes the wrong branch when it runs.
Practical Bit Manipulation Tricks
Bitwise math lets you pull off a few neat performance wins in a single clock cycle:
Check even/odd without modulo:
C
if (num & 1) {
// num is odd
}This ends up noticeably lighter than num % 2 because it skips the hardware division step entirely.
Toggle a specific bit:
C
uint8_t reg = 0b00001010;
reg ^= (1 << 3); // toggles bit 3You'll see this everywhere in microcontroller projects when toggling status pins or blinking an LED.
Check if a number is a power of 2:
C
if (n > 0 && (n & (n - 1)) == 0) {
// n is a power of 2
}Left/right shift as fast multiply/divide:
C
int x = 4;
x = x << 1; // x = 8 (multiply by 2)
x = x >> 1; // x = 4 (divide by 2)Memory and Pointer Operators
Working in C means you're talking directly to raw memory. The address-of operator (&) grabs the actual RAM address where your variable lives, while the dereference operator (*) goes straight to that address to read or rewrite whatever is stored there.
C
int val = 42;
int *ptr = &val; // ptr holds the address of val
printf("%d", *ptr); // dereferences ptr, prints 42Whatever you do, don't try dereferencing an uninitialized pointer. It's essentially pointing at whatever garbage data happened to be left in memory, which will trash your state or cause a crash. Get in the habit of setting pointers to NULL right away if you don't have an address ready for them yet.
C
int *ptr = NULL; // safe initialization
if (ptr != NULL) {
// only dereference when safe
printf("%d", *ptr);
}The Ternary (?:) and Comma Operators
The ternary operator (?:) is the only one in the language that juggles three arguments at once. It's basically shorthand for a mini if-else statement wrapped into a single expression.
C
int max = (x > y) ? x : y;A good rule of thumb: keep them short and obvious. A single condition is great for readability, but as soon as you nest two or three of these inside each other, nobody on your team will want to read it.
The comma operator (,) evaluates its first piece, tosses that result out the window, and then evaluates the second piece. You'll run into it mostly when setting up and updating two counters at once inside a for loop header:
C
for (int i = 0, j = 10; i < j; i++, j--) {
// i and j move toward each other
}Sizeof and Type Casting
The sizeof operator tells you the actual size of a variable or type in bytes on the machine running the code. You should lean on it every time you call malloc; hardcoding arbitrary numbers like 4 or 8 bytes is a surefire way to break things the second your code moves to another platform.
C
int *arr = malloc(10 * sizeof(int)); // correct: adapts to platform
int *arr2 = malloc(10 * 4); // wrong: assumes int is always 4 bytesCasting lets you step in and tell the compiler to treat a value as a specific type instead of its default:
C
float result = (float)7 / 2; // result = 3.5, not 3Just be mindful when converting floats down to integers; C won't round the number up or down for you, it simply chops the entire decimal off.
Operator Precedence: Common Traps
Precedence is just the rulebook deciding what gets run first when multiple operators land on the same line. Multiplication beats addition, logical NOT (!) hits before equality (==), and dereferencing (*) actually sits lower on the totem pole than member access (->).
C
// Common trap: NOT applied before comparison
if (!flag == 1) { } // reads as (!flag) == 1, NOT as !(flag == 1)
// Fix: use parentheses
if (!(flag == 1)) { }When you're not 100% sure, just wrap things in parentheses. They completely clear up any ambiguity for the compiler, and more importantly, the person maintaining your code won't have to guess what you meant.
A lot of textbook guides throw a massive precedence table at you and leave it at that, but the real-world headaches usually come down to everyday patterns - like accidentally writing *ptr++ (which shifts your pointer forward rather than bumping the underlying value) or a & b == c (where that equality check runs before your bitwise AND). Throwing in a couple of parentheses doesn't cost any performance, and it'll save you an afternoon of hunting down ghost bugs.