How C computes and compares values

Real-World Analogy

Think of operators as the verbs of C — they are the actions that operate on values (operands). Arithmetic operators are like a calculator: add, subtract, multiply, divide. Relational operators are like a scale: is this greater than that? Logical operators are like decisions in plain English: "if it's raining AND cold, wear a coat." Bitwise operators act directly on the binary representation of numbers — flipping individual bits, like editing individual switches on a circuit board.

Most operators in C behave just like Python or Java. The main surprises come from two things: integer division truncates (7/2 is 3, not 3.5), and bitwise operators work on the raw binary bits of integers — something Python also supports but is more commonly used in C systems programming.

Integer division trap: In C, when both operands of / are integers, the result is also an integer and the fractional part is discarded — it does not round, it truncates towards zero. To get a floating-point result, cast one operand: (double)a / b.

Short-circuit evaluation: && and || are lazy. For A && B, if A is false, B is never evaluated. For A || B, if A is true, B is never evaluated. This is not just an optimisation — it's a language guarantee you can rely on (e.g., check for NULL before dereferencing).

sizeof: This operator returns the size in bytes of a type or variable. It returns a size_t value (an unsigned integer type), so always print it with %zu. sizeof(int) is typically 4 on modern systems. sizeof is evaluated at compile time for types and fixed-size arrays.

Python / Java (what you know)
# Integer division in Python 3
7 // 2   # → 3  (floor division)
7 / 2    # → 3.5 (true division)

# Bitwise — same symbols as C
5 & 3    # → 1
5 | 3    # → 7
5 ^ 3    # → 6
~5       # → -6
5 << 1   # → 10

# No ternary? Python has:
x = a if cond else b
C (what you are learning)
/* Integer division in C truncates */
7 / 2     /* → 3, NOT 3.5 */
(double)7 / 2  /* → 3.5 — cast first! */

/* Bitwise — identical symbols */
5 & 3    /* → 1 */
5 | 3    /* → 7 */
5 ^ 3    /* → 6 */
~5       /* → -6 */
5 << 1   /* → 10 */

/* Ternary in C: */
x = cond ? a : b;
Integer division truncates — it does NOT round

7 / 2 in C is 3, not 3 or 4. The decimal part is thrown away. -7 / 2 is -3 (truncation towards zero, not floor). If you need a float result, cast: (double)7 / 23.5. The cast must happen before the division — (double)(7 / 2) gives 3.0 because the integer division happens first.

Prefix vs postfix increment: ++x increments x first, then returns the new value. x++ returns the current value, then increments. In isolation (x++; as a statement) there is no difference — the distinction only matters when the expression's value is used: y = ++x vs y = x++.

Every operator category with examples

Arithmetic Operators

OperatorNameExampleResult (int)
+Addition5 + 38
-Subtraction5 - 32
*Multiplication5 * 315
/Division (truncates for int)7 / 23
%Modulo (remainder)7 % 21

Relational Operators — result is 1 (true) or 0 (false)

OperatorMeaningExampleResult
==Equal to5 == 51
!=Not equal to5 != 31
<Less than3 < 51
>Greater than5 > 31
<=Less than or equal3 <= 31
>=Greater than or equal5 >= 60

Logical & Bitwise Operators

OperatorCategoryMeaningExample
&&LogicalAND (short-circuits)a > 0 && b > 0
||LogicalOR (short-circuits)a > 0 || b > 0
!LogicalNOT!flag
&BitwiseAND (bit by bit)0b1010 & 0b1100 → 0b1000
|BitwiseOR (bit by bit)0b1010 | 0b0101 → 0b1111
^BitwiseXOR (bit by bit)0b1010 ^ 0b1100 → 0b0110
~BitwiseNOT (flip all bits)~0 → all 1s (= -1 for int)
<<BitwiseLeft shift (multiply by 2^n)1 << 3 → 8
>>BitwiseRight shift (divide by 2^n)8 >> 1 → 4

Operator Precedence (high to low, selected)

PrecedenceOperatorsAssociativity
1 (highest)() [] -> .Left to right
2! ~ ++ -- (cast) sizeof & * (unary)Right to left
3* / %Left to right
4+ -Left to right
5<< >>Left to right
6< <= > >=Left to right
7== !=Left to right
8& (bitwise AND)Left to right
9^Left to right
10|Left to right
11&&Left to right
12||Left to right
13?: (ternary)Right to left
14 (lowest)= += -= *= /= %= &= |= ^= <<= >>=Right to left
/* Ternary operator: condition ? value_if_true : value_if_false */
int max = (a > b) ? a : b;
/* sizeof — returns size_t, print with %zu */
printf("%zu\n", sizeof(int));    // typically 4
/* Cast for float division */
double result = (double)a / b;
/* Compound assignment shorthand */
x += 5;   /* same as x = x + 5 */
flags |= 0x01;  /* set bit 0 */
flags &= ~0x01; /* clear bit 0 */
Prefix vs postfix: y = ++x — x increments first, y gets new value. y = x++ — y gets old value, then x increments. As standalone statements they are equivalent.

Complete programs you can compile and run

Example 1 — Arithmetic and the integer division trap Week 1 Lecture
#include <stdio.h>

int main(void) {
    int a = 7, b = 2;

    /* Integer division — truncates, does NOT round */
    printf("%d / %d = %d\n", a, b, a / b);     /* 3, not 3.5 */
    printf("%d %% %d = %d\n", a, b, a % b);    /* 1 (remainder) */

    /* To get a float result, cast one operand to double BEFORE dividing */
    printf("(double)%d / %d = %.1f\n", a, b, (double)a / b); /* 3.5 */

    /* Careful: (double)(a/b) is NOT the same — int division first! */
    printf("(double)(%d/%d) = %.1f\n", a, b, (double)(a / b)); /* 3.0 */

    /* Modulo: useful for even/odd check, cycling, etc. */
    for (int i = 0; i < 6; i++) {
        printf("i=%d  i%%3=%d\n", i, i % 3);   /* cycles 0,1,2,0,1,2 */
    }
    return 0;
}
Output
7 / 2 = 3
7 % 2 = 1
(double)7 / 2 = 3.5
(double)(7/2) = 3.0
i=0 i%3=0
i=1 i%3=1
i=2 i%3=2
i=3 i%3=0
i=4 i%3=1
i=5 i%3=2
Example 2 — Bitwise operations with binary explanation Week 1 Lecture
#include <stdio.h>

int main(void) {
    unsigned int a = 0b1010;  /* 10 in decimal */
    unsigned int b = 0b1100;  /* 12 in decimal */

    printf("a   = %u  (binary: 1010)\n", a);
    printf("b   = %u  (binary: 1100)\n", b);
    printf("a&b = %u  (binary: 1000) — AND: both bits must be 1\n", a & b);
    printf("a|b = %u  (binary: 1110) — OR: at least one bit 1\n",  a | b);
    printf("a^b = %u  (binary: 0110) — XOR: bits differ\n",        a ^ b);
    printf("~a  = %u  — NOT: all bits flipped\n", ~a);
    printf("a<<1= %u  (binary: 10100) — left shift: multiply by 2\n", a << 1);
    printf("b>>1= %u  (binary: 0110)  — right shift: divide by 2\n",  b >> 1);

    /* Practical: test if a bit is set */
    int flags = 0b00001010;
    int bit1 = (flags >> 1) & 1;   /* extract bit 1 */
    printf("Bit 1 of flags: %d\n", bit1);  /* 1 */

    /* Set bit 2 */
    flags |= (1 << 2);
    printf("After setting bit 2: %d\n", flags);  /* 14 = 0b00001110 */

    return 0;
}
Output
a = 10 (binary: 1010)
b = 12 (binary: 1100)
a&b = 8 (binary: 1000) — AND: both bits must be 1
a|b = 14 (binary: 1110) — OR: at least one bit 1
a^b = 6 (binary: 0110) — XOR: bits differ
...
Bit 1 of flags: 1
After setting bit 2: 14
Example 3 — Prefix vs postfix and short-circuit evaluation Week 1 Lecture
#include <stdio.h>

int main(void) {
    int x = 5, y;

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

    /* Prefix: x becomes 7 first, then y gets new value (7) */
    y = ++x;
    printf("x=%d, y=%d\n", x, y);  /* x=7, y=7 */

    /* Short-circuit: second operand not evaluated if unnecessary */
    int a = 0, b = 0;
    /* a is 0 (false), so the whole && is false — b++ never runs */
    if (a && b++) {
        printf("never reached\n");
    }
    printf("b after a&&b++: %d\n", b);  /* b is still 0 */

    /* a is 0, but || checks second: b++ runs */
    if (a || b++) {
        printf("reached because b++ made it true\n");
    }
    printf("b after a||b++: %d\n", b);  /* b is now 1 */

    /* Ternary operator */
    int m = (x > y) ? x : y;
    printf("max(%d,%d) = %d\n", x, y, m);

    return 0;
}
Output
x=6, y=5
x=7, y=7
b after a&&b++: 0
reached because b++ made it true
b after a||b++: 1
max(7,7) = 7

Practice problems with solutions

P1 — Predict the output: precedence traps Week 2 Tutorial

Without running the code, predict the value of each expression.

int a = 6, b = 4, c = 2;
printf("%d\n", a + b * c);       /* (1) */
printf("%d\n", (a + b) * c);     /* (2) */
printf("%d\n", a / b);           /* (3) */
printf("%d\n", a % b);           /* (4) */
printf("%d\n", a >> 1);          /* (5) */
printf("%d\n", a & b);           /* (6) */
(1) 14   — * before +: a + (b*c) = 6 + 8 = 14
(2) 20   — parentheses first: (6+4)*2 = 20
(3) 1    — integer division: 6/4 = 1 (truncates)
(4) 2    — 6 mod 4 = 2 (remainder)
(5) 3    — right shift by 1 = divide by 2: 6>>1 = 3
(6) 4    — binary: 0110 & 0100 = 0100 = 4
Key takeaways: * binds tighter than +. Integer division truncates toward zero. Right shift by 1 is equivalent to integer divide by 2. Bitwise AND checks each bit position independently — 0110 AND 0100 = 0100 = 4.
P2 — Predict the output: increment/decrement side effects Week 2 Tutorial

What is the output of this program?

#include <stdio.h>
int main(void) {
    int x = 3, y = 3;
    int a = x++;   /* postfix */
    int b = ++y;   /* prefix  */
    printf("x=%d a=%d\n", x, a);
    printf("y=%d b=%d\n", y, b);

    int i = 5;
    printf("%d\n", i-- + --i);  /* tricky — see solution */
    return 0;
}
x=4 a=3
y=4 b=4
(undefined behavior on line 3)
Lines 1-2: Postfix x++: a gets the current value (3), then x becomes 4. Prefix ++y: y increments to 4 first, then b gets 4.
Line 3: i-- + --i modifies i twice without a sequence point — this is undefined behavior in C. The compiler can produce any result. Never write expressions that modify a variable more than once between sequence points. In practice, always split such operations into separate statements.
P3 — Write a function: is_power_of_two using bitwise operators Lecture + Tutorial

A positive integer n is a power of two if and only if it has exactly one bit set in binary. Using bitwise operators, write an expression (not a loop) that evaluates to 1 if n is a power of two, and 0 otherwise. Hint: what is n & (n-1) when n is a power of two?

#include <stdio.h>

/* Returns 1 if n is a power of two, 0 otherwise */
int is_power_of_two(int n) {
    return (n > 0) && ((n & (n - 1)) == 0);
}

int main(void) {
    for (int i = 0; i <= 16; i++) {
        printf("%2d: %s\n", i, is_power_of_two(i) ? "power of 2" : "no");
    }
    return 0;
}
Why it works: If n is a power of two (e.g. 8 = 1000), then n-1 = 0111. The AND of 1000 & 0111 = 0000. For any non-power-of-two, there are at least two bits set, and n & (n-1) removes only the lowest set bit — the result is nonzero. The n > 0 guard handles the n=0 edge case (0 is not a power of two but 0 & -1 = 0 would pass without it).
P4 — sizeof and casting: float division Week 1 Lecture

Write a program that: (a) prints the size in bytes of char, int, long, float, and double; (b) demonstrates that integer division of 5 / 2 gives 2, and shows two ways to get 2.5 using casts.

#include <stdio.h>

int main(void) {
    /* (a) sizeof — use %zu for size_t */
    printf("char:   %zu byte(s)\n",  sizeof(char));
    printf("int:    %zu byte(s)\n",  sizeof(int));
    printf("long:   %zu byte(s)\n",  sizeof(long));
    printf("float:  %zu byte(s)\n",  sizeof(float));
    printf("double: %zu byte(s)\n",  sizeof(double));

    /* (b) Integer division vs float division */
    int a = 5, b = 2;
    printf("5 / 2 = %d  (integer division)\n", a / b);

    /* Method 1: cast one operand before division */
    printf("(double)5 / 2 = %.1f\n", (double)a / b);

    /* Method 2: use a double literal */
    printf("5.0 / 2 = %.1f\n", 5.0 / b);

    return 0;
}
Remember: sizeof returns size_t — always use %zu to print it. Typical sizes: char = 1, int = 4, long = 8, float = 4, double = 8 (on 64-bit Linux). For float division, the cast must happen before the division operation occurs — casting the result after integer division just converts an already-truncated value to double.

Manual bitfield pack / unpack — the MODE_MASK / PRIORITY_MASK pattern

A bitfield is a technique for storing multiple small integer values inside the bits of a single larger integer. Instead of wasting a full int per value, you carve the integer into named "slots" using masks and shifts.

There are two approaches in C: the manual approach (masks + shifts — portable, explicit, no compiler magic) and C struct bitfields (unsigned x:3; — convenient but ABI-dependent). COMP2017 focuses on the manual approach.

Example 4 — Pack a 3-bit mode and 3-bit priority into one byte Wk4A Tutorial
#include <stdio.h>
#include <stdint.h>

// Pack a 3-bit mode (bits 5-3) and 3-bit priority (bits 2-0) into one byte.
// Bit layout: [unused][unused][mode2][mode1][mode0][pri2][pri1][pri0]
#define MODE_MASK     0x38   // 0011 1000
#define PRIORITY_MASK 0x07   // 0000 0111
#define MODE_SHIFT    3

uint8_t pack(int mode, int priority) {
    return ((mode << MODE_SHIFT) & MODE_MASK) | (priority & PRIORITY_MASK);
}

int get_mode(uint8_t packed)     { return (packed & MODE_MASK) >> MODE_SHIFT; }
int get_priority(uint8_t packed) { return packed & PRIORITY_MASK; }

int main(void) {
    uint8_t p = pack(5, 3);           // mode=5 (101), priority=3 (011)
    printf("packed = 0x%02X\n", p);   // 0x2B = 0010 1011
    printf("mode = %d\n", get_mode(p));       // 5
    printf("priority = %d\n", get_priority(p)); // 3
    return 0;
}
Output
packed = 0x2B
mode = 5
priority = 3
Why this pattern matters

This pattern appears everywhere in systems programming — network packet headers, hardware registers, OS file permission bits. Packing multiple small values into one integer saves memory and enables atomic operations. The recipe is always the same: define a MASK for the field's bits, define a SHIFT for its position, pack with (value << SHIFT) & MASK, unpack with (packed & MASK) >> SHIFT.

P5 — Bitfield pack/unpack: add an id field Wk4A Tutorial

Extend the example above to also pack an 8-bit id field into bits 15–8 of a uint16_t. Define ID_MASK, ID_SHIFT, a new pack16() function, and a get_id() extractor. Then pack id=200, mode=5, priority=3 and verify extraction gives the original values back.

#include <stdio.h>
#include <stdint.h>

#define MODE_MASK     0x0007u   // bits 2-0
#define PRIORITY_MASK 0x00F8u   // bits 7-3  (5 bits)
#define ID_MASK       0xFF00u   // bits 15-8 (8 bits)

#define MODE_SHIFT     0
#define PRIORITY_SHIFT 3
#define ID_SHIFT       8

uint16_t pack16(int id, int priority, int mode) {
    return ((id       << ID_SHIFT)       & ID_MASK)
         | ((priority << PRIORITY_SHIFT) & PRIORITY_MASK)
         | ((mode     << MODE_SHIFT)     & MODE_MASK);
}

int get_id(uint16_t v)       { return (v & ID_MASK)       >> ID_SHIFT; }
int get_priority(uint16_t v) { return (v & PRIORITY_MASK) >> PRIORITY_SHIFT; }
int get_mode(uint16_t v)     { return (v & MODE_MASK)     >> MODE_SHIFT; }

int main(void) {
    uint16_t p = pack16(200, 5, 3);
    printf("id=%d  priority=%d  mode=%d\n",
           get_id(p), get_priority(p), get_mode(p));
    // id=200  priority=5  mode=3
    return 0;
}
Key steps: Each field gets a MASK (which bits it occupies) and a SHIFT (how far left from bit 0). To pack, shift the value up into position then AND with the mask to prevent overflow into adjacent fields. To unpack, AND with the mask to isolate the field, then shift right to bring it back to zero-based.

Linux file permissions as bitfields

Linux stores file permissions as a 9-bit value — three groups of three bits: owner, group, and others, each with read (r), write (w), and execute (x) bits. The rwx letters are just human-readable labels; underneath, each position is 1 (allowed) or 0 (not allowed).

Bit layout for chmod 755 — visualised

Group r (4) w (2) x (1) Octal digit Symbolic
Owner 1 1 1 7 rwx
Group 1 0 1 5 r-x
Others 1 0 1 5 r-x

The 9 bits in order (MSB to LSB): 1 1 1   1 0 1   1 0 1 — that is 0755 in octal.

P6 — Linux file permissions: reading and testing mode bits Wk4A Tutorial

Linux stores file permissions as a 9-bit value. The command chmod 755 file sets: owner=rwx (7), group=r-x (5), others=r-x (5). Given mode value 0644, which permissions are set? Write a C expression to check if the owner has write permission.

#include <stdio.h>
#include <sys/stat.h>

// Permission bit constants (from sys/stat.h):
// S_IRUSR = 0400  owner read
// S_IWUSR = 0200  owner write
// S_IXUSR = 0100  owner execute
// S_IRGRP = 0040  group read
// S_IWGRP = 0020  group write
// S_IXGRP = 0010  group execute
// S_IROTH = 0004  others read
// S_IWOTH = 0002  others write
// S_IXOTH = 0001  others execute

// 0644 = 110 100 100 = rw- r-- r--
mode_t mode = 0644;

// Check owner write permission:
if (mode & S_IWUSR) {
    printf("Owner can write\n");  // prints this for 0644
}

// 0644: owner=rw(6), group=r(4), others=r(4)
// Owner has read+write but NOT execute
Analysis of 0644: In octal, 6 = 110 (r+w, no x), 4 = 100 (r only). So owner has read+write, group has read only, others have read only.
Testing a permission bit: Use bitwise AND — mode & S_IWUSR is nonzero (true) if the owner-write bit is set, zero (false) if it is clear. This is the same mask-and-test pattern used for any bitfield.
Why sys/stat.h? The named constants (S_IRUSR etc.) are defined there. You can also use raw octal: mode & 0200 is equivalent to mode & S_IWUSR but less readable.

Key concepts to memorize

Card 1 of 10
Question — click to flip
Answer
Click card to flip • Use buttons to navigate

Test your understanding

Topic 03 Quiz — Operators & Expressions Score: 0 / 6
1
What is the value of 7 / 2 in C when both operands are int?LO1
multiple choice
2
True or False: In A && B, if A evaluates to false (0), B is still evaluated.LO1
true / false
3
What is the result of 5 | 3 (bitwise OR)?LO1
multiple choice
4
Fill in the blank: to print the size of an int correctly, use printf("___ bytes", sizeof(int)); — what is the format specifier?LO1
fill in the blank
5
Spot the bug: what is wrong with this code that is supposed to compute 5 divided by 2 as a decimal?LO1
int a = 5, b = 2;
double result = (double)(a / b);
printf("%.1f\n", result);
spot the bug — multiple choice
6
Given int x = 4; int y = x++;, what are the values of x and y after this statement?LO1
multiple choice
0/6
Quiz complete!