Operators & Expressions
Arithmetic, relational, logical, bitwise, and assignment operators — how C computes and compares values.
How C computes and compares values
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.
# 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
/* 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;
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 / 2 → 3.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
| Operator | Name | Example | Result (int) |
|---|---|---|---|
+ | Addition | 5 + 3 | 8 |
- | Subtraction | 5 - 3 | 2 |
* | Multiplication | 5 * 3 | 15 |
/ | Division (truncates for int) | 7 / 2 | 3 |
% | Modulo (remainder) | 7 % 2 | 1 |
Relational Operators — result is 1 (true) or 0 (false)
| Operator | Meaning | Example | Result |
|---|---|---|---|
== | Equal to | 5 == 5 | 1 |
!= | Not equal to | 5 != 3 | 1 |
< | Less than | 3 < 5 | 1 |
> | Greater than | 5 > 3 | 1 |
<= | Less than or equal | 3 <= 3 | 1 |
>= | Greater than or equal | 5 >= 6 | 0 |
Logical & Bitwise Operators
| Operator | Category | Meaning | Example |
|---|---|---|---|
&& | Logical | AND (short-circuits) | a > 0 && b > 0 |
|| | Logical | OR (short-circuits) | a > 0 || b > 0 |
! | Logical | NOT | !flag |
& | Bitwise | AND (bit by bit) | 0b1010 & 0b1100 → 0b1000 |
| | Bitwise | OR (bit by bit) | 0b1010 | 0b0101 → 0b1111 |
^ | Bitwise | XOR (bit by bit) | 0b1010 ^ 0b1100 → 0b0110 |
~ | Bitwise | NOT (flip all bits) | ~0 → all 1s (= -1 for int) |
<< | Bitwise | Left shift (multiply by 2^n) | 1 << 3 → 8 |
>> | Bitwise | Right shift (divide by 2^n) | 8 >> 1 → 4 |
Operator Precedence (high to low, selected)
| Precedence | Operators | Associativity |
|---|---|---|
| 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 */
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
#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;
}
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
#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;
}
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
#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;
}
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
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
* 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.
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)
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.
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;
}
n > 0 guard handles the n=0 edge case (0 is not a power of two but 0 & -1 = 0 would pass without it).
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;
}
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.
#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;
}
mode = 5
priority = 3
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.
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;
}
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.
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
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
Test your understanding
7 / 2 in C when both operands are int?LO1A && B, if A evaluates to false (0), B is still evaluated.LO15 | 3 (bitwise OR)?LO1int correctly, use printf("___ bytes", sizeof(int)); — what is the format specifier?LO1int a = 5, b = 2;
double result = (double)(a / b);
printf("%.1f\n", result);
int x = 4; int y = x++;, what are the values of x and y after this statement?LO1