C Operators: Arithmetic, Logic, Bits, and Precedence

Use every category of C operator correctly — arithmetic, relational, equality, logical, bitwise, shift, assignment and compound assignment, increment/decrement, the ternary conditional, sizeof, casts, and the comma operator — and reason accurately about operator precedence and short-circuit evaluation.

Every expression in C is built out of operators, and Objective-C inherits every single one of them unchanged — an NSInteger is still added with +, an NSUInteger flag is still tested with &, and a BOOL condition is still short-circuited with &&. Before Objective-C layers on message sends and object references, you need to be fluent in C's operator set: what each operator does, what type its result has, and — critically — in what order a compiler evaluates a mixed expression. Precedence bugs are some of the most common silent-wrong-answer bugs in C-family code, so this lesson treats precedence and short-circuiting as first-class topics, not an afterthought.

Arithmetic, Relational, and Equality Operators

C's arithmetic operators are + (addition), - (subtraction), * (multiplication), / (division), and % (modulo, remainder after division). All five work on integers and floating-point types, except %, which is defined only for integer operands — 7 % 3 is 1, but 7.0 % 3.0 is a compile error (use fmod() from <math.h> for floating-point remainders instead). Integer division truncates toward zero: 7 / 2 is 3, not 3.5, and -7 / 2 is -3, not -4 — this truncation-not-rounding behavior is one of the single most common sources of subtle bugs in C code, especially when a programmer expects a fractional result but both operands happen to be int.

The relational operators <, >, <=, and >= compare two numeric values and produce an int result: 1 for true, 0 for false (C has no dedicated boolean type — this matters again in the logical operators below). The equality operators == and != test for equality and inequality respectively, and also produce 1 or 0. A classic and infamous C bug is writing if (x = 5) when if (x == 5) was intended — the single = is a valid assignment expression that evaluates to 5 (truthy), silently overwriting x and always entering the branch; most compilers warn about this with a flag like -Wparentheses, and it's worth always enabling warnings for exactly this reason.

Because C has no separate boolean type, any nonzero value (of any numeric type) is treated as true in a condition, and exactly 0 is treated as false. This is why comparisons like x == 0 and simply !x are interchangeable in a condition, and why a stray assignment inside an if compiles without complaint — the language has no way to distinguish 'a truth value' from 'any integer' at the type level.

• Arithmetic: + - * / % (modulo is integer-only). • Integer division truncates toward zero: 7/2 == 3, -7/2 == -3. • Relational: < > <= >=, producing int 1 (true) or 0 (false). • Equality: == and != — do not confuse == with the assignment operator =. • No boolean type in C: 0 is false, every nonzero value is true.

Logical Operators and Short-Circuit Evaluation

The logical operators are && (logical AND), || (logical OR), and ! (logical NOT). Like the relational operators, they always produce 1 or 0, regardless of what their operands' actual values were. && and || are short-circuiting: in a && b, if a evaluates to false (0), C never evaluates b at all, because the overall result is already determined to be false; symmetrically, in a || b, if a is true (nonzero), b is never evaluated because the result is already true.

Short-circuiting is not just a performance optimization — it's a guarantee the language makes, and real code relies on it for correctness, not just speed. The classic pattern is a null/bounds check guarding a dereference: if (ptr != NULL && ptr->value > 0) is safe precisely because ptr->value is never evaluated when ptr is NULL — if && evaluated both sides unconditionally, this would crash on a null pointer dereference every time ptr was NULL. The same pattern appears constantly in Objective-C: if (array != nil && array.count > 0).

! simply inverts truthiness: !0 is 1, and !anything-nonzero is 0. A common style question is whether to write if (!found) or if (found == 0) — both are correct and equivalent, and most C and Objective-C style guides prefer the terser !found for a variable that is conceptually boolean-like, reserving explicit == 0 comparisons for genuinely numeric values being checked against zero.

• && (AND), || (OR), ! (NOT) — all produce 1 or 0. • && short-circuits: if the left side is false, the right side is never evaluated. • || short-circuits: if the left side is true, the right side is never evaluated. • Short-circuiting enables safe guard patterns: ptr != NULL && ptr->field. • ! inverts truthiness: !0 is 1, !(nonzero) is 0.

Bitwise and Shift Operators

C's bitwise operators act directly on the binary representation of integer operands: & (bitwise AND), | (bitwise OR), ^ (bitwise XOR), and ~ (bitwise NOT / one's complement, a unary operator that flips every bit). These are entirely distinct from the logical operators &&, ||, and ! — a single & combines bits position by position across the whole value, while && treats each operand as one overall true/false. Confusing & for && (or | for ||) is a very common beginner mistake that usually still compiles, because both produce integer results, but produces a completely different number instead of a boolean-like 1/0.

Bitwise operators are the mechanism behind C-style bit flags, where a set of independent boolean options is packed into the bits of a single integer: each option gets its own bit (typically written as 1 << n), | combines flags together, & tests whether a flag is set, and ~ combined with & clears a flag. This exact pattern is precisely how Objective-C's NSOptions-style bitmask enums (like UIViewAutoresizing or NSJSONReadingOptions) work under the hood — they are C integers with named bit positions, combined with the same | and tested with the same & you'd use in plain C.

The shift operators << (left shift) and >> (right shift) move an integer's bits left or right by a given number of positions, filling vacated bits with 0 on the left. x << n is equivalent to multiplying x by 2^n (for values that don't overflow), and x >> n is equivalent to dividing by 2^n for unsigned or non-negative values (right-shifting a negative signed integer is implementation-defined in classic C, though most modern compilers perform an arithmetic shift that preserves the sign bit). Left shift is the idiomatic way to define a bit-flag constant: #define FLAG_READ (1 << 0), #define FLAG_WRITE (1 << 1), #define FLAG_EXEC (1 << 2).

• Bitwise: & (AND), | (OR), ^ (XOR), ~ (NOT/complement) — operate on individual bits. • Do not confuse & with && or | with || — bitwise vs logical are entirely different operations. • Shift: << (left shift, ~multiply by 2^n), >> (right shift, ~divide by 2^n). • Bit flags: 1 << n defines a flag bit; | combines flags; & tests a flag; ~ + & clears one. • This exact bitmask pattern underlies Objective-C's NSOptions-style bitmask enums.

Assignment, Compound Assignment, and Increment/Decrement

Plain assignment = stores a value into a variable and, importantly, is itself an expression that evaluates to the assigned value — which is what enables chained assignment like a = b = c = 0; (evaluated right-to-left: c gets 0, that 0 is assigned to b, that 0 is assigned to a). Compound assignment operators combine an arithmetic or bitwise operation with assignment in one step: +=, -=, *=, /=, %=, &=, |=, ^=, <<=, and >>=. x += 5; is shorthand for x = x + 5;, but is not merely cosmetic — for a complex left-hand side like array[computeIndex()] += 1;, the compound form evaluates computeIndex() only once, whereas writing it out longhand would (incorrectly, if the call has side effects) evaluate it twice.

The increment and decrement operators ++ and -- add or subtract exactly 1 from a variable, and each comes in a prefix form (++x) and a postfix form (x++) with different expression values: ++x increments x first and the expression evaluates to the new value, while x++ evaluates to the old value and increments x afterward as a side effect. Both forms have the same net effect on x itself when used as a standalone statement (x++; and ++x; do exactly the same thing to x), but they differ when the expression's value is used, e.g. int y = x++; vs int y = ++x; leave y with different values.

Using ++ or -- on the same variable more than once within a single expression without an intervening sequence point (for example x = x++ + ++x;) is undefined behavior in C — the compiler is free to evaluate it in any order or produce any result, and different compilers genuinely disagree. The practical rule is simple: never modify the same variable twice in one expression via ++/-- or by mixing them with a plain assignment to that variable; keep each increment/decrement as its own statement or clearly isolated sub-expression.

• = is itself an expression; chained assignment a = b = c = 0 works right-to-left. • Compound assignment: += -= *= /= %= &= |= ^= <<= >>= — also evaluates a complex left-hand side only once. • Prefix ++x / --x: modifies then yields the new value. • Postfix x++ / x--: yields the old value, then modifies. • Never use ++/-- on the same variable twice in one expression — undefined behavior.

Ternary, sizeof, Cast, and Comma Operators

The conditional (ternary) operator condition ? valueIfTrue : valueIfFalse is C's only operator that takes three operands, and it is an expression, not a statement — it produces a value and can be used anywhere a value is expected, such as inside a function call or another expression: int max = (a > b) ? a : b;. It should generally be reserved for short, simple value selection; nesting ternaries deeply (a ? b : c ? d : e) is legal but reads poorly and is better expressed with an if/else if chain once it grows past one condition.

sizeof is a compile-time (for most types) unary operator that returns the size, in bytes, of a type or of the type of an expression, as a value of type size_t: sizeof(int) is typically 4, sizeof(double) is typically 8, and sizeof(someVariable) reports the size of whatever type someVariable has without evaluating it at runtime. sizeof is essential for correct, portable memory allocation — malloc(n * sizeof(int)) allocates space for n ints regardless of the platform's actual int width, which is far more robust than hardcoding malloc(n * 4).

A cast, written (type)expression, explicitly converts a value from one type to another, overriding C's normal implicit conversion rules where necessary — (double)total / count forces floating-point division even when both total and count are integers, because casting one operand to double promotes the entire division to floating-point arithmetic before it happens. The comma operator , (distinct from commas separating function arguments or declarations) evaluates its left operand, discards the result, evaluates its right operand, and yields that right operand's value — most commonly seen in the update clause of a for loop to advance two variables at once: for (int i = 0, j = 10; i < j; i++, j--).

• Ternary: condition ? a : b — an expression that yields a value; avoid deep nesting. • sizeof(type or expr) — compile-time size in bytes, type size_t; use it for portable allocation math. • Cast (type)expr — explicit conversion, e.g. forcing floating-point division: (double)a / b. • Comma operator: left evaluated and discarded, right evaluated and returned; common in for-loop update clauses.

Operator Precedence and Evaluation Order

When an expression mixes several operators, C resolves ambiguity using a fixed precedence table: operators with higher precedence bind more tightly (are grouped first) than operators with lower precedence. From highest to lowest among the operators covered in this lesson: () (grouping) and postfix ++/-- bind tightest; then unary !, ~, prefix ++/--, and casts; then *, /, %; then binary +, -; then <<, >>; then relational <,<=,>,>=; then equality ==,!=; then &; then ^; then |; then &&; then ||; then the ternary ?:; then assignment and compound assignment; and finally the comma operator, which binds loosest of all.

This is exactly why a + b * c computes b * c first and adds a to the result, not the other way around — * has higher precedence than +. It's also why bitwise &/| sitting below the relational and equality operators in precedence is a genuinely dangerous trap: if (flags & MASK == 0) does NOT test 'is the masked bit zero' the way it visually appears to — because == binds tighter than &, this parses as flags & (MASK == 0), comparing MASK to 0 first (almost always false, evaluating to 0) and then AND-ing flags with that 0 or 1. The fix is always to parenthesize explicitly: if ((flags & MASK) == 0).

Given the size of the precedence table, the professional habit — not a beginner's crutch — is to use explicit parentheses in any expression mixing more than one or two operator categories, even when you're confident of the default precedence. Parentheses cost nothing at runtime (they're resolved at compile time) and they remove any need for a reader, or for you six months later, to recall the table from memory. The one precedence rule worth memorizing cold is that *, /, % bind tighter than +, - — that one is universal across nearly every C-family language and appears constantly.

• Higher precedence binds first: (), postfix ++/-- > unary/cast > * / % > + - > shifts > relational > == != > & > ^ > | > && > || > ?: > assignment > , . • *, /, % bind tighter than + and - — the one rule to always know cold. • Bitwise & | sit BELOW relational/equality in precedence — flags & MASK == 0 is a classic trap; always write (flags & MASK) == 0. • && binds tighter than || — a || b && c means a || (b && c). • When in doubt, or when mixing operator categories, parenthesize explicitly — it costs nothing and removes ambiguity for every future reader.

Operator Precedence in Action

r1 demonstrates * binding before +; r2 shows parentheses forcing a different grouping. The commented-out if line is the classic bitwise-vs-equality precedence trap. The guard expression relies on && short-circuiting to avoid a division by zero when x == 0.

int a = 2, b = 3, c = 4;

int r1 = a + b * c;        // 14, not 20: * binds tighter than +
int r2 = (a + b) * c;      // 20: parentheses override default precedence

int flags = 0b0110;
int MASK  = 0b0010;
if ((flags & MASK) == 0) { /* correctly tests the bit */ }
// if (flags & MASK == 0)  // BUG: parses as flags & (MASK == 0)

int x = 5;
int guard = (x != 0) && (100 / x > 10); // short-circuits safely if x == 0

Code Examples

Integer division truncates toward zero, so 17 / 5 is 3, not 3.4 — combining it with % to get the remainder is the standard idiom for splitting a count into groups plus leftovers. Expected result: 3 full groups, 2 left over

Because && short-circuits, *ptr is never dereferenced when ptr is NULL — the second operand is only evaluated once the first has already proven ptr is safe to read. This exact guard style reappears constantly in Objective-C as if (obj != nil && obj.someProperty > 0). Expected result: Positive: 42 Skipped (null or non-positive)

| combines flags into one integer, & tests whether a specific bit is set, and &= ~FLAG clears a bit — this is precisely the mechanism Objective-C's NSOptions-style bitmask constants use under the hood. Expected result: Write is allowed Exec is NOT allowed After clearing write: 1

#include <stdio.h>

int main(void) {
    int total = 17;
    int perGroup = 5;

    int groups = total / perGroup;     // 3 (truncated, not 3.4)
    int remainder = total % perGroup;  // 2

    printf("%d full groups, %d left over\n", groups, remainder);
    return 0;
}
#include <stdio.h>
#include <stddef.h>

void printIfPositive(int *ptr) {
    if (ptr != NULL && *ptr > 0) {
        printf("Positive: %d\n", *ptr);
    } else {
        printf("Skipped (null or non-positive)\n");
    }
}

int main(void) {
    int value = 42;
    printIfPositive(&value);
    printIfPositive(NULL);
    return 0;
}
#include <stdio.h>

#define FLAG_READ  (1 << 0)  // 0b001
#define FLAG_WRITE (1 << 1)  // 0b010
#define FLAG_EXEC  (1 << 2)  // 0b100

int main(void) {
    int permissions = FLAG_READ | FLAG_WRITE;  // combine flags

    if (permissions & FLAG_WRITE) {
        printf("Write is allowed\n");
    }
    if (!(permissions & FLAG_EXEC)) {
        printf("Exec is NOT allowed\n");
    }

    permissions &= ~FLAG_WRITE;  // clear the write flag
    printf("After clearing write: %d\n", permissions);
    return 0;
}

Precedence Ladder (High to Low, Subset)

Notice & sits below == and != — this is the source of the classic flags & MASK == 0 bug; always parenthesize bitwise tests.

() postfix++/--
  !  ~  prefix++/--  (cast)
  *  /  %
  +  -
  <<  >>
  <  <=  >  >=
  ==  !=
  &
  ^
  |
  &&
  ||
  ?:
  =  +=  -=  ...
  ,  (comma, loosest)

Important Rules

Never write if (x = value) when you mean if (x == value) — enable compiler warnings so this is caught.

Use % only with integer operands; use fmod() for floating-point remainders.

Rely on && / || short-circuiting for guard patterns, but never rely on evaluation order beyond that guarantee.

Always parenthesize a bitwise test combined with a comparison: (flags & MASK) == 0, never flags & MASK == 0.

Never modify the same variable more than once via ++/-- (or mixed with assignment) within a single expression.

Bitmasks in Objective-C Frameworks

Cocoa APIs like UIViewAutoresizing, NSJSONReadingOptions, and NSStringCompareOptions are plain C integers with named bit positions defined the same way as FLAG_READ above; combining options with | and testing them with & is standard practice, e.g. NSJSONReadingMutableContainers | NSJSONReadingAllowFragments.

if (userInput = 1) { /* always true, and userInput is now 1 */ } if (userInput == 1) { /* correct equality test */ }

A single = is assignment, not comparison; it silently overwrites userInput with 1 and the condition is always truthy, since the expression evaluates to the assigned value 1.

if (flags & READY == 0) { /* thinks this tests the READY bit */ } if ((flags & READY) == 0) { /* correctly tests the READY bit */ }

== has higher precedence than &, so the unparenthesized form parses as flags & (READY == 0), comparing READY to 0 first instead of masking flags — parentheses are required to get the intended grouping.

int half = 7 / 2; // programmer expected 3.5 double half = 7.0 / 2.0; // or (double)7 / 2

Integer division truncates; to get a fractional result at least one operand must be a floating-point type, either via a literal like 7.0 or an explicit cast.

What does the expression 7 / 2 evaluate to in C when both operands are int? A. 3.5 B. 4 C. 3 D. It is a compile error

C. 3 — Integer division truncates toward zero, so 7 / 2 produces the int 3, discarding the fractional part entirely rather than rounding.

Why does if (ptr != NULL && ptr->value > 0) never crash on a NULL pointer? A. C automatically checks pointers before every dereference B. && short-circuits: ptr->value is never evaluated once ptr != NULL is already false C. The compiler reorders the condition automatically D. NULL pointers are treated as 0 by ptr->value

B. && short-circuits: ptr->value is never evaluated once ptr != NULL is already false — && only evaluates its right operand if the left operand is true; when ptr is NULL, ptr != NULL is false, so ptr->value is never reached, avoiding a null dereference.

Given int flags = 6; int r = flags & 2 == 0;, what does r actually receive, and why? A. r gets the result of testing bit 2 of flags, because & is evaluated first B. r gets flags & (2 == 0), because == has higher precedence than &, so 2==0 (false/0) is computed first C. This is a syntax error in C D. r always equals flags regardless of the mask

B. r gets flags & (2 == 0), because == has higher precedence than &, so 2==0 (false/0) is computed first — == binds tighter than &, so the expression parses as flags & (2 == 0) → flags & 0 → 0, not the intended bit test; parentheses around (flags & 2) are required to fix this.

What is the key difference between ++x and x++ when their result is used in a larger expression? A. There is no difference; both always behave identically B. ++x increments then yields the new value; x++ yields the old value then increments C. ++x only works on floats; x++ only works on integers D. x++ increments twice; ++x increments once

B. ++x increments then yields the new value; x++ yields the old value then increments — Prefix ++x performs the increment first and the expression's value is the updated variable; postfix x++ evaluates to the variable's value before incrementing, with the increment applied as a side effect afterward.

Write a C function int hasBit(int flags, int mask) that returns 1 if every bit in mask is set within flags, and 0 otherwise, using only bitwise operators and correct parenthesization. Then write a short main() that defines three flag constants using left shift and tests one against a combined value.

The core test is (flags & mask) == mask — this checks all bits in mask are present, not just any single one.