Mid-level 7 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
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
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
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 is 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 will 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 will know the most common operator mistakes beginners make — so you can avoid them from day one.

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.

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.

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.

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.

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.

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.

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.

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.
● 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 CategoryPurposeReturnsCommon Use Case
Arithmetic (+, -, *, /, %)Perform math — integer division truncates, % sign follows dividendNumeric resultScore calculation, geometry, counters, circular buffer wrapping
Relational (==, !=, >, <, >=, <=)Compare two values1 (true) or 0 (false)if-conditions, loop bounds, sorting, range checks
Logical (&&, ||, !)Combine boolean conditions with short-circuit evaluation1 (true) or 0 (false)Multi-condition checks, null-guard before dereference, access control
Assignment (=, +=, -=, *=, /=, %=)Store values in variablesThe assigned valueUpdating running totals, applying deltas
Bitwise compound (&=, |=, ^=, <<=, >>=)Combine bitwise operation with assignmentThe new value after the operationSetting, clearing, toggling hardware or permission flags in place
Increment/Decrement (++, --)Add or subtract exactly 1 — prefix returns new value, postfix returns oldOld or new value depending on positionLoop counters, pointer traversal — never inside a larger expression
Bitwise (&, |, ^, ~, <<, >>)Manipulate individual bits — always use on unsigned typesNumeric result (new bit pattern)Permission flags, hardware registers, fast power-of-2 math
Ternary (? :)Conditional expression returning one of two valuesValue of the chosen branchSimple conditional assignment — not for complex or nested logic
Comma (,)Evaluate both operands, return the rightmost — lowest precedenceValue of the second (right) operandFor loop header with multiple variables — avoid elsewhere

Key takeaways

1
Integer division in C silently truncates toward zero
7 / 2 is 3, not 3.5. The sign of % follows the dividend — (-7) % 3 is -1, not 2. Both are silent with no compiler warning. Cast to double before division when you need decimals, and use ((n % m) + m) % m when you need a guaranteed non-negative remainder.
2
= assigns, == compares. Writing = in an if condition compiles cleanly, always evaluates to true (if the assigned value is non-zero), and silently overwrites the variable. This is one of C's most dangerous silent bugs. Always compile with -Wall -Wextra
GCC will warn about it.
3
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. Modifying the same variable twice in one expression
arr[i++] + arr[i++] — is undefined behaviour. Always separate increments onto their own line.
4
Bitwise operators work on individual bits and must always be applied to unsigned integer types. Right shift on a signed negative integer is implementation-defined. Left shift that overflows signed integers is undefined behaviour. Use uint8_t, uint32_t from stdint.h for portable, well-defined bitwise code.
5
Ternary is return-value if-else
use it for simple assignments, avoid nesting more than two levels deep. The comma operator evaluates left, returns right, and has the lowest precedence in C. It belongs in for-loop headers and almost nowhere else.
6
Always compile with -Wall -Wextra from day one, in CI and locally. The warnings are not noise
they catch the = versus ==, the suspicious shift counts, and the unused-variable bugs that cost hours to debug in production.

Common mistakes to avoid

5 patterns
×

Using = instead of == in conditions

Symptom
The condition always evaluates to true (or always to false if assigning zero), the variable is silently overwritten, and the else branch becomes permanently unreachable. No compile error is generated by default.
Fix
Use == for comparisons. Compile with gcc -Wall -Wextra on every build — GCC emits 'suggest parentheses around assignment used as truth value' for this exact pattern. If the assignment inside the condition is intentional (reading in a loop), wrap it in double parentheses to silence the warning and signal intent: while ((c = getchar()) != EOF).
×

Assuming integer division gives a decimal result

Symptom
7 / 2 produces 3 instead of 3.5. The fractional part is silently discarded. No warning, no error — just a subtly wrong answer that compounds over time in financial, scientific, or averaging calculations.
Fix
Cast at least one operand to double before the division occurs: (double)numerator / denominator. Casting after the fact does not help — the truncation has already happened. Also ensure the receiving variable is double, not int, or you will truncate again on assignment.
×

Relying on negative modulus to give a positive remainder

Symptom
(-7) % 3 gives -1 instead of the expected 2. This breaks circular buffer index wrapping, hash table slot calculations, and angle normalisation when inputs can be negative.
Fix
Use the safe idiom: ((n % m) + m) % m. This always returns a value in [0, m-1] regardless of the sign of n. Add a comment explaining why — the extra addition looks odd without context.
×

Confusing prefix and postfix ++ inside expressions, or using them twice on the same variable in one expression

Symptom
Off-by-one errors in array indexing. With arr[i++] the write uses the current index; with arr[++i] it uses the incremented index. With arr[i++] + arr[i++], the behaviour is undefined — the compiler may evaluate operands in any order.
Fix
Never use ++ or -- inside a larger expression. Place the increment on its own line before or after the statement that uses the variable. This is unambiguous, compiler-warning-free, and portable.
×

Applying bitwise operators to signed integers

Symptom
Right shift on a signed negative integer produces implementation-defined results. Left shift on a signed integer that overflows is undefined behaviour. Results differ between GCC and Clang, between optimisation levels, and between ARM and x86.
Fix
Use unsigned int, uint8_t, uint16_t, or uint32_t from stdint.h for all bitwise operations. This makes the behaviour fully defined and portable. Include stdint.h and be explicit about the width of the type.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
Explain the difference between prefix and postfix increment in C. Given ...
Q02JUNIOR
Explain short-circuit evaluation in && and ||. Why does the order of con...
Q03SENIOR
How do you check if a number is a power of 2 using bitwise operators in ...
Q04SENIOR
What is the difference between bitwise AND (&) and logical AND (&&)? Giv...
Q05SENIOR
Implement a swap of two integers using XOR without a temporary variable....
Q01 of 05JUNIOR

Explain the difference between prefix and postfix increment in C. Given int x = 5; int a = x++; int b = ++x; what are the values of a, b, and x?

ANSWER
Postfix (x++) returns the current value and then increments. Prefix (++x) increments first and then returns the new value. Step by step: x starts at 5. int a = x++ — a gets 5 (the current value), then x becomes 6. int b = ++x — x becomes 7 first, then b gets 7. Final values: a=5, b=7, x=7. Note: writing x++ + ++x in a single expression would be undefined behaviour because x is modified twice between sequence points — the safe approach is always to separate increments onto their own lines.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Which operator has the highest precedence in C?
02
What is the result of a bitwise shift beyond the size of the variable?
03
What is the modulus operator in C and when should I use it?
04
Why do bitwise operators exist if we already have logical operators like && and ||?
05
Is there a power operator like ** in Python or ^ in some languages for exponentiation in C?
06
What does the sizeof operator do and when does it return the wrong answer?
🔥

That's C Basics. Mark it forged?

7 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