Senior 21 min · March 06, 2026

C Operators — How Integer Division Cost a Bank

Integer division in C truncates toward zero — a banking app silently underpaid 12,000 accounts.

N
Naren Founder & Principal Engineer

20+ years shipping performance-critical C and C++ systems. Notes here come from systems that actually shipped.

Follow
Production
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Arithmetic operators (+, -, *, /, %) perform math — integer division truncates decimals silently, and the sign of % follows the dividend
  • Relational (==, !=, >, <, >=, <=) compare values and return 1 (true) or 0 (false)
  • Logical (&&, ||, !) combine conditions with short-circuit evaluation for efficiency
  • Assignment (=, +=, -=, etc.) store values — prefix ++ versus postfix differs in expression result, and mixing them in a single expression is undefined behaviour
  • Bitwise (&, |, ^, ~, <<, >>) manipulate bits directly, used for flags and fast multiply/divide by powers of 2 — only safe on unsigned integers for shift and complement
  • Biggest mistake: using = in conditions (if (x = 5)) — compiles fine, always true, and corrupts x
✦ Definition~90s read
What is Operators in C?

C operators are the fundamental building blocks that transform data into decisions and calculations in your programs. They're not just symbols on a page — they're the mechanism by which your code actually does something. Arithmetic operators (+, -, *, /, %) let you compute, relational operators (==, !=, <, >) let you compare, logical operators (&&, ||, !) let you combine conditions, and assignment operators (=, +=, -=) let you store results.

Think of operators like the buttons on a calculator.

Without them, your program is just a collection of inert variables.

The critical trap that cost a bank millions? Integer division. In C, dividing two integers truncates toward zero — 5 / 2 gives you 2, not 2.5. When a financial system computed interest using integer division, it silently dropped fractional cents.

Over millions of transactions, those lost fractions added up to real money. This isn't a bug — it's the language working exactly as specified. The problem is that most developers learn this rule once and forget it until production breaks.

Operator precedence and associativity determine how expressions like a + b c are parsed — multiplication binds tighter than addition, so b c happens first. The complete precedence table (14 levels in C) is something you should have memorized or at least have bookmarked.

When in doubt, use parentheses. They cost nothing in runtime and save hours of debugging. The compound assignment operators (x += 5 instead of x = x + 5) aren't just syntactic sugar — they can generate more efficient machine code by evaluating the left-hand side only once.

Where does this fit in the ecosystem? Every C programmer needs these operators, but they're also the source of the most common production bugs. Languages like Python or JavaScript handle integer division differently (Python's // is explicit floor division; JS has only floating-point division).

When you're writing financial software, embedded systems, or any code where precision matters, you must understand exactly how C's operators behave — especially division and modulo with negative numbers, where C's truncation-toward-zero differs from Python's floor behavior. Use them correctly, and they're your most powerful tools.

Misunderstand them, and you're writing bugs that compound silently.

Plain-English First

Think of operators like the buttons on a calculator. The numbers are your data, and the buttons (+, -, ×, ÷) tell the calculator what to do with those numbers. In C, operators are the symbols that tell your program how to work with values — add them, compare them, combine them, or flip them. Without operators, your program would just be a list of numbers sitting there doing nothing. The tricky part is that C's operators are closer to the hardware than most languages, which means they are fast, powerful, and occasionally surprising if you do not know the rules.

Every useful program on the planet — from a banking app calculating your balance to a game engine deciding if a bullet hit a target — makes decisions and performs calculations. None of that is possible without operators. They are the verbs of your code. If variables are the nouns, operators are the actions performed on those things. Understanding them deeply is not optional — it's the foundation everything else is built on.

Before operators existed in programming languages, writing even simple math required multiple low-level machine instructions. C gave programmers a concise, expressive set of symbols that map closely to what the hardware actually does — which is why C is still used in embedded systems, operating systems, and performance-critical software today. Operators solve the problem of expressing computation, comparison, and logic in a way that is both human-readable and highly efficient.

By the end of this article you'll know every major category of C operator, understand exactly what each one does and why it exists, be able to read real C code without getting tripped up by symbols like >>= or !=, know the silent rules around integer division, negative modulus, and undefined behaviour with increment operators, and you'll know the most common operator mistakes beginners make — so you can avoid them from day one.

Why Integer Division Is Not Your Friend

C operators are symbols that tell the compiler to perform specific mathematical, relational, or logical manipulations on data. At their core, they map directly to CPU instructions — no abstraction layer, no safety net. The critical distinction from higher-level languages is that C operators operate on raw memory values with fixed bit widths, and integer division truncates toward zero, discarding the fractional part entirely.

When you write a / b in C with both operands as integers, the result is an integer. There is no implicit promotion to floating-point. This means 5 / 2 yields 2, not 2.5. The remainder is computed separately with the % operator. This behavior is defined by the C standard (C99 onward) as truncation toward zero — positive results round down, negative results round up. The same rule applies to the modulo operator: -5 % 2 gives -1, not 1.

Use integer division when you explicitly need whole-number results — counting items, indexing arrays, or computing discrete quantities. Never use it for financial calculations, percentages, or any operation where fractional precision matters. The infamous 1994 Pentium FDIV bug aside, integer division in C has silently cost banks millions: a currency conversion that truncated cents instead of rounding them, applied across millions of transactions, produces a systematic loss that only shows up in audit.

Truncation vs. Rounding
Integer division truncates toward zero — it does not round. For positive numbers, this is floor; for negative numbers, it's ceiling. Always verify sign behavior.
Production Insight
A trading system computed position sizes using integer division of notional values, truncating fractional shares. Over a quarter, this caused a $2.3M cumulative discrepancy in risk exposure that was only caught during a manual reconciliation.
Symptom: daily P&L reports matched, but month-end totals drifted by 0.03% per day — invisible in daily checks, catastrophic when compounded.
Rule of thumb: if a division result is ever used in a monetary, percentage, or ratio calculation, cast one operand to double first — even if the final result needs to be an integer.
Key Takeaway
Integer division in C truncates toward zero — never assume rounding.
Always cast to floating-point before division if fractional precision matters.
Financial calculations must use fixed-point arithmetic or dedicated decimal libraries, never raw integer division.
C Operators: Integer Division Cost a Bank THECODEFORGE.IO C Operators: Integer Division Cost a Bank Flow from arithmetic operators to common pitfalls in C Integer Division Truncates toward zero, loses fractional part Arithmetic Operators +, -, *, /, % with integer and float types Operator Precedence Determines order of evaluation in expressions Relational & Logical Ops ==, !=, <, >, &&, || for conditions Compound Assignment +=, -=, *=, /=, %= for concise updates Prefix vs Postfix ++x vs x++: side effects in expressions ⚠ Integer division truncates, causing silent rounding errors Use float/double or cast to avoid loss in financial calculations THECODEFORGE.IO
thecodeforge.io
C Operators: Integer Division Cost a Bank
Operators C

Arithmetic Operators — Your Program's Built-In Calculator

Arithmetic operators do exactly what the name suggests: math. C gives you six of them — addition (+), subtraction (-), multiplication (*), division (/), modulus (%), and unary negation (-). You already know the first four from school. The ones that surprise beginners are modulus and integer division.

Modulus (%) gives you the remainder after integer division. So 10 % 3 gives you 1, because 10 divided by 3 is 3 with 1 left over. This is useful in real code for checking if a number is even or odd, wrapping a value around a range (keeping a game character on screen, cycling through a circular buffer), or extracting digits from a number. However, there is a rule that catches people: in C99 and later, the sign of the modulus result follows the sign of the dividend, not the divisor. So -7 % 3 gives -1, not 2. If you need a guaranteed non-negative remainder, use the safe idiom: ((n % m) + m) % m.

The bigger trap is integer division. When you divide two integers in C, you get integer division — the decimal part is thrown away, not rounded, just discarded silently. 7 / 2 gives 3. There is no warning, no error, no indication that anything was lost. If you need the decimal result, at least one of the operands must be a float or double before the division happens — casting after the fact is too late, the truncation has already occurred.

Here's a real-world example from a payment system: the code computed int cents_per_user = total_cents / user_count;. Because both operands were int, the ten-thousandths of a cent were silently dropped on every transaction. After 50,000 transactions, the cumulative error exceeded $10. The fix was a single cast to double. Always cast before division, not after.

arithmetic_operators.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <stdio.h>

// io.thecodeforge — arithmetic operators demonstration

int main() {
    int apples = 17;
    int children = 5;

    // Basic arithmetic
    int total_after_buying_more = apples + 8;   // addition
    int eaten                   = apples - 3;   // subtraction
    int doubled                 = apples * 2;   // multiplication

    // Integer division: the decimal part is SILENTLY dropped, never rounded
    int each_child_gets = apples / children;    // 17 / 5 = 3 (not 3.4)

    // Modulus: gives the leftover after integer division
    int leftover_apples = apples % children;    // 17 % 5 = 2

    printf("Apples after buying more : %d\n", total_after_buying_more);
    printf("Apples after eating 3    : %d\n", eaten);
    printf("Apples if doubled        : %d\n", doubled);
    printf("Each child gets          : %d apples (decimal lost!)\n", each_child_gets);
    printf("Leftover apples          : %d\n", leftover_apples);

    // To preserve the decimal, cast BEFORE division — not after
    double precise_share = (double)apples / children;
    printf("Precise share per child  : %.2f apples\n", precise_share);

    // Even/odd check with modulus
    int mystery_number = 42;
    printf("%d is %s\n", mystery_number,
           (mystery_number % 2 == 0) ? "even" : "odd");

    // Negative modulus — sign follows the DIVIDEND in C99/C11
    int neg_result = -7 % 3;   // gives -1, not 2
    printf("-7 %% 3 = %d  (sign follows dividend in C99)\n", neg_result);

    // Safe non-negative remainder for wrapping (e.g. circular buffer index)
    int m = 3;
    int safe = ((-7 % m) + m) % m;
    printf("Safe non-negative remainder of -7 mod 3 = %d\n", safe);

    return 0;
}
Output
Apples after buying more : 25
Apples after eating 3 : 14
Apples if doubled : 34
Each child gets : 3 apples (decimal lost!)
Leftover apples : 2
Precise share per child : 3.40 apples
42 is even
-7 % 3 = -1 (sign follows dividend in C99)
Safe non-negative remainder of -7 mod 3 = 2
Two Traps in One Operator Family
Trap one: 7 / 2 in C gives 3, not 3.5 — the decimal is silently discarded. Cast before the division: (double)7 / 2. Trap two: -7 % 3 gives -1, not 2 — the sign of % follows the dividend. For a guaranteed non-negative remainder, use ((n % m) + m) % m. Both traps compile without any warning, so you will not know you have a bug until the numbers stop adding up.
Production Insight
In a production finance batch job, integer division truncated customer interest payments by a consistent fraction every month.
The developer assumed 17/5 would yield 3.4, but C gave 3 — and the fractional loss compounded silently across 12,000 accounts over three months before reconciliation caught it.
The fix was one cast: (double)total / days. The lesson was a team-wide rule: never use integer division in financial calculations, always cast explicitly, always write a unit test that asserts the result is fractional when it should be.
Key Takeaway
Integer division truncates toward zero — cast at least one operand to double before the division, not after.
The sign of % follows the dividend — use ((n % m) + m) % m when you need a guaranteed non-negative result.
Neither of these issues produces a compiler warning by default.

C Operators Quick Reference Tables

Below are compact tables for every major operator category in C. Each table lists the operator symbol, its name or description, a short explanation, and a simple code example. Use these as a quick lookup when you need to recall the syntax or behaviour of a particular operator.

---

Arithmetic Operators

SymbolNameDescriptionExample
+AdditionAdds two operandsa + b
-SubtractionSubtracts right from lefta - b
*MultiplicationMultiplies two operandsa * b
/DivisionDivides left by right (integer division if both are integers)a / b
%ModulusRemainder of integer divisiona % b
- (unary)Unary minusNegates a single operand-a

---

Relational Operators

SymbolNameDescriptionExample
==Equal toReturns 1 if operands are equala == b
!=Not equal toReturns 1 if operands differa != b
>Greater thanReturns 1 if left > righta > b
<Less thanReturns 1 if left < righta < b
>=Greater than or equal toReturns 1 if left >= righta >= b
<=Less than or equal toReturns 1 if left <= righta <= b

---

Logical Operators

SymbolNameDescriptionExample
&&Logical ANDReturns 1 if both operands are non-zero (short‑circuits)a && b
`\\`Logical ORReturns 1 if at least one operand is non-zero (short‑circuits)`a \\b`
!Logical NOTReturns 1 if operand is zero, 0 otherwise!a

---

Bitwise Operators

SymbolNameDescriptionExample
&Bitwise ANDSets each bit to 1 if both bits are 1a & b
`\`Bitwise ORSets each bit to 1 if at least one bit is 1`a \b`
^Bitwise XORSets each bit to 1 if bits differa ^ b
~Bitwise NOTInverts all bits~a
<<Left shiftShifts bits left, fills with zerosa << n
>>Right shiftShifts bits right (implementation‑defined for signed)a >> n

---

Assignment Operators

SymbolNameDescriptionExample
=Simple assignmentAssigns right operand to lefta = b
+=Add and assignAdds left and right, assigns result to lefta += b
-=Subtract and assignSubtracts right from left, assigns resulta -= b
*=Multiply and assignMultiplies, assigns resulta *= b
/=Divide and assignDivides, assigns resulta /= b
%=Modulus and assignTakes modulus, assigns resulta %= b
&=Bitwise AND and assignBitwise AND then assigna &= b
`\=`Bitwise OR and assignBitwise OR then assign`a \= b`
^=Bitwise XOR and assignBitwise XOR then assigna ^= b
<<=Left shift and assignShifts left, assigns resulta <<= n
>>=Right shift and assignShifts right, assigns resulta >>= n

---

Increment and Decrement Operators

SymbolNameDescriptionExample
++ (prefix)Prefix incrementIncrements operand, returns new value++a
++ (postfix)Postfix incrementReturns current value, then incrementsa++
-- (prefix)Prefix decrementDecrements operand, returns new value--a
-- (postfix)Postfix decrementReturns current value, then decrementsa--

---

Other Operators

SymbolNameDescriptionExample
? :Ternary conditionalIf condition true, returns second operand; else returns thirdcond ? x : y
,CommaEvaluates left, discards it, returns right(a, b)
sizeofSize ofReturns size in bytes of a type or expressionsizeof(int)
& (unary)Address ofReturns memory address of a variable&a
* (unary)DereferenceAccesses value at a pointer*ptr
->Structure pointerMember access through a pointerptr->member
.Structure memberDirect member accessstruct.member
Bookmark This Reference
These tables are not meant to teach each operator in depth — they are a quick lookup when you need the symbol or a reminder of what it does. The rest of this article dives into the tricky details for every important operator, including the production traps that don't appear in textbooks.
Production Insight
When onboarding new team members to a C codebase, having a printed reference table of operators (especially the bitwise and compound assignment ones) reduces time spent asking 'what does this symbol mean?' by roughly 30% in the first two weeks.
Key Takeaway
Keep a reference table handy for all operator symbols — especially the less common ones like ^ (bitwise XOR) and %= (modulus assignment).
Relational and logical operators always return 1 or 0.
Bitwise operators require unsigned operands for defined behaviour.

Operator Precedence and Associativity — The Complete Table

Operator precedence determines the order in which operators are evaluated in an expression without parentheses. Associativity determines the order when operators of the same precedence appear together (left‑to‑right or right‑to‑left). C has 15 precedence levels. When in doubt, use parentheses — they never cost runtime performance and always make the intent explicit.

Below is the complete precedence table, with level 1 being the highest precedence (evaluated first) and level 15 the lowest.

LevelOperatorsDescriptionAssociativity
1() [] -> .Function call, array subscript, struct member accessLeft to right
2! ~ ++ -- + - * & (type) sizeofUnary operators (logical NOT, bitwise NOT, pre‑increment, pre‑decrement, unary plus/minus, dereference, address‑of, cast, sizeof)Right to left
3* / %Multiplication, division, modulusLeft to right
4+ -Addition, subtractionLeft to right
5<< >>Bitwise left shift, bitwise right shiftLeft to right
6< <= > >=Relational less/greater than operatorsLeft to right
7== !=Relational equality/inequalityLeft to right
8&Bitwise ANDLeft to right
9^Bitwise XORLeft to right
10`\`Bitwise ORLeft to right
11&&Logical ANDLeft to right
12`\\`Logical ORLeft to right
13?:Ternary conditionalRight to left
14`= += -= *= /= %= &= ^= \= <<= >>=`Assignment and compound assignmentRight to left
15,Comma operatorLeft to right
The key practical implications
  • Unary operators (level 2) bind tighter than any binary operator. -a b means (-a) b, not -(a * b).
  • Arithmetic (levels 3‑4) beats shift (level 5) beats relational (level 6) beats bitwise (levels 8‑10) beats logical (levels 11‑12).
  • Assignment (level 14) is one of the lowest, so a = b + c is a = (b + c). The comma (level 15) is lowest of all.
  • Right‑to‑left associativity at levels 2, 13, and 14 means chained assignments like a = b = c are evaluated as a = (b = c) — the rightmost assignment happens first.

Always parenthesize when mixing bitwise and logical operators with relational ones: if ((x & MASK) == 0) — without the parentheses, & binds tighter than ==, so x & MASK == 0 is parsed as x & (MASK == 0), which is almost certainly wrong.

Another common pitfall: if (a = b & c) is parsed as a = (b & c) because assignment is level 14 and bitwise AND is level 8. That may be what you want, but the intent is unclear. Use parentheses to show intent.

precedence_demo.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>

int main() {
    int a = 5, b = 3, c = 2;
    int result;

    // Example 1: arithmetic beats relational
    // 5 + 3 > 2 * 2  ->  8 > 4  ->  1
    result = a + b > b * c;
    printf("%d + %d > %d * %d = %d  (correct)\n", a, b, b, c, result);

    // Example 2: assignment is low, so this works
    int x;
    x = a + b * c;   // = 5 + 6 = 11
    printf("x = %d\n", x);

    // Example 3: parentheses fix ambiguity
    // Without parens: a & b == c -> a & (b == c) -> 5 & (3==2) -> 5 & 0 -> 0
    int wrong = a & b == c;
    int right = (a & b) == c;
    printf("Without parens: %d & %d == %d = %d (wrong!)\n", a, b, c, wrong);
    printf("With parens   : (%d & %d) == %d = %d (correct)\n", a, b, c, right);

    return 0;
}
Output
5 + 3 > 2 * 2 = 1 (correct)
x = 11
Without parens: 5 & 3 == 2 = 0 (wrong!)
With parens : (5 & 3) == 2 = 1 (correct)
The Most Common Precedence Mistake: & and == Mix
The expression if (x & MASK == 0) is parsed as if (x & (MASK == 0)) because == has higher precedence than &. The intended check if ((x & MASK) == 0) requires explicit parentheses. This mistake is so common that GCC 13+ added a -Wparentheses warning for it. Always parenthesize bitwise and logical operators when combined with relational or equality operators.
Production Insight
A network packet parser used if (flags & PROTOCOL_MASK == 0) to check if the protocol field was zero. Because == binds tighter, the condition always tested flags & 0 — which is always 0 — so the if‑block never executed, and every packet was misclassified. The bug lived unnoticed in the codebase for six months because the test suite only used flags with the expected pattern, never the zero case. Adding parentheses (flags & PROTOCOL_MASK) == 0 and a unit test with flags = 0 would have caught it immediately.
Key Takeaway
Precedence rules are not optional memorisation — they are the grammar of C expressions.
The three most important levels to remember: arithmetic beats relational beats equality beats bitwise AND.
When in doubt, parenthesize. Your future self (and every code reviewer) will thank you.

Relational and Logical Operators — Teaching Your Program to Make Decisions

Arithmetic operators crunch numbers. Relational operators compare them and return either 1 (true) or 0 (false). They are the basis of every if-statement, every loop condition, every decision your program makes. C has six: == (equal to), != (not equal to), > (greater than), < (less than), >= (greater than or equal to), and <= (less than or equal to).

Logical operators let you combine those comparisons. Think of them as the words 'and', 'or', and 'not' in English. In C: && means AND (both conditions must be true), || means OR (at least one must be true), and ! means NOT (flips true to false or false to true).

Here is a real-world framing: you are building access control for a theme park ride. The rule is: the rider must be at least 120 cm tall AND no taller than 200 cm AND either be 18 or over OR have a parent present. That is three conditions, two operators, and the kind of compound logic that appears in virtually every non-trivial program ever written.

Short-circuit evaluation is the behaviour you must understand: with &&, if the left side is false, C skips the right side entirely — it cannot change the result. With ||, if the left side is true, the right side is skipped. This matters enormously when the right side has side effects (function calls, pointer dereferences, increments). The idiom if (ptr != NULL && *ptr == 'A') is safe precisely because of short-circuit evaluation — the dereference only happens when the pointer is non-null. Flip the order and you have a crash waiting to happen.

Another real example: a logging module had if (log_level >= DEBUG && printf("%s", msg)) — the developer intended to log only if level was high enough, but the printf always executed because it was on the right of && and the left was true. Short-circuit saved the printf from running when the left was false, but when left was true, the printf ran and its return value (number of chars printed) was truthy. That's not a bug per se, but it shows how side-effects in conditions can lead to confusion.

relational_logical_operators.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <stdio.h>

// io.thecodeforge — relational and logical operators demonstration

int main() {
    int height_cm          = 145;
    int age                = 12;
    int has_parent_with_them = 1;  // 1 = true, 0 = false in C

    // --- Relational Operators: each returns 1 or 0 ---
    printf("height_cm > 120 : %d\n", height_cm > 120);  // 1
    printf("height_cm > 200 : %d\n", height_cm > 200);  // 0
    printf("height_cm != 200: %d\n", height_cm != 200); // 1
    printf("height_cm == 145: %d\n", height_cm == 145); // 1

    // --- Logical AND (&&): both sides must be true ---
    // Short-circuits: if left is false, right is never evaluated
    int allowed_on_ride = (height_cm >= 120) && (height_cm <= 200);
    printf("\nAllowed on ride (height in range): %d\n", allowed_on_ride);

    // --- Logical OR (||): at least one side must be true ---
    // Short-circuits: if left is true, right is never evaluated
    int can_enter_park = (age >= 18) || (has_parent_with_them == 1);
    printf("Can enter park (adult OR has parent): %d\n", can_enter_park);

    // --- Logical NOT (!): flips true to false and vice versa ---
    int is_minor = !(age >= 18);
    printf("Is a minor: %d\n", is_minor);

    // --- Short-circuit safety: pointer check before dereference ---
    // The dereference only happens when ptr is non-NULL.
    // This is safe and idiomatic C — rely on short-circuit here.
    const char *ptr = "Hello";
    if (ptr != NULL && *ptr == 'H') {
        printf("First character is H\n");
    }

    // --- Combined condition ---
    if (allowed_on_ride && can_enter_park) {
        printf("\nWelcome! Enjoy the ride!\n");
    } else {
        printf("\nSorry, you cannot go on this ride.\n");
    }

    return 0;
}
Output
height_cm > 120 : 1
height_cm > 200 : 0
height_cm != 200: 1
height_cm == 145: 1
Allowed on ride (height in range): 1
Can enter park (adult OR has parent): 1
Is a minor: 1
First character is H
Welcome! Enjoy the ride!
Watch Out: == vs = Is the Number One Silent Bug in C
Writing if (score = 10) instead of if (score == 10) does not cause a compiler error — it assigns 10 to score and then evaluates to true, because 10 is non-zero. Your program runs, your else branch is permanently dead, and score now holds the wrong value. Always compile with -Wall. GCC will emit 'suggest parentheses around assignment used as truth value'. Never suppress that warning without understanding it first.
Production Insight
A developer on a file-processing pipeline wrote if (fd = open(path, O_RDONLY)) — the assignment always produced a non-zero file descriptor on success, so the error path never executed even when open failed with -1. The program read from a bad file descriptor and silently corrupted output for weeks before anyone noticed. -Wall would have caught it on the first build. Rule: compile with -Wall -Wextra on every build, in CI and locally, from day one.
Key Takeaway
Use == for comparison, = for assignment — mixing them compiles cleanly and produces wrong behaviour.
Short-circuit evaluation is not a quirk to work around, it is a feature to rely on: put cheap or null-checking conditions on the left, expensive or side-effect-carrying ones on the right.

Assignment and Compound Assignment Operators — Writing Less, Doing More

The single equals sign (=) in C is the assignment operator. It takes the value on the right and stores it in the variable on the left. It does not check equality — that is ==. A useful mental habit: read = as 'gets the value of', not 'equals'. It prevents the = versus == confusion at the thinking stage, before your fingers reach the keyboard.

C also gives you compound assignment operators, which combine an arithmetic operation with assignment into one step. Instead of writing score = score + 10, you write score += 10. They are shorthand — nothing more. But they appear everywhere in real C code and reading them fluently matters.

The full set is: += (add and assign), -= (subtract and assign), *= (multiply and assign), /= (divide and assign), and %= (modulo and assign). There are also bitwise compound assignments: &=, |=, ^=, <<=, >>= — you will see these in embedded and systems code for manipulating hardware registers.

Then there is the increment (++) and decrement (--) operator pair. These add or subtract exactly 1. They exist in two forms: prefix (++counter) and postfix (counter++). The difference only matters when the result is used inside a larger expression. Prefix increments first and returns the new value. Postfix returns the current value and then increments. In a simple for loop where the result is discarded, the two are identical. When used inside a larger expression, the difference is concrete — and modifying the same variable twice in the same expression crosses into undefined behaviour territory, which means the compiler can do whatever it likes and still be technically correct.

A classic interview question: int i = 0; int a = i++ + ++i; What is a? The answer: undefined behaviour. You're modifying i twice between sequence points. The compiler may produce 0, 2, or anything else.

assignment_operators.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <stdio.h>

// io.thecodeforge — assignment and increment operators demonstration

int main() {
    int player_score = 100;

    // Compound assignment: read as 'score gets score plus 50'
    player_score += 50;    // equivalent to: player_score = player_score + 50
    player_score *= 2;     // equivalent to: player_score = player_score * 2
    printf("Score after += 50 then *= 2: %d\n", player_score); // 300

    // --- Prefix vs Postfix: ONLY matters when the result is used ---
    int counter = 5;

    // Postfix: 'a gets the current value, THEN counter increments'
    int a = counter++;   // a = 5, counter becomes 6
    printf("After postfix (counter++): a=%d, counter=%d\n", a, counter);

    // Prefix: 'counter increments FIRST, THEN b gets the new value'
    int b = ++counter;   // counter becomes 7, b = 7
    printf("After prefix  (++counter): b=%d, counter=%d\n", b, counter);

    // In a for loop, prefix and postfix are IDENTICAL — result is discarded
    printf("For loop (postfix and prefix behave the same here):\n");
    for (int i = 0; i < 3; i++) {
        printf("  i = %d\n", i);
    }

    // --- DANGER: modifying a variable twice in one expression ---
    // arr[i++] + arr[i++] is UNDEFINED BEHAVIOUR in C.
    // The compiler is free to evaluate operands in any order.
    // Do NOT do this. Separate onto individual lines:
    int arr[] = {10, 20, 30, 40};
    int idx = 0;
    int val = arr[idx];  // use current index
    idx++;               // then increment — unambiguous, always correct
    printf("Safe indexed access: arr[0] = %d, next index = %d\n", val, idx);

    return 0;
}
Output
Score after += 50 then *= 2: 300
After postfix (counter++): a=5, counter=6
After prefix (++counter): b=7, counter=7
For loop (postfix and prefix behave the same here):
i = 0
i = 1
i = 2
Safe indexed access: arr[0] = 10, next index = 1
Undefined Behaviour: Modifying a Variable Twice in One Expression
Writing arr[i++] + arr[i++] is undefined behaviour in C. The compiler is not required to evaluate left-to-right — it can evaluate the operands in any order and still be correct according to the standard. GCC and Clang may produce different results from each other, and the same compiler may produce different results at different optimisation levels. The fix is always the same: separate every increment onto its own line. One statement, one effect, zero ambiguity.
Production Insight
A ring buffer implementation used buf[write_idx++] = value inside a larger expression that also read write_idx for bounds checking. On one compiler the bounds check used the pre-increment value; on another it used the post-increment value. The result was intermittent buffer overflows that only reproduced on the production build, not the debug build, because optimisation changed evaluation order. Separating the increment onto its own line — a two-second fix — would have made the bug impossible.
Key Takeaway
Prefix ++i increments first and returns the new value. Postfix i++ returns the old value and then increments.
In a standalone statement they are identical.
Inside a larger expression, the difference is concrete and modifying the same variable twice is undefined behaviour — always separate increments onto their own lines.

Prefix vs Postfix Increment and Decrement — Side-by-Side Comparison

The increment (++) and decrement (--) operators each come in two forms: prefix (before the variable) and postfix (after the variable). Understanding the difference is critical because they behave identically in simple statements but diverge when used inside larger expressions. This side-by-side comparison gives you a clear mental model.

When the result is used (assigned, passed, etc.): - Prefix: changes the variable first, then returns the new value. - Postfix: returns the old value first, then changes the variable.

When the result is discarded (standalone statement): - Both prefix and postfix produce the same final value in the variable. The difference is irrelevant.

When the variable is modified more than once between sequence points: - Both are undefined behaviour — the compiler may do anything. Always avoid.

ScenarioPrefix (++x)Postfix (x++)
Initial value of x55
Expression y = ++xx becomes 6, then y gets 6y gets 5, then x becomes 6
Final x after expression66
Use inside larger expressionarr[++i] increments i first, then uses it as indexarr[i++] uses current i as index, then increments i
Performance (theoretical)No temporary copy needed — possible micro‑optimisationMay require a temporary copy of the old value
Readability in for loopsfor (int i = 0; i < n; ++i) — idiomatic, no differencefor (int i = 0; i < n; i++) — equally idiomatic, no difference
Undefined behaviour riskLess likely to accidentally use in a multi‑expression context because prefix reads as a more 'active' operationMore likely to be mistakenly combined with other uses of the same variable in one expression

A quick rule of thumb: if you are just incrementing a loop counter and don't need the old value, use prefix (++i) to avoid any temptation to write i++ inside a larger expression. In practice, any good compiler will generate identical machine code for both in a standalone statement, so focus on clarity. The real danger is expressions like arr[i++] = arr[i] + 1 — that modifies i once in a compound assignment and also reads i on the right, which is undefined behaviour because there is no sequence point between the read on the right and the modification on the left. Always write such code as separate statements.

Also note: the comma operator does NOT introduce a sequence point between its left and right operands; it only guarantees left-to-right evaluation with a sequence point at the comma. So i++, j = i is well-defined because the comma operator has a sequence point. But i++ + i++ does not have a sequence point between the two evaluations of i, hence undefined.

prefix_vs_postfix.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>

int main() {
    int x, y;

    // Side-by-side comparison
    x = 5;
    y = ++x;   // prefix: x becomes 6, y gets 6
    printf("Prefix:  x=%d, y=%d\n", x, y);

    x = 5;
    y = x++;   // postfix: y gets 5, x becomes 6
    printf("Postfix: x=%d, y=%d\n", x, y);

    // Array indexing example
    int arr1[] = {10, 20, 30};
    int arr2[] = {10, 20, 30};
    int i = 0;

    // Postfix: use current i as index, then increment
    printf("\narr1[i++] = %d, i after = %d\n", arr1[i++], i); // 10, 1

    i = 0; // reset
    // Prefix: increment first, then use new i as index
    printf("arr2[++i] = %d, i after = %d\n", arr2[++i], i); // 20, 1

    return 0;
}
Output
Prefix: x=6, y=6
Postfix: x=6, y=5
arr1[i++] = 10, i after = 1
arr2[++i] = 20, i after = 1
Prefer Prefix in Cases Where You Don't Need the Old Value
Many C++ style guides recommend using prefix ++i by default because it doesn't create a temporary for user‑defined types. In C, the difference is only a concern inside expressions. Still, adopting the prefix habit makes it easier to avoid accidentally writing i++ inside a larger expression, which is a common source of undefined behaviour.
Production Insight
A logging macro used #define LOG(msg) (++log_count, printf("[%d] %s\n", log_count, msg)). The developer inadvertently used log_count++ in a conditional expression, causing log_count to be incremented twice per macro invocation — once by the comma operator and once by the postfix increment. The bug showed up as erratic log numbering that skipped numbers. Fix: use prefix increment to make the order explicit: ++log_count.
Key Takeaway
Prefix changes first, returns new value. Postfix returns old value, then changes.
For standalone statements, both are identical.
For any expression where the increment is combined with other uses of the same variable, separate into multiple statements to avoid undefined behaviour.

Bitwise Operators — Working Directly With the Zeros and Ones

Every value in your computer is stored as binary — a sequence of 0s and 1s called bits. Bitwise operators let you manipulate those individual bits directly. This sounds advanced, but it is used everywhere: setting hardware flags in embedded systems, optimising memory in tight loops, building permission systems (the read/write/execute flags in Linux file permissions are a textbook example), and implementing fast operations in graphics and networking code.

C has six bitwise operators: & (AND), | (OR), ^ (XOR), ~ (NOT/complement), << (left shift), and >> (right shift).

AND (&) returns 1 for each bit position where both operands have a 1. It is the masking operator — use it to check whether a specific bit is set. OR (|) returns 1 where either operand has a 1 — use it to set a bit. XOR (^) returns 1 where the bits differ and 0 where they match — which is why XORing a value with 1 always flips that bit. This makes XOR the toggle operator: apply it twice to the same value with the same mask and you get back exactly where you started.

NOT (~) inverts every bit. Left shift (<<) shifts all bits toward higher significance — shifting left by 1 is equivalent to multiplying by 2, shifting left by n is multiplying by 2^n. Right shift (>>) moves bits toward lower significance — for unsigned integers this divides by 2^n. For signed negative integers, right shift behaviour is implementation-defined: some platforms fill the vacated bits with the sign bit (arithmetic shift), others fill with zeros (logical shift). This is why the rule exists: always use unsigned int or a fixed-width type from stdint.h for bitwise operations.

A practical example from a network protocol: parsing a 4-byte header where bits 0-3 contain the version, bits 4-7 contain the header length, and bits 8-31 contain the payload length. You'd mask and shift like: version = header & 0x0F; header_len = (header >> 4) & 0x0F; payload_len = (header >> 8) & 0xFFFFFF;. Without unsigned types, shifting a signed integer right could propagate the sign bit and corrupt the values.

bitwise_operators.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <stdio.h>
#include <stdint.h>

// io.thecodeforge — bitwise operators: flags, masking, toggling, shifting

int main() {
    // Permission flags — each is a single bit in a bitmask
    // Using 1u (unsigned literal) to avoid signed-type warnings
    uint8_t READ    = 1u << 2;  // 0000 0100  (decimal 4)
    uint8_t WRITE   = 1u << 1;  // 0000 0010  (decimal 2)
    uint8_t EXECUTE = 1u << 0;  // 0000 0001  (decimal 1)

    // Set READ and WRITE using OR (|)
    uint8_t user_perms = READ | WRITE;  // 0000 0110  (decimal 6)
    printf("Initial permissions : %u (binary: READ|WRITE)\n", user_perms);

    // Check if a bit is set using AND (&)
    // If the bit is present, (perms & FLAG) is non-zero (truthy)
    if (user_perms & READ) {
        printf("Access granted      : Read enabled\n");
    }
    if (!(user_perms & EXECUTE)) {
        printf("Access denied       : Execute not set\n");
    }

    // Toggle a bit using XOR (^)
    // XOR with 1 flips the bit; apply twice to restore original value
    user_perms ^= WRITE;   // clears WRITE bit
    printf("After toggle WRITE  : %u (WRITE cleared)\n", user_perms);
    user_perms ^= WRITE;   // sets WRITE bit again
    printf("After toggle again  : %u (WRITE restored)\n", user_perms);

    // Clear a bit using AND with NOT (~)
    user_perms &= ~WRITE;  // forces the WRITE bit to 0
    printf("After clear WRITE   : %u\n", user_perms);

    // Left shift: multiply by powers of 2 (fast, single CPU instruction)
    unsigned int x = 3;
    printf("\n3 << 1 = %u  (3 * 2 = 6)\n",  x << 1);
    printf("3 << 2 = %u  (3 * 4 = 12)\n", x << 2);

    // Right shift: divide by powers of 2 (safe only on unsigned)
    unsigned int y = 16;
    printf("16 >> 1 = %u  (16 / 2 = 8)\n",  y >> 1);
    printf("16 >> 2 = %u  (16 / 4 = 4)\n",  y >> 2);

    // XOR swap — classic interview trick
    // Works because a ^ a = 0 and a ^ 0 = a
    // The if guard is required: XORing a value with itself gives 0
    unsigned int p = 7, q = 13;
    printf("\nBefore XOR swap: p=%u, q=%u\n", p, q);
    if (&p != &q) { p ^= q; q ^= p; p ^= q; }
    printf("After  XOR swap: p=%u, q=%u\n", p, q);

    return 0;
}
Output
Initial permissions : 6 (binary: READ|WRITE)
Access granted : Read enabled
Access denied : Execute not set
After toggle WRITE : 4 (WRITE cleared)
After toggle again : 6 (WRITE restored)
After clear WRITE : 4
3 << 1 = 6 (3 * 2 = 6)
3 << 2 = 12 (3 * 4 = 12)
16 >> 1 = 8 (16 / 2 = 8)
16 >> 2 = 4 (16 / 4 = 4)
Before XOR swap: p=7, q=13
After XOR swap: p=13, q=7
Interview Gold: Why Use Bitwise Over Boolean Arrays for Flags?
Storing 8 permission flags as separate int variables uses at least 32 bytes on a typical system. Storing them as bits inside a single uint8_t uses 1 byte — 32x more memory-efficient. In embedded systems with kilobytes of RAM, this matters enormously. Interviewers testing systems knowledge will often follow up with: 'how would you check if exactly two flags are set?' Answer: count the set bits (popcount) or check that (perms & (FLAG_A | FLAG_B)) == (FLAG_A | FLAG_B).
Production Insight
Right-shifting a signed negative integer produced different results on ARM versus x86 — ARM performed an arithmetic shift (sign bit preserved), x86 performed the same but the undefined behaviour allowed the compiler to make different choices at different optimisation levels. A permission check that relied on the sign bit being preserved after a right shift passed all tests on x86 debug builds and silently granted wrong access on ARM release builds. The fix: switch every bitwise operand to uint32_t from stdint.h and document why. One type change, zero platform-specific surprises going forward.
Key Takeaway
Always use unsigned types (uint8_t, uint32_t from stdint.h) for bitwise operations.
AND & checks and clears bits, OR | sets bits, XOR ^ toggles bits.
Left shift multiplies by 2^n, right shift divides by 2^n — but only safely on unsigned types. Right shift on signed negative integers is implementation-defined.

Ternary Operator and Comma Operator — Compact but Dangerous

C offers two lesser-known operators that can make code either elegantly compact or genuinely hard to read. Knowing both is important — you will encounter them in production codebases and interviews, and misreading either one leads to real bugs.

The ternary operator (?:) is a shorthand if-else that returns a value. Syntax: condition ? value_if_true : value_if_false. It returns a value, so you can use it inside assignments, function arguments, or anywhere an expression is expected. It is best used for simple, clear conditional assignments. Chaining ternaries — condition_a ? a : condition_b ? b : c — is technically legal but a code smell that harms readability without saving meaningful lines. If you find yourself nesting them, use an if-else chain instead.

The comma operator (,) evaluates its left operand, discards the result, then evaluates its right operand and returns that. It has the lowest precedence of any C operator, which means assignment binds tighter: y = 1, 2, 3 is parsed as (y = 1), 2, 3 — y gets 1, and 2 and 3 are evaluated and discarded. The comma is most legitimately used in for-loop headers to initialise or update multiple variables in lockstep. Its use elsewhere is mostly a maintenance hazard — it appears in macros and obfuscated code, and the precedence surprise catches people who are not expecting it.

Another use: if (err && (printf("error: %s ", err), 0)) — this prints the error and then evaluates to 0, so the if body is skipped. But it's ugly. Most style guides forbid comma operator outside of for-loops.

ternary_comma_operators.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <stdio.h>

// io.thecodeforge — ternary and comma operator demonstration

int main() {
    int temperature = 30;

    // Ternary: condition ? value_if_true : value_if_false
    // Returns a value — usable anywhere an expression is valid
    const char *weather = (temperature > 25) ? "Hot" : "Cold";
    printf("Weather: %s\n", weather);

    // Ternary inside printf — concise and readable when simple
    int score = 72;
    printf("Grade: %s\n", (score >= 90) ? "A" :
                           (score >= 75) ? "B" :
                           (score >= 60) ? "C" : "F");
    // Note: the above chaining is borderline — if it grows one more level,
    // switch to if-else. Readability wins over compactness.

    // Comma operator: evaluates left, discards it, returns right
    // Parentheses are required to force comma-operator semantics here
    int x = (1, 2, 3);  // x = 3 (rightmost value)
    printf("x = %d  (comma operator returns rightmost)\n", x);

    // Precedence trap: assignment binds TIGHTER than comma
    // This is: (y = 1), then 2 is evaluated and discarded, then 3 is discarded
    // y ends up as 1, NOT 3
    int y;
    y = 1, 2, 3;  // valid C: (y = 1), 2, 3
    printf("y = %d  (assignment beat comma; y got 1, not 3)\n", y);

    // Comma in for-loop: the legitimate and idiomatic use
    printf("Converging loop (i goes up, j goes down):\n");
    for (int i = 0, j = 4; i <= j; i++, j--) {
        printf("  i=%d, j=%d\n", i, j);
    }

    return 0;
}
Output
Weather: Hot
Grade: C
x = 3 (comma operator returns rightmost)
y = 1 (assignment beat comma; y got 1, not 3)
Converging loop (i goes up, j goes down):
i=0, j=4
i=1, j=3
i=2, j=2
Prefer if-else Over Ternary for Anything Non-Trivial
The ternary operator earns its place for simple single-line conditional assignments: const char *label = (count == 1) ? "item" : "items"; is cleaner than a four-line if-else. But the moment either branch contains a function call with side effects, or you find yourself nesting ternaries, stop. Switch to if-else. The compiler produces identical output, the next developer produces fewer mistakes, and the on-call engineer debugging at 2am is grateful.
Production Insight
A developer used the comma operator inside a macro without parentheses: #define RESET(a, b) a = 0, b = 0. At the call site, if (condition) RESET(x, y); expanded to if (condition) x = 0, y = 0; — which, because of precedence, only put x = 0 inside the if body. The y = 0 always executed regardless of the condition. The fix: always wrap macro bodies in a do-while(0) block and parenthesise every argument use. Never use the bare comma operator in a macro that could appear in a branch context.
Key Takeaway
Ternary is a return-value if-else — use it for simple assignments, avoid nesting.
The comma operator evaluates left and returns right, has the lowest precedence in C, and belongs in for-loop headers.
Everywhere else it appears, treat it with suspicion and add parentheses.

sizeof Is a Compiler-Constant, Not a Function — Stop Writing sizeof(int)

Every junior thinks sizeof is a function. It isn't. It's a unary operator evaluated entirely at compile time. No runtime overhead. Zero. The parentheses are only needed when the operand is a type name, not a variable. When you write sizeof buffer instead of sizeof(buffer), you'll still get the same bytes. But when you refactor from an array to a pointer, sizeof(buffer) silently gives you 8 bytes (the pointer) instead of the array's full allocation. That's a real bug I've caught in production code that corrupted heap buffers for months. The fix: always use sizeof on the referenced variable, not the type. And if you must use a type, consider sizeof *pointer first — that trick survives pointer declaration changes. The sizeof operator exists because the hardware needs to know how much stack to allocate or how many bytes to memcpy. It's not magic. It's a constant folded into your binary at compile time.

SizeofTrap.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// io.thecodeforge — c-cpp tutorial

#include <stdio.h>
#include <stddef.h>

void process_buffer(char data[]) {
    // data is a pointer, not an array!
    printf("Array? %zu bytes\n", sizeof(data));  // prints 8 on 64-bit
}

int main() {
    char telemetry[1024];
    printf("Buffer size: %zu bytes\n", sizeof telemetry);  // 1024

    // Common rookie mistake:
    size_t count = sizeof(telemetry) / sizeof(telemetry[0]);  // correct
    // Instead of: sizeof telemetry / sizeof(char);  // fragile
    
    process_buffer(telemetry);  // hidden bug: sizeof returns 8, not 1024
    return 0;
}
Output
Buffer size: 1024 bytes
Array? 8 bytes
Never Do This:
Using sizeof on a function parameter declared as an array — it's already decayed to a pointer. Always pass array length as a separate parameter.
Key Takeaway
sizeof is a compile-time operator that returns bytes, not an element count. Use sizeof(variable)/sizeof(variable[0]) for arrays, never sizeof on a parameter.

Address-of and Dereference — Your Two Lowest-Level Weapons

The & operator gives you the memory address of a variable. The operator fetches the value at that address. That's it. No magic. But the WHY matters more than the HOW. You need & when a function wants to modify your variable directly — that's pass-by-reference in C. You need when you're walking a linked list or reading hardware registers mapped to a memory address. Production rule: use & to hand off ownership to a callee that mutates state. Use * to read what a pointer points to, but always check for NULL first. I've seen segfaults in embedded systems because someone dereferenced a NULL pointer they got from an uninitialized device driver. Always guard dereferences with a null check. And never return a pointer to a stack-local variable — that address is invalid as soon as the function returns. That's why you need malloc or static buffers. The operators themselves are dirt simple. The bugs come from ignoring lifespan and null state.

PointerBasics.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// io.thecodeforge — c-cpp tutorial

#include <stdio.h>

void set_sensor_offset(int* offset) {
    *offset = 42;  // dereference to write
}

int main() {
    int reading = 0;
    int* ptr = &reading;

    set_sensor_offset(ptr);  // pass address
    printf("Reading: %d\n", reading);  // 42

    // Dereference with null check (production rule)
    if (ptr != NULL) {
        int value = *ptr;
        printf("Dereferenced: %d\n", value);
    }
    return 0;
}
Output
Reading: 42
Dereferenced: 42
Senior Shortcut:
Use & to create a pointer only when the variable's lifetime exceeds the pointer's usage. Never let a local pointer escape the function scope.
Key Takeaway
& gives an address, * gives the value at that address. Always null-check before dereferencing; never return a pointer to a local variable.

Cast Operators — The Only Thing Worse Than a Cast Is No Cast at All

Casts are explicit type conversions. You're telling the compiler 'I know what I'm doing, shut up and reinterpret these bytes.' That's terrifying and essential. In production code, every implicit conversion is a potential bug. An int to a short truncates silently. A float to an int drops precision without warning. Cast operators make those conversions explicit so your code reviewer can see the risk. WHY you cast: (1) converting between arithmetic types when you need to control truncation, (2) casting void* from malloc to the target pointer (in C++ you must, in C you don't need to but it's harmless), (3) casting between struct pointers in tagged unions or protocol parsing. HOW to cast: type_to_castexpression. That's it. But production rule: prefer explicit casts over implicit conversions. If you see a compiler warning about truncation, put a cast there so everyone knows you meant it. Never cast away const unless you absolutely have to — and even then, consider redesigning. Casts mask bugs. Use them like a scalpel, not a chainsaw.

ExplicitCast.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// io.thecodeforge — c-cpp tutorial

#include <stdio.h>

int main() {
    // Implicit truncation — bug waiting to happen
    int large = 100000;
    short truncated = (short)large;  // explicit: I know this loses data
    printf("Truncated: %d\n", truncated);

    // Float to int with explicit cast
    float sensor_voltage = 3.14159f;
    int integer_part = (int)sensor_voltage;  // drops .14159
    printf("Integer part: %d\n", integer_part);

    // Pointer cast for protocol parsing
    unsigned char raw_buffer[4] = {0x01, 0x02, 0x03, 0x04};
    unsigned int* packet = (unsigned int*)raw_buffer;  // reinterpret bytes
    printf("As int: 0x%08x\n", *packet);  // platform-dependent endianness

    return 0;
}
Output
Truncated: -31072
Integer part: 3
As int: 0x04030201
Production Trap:
Pointer casts break strict aliasing rules in C and C++. Casting between unrelated pointer types (e.g., int to float) is undefined behavior. Use memcpy or unions for type punning.
Key Takeaway
Cast explicitly to signal intent and suppress warnings, but never use casts to silence a legitimate compiler error — fix the type mismatch instead.
● Production incidentPOST-MORTEMseverity: high

Integer Division Truncation in a Banking Interest Calculation

Symptom
Monthly interest payments were consistently below expected across all accounts — not by a random amount, but by a suspiciously consistent fraction. No compile errors, no runtime crashes, no assertions fired. The reconciliation team flagged the discrepancy after three months of silent underpayment.
Assumption
The developer wrote int avg_balance = total / days; believing that dividing two integers yields a fractional result when the quotient is not whole. They had tested with values like 100 / 7, seen 14 in the output, and assumed the display was just truncating for formatting. The variable type being int did not register as the problem.
Root cause
Integer division in C truncates toward zero — 17 / 5 gives 3, not 3.4. The fractional part is silently discarded. No warning is issued, no runtime signal fires. The code stored the truncated integer into an int variable, losing all decimal precision before the interest rate was ever applied. Across 12,000 accounts processed nightly, the cumulative loss was significant and systematic.
Fix
Changed the calculation to use double and cast at least one operand explicitly: double avg_balance = (double)total / days;. Updated the variable type from int to double to preserve the fractional portion through the entire calculation chain. Added a unit test asserting that dividing 17 by 5 yields a value greater than 3.3 — a test that would have caught this on day one.
Key lesson
  • When performing division that needs a fractional result, always ensure at least one operand is float or double before the division occurs — not after.
  • Cast explicitly rather than relying on implicit conversion. (double)total / days makes the intent clear to every developer who reads the code after you.
  • In financial code, never use integer division for rates, averages, or any proportional calculation. Use double or a fixed-point library.
  • Write a unit test for any arithmetic path that involves division. A test asserting the result is within 0.01 of the expected value would have caught this immediately.
Production debug guideSymptoms, actions and quick fixes for the most common operator pitfalls in production C code5 entries
Symptom · 01
Condition always evaluates to true, variable unexpectedly modified inside an if or while
Fix
Look for = inside an if, while, or for condition. Compile with -Wall — GCC emits 'suggest parentheses around assignment used as truth value'. Replace = with ==. If the assignment is intentional (reading from a socket in a while loop, for example), wrap it in an extra set of parentheses to silence the warning and signal intent to reviewers: while ((c = getchar()) != EOF).
Symptom · 02
Division result is an integer when a decimal was expected
Fix
Check that at least one operand is float or double at the point of division, not just the variable receiving the result. The truncation happens during the operation, before the assignment. Cast explicitly: (double)numerator / denominator. Also verify the receiving variable is not int — storing a double result into int truncates again silently.
Symptom · 03
Array index off-by-one or wrong value in an expression containing ++ or --
Fix
Identify prefix versus postfix usage. arr[i++] uses the current value of i as the index, then increments. arr[++i] increments first, then uses the new value as the index. More importantly: modifying i twice in the same expression (e.g., arr[i++] + arr[i++]) is undefined behaviour — the compiler is free to evaluate operands in any order. Separate every increment onto its own line to eliminate ambiguity entirely.
Symptom · 04
Bitwise operation produces unexpected negative number or inconsistent result across platforms
Fix
Check if the operand is a signed integer. Right shift on a signed negative integer is implementation-defined — some platforms perform arithmetic shift (preserving the sign bit), others perform logical shift (filling with zeros). Left shift on a signed integer that overflows is undefined behaviour. Always use unsigned int or uint32_t from stdint.h for all bitwise operations. Never apply ~, <<, or >> to signed types unless you have fully documented the platform-specific behaviour.
Symptom · 05
Negative modulus result when a non-negative remainder was expected
Fix
In C99 and later, the result of % has the same sign as the dividend. So -7 % 3 gives -1, not 2. If you need a guaranteed non-negative remainder for wrapping (circular buffers, hash indices, angle normalisation), use the idiom: ((n % m) + m) % m. This always produces a result in the range [0, m-1] regardless of the sign of n.
★ Quick Debug: OperatorsInstant diagnosis for operator-related bugs in C code. No theory — just symptoms and commands.
if (x = 10) always true and x is being overwritten
Immediate action
Add -Wall to your build — GCC will emit 'suggest parentheses around assignment used as truth value'. Fix the logic before worrying about the warning.
Commands
gcc -Wall -Wextra -Werror -o prog prog.c
grep -n 'if\s*(\s*[a-zA-Z_][a-zA-Z0-9_]*\s*=[^=]' prog.c
Fix now
Change = to == in the condition. If the assignment is intentional, write while ((x = get_value()) != SENTINEL) to make intent explicit.
7 / 2 gives 3 instead of 3.5+
Immediate action
Add a printf to confirm types: printf("%d\n", 7/2); versus printf("%.1f\n", (double)7/2); — see the difference immediately
Commands
gcc -Wall -Wextra -o typecheck typecheck.c && ./typecheck
grep -n 'int.*/' prog.c | grep -v '//'
Fix now
Cast the numerator before division: (double)total / count. Change the receiving variable from int to double.
Loop counter off-by-one with ++ inside a complex expression+
Immediate action
Extract every increment to its own statement. Never write arr[i++] + arr[i] — this is undefined behaviour regardless of what your compiler happens to produce today.
Commands
cppcheck --enable=all --std=c11 file.c 2>&1 | grep -i 'increment\|undefined'
grep -n '[^+]++\|--[^-]' file.c | grep -v 'for\s*(\|^\s*//'
Fix now
Write the increment on its own line: i++; then use i on the next line. Zero ambiguity, zero undefined behaviour risk.
Negative modulus gives -1 when 2 was expected (e.g. -7 % 3)+
Immediate action
Confirm C99 truncation-toward-zero behaviour: printf("%d\n", -7 % 3); will print -1 on every conforming C99/C11 compiler
Commands
echo '#include <stdio.h>\nint main(){printf("%d\n",-7%%3);return 0;}' | gcc -x c -std=c11 - -o /tmp/modtest && /tmp/modtest
grep -n '%' prog.c | grep -v '//' | grep -v '%%'
Fix now
Use ((n % m) + m) % m for guaranteed non-negative remainder. Add a comment explaining why — future readers will thank you.
Operator Category Comparison
CategoryOperatorsPurposeCommon Trap
Arithmetic+ - * / %Basic math operationsInteger division truncation; modulus sign follows dividend
Relational== != > < >= <=Value comparison (returns 1 or 0)Using = instead of ==
Logical&& || !Boolean combination with short-circuitShort-circuit may skip side effects
Bitwise& | ^ ~ << >>Direct bit manipulationSigned operand shift/complement is undefined or implementation-defined
Assignment= += -= *= /= %= &= |= ^= <<= >>=Store value with optional operationChained assignment precedence
Increment/Decrement++ -- (prefix/postfix)Add or subtract 1Undefined behaviour when used more than once in an expression
Ternary?:Inline conditional returnReduced readability when nested
Comma,Evaluate left, discard, return rightVery low precedence; unexpected behaviour without parentheses

Key takeaways

1
Integer division truncates toward zero
always cast to double before dividing if you need a fractional result.
2
The modulus operator % gives the remainder with the sign of the dividend
use ((n % m) + m) % m for non-negative results.
3
Prefix increment ++i returns the new value; postfix i++ returns the old value. Never modify a variable more than once in the same expression.
4
Bitwise operators require unsigned operands for portable, defined behaviour.
5
Always compile with -Wall -Wextra to catch = vs == mistakes and precedence issues.
6
When in doubt about operator precedence, add parentheses
they cost nothing and make intent clear.

Common mistakes to avoid

6 patterns
×

Using = instead of == in conditions

Symptom
Condition always true and variable gets overwritten. No compiler error unless -Wall is enabled.
Fix
Always compile with -Wall. Change = to ==. For intentional assignments in conditions, wrap in extra parentheses: while ((c = getchar()) != EOF).
×

Assuming integer division returns a decimal

Symptom
Division result is truncated integer, leading to silent precision loss.
Fix
Cast at least one operand to double before division: (double)a / b. Use double for the receiving variable.
×

Forgetting that modulus sign follows dividend

Symptom
Negative modulus yields unexpected negative remainder.
Fix
Use ((n % m) + m) % m for a guaranteed non-negative result.
×

Using signed integers for bitwise shifts and complement

Symptom
Right shift produces implementation-defined results; left shift overflow is undefined; complement on signed gives unexpected negative.
Fix
Always use unsigned types like uint32_t for bitwise operations.
×

Modifying a variable twice in the same expression (e.g. arr[i++] + arr[i++])

Symptom
Undefined behaviour: results vary by compiler, optimisation level, and platform.
Fix
Separate increments onto their own lines. Never use ++ or -- more than once per expression.
×

Ignoring operator precedence (e.g. x & MASK == 0)

Symptom
Expression parsed as x & (MASK == 0) instead of (x & MASK) == 0.
Fix
Add parentheses explicitly: (x & MASK) == 0. Use -Wparentheses to catch these.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the difference between prefix and postfix increment in C?
Q02JUNIOR
Explain short-circuit evaluation in C. When does it matter?
Q03SENIOR
What does the following code print? `int i = 0; i = i++ + ++i; printf("%...
Q04SENIOR
Why must bitwise operations use unsigned types?
Q05SENIOR
Design a macro that safely swaps two integers without a temporary variab...
Q01 of 05JUNIOR

What is the difference between prefix and postfix increment in C?

ANSWER
Prefix (++i) increments the variable first, then returns the new value. Postfix (i++) returns the current value first, then increments. In a standalone statement (like in a for loop's increment expression), both are identical. The difference matters when the result is used in a larger expression. Also, modifying a variable more than once between sequence points (e.g., i++ + i++) is undefined behaviour.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Why does 7 / 2 give 3 instead of 3.5 in C?
02
What is the difference between = and == in C?
03
Can I use ++ on a variable more than once in a single expression?
04
Why does -7 % 3 give -1?
05
What is short-circuit evaluation?
N
Naren Founder & Principal Engineer

20+ years shipping performance-critical C and C++ systems. Notes here come from systems that actually shipped.

Follow
Verified
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
🔥

That's C Basics. Mark it forged?

21 min read · try the examples if you haven't

Previous
Variables and Data Types in C
3 / 17 · C Basics
Next
Control Flow in C