A segmentation fault often triggers when a null pointer bypasses your logic checks due to poorly structured operator conditions. Knowing how C evaluates each operator, and in what order, is what separates safe code from code that crashes silently 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, Right-to-Left for assignments and unary operators.
Memory Operators: Address-of (&), Dereference (), Sizeof.
Arithmetic and Assignment Operators
Basic math operators (+, -, *, /, %) handle standard calculations. However, the real complexity lies in how assignment and increment operations behave under the hood.
Increment/Decrement Quirks (Pre vs. Post)
The position of the increment operator (++) changes everything. Prefix (++x) increments the value before evaluating the expression. Postfix (x++) evaluates the expression first, then increments the value.
Mixing these up inside loop conditions causes off-by-one errors. Always use prefix notation in simple loops unless you specifically need the old value. It saves memory overhead in older compilers.
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
Most operators read from left to right. Assignment operators (=, +=, -=) break this rule.
When you write a = b = 5, the compiler evaluates b = 5 first. Then it assigns the result of that operation to a. This right-to-left associativity allows clean variable initialization. Be careful when mixing assignments with logic operators in the same line.
int a, b;
a = b = 5; // b = 5 first, then a = 5Relational and Logical Operators
Relational operators (==, !=, >, <, >=, <=) compare values and return 1 (true) or 0 (false). Logical operators (&&, ||, !) combine these conditions.
| 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 rely on short-circuit evaluation. If the first operand of a logical AND (&&) is false, the compiler completely ignores the second operand.
This is a built-in safety net. Consider:
if (ptr != NULL && ptr->value == 1) {
// safe to access ptr->value
}If the pointer is null, the first check fails. The program never attempts to read the value. If the compiler evaluated both sides, your program would immediately crash with a null pointer dereference. Short-circuiting is not a convenience feature: it is how experienced C developers write memory-safe conditionals.
Bitwise Operators in Embedded Systems
Standard applications rarely use bitwise operators (&, |, ^, ~, <<, >>). In embedded systems, they are absolutely essential for manipulating hardware registers without affecting neighboring bits.
| 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 &&
A common mistake is using the bitwise AND (&) instead of the logical AND (&&). The logical && checks if both conditions are non-zero (true/false evaluation). The bitwise & compares two numbers bit by bit and returns a numeric result.
// 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)Using & when you mean && creates silent logic bugs that compile without warnings. The condition passes type checking but produces the wrong behavior at runtime.
Practical Bit Manipulation Tricks
Bitwise operators let you perform complex checks in a single CPU cycle:
Check even/odd without modulo:
if (num & 1) {
// num is odd
}This is faster than num % 2 because it avoids division.
Toggle a specific bit:
uint8_t reg = 0b00001010;
reg ^= (1 << 3); // toggles bit 3Standard for LED blinking in microcontroller code.
Check if a number is a power of 2:
if (n > 0 && (n & (n - 1)) == 0) {
// n is a power of 2
}Left/right shift as fast multiply/divide:
int x = 4;
x = x << 1; // x = 8 (multiply by 2)
x = x >> 1; // x = 4 (divide by 2)Memory and Pointer Operators
C gives you direct access to RAM. The address-of operator (&) fetches the physical memory address of a variable. The dereference operator (*) accesses the value stored at that specific address.
int val = 42;
int *ptr = &val; // ptr holds the address of val
printf("%d", *ptr); // dereferences ptr, prints 42Never dereference an uninitialized pointer. It points to a random memory location and will corrupt your system state. Always initialize pointers to NULL if you are not assigning a value immediately.
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 operator in C that takes three operands. It functions as a compact if-else statement.
int max = (x > y) ? x : y;Keep ternary operations simple. One level of nesting is manageable; two or more makes the code unreadable and error-prone.
The comma operator (,) evaluates its first operand, discards the result, then evaluates the second operand. Its main use is initializing multiple variables inside a for-loop declaration:
for (int i = 0, j = 10; i < j; i++, j--) {
// i and j move toward each other
}Sizeof and Type Casting
The sizeof operator returns the exact memory footprint of a variable or data type in bytes. Always use it when allocating memory with malloc; hardcoding byte sizes causes cross-platform compatibility issues.
int *arr = malloc(10 * sizeof(int)); // correct: adapts to platform
int *arr2 = malloc(10 * 4); // wrong: assumes int is always 4 bytesType casting forces the compiler to treat a variable as a different data type:
float result = (float)7 / 2; // result = 3.5, not 3Use it carefully when converting floating-point numbers to integers; the decimal part is silently truncated.
Operator Precedence: Common Traps
Operator precedence determines which operation executes first when multiple operators appear on the same line. Multiplication beats addition. Logical NOT (!) has higher precedence than equality (==). Pointer dereferencing (*) has lower precedence than member access (->).
// Common trap: NOT applied before comparison
if (!flag == 1) { } // reads as (!flag) == 1, NOT as !(flag == 1)
// Fix: use parentheses
if (!(flag == 1)) { }When in doubt, use parentheses. They override all precedence rules and make your intent unambiguous to both the compiler and the next developer reading your code.
This is the part most C tutorials gloss over with a table and move on. The actual bugs show up in code like *ptr++ (increments the pointer, not the value) or a & b == c (equality check runs before bitwise AND). Parentheses cost nothing and save real debugging hours.
Comments (0)
Sign in to comment
Report