Module I · Basics of C · Lecture 8
🎯 Why This Matters
Every value in C — no matter its type — is ultimately just a pattern of bits in memory, as the “Data Types in C” post explained. Most of the operators you’ve used so far (+, >, &&) treat that pattern as a single number or a single true/false value. Bitwise operators are different: they reach inside a value and work on its individual bits, one by one. This is how real systems pack multiple settings into a single number, talk to hardware, and squeeze out extra performance — skills this lecture’s short quiz and coding task will test directly.
🔢 A Quick Refresher: Numbers as Bits
Recall from the Data Types post that an int is typically 4 bytes — 32 individual bits, each either 0 or 1. The number 22, for example, is stored as ...00010110 in binary. Bitwise operators look at this pattern directly, rather than at the number it represents.
#include <stdio.h>
int main() {
int x = 22;
printf("Size of int: %zu bytes = %zu bits\n", sizeof(x), sizeof(x) * 8);
return 0;
}
🖥️ Output:
Size of int: 4 bytes = 32 bits

🔀 The Six Bitwise Operators
| Operator | Name | What it does |
|---|---|---|
& | AND | Sets a bit to 1 only if both corresponding bits are 1 |
| | OR | Sets a bit to 1 if either corresponding bit is 1 |
^ | XOR | Sets a bit to 1 if the two corresponding bits differ |
~ | NOT (complement) | Flips every bit — the only unary bitwise operator |
<< | Left shift | Slides every bit left, filling with zeros on the right |
>> | Right shift | Slides every bit right |

⚠️ Easy to confuse: & and && look similar but do completely different jobs. && (covered in the last post) combines two whole true/false values and short-circuits. & combines two numbers bit by bit and always evaluates both sides. The same distinction applies to | versus ||.
Bitwise AND (&)
#include <stdio.h>
int main() {
int a = 0x5C; // 0101 1100
int b = 0x3A; // 0011 1010
int result = a & b;
printf("a & b = %d (0x%X)\n", result, result);
return 0;
}
🖥️ Output:
a & b = 24 (0x18)

Line up the two patterns and compare column by column: a 1 appears in the result only where both inputs have a 1 in that exact position. Every other column becomes 0. This is the single most useful bitwise operator — it’s how you check or extract specific bits from a larger value.
Bitwise OR (|) and XOR (^)
#include <stdio.h>
int main() {
int a = 0x5C; // 0101 1100
int b = 0x3A; // 0011 1010
printf("a | b = %d (0x%X)\n", a | b, a | b);
printf("a ^ b = %d (0x%X)\n", a ^ b, a ^ b);
return 0;
}
🖥️ Output:
a | b = 126 (0x7E)
a ^ b = 102 (0x66)

| turns a bit on if either input has it on — it’s used to combine settings together. ^ is more subtle: it turns a bit on only where the two inputs disagree. A useful side effect: x ^ x is always 0, and XOR-ing the same value twice always cancels back to the original — a property put to use in some classic bit-toggling tricks later in this post.
Bitwise NOT (~)
#include <stdio.h>
int main() {
unsigned char a = 0x0F; // 0000 1111
printf("a = %u (0x%X)\n", a, a);
printf("~a = %u (0x%X)\n", (unsigned char)~a, (unsigned char)~a);
return 0;
}
🖥️ Output:
a = 15 (0xF)
~a = 240 (0xF0)

~ is the only bitwise operator that takes a single operand — it flips every single bit, turning every 0 into a 1 and every 1 into a 0. Notice this example uses unsigned char (1 byte) specifically so the flipped pattern is easy to read; applying ~ to a full 4-byte int would flip all 32 bits, most of which would just show up as a large negative number when printed as signed.
↔️ Shift Operators
The shift operators slide every bit in a value left or right by a given number of positions — and because of how binary numbers work, this has a beautifully simple side effect.

#include <stdio.h>
int main() {
int x = 22;
int y = 88;
printf("x << 2 = %d\n", x << 2);
printf("y >> 2 = %d\n", y >> 2);
return 0;
}
🖥️ Output:
x << 2 = 88
y >> 2 = 22
Shifting left by n positions is the same as multiplying by 2ⁿ, and shifting right by n is the same as integer-dividing by 2ⁿ — 22 << 2 is exactly 22 × 4 = 88. Compilers historically used this trick to replace multiplication with faster shift instructions; today’s compilers do this automatically, but the equivalence is still worth knowing, since shifts show up constantly in real embedded and systems code.
⚠️ A genuine warning: Shifting a signed value that’s already negative, or shifting by a number of bits greater than or equal to the type’s width (e.g. x << 40 on a 32-bit int), is undefined behavior in C. Stick to unsigned types and small, sensible shift amounts unless you know exactly what you’re doing.
🚩 A Real Application: Bit Flags
One of the most common real-world uses of bitwise operators is packing several yes/no settings into a single number — each bit acting as its own independent on/off switch, called a flag.

#include <stdio.h>
#define READ 4 // 0000 0100
#define WRITE 2 // 0000 0010
#define EXECUTE 1 // 0000 0001
int main() {
int permissions = READ | WRITE | EXECUTE;
printf("Permissions value: %d\n", permissions);
int canWrite = permissions & WRITE;
printf("Has write access: %d\n", canWrite);
int readOnly = permissions & ~WRITE;
printf("Permissions after removing write: %d\n", readOnly);
return 0;
}
🖥️ Output:
Permissions value: 7
Has write access: 2
Permissions after removing write: 5
Three techniques worth naming individually, since you’ll see this exact pattern throughout real C code:
- Setting a flag:
flags | FLAGturns a specific bit on without disturbing any others. - Checking a flag:
flags & FLAGgives a non-zero result only if that specific bit is set. - Clearing a flag:
flags & ~FLAGturns a specific bit off —~WRITEflips every bit except the write bit, so ANDing with it keeps everything except write access.
💡 Where this actually shows up: This is precisely how Unix file permissions work (chmod 7 means read+write+execute, exactly as shown above), and it’s the standard way embedded systems pack multiple hardware settings into a single control register — one byte, eight independent switches.
🎓 More Practical Bitwise Tricks
Checking Odd or Even Without %
#include <stdio.h>
int main() {
int n = 17;
int isOdd = n & 1;
printf("Is odd: %d\n", isOdd);
n = 24;
isOdd = n & 1;
printf("Is odd: %d\n", isOdd);
return 0;
}
🖥️ Output:
Is odd: 1
Is odd: 0

In binary, only the very last bit determines whether a number is odd or even — every odd number ends in 1, every even number ends in 0. ANDing with 1 isolates exactly that bit, giving a faster alternative to n % 2 that’s still used in performance-critical code today.
Swapping Two Variables with XOR
#include <stdio.h>
int main() {
int a = 12, b = 25;
printf("Before: a = %d, b = %d\n", a, b);
a = a ^ b;
b = a ^ b;
a = a ^ b;
printf("After: a = %d, b = %d\n", a, b);
return 0;
}
🖥️ Output:
Before: a = 12, b = 25
After: a = 25, b = 12

This is a classic party trick: swapping two variables using XOR, without needing a third temporary variable like the swap program from the Data Types exercise sheet. It works because of that self-cancelling property mentioned earlier — x ^ x = 0. In practice, a plain temporary variable is still clearer and just as fast on modern compilers, but recognizing this pattern is a genuine rite of passage in C.
✳️ Special Operators: A Quick Tour
Beyond arithmetic, relational, logical, and bitwise operators, C has a handful of operators that don’t fit neatly into any single category. Two are worth introducing now.
sizeof — Already Familiar
You’ve used sizeof throughout this course to check a type’s size in bytes — it’s technically classified as a special/unary operator, not a function, which is why it can be written without parentheses in most contexts.
#include <stdio.h>
int main() {
int x = 100;
printf("sizeof x = %zu\n", sizeof x);
printf("sizeof(int) = %zu\n", sizeof(int));
printf("sizeof(double) = %zu\n", sizeof(double));
return 0;
}
🖥️ Output:
sizeof x = 4
sizeof(int) = 4
sizeof(double) = 8
The Comma Operator
#include <stdio.h>
int main() {
int a, b, sum;
sum = (a = 5, b = 10, a + b);
printf("sum = %d\n", sum);
return 0;
}
🖥️ Output:
sum = 15

The comma operator evaluates several expressions in sequence, from left to right, and the entire comma-separated group evaluates to the value of the last one. Here, a gets 5, then b gets 10, and the whole parenthesized group evaluates to a + b, which is what gets assigned to sum. You’ll meet this operator again once we reach for loops, where it’s commonly used to update two counters in a single loop header.
🚫 Common Mistakes Beginners Make
| Mistake | Why it happens |
|---|---|
Using & or | where && or || was meant | Bitwise operators compare individual bits, not whole true/false values — they don’t short-circuit either |
Expecting ~5 to just mean “not five” | ~ flips every bit of the value’s full binary representation, usually producing a large or negative-looking number |
| Shifting a negative number or shifting too far | Both are undefined behavior in C — stick to unsigned values and shift amounts smaller than the type’s bit width |
| Forgetting parentheses around a bitwise expression mixed with comparisons | & and | have lower precedence than ==, so x & 1 == 1 doesn’t do what it looks like — it evaluates 1 == 1 first. Always write (x & 1) == 1. |
🎓 Practice Exercises
Exercise 1: Predict the Output
int a = 6, b = 3;
printf("%d\n", a & b);
printf("%d\n", a | b);
printf("%d\n", a ^ b);
(Answer: 2, 7, 5 — trace 6 = 0110 and 3 = 0011 column by column for each operator.)
Exercise 2: Trace the Shift
int x = 5;
printf("%d\n", x << 3);
(Answer: 40 — shifting left by 3 is the same as multiplying by 2³ = 8, and 5 × 8 = 40.)
Exercise 3: Build It Yourself
Using the READ, WRITE, and EXECUTE flags from this post, write a program that starts with full permissions (7), removes the EXECUTE flag using & and ~, and then prints whether READ access is still present.
❓ Frequently Asked Questions
Q: When would I actually use bitwise operators in a real program, outside of a course exercise?
Anywhere memory or bandwidth is tight and settings need to be packed efficiently — embedded systems reading hardware registers, network protocols packing multiple fields into a few bytes, graphics code manipulating color channels, and permission systems like the Unix file example in this post.
Q: Is there a bitwise equivalent of the logical NOT (!) that only flips truthiness, not every bit?
Not directly — ~ always flips the full bit pattern. If you specifically want “is this value zero or non-zero, flipped,” that’s exactly what ! already does; the two operators solve genuinely different problems and aren’t meant to substitute for each other.
Q: Why does right-shifting a negative number behave unpredictably?
Negative numbers are stored using a representation called two’s complement (a topic for a later, more advanced post), and different compilers are allowed to fill the vacated bits differently when right-shifting a negative value — which is exactly why the C standard leaves it undefined. Stick to unsigned types whenever you’re deliberately manipulating bits.
✅ Key Takeaways
- Bitwise operators (
& | ^ ~ << >>) work on the individual bits of a value, not on the value as a whole — a completely different job from the logical operators covered in the previous post. &is used to check or extract specific bits,|to set them, and& ~together to clear them — the three core operations behind bit-flag systems like Unix permissions.- Shifting left by
nmultiplies by2ⁿ; shifting right byndivides by2ⁿ— a direct consequence of how binary place value works. sizeofand the comma operator are two “special” operators that don’t fit the arithmetic/relational/logical/bitwise categories, but show up regularly in real C code.- Bitwise and logical operators look similar (
&vs&&,|vs||) but are never interchangeable — mixing them up is a common and often silent bug.
🚀 Next up: With every category of operator now covered, we’ll finally put them to work inside real decision-making — control structures, starting with
if,else if, andelse, and how C actually branches program flow based on everything we’ve built toward across this operators series.