Bitwise Operations
Bitwise operations let you work with the individual bits of an integer. They’re super powerful for bit manipulation, optimization tricks, and things like generating all subsets of a set.
Binary Numbers
Computers store integers in binary — using only 0 and 1. We call a bit to be set if it is equal to 1. Each position (bit) represents a power of 2:
Bit position: 7 6 5 4 3 2 1 0
Value: 128 64 32 16 8 4 2 1
To get the value of a binary number, you add the powers of 2 where there is a 1.
Examples
00000110 = 4 + 2 = 6
00001010 = 8 + 2 = 10
00001100 = 8 + 4 = 12
00000001 = 1 = 1
00000000 = 0 = 0
So when we do bitwise operations (&, |, ^, etc.), we are combining or modifying these individual bits.
Two’s Complement and Negative Numbers
So far we only talked about non-negative integers. But in C++ (and basically all modern CPUs), signed integers are stored in a format called two’s complement.
Idea:
- Use the highest bit (most significant bit) as a sign bit.
Let’s pretend we have 8-bit signed integers.
Bit position: 7 6 5 4 3 2 1 0
Value: -128 64 32 16 8 4 2 1
The range of an 8-bit signed number is:
-128 ... +127
How positive numbers look
Positive numbers (and zero) look the same as before:
00000101 = 5
00000000 = 0
00001010 = 10
How negative numbers are stored (two’s complement)
To get -x in two’s complement:
- Write
xin binary. - Flip all bits (
0 -> 1,1 -> 0). - Add 1.
Example: -5 on 8 bits.
+5 = 00000101
flip = 11111010
+ 1 = 11111011 = -5
Check -1:
+1 = 00000001
flip = 11111110
+ 1 = 11111111 = -1
So:
11111111 = -1
11111011 = -5
10000000 = -128
Why this is nice
With two’s complement:
- Normal binary addition still works for negative numbers.
- There is only one representation of zero.
- Subtraction is just
a + (-b).
Right shift and negatives
For unsigned types, >> always shifts in 0 bits on the left.
For signed types, most compilers perform an arithmetic right shift:
- They copy the sign bit (the top bit), so negative numbers stay negative as you shift.
Example (assuming arithmetic shift):
x = -4 -> 11111100 (8-bit view)
x >> 1 -> 11111110 (still negative: -2)
This is why, when you care about raw bits, it’s often safer to use unsigned types (uint32_t, unsigned long long, etc.).
The Main Bitwise Operators
All of these work on integer types (int, long long, etc.):
| Operator | Name | What it does (bitwise) |
|---|---|---|
& |
AND | 1 if both bits are 1, otherwise 0 |
| |
OR | 1 if at least one bit is 1, otherwise 0 |
^ |
XOR | 1 if bits are different, otherwise 0 |
~ |
NOT / complement | Flips all bits (1 → 0, 0 → 1) |
<< |
Left shift | Shifts bits left (multiply by powers of 2) |
>> |
Right shift | Shifts bits right (divide by powers of 2) |
Seeing Bitwise Operations in Binary
Let’s use small numbers and pretend they’re 8-bit for clarity:
a = 6 -> 00000110
b = 10 -> 00001010
AND &
a & b -> 00000110
00001010
--------
00000010 = 2
OR |
a | b -> 00000110
00001010
--------
00001110 = 14
XOR ^
a ^ b -> 00000110
00001010
--------
00001100 = 12 (bits that are different become 1)
NOT ~
~a -> 11111001 (on 8 bits, but in real C++ it’s on full `int`/`long long`)
Shifts
a << 1 -> 00001100 = 12 (6 * 2)
a << 2 -> 00011000 = 24 (6 * 4)
b >> 1 -> 00000101 = 5 (10 / 2)
b >> 2 -> 00000010 = 2 (10 / 4, integer division)
Simple C++ Example
#include <iostream>
using namespace std;
void printBits(int x) {
for (int i = 7; i >= 0; i--) {
cout << ((x >> i) & 1);
}
}
int main() {
int a = 6; // 00000110
int b = 10; // 00001010
cout << "a = "; printBits(a); cout << " (" << a << ")\n";
cout << "b = "; printBits(b); cout << " (" << b << ")\n\n";
cout << "a & b = "; printBits(a & b); cout << " (" << (a & b) << ")\n";
cout << "a | b = "; printBits(a | b); cout << " (" << (a | b) << ")\n";
cout << "a ^ b = "; printBits(a ^ b); cout << " (" << (a ^ b) << ")\n";
cout << "~a = "; printBits((unsigned char)~a); cout << " (only low 8 bits)\n";
cout << "a << 1 = "; printBits(a << 1); cout << " (" << (a << 1) << ")\n";
cout << "b >> 1 = "; printBits(b >> 1); cout << " (" << (b >> 1) << ")\n";
}The function printBits goes through all bit positions i (from 7 to 0), and does x >> i, which shifts the given value x that many positions to the right. This makes the i-th bit come to the rightmost position. Then it does & 1 which “ANDs” that value with 00000001, which has an effect of keeping only the rightmost bit of that value. So these two operations together result in extracting the bit at the i-th position.