C Operators — How Integer Division Cost a Bank
Integer division in C truncates toward zero — a banking app silently underpaid 12,000 accounts.
- 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
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.
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.
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.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.
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.
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.
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.#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.Integer Division Truncation in a Banking Interest Calculation
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.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.- 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 / daysmakes 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.
getchar()) != EOF).get_value()) != SENTINEL) to make intent explicit.Key takeaways
Common mistakes to avoid
5 patternsUsing = instead of == in conditions
getchar()) != EOF).Assuming integer division gives a decimal result
Relying on negative modulus to give a positive remainder
Confusing prefix and postfix ++ inside expressions, or using them twice on the same variable in one expression
Applying bitwise operators to signed integers
Interview Questions on This Topic
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?
Frequently Asked Questions
That's C Basics. Mark it forged?
7 min read · try the examples if you haven't