Bitwise operators manipulate individual bits of integer types in a single CPU cycle
AND (&) for masking, OR (|) for setting bits, XOR (^) for toggling, NOT (~) for inverting
Left shift (<<) multiplies by 2^N; right shift (>>) divides by 2^N on unsigned types
Always use unsigned fixed-width types (uint32_t) to avoid undefined behavior on shifts
Forgetting operator precedence (e.g., & vs ==) is the #1 production mistake
Mask the result of NOT with the bit width: (~MASK) & 0xFF keeps it predictable
✦ Definition~90s read
What is Bitwise Operators in C?
Bitwise NOT in C (~) is a unary operator that flips every bit in its operand: 0 becomes 1, 1 becomes 0. It exists because hardware registers and permission bits are often inverted masks — clearing adjacent bits requires inverting a mask before ANDing.
★
Imagine a row of 8 light switches on a wall — each one is either ON (1) or OFF (0).
For example, to clear bits 3 and 4 in a flags register, you write flags &= ~(0x18), where ~ flips the mask so only those bits become 0. Without it, you'd need two operations or a hardcoded inverted constant, which breaks when masks change. The gotcha: ~ operates on the promoted integer type, so ~0U is 0xFFFFFFFF on 32-bit, but ~0 is -1 (all bits set in two's complement).
This matters when you mask with uint8_t — ~(uint8_t)0x01 promotes to int first, giving 0xFFFFFFFE, not 0xFE. Real-world: Linux kernel uses ~ extensively in clear_bit() macros and DMA descriptor flags. Don't use it for boolean negation — that's !.
Use it when you need to invert a bitmask for selective clearing, like permissions &= ~(READ | WRITE) to revoke both flags in one shot.
Plain-English First
Imagine a row of 8 light switches on a wall — each one is either ON (1) or OFF (0). Bitwise operators are like instructions for flipping, checking, or combining those switches in bulk, all at once, in a single CPU instruction. Instead of 'is the number greater than 5?', you're asking 'is this specific switch flipped on?' That's it. Numbers are just patterns of switches, and bitwise operators let you manipulate those patterns with surgical precision.
⚙ Browser compatibility
Latest versions — ✓ supported
Chrome
Firefox
Safari
Edge
✓
✓
✓
✓
Embedded systems, operating system kernels, game engines, network protocols, and cryptography libraries all share one thing in common — they rely heavily on bitwise operators. When a Linux kernel sets a file permission flag, it flips a bit. When a graphics engine packs RGBA color channels into a single 32-bit integer, it uses bit shifts. When a network driver extracts the IP header length from a raw packet, it masks bits. This isn't niche knowledge — it's the lingua franca of systems programming.
The problem bitwise operators solve is deceptively simple: speed and space. Storing 8 boolean flags as 8 separate int variables wastes memory and slows things down. Packing them into a single byte and using bitwise operators to read and write individual flags is both faster and leaner. The CPU handles bitwise operations in a single clock cycle — there's nothing faster in your toolbox.
By the end of this article you'll be able to read and write bit flags confidently, understand exactly how masking and shifting work under the hood, spot the classic bugs that trip up even experienced developers, and answer the bitwise questions that interviewers love to spring on candidates. Let's get into it.
What Bitwise NOT in C Actually Does — And Why It Breaks Permission Bits
Bitwise NOT (~) in C flips every bit in its operand: 0 becomes 1, 1 becomes 0. It operates on the full width of the integer type — for an unsigned char (8 bits), ~0x03 yields 0xFC. This is a unary operator, applied to a single integer expression, and it works on any integer type (char, int, long, etc.). The result is a one's complement of the operand.
Key property: ~ is not a logical negation. It inverts all bits, not just the truthiness of the value. For signed types, the result depends on the underlying two's complement representation — ~0 is -1, not 0. This catches many developers off guard when they expect a simple boolean flip. The operation is O(1) and compiles to a single machine instruction (NOT or XOR with all-ones).
Use ~ when you need to clear specific bits by ANDing with the complement of a mask: flags &= ~PERM_WRITE. This is the canonical pattern for clearing permission bits in systems programming — file access masks, memory page protections, or interrupt enables. Without ~, you'd have to know the exact bit pattern to clear, which is error-prone and unmaintainable.
⚠ Signed vs Unsigned Trap
Applying ~ to a signed int and then using it as a bitmask can sign-extend, corrupting higher bits. Always cast to unsigned before bitwise NOT when building masks.
📊 Production Insight
A team used ~ on a signed int to clear a permission bit in a Linux capabilities mask, but the sign extension set all high bits to 1, granting unintended privileges.
The symptom: a process gained CAP_SYS_ADMIN when only CAP_NET_RAW was requested, causing a security audit failure.
Rule: always use unsigned types (uint32_t, uint64_t) for bitmask operations — never rely on signed integer behavior.
🎯 Key Takeaway
Bitwise NOT inverts every bit of the operand, not just the bits you care about.
Always cast to unsigned before applying ~ to avoid sign extension disasters.
The canonical pattern to clear a flag is flags &= ~FLAG — never flags = flags & ~FLAG (same result, but less idiomatic).
thecodeforge.io
Bitwise Operators C
The Six Bitwise Operators and What They Actually Do
C gives you six bitwise operators: AND (&), OR (|), XOR (^), NOT (~), left shift (<<), and right shift (>>). Each one operates on the individual bits of an integer — not the integer as a whole mathematical value, but the raw binary pattern sitting in memory.
Think of AND as 'both switches must be ON'. OR as 'at least one switch must be ON'. XOR (exclusive OR) as 'exactly one switch must be ON — but not both'. NOT flips every single switch. Left shift moves all switches to the left by N positions, filling vacated positions with zeros — which multiplies by powers of two. Right shift moves everything right, which divides by powers of two.
The key insight beginners miss: these operators don't care about the numeric value you intended. They only see the bit pattern. The number 5 (binary: 00000101) and the number 6 (binary: 00000110) ANDed together produce 4 (binary: 00000100) — not because 5 AND 6 means anything mathematically, but because only the third bit was ON in both patterns.
This distinction between 'value' and 'bit pattern' is everything. Once it clicks, the rest falls into place naturally.
bitwise_basics.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <stdio.h>
// Helper: print an integer as 8-bit binary so we can SEE what's happening
voidprint_binary(constchar *label, unsigned int value) {
printf("%-20s = ", label);
for (int bit_position = 7; bit_position >= 0; bit_position--) {
// Shift the bit we care about all the way to position 0, then mask it
printf("%d", (value >> bit_position) & 1);
}
printf(" (decimal: %u)\n", value);
}
intmain(void) {
unsigned int lights_a = 0b00110101; // switches: 6,5 and 4,2 and 0 are ON = 53
unsigned int lights_b = 0b00101110; // switches: 5,3,2,1 are ON = 46printf("=== Starting values ===\n");
print_binary("lights_a", lights_a);
print_binary("lights_b", lights_b);
printf("\n=== AND — both must be ON ===\n");
// Only bits that are 1 in BOTH patterns survive
print_binary("a & b", lights_a & lights_b);
printf("\n=== OR — at least one ON ===\n");
// Any bit that is 1 in EITHER pattern survives
print_binary("a | b", lights_a | lights_b);
printf("\n=== XOR — exactly one ON ===\n");
// Bits that differ between the two patterns become 1print_binary("a ^ b", lights_a ^ lights_b);
printf("\n=== NOT — flip everything ===\n");
// Every0 becomes 1 and every 1 becomes 0 (we mask to 8 bits for clarity)
print_binary("~a (8-bit masked)", (~lights_a) & 0xFF);
printf("\n=== LEFT SHIFT — multiply by 2 per shift ===\n");
// Shifting left by 2 is the same as multiplying by 4print_binary("a << 2", (lights_a << 2) & 0xFF);
printf("\n=== RIGHT SHIFT — divide by 2 per shift ===\n");
// Shifting right by 1 is the same as integer division by 2print_binary("a >> 1", lights_a >> 1);
return0;
}
Output
=== Starting values ===
lights_a = 00110101 (decimal: 53)
lights_b = 00101110 (decimal: 46)
=== AND — both must be ON ===
a & b = 00100100 (decimal: 36)
=== OR — at least one ON ===
a | b = 00111111 (decimal: 63)
=== XOR — exactly one ON ===
a ^ b = 00011011 (decimal: 27)
=== NOT — flip everything ===
~a (8-bit masked) = 11001010 (decimal: 202)
=== LEFT SHIFT — multiply by 2 per shift ===
a << 2 = 11010100 (decimal: 212)
=== RIGHT SHIFT — divide by 2 per shift ===
a >> 1 = 00011010 (decimal: 26)
💡Pro Tip: Use print_binary() as Your Debugger
Copy the print_binary() helper from this example into any project where you're doing bit manipulation. Seeing the raw bit pattern when debugging saves hours. Most bit bugs are immediately obvious the moment you print the binary representation.
📊 Production Insight
Shifting a signed int left or right with a negative value is undefined behavior.
Always use unsigned types for bitwise operations to avoid compiler-dependent surprises.
Rule: if you see a bitwise operator, the operand should be unsigned.
🎯 Key Takeaway
Bitwise operators work on bit patterns, not numeric values.
Memorize the truth tables: AND requires both, OR requires either, XOR requires exactly one.
Use unsigned types (uint8_t, uint32_t) for all bit manipulations.
Bit Flags and Masking — The Real-World Pattern You'll Use Every Day
The single most common real-world use of bitwise operators is the bit flag pattern. Instead of storing eight separate boolean variables, you pack them all into one integer — each bit represents one flag. This is how file permissions work in Linux (rwxrwxrwx is nine bits), how TCP/IP headers store control flags (SYN, ACK, FIN), and how game engines store entity states.
Three operations drive the entire pattern:
Setting a bit (turning a flag ON): Use OR with a mask. The mask has a 1 only in the position you want to set. Everything else stays untouched because OR-ing with 0 changes nothing.
Clearing a bit (turning a flag OFF): Use AND with an inverted mask. The inverted mask has a 0 only in the target position, which forces that bit to 0. Everything else stays untouched because AND-ing with 1 changes nothing.
Checking a bit (reading a flag): Use AND with the mask. If the result is non-zero, the bit was set. If zero, it wasn't.
The mask itself is almost always defined using a left shift: 1 << N gives you a mask with exactly one 1 bit at position N. This pattern is so idiomatic in C that you'll see it in virtually every low-level codebase.
permission_flags.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <stdio.h>
// Define each permission as a bit flag using left shifts
// This makes the intent crystal-clear and avoids magic numbers
#define PERMISSION_READ (1 << 0) // bit 0 = 00000001 = 1
#define PERMISSION_WRITE (1 << 1) // bit 1 = 00000010 = 2
#define PERMISSION_EXECUTE (1 << 2) // bit 2 = 00000100 = 4
#define PERMISSION_DELETE (1 << 3) // bit 3 = 00001000 = 8
#define PERMISSION_SHARE (1 << 4) // bit 4 = 00010000 = 16
// A single byte can hold all 5 permissions — no wasted memory
typedef unsigned charPermissionSet;
voidprint_permissions(PermissionSet perms) {
printf("Permissions: [%s] [%s] [%s] [%s] [%s]\n",
(perms & PERMISSION_READ) ? "READ" : "----",
(perms & PERMISSION_WRITE) ? "WRITE" : "-----",
(perms & PERMISSION_EXECUTE) ? "EXECUTE" : "-------",
(perms & PERMISSION_DELETE) ? "DELETE" : "------",
(perms & PERMISSION_SHARE) ? "SHARE" : "-----"
);
}
intmain(void) {
// Start with no permissions at all — a clean slate
PermissionSet user_permissions = 0;
printf("--- Initial state ---\n");
print_permissions(user_permissions);
// SET bits: grant read and write using OR
// OR with the flag mask forces that bit to 1 without touching others
user_permissions |= PERMISSION_READ;
user_permissions |= PERMISSION_WRITE;
printf("\n--- After granting READ and WRITE ---\n");
print_permissions(user_permissions);
// SET multiple flags at once by OR-ing them together in one line
user_permissions |= (PERMISSION_EXECUTE | PERMISSION_SHARE);
printf("\n--- After granting EXECUTE and SHARE ---\n");
print_permissions(user_permissions);
// CLEAR a bit: revoke write using AND with the inverted mask
// ~PERMISSION_WRITE = 11111101 — forces bit 1 to 0, leaves all others
user_permissions &= ~PERMISSION_WRITE;
printf("\n--- After revoking WRITE ---\n");
print_permissions(user_permissions);
// CHECK a bit: test if the user can execute
// AND with the flag mask — non-zero means the bit is set
if (user_permissions & PERMISSION_EXECUTE) {
printf("\n--- Access check: EXECUTE permission GRANTED ---\n");
}
// TOGGLE a bit: flip SHARE using XOR
// XOR with 1 flips the bit; XOR with 0 leaves it unchanged
user_permissions ^= PERMISSION_SHARE;
printf("\n--- After toggling SHARE (was ON, now OFF) ---\n");
print_permissions(user_permissions);
return0;
}
Memorise these three lines cold — SET: flags |= mask, CLEAR: flags &= ~mask, CHECK: flags & mask. Interviewers at embedded, systems, and game companies ask you to implement these from scratch. If you can write them without hesitation and explain WHY each one works, you immediately stand out.
📊 Production Insight
Using ~mask on a wider integer type than the flags variable clears unintended bits.
The NOT operator flips ALL bits of its operand, not just the low-order ones.
Rule: always ensure the mask and flags are the same type, or mask the NOT result.
Always mask the result of NOT to the width of your flags variable.
thecodeforge.io
Bitwise Operators C
Bit Shifting for Fast Math and Data Packing
Bit shifts are doing two different jobs in real codebases, and conflating them leads to bugs. The first job is fast arithmetic: shifting left by N multiplies by 2^N, shifting right by N divides by 2^N (integer division). Modern compilers do this automatically for power-of-two constants, but you'll still see explicit shifts in performance-critical hot paths and in code that needs to be portable to compilers without optimisation.
The second job is data packing and unpacking — cramming multiple values into a single integer. This is everywhere in real protocols. A 32-bit RGBA color value packs four 8-bit channels. An IPv4 header packs the version number and IHL into a single byte. An audio sample format encodes bit depth, channel count, and sample rate into a single flags integer.
The formula is always the same: shift a value LEFT into its position to pack it, shift RIGHT and mask to unpack it. The mask (typically 0xFF for an 8-bit field, 0xF for a 4-bit field) ensures you only extract the bits that belong to that field and nothing bleeds in from adjacent fields.
This packing technique is also why bitfields in structs exist — they're syntactic sugar over exactly this pattern.
color_packing.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <stdio.h>
#include <stdint.h> // for uint32_t, uint8_t — always use fixed-width types for bit work
// Pack four 8-bit color channels into one 32-bit unsigned integer
// Layout: [ALPHA: bits 31-24] [RED: bits 23-16] [GREEN: bits 15-8] [BLUE: bits 7-0]
uint32_t pack_rgba(uint8_t red, uint8_t green, uint8_t blue, uint8_t alpha) {
uint32_t packed = 0;
packed |= ((uint32_t)alpha << 24); // shift alpha into the top 8 bits
packed |= ((uint32_t)red << 16); // shift red into bits 23-16
packed |= ((uint32_t)green << 8); // shift green into bits 15-8
packed |= ((uint32_t)blue << 0); // blue sits in the lowest 8bits (no shift needed)
return packed;
}
// Unpack: shift the target field down to bit-0, then mask off everything above it
uint8_t extract_red(uint32_t packed_color) {
return (packed_color >> 16) & 0xFF; // shift down 16, keep only lowest 8 bits
}
uint8_t extract_green(uint32_t packed_color) {
return (packed_color >> 8) & 0xFF; // shift down 8, keep only lowest 8 bits
}
uint8_t extract_blue(uint32_t packed_color) {
return packed_color & 0xFF; // blue is already at bit 0, just mask
}
uint8_t extract_alpha(uint32_t packed_color) {
return (packed_color >> 24) & 0xFF; // shift down 24, keep only lowest 8 bits
}
// Demonstrate fast multiply/divide with shifts
voiddemonstrate_shift_arithmetic(void) {
unsigned int pixel_count = 13;
printf("\n=== Shift Arithmetic Demo ===\n");
printf("pixel_count = %u\n", pixel_count);
printf("pixel_count << 1 = %u (x2 = multiply by 2^1)\n", pixel_count << 1);
printf("pixel_count << 3 = %u (x8 = multiply by 2^3)\n", pixel_count << 3);
printf("pixel_count >> 1 = %u (÷2 = divide by 2^1)\n", pixel_count >> 1);
}
intmain(void) {
// Pack a coral-ish color: R=255, G=100, B=80, A=200
uint8_t r = 255, g = 100, b = 80, a = 200;
uint32_t coral = pack_rgba(r, g, b, a);
printf("=== RGBA Color Packing Demo ===\n");
printf("Input — R:%3u G:%3u B:%3u A:%3u\n", r, g, b, a);
printf("Packed — 0x%08X (decimal: %u)\n", coral, coral);
// Now unpack and verify we get the original values back
printf("\n=== Unpacking Back Out ===\n");
printf("Extracted — R:%3u G:%3u B:%3u A:%3u\n",
extract_red(coral),
extract_green(coral),
extract_blue(coral),
extract_alpha(coral)
);
demonstrate_shift_arithmetic();
return0;
}
Output
=== RGBA Color Packing Demo ===
Input — R:255 G:100 B: 80 A:200
Packed — 0xC8FF6450 (decimal: 3372425296)
=== Unpacking Back Out ===
Extracted — R:255 G:100 B: 80 A:200
=== Shift Arithmetic Demo ===
pixel_count = 13
pixel_count << 1 = 26 (x2 = multiply by 2^1)
pixel_count << 3 = 104 (x8 = multiply by 2^3)
pixel_count >> 1 = 6 (÷2 = divide by 2^1)
⚠ Watch Out: Always Cast Before Shifting
Notice the (uint32_t) casts before each left shift in pack_rgba(). Without them, you're shifting an 8-bit uint8_t, which gets promoted to int (likely 32 bits, but signed). Shifting into the sign bit of a signed int is undefined behaviour in C. Always cast to the target type before a left shift — it costs nothing and prevents nasty surprises on different architectures.
📊 Production Insight
Right shifting a signed negative number is implementation-defined (usually arithmetic shift with sign extension).
On embedded systems with limited registers, this can silently corrupt data.
Rule: always use unsigned types for any shift operation, and cast before shifting.
🎯 Key Takeaway
Left shift multiplies by 2^n; right shift divides by 2^n (unsigned).
Pack multiple values into one integer by shifting each into its bit region.
Always cast to unsigned (e.g., (uint32_t)value) before shifting.
XOR Tricks and the Swap Without a Temp Variable
XOR has a remarkable mathematical property: it's its own inverse. If you XOR a value with a key to 'encode' it, XOR-ing the result with the same key gives you the original value back. This property powers simple (though not secure) data obfuscation, checksum algorithms, and one of the most famous C interview tricks: swapping two variables without a temporary.
The XOR swap works because of three applications of the inverse property. After a ^= b, the variable a holds the combination of both values. After b ^= a, b now holds the original a. After a ^= b, a now holds the original b. It reads like magic until you trace the bits.
But here's the honest truth a senior dev will tell you: don't use XOR swap in production. Modern compilers generate a three-instruction register swap that's faster, and the XOR swap silently corrupts both variables if you accidentally call it with the same memory address (i.e., xor_swap(&value, &value) zeroes the variable). It's a brilliant interview answer — and a terrible production choice.
Where XOR legitimately earns its keep is in parity checking, error detection (CRC algorithms use XOR extensively), and finding the unique element in an array where every other element appears twice — a classic algorithm problem that becomes a single-line solution with XOR.
xor_tricks.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <stdio.h>
#include <stdint.h>
// XOR swap — brilliant to understand, don't use in real code
voidxor_swap(int *value_a, int *value_b) {
if (value_a == value_b) return; // CRITICAL guard — same address would zero both out
*value_a ^= *value_b; // a now holds 'a XOR b'
*value_b ^= *value_a; // b now holds 'b XOR (a XOR b)' = original a
*value_a ^= *value_b; // a now holds '(a XOR b) XOR a' = original b
}
// XOR's inverse property: encode then decode with the same key
voiddemonstrate_xor_encode(void) {
uint8_t original_byte = 0b10110011; // 179 — our data
uint8_t secret_key = 0b01101010; // 106 — our key
uint8_t encoded = original_byte ^ secret_key; // XOR to obscure
uint8_t decoded = encoded ^ secret_key; // XOR again with same key to recover
printf("=== XOR Encoding Demo ===\n");
printf("Original : %3u (0x%02X)\n", original_byte, original_byte);
printf("Key : %3u (0x%02X)\n", secret_key, secret_key);
printf("Encoded : %3u (0x%02X)\n", encoded, encoded);
printf("Decoded : %3u (0x%02X) — matches original: %s\n",
decoded, decoded, (decoded == original_byte) ? "YES" : "NO");
}
// Classic algorithm: find the one number that appears only once
// Every other number in the array appears exactly twice
// XOR-ing a number with itself gives 0; XOR-ing with 0 gives the number itself
intfind_unique_element(int *readings, int count) {
int unique = 0;
for (int index = 0; index < count; index++) {
unique ^= readings[index]; // pairs cancel out (N^N=0); the lone value survives
}
return unique;
}
intmain(void) {
// DemonstrateXOR swap
int temperature_celsius = 37;
int boiling_point = 100;
printf("Before swap: temp=%d, boiling=%d\n", temperature_celsius, boiling_point);
xor_swap(&temperature_celsius, &boiling_point);
printf("After swap: temp=%d, boiling=%d\n\n", temperature_celsius, boiling_point);
demonstrate_xor_encode();
// Sensor readings — every reading appears twice except one faulty sensor IDint sensor_readings[] = {4, 7, 2, 9, 7, 4, 2}; // 9 appears only once
int reading_count = sizeof(sensor_readings) / sizeof(sensor_readings[0]);
int faulty_sensor = find_unique_element(sensor_readings, reading_count);
printf("\n=== Find Unique Sensor ID ===\n");
printf("Faulty sensor ID: %d (appears only once in the data)\n", faulty_sensor);
return0;
}
Output
Before swap: temp=37, boiling=100
After swap: temp=100, boiling=37
=== XOR Encoding Demo ===
Original : 179 (0xB3)
Key : 106 (0x6A)
Encoded : 217 (0xD9)
Decoded : 179 (0xB3) — matches original: YES
=== Find Unique Sensor ID ===
Faulty sensor ID: 9 (appears only once in the data)
🔥Interview Gold: The Unique Element Problem
The 'find the non-duplicate in an array' question is a staple at Google, Meta, and Amazon screening rounds. The brute-force answer is O(n²) with nested loops or O(n) with a hash map. The XOR answer is O(n) time and O(1) space — no extra memory at all. If you can code it in under 60 seconds and explain why it works, you've just separated yourself from 80% of candidates.
📊 Production Insight
XOR swap corrupts both values when called with the same memory address.
Modern CPUs have dedicated register-exchange instructions that are faster and safer.
Rule: don't use XOR swap in production — use a temporary variable or std::swap.
🎯 Key Takeaway
XOR is self-inverse: a ^ b ^ b = a.
Use XOR for toggling flags and finding non-duplicates in O(n) time and O(1) space.
Avoid XOR swap in production — it's a party trick, not a performance optimisation.
Bitwise Operator Precedence and Common Gotchas
Bitwise operators have lower precedence than you think. The most dangerous trap is that ==, !=, and relational operators (<, >, <=, >=) all have higher precedence than &, ^, and |.
So if (flags & MASK == 0) is parsed as if (flags & (MASK == 0)), which compares MASK to 0 (always false, resulting in 0), then ANDs flags with 0 — always false. The fix is parentheses: if ((flags & MASK) == 0).
Similarly, if (flags & MASK != 0) is parsed as if (flags & (MASK != 0)). Since MASK is non-zero, MASK != 0 is true (1), so you effectively check flags & 1 — which is rarely what you want.
Other precedence pitfalls
Shift operators (<<, >>) have higher precedence than AND, OR, XOR. So mask << 2 & 0xFF parses as (mask << 2) & 0xFF, which is usually correct, but x & mask << 1 is x & (mask << 1), not (x & mask) << 1.
The NOT operator ~ binds tightly, so ~mask >> 1 is (~mask) >> 1, not ~(mask >> 1).
Production rule: When in doubt, parenthesize every bitwise expression. It costs nothing and prevents bugs that can take hours to find.
precedence_gotchas.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>
intmain(void) {
unsigned int flags = 0b00001100; // bits 2 and 3 are set
unsigned intMASK = 0b00000100; // bit 2printf("flags = 0x%X, MASK = 0x%X\n", flags, MASK);
// WRONG: == has higher precedence than &
if (flags & MASK == 0) {
printf("Mistake: (flags & (MASK == 0)) is false, so this prints always\n");
}
// CORRECT: explicitly parenthesize
if ((flags & MASK) != 0) {
printf("Correct: bit 2 is set\n");
}
// Another common trap: mixing bitwise and logical operators without parentheses
if (flags & MASK && 1) {
// This is parsed as (flags & MASK) && 1
// Which works by accident, but don't rely on it
printf("Works but ambiguous — always parenthesize\n");
}
// Shift + AND: make intent clear
unsigned int extract = (flags >> 2) & 0x0F; // extract bits 2-5printf("Extracted bits 2-5: 0x%X\n", extract);
return0;
}
Output
flags = 0xC, MASK = 0x4
Mistake: (flags & (MASK == 0)) is false, so this prints always
Correct: bit 2 is set
Works but ambiguous — always parenthesize
Extracted bits 2-5: 0x3
⚠ Precedence Bug: The Mask That Never Matches
We once spent three hours debugging a network packet parser because someone wrote if (flags & MASK == 0x80). The parser passed all unit tests but never matched the flag. The fix: if ((flags & MASK) == 0x80). Parentheses would have saved half a day.
📊 Production Insight
Forgetting precedence leads to silent logic bugs that pass code review because the expression 'looks right'.
These bugs are notoriously hard to catch in testing because they often evaluate to a consistent (but wrong) value.
Rule: always parenthesize bitwise expressions in conditions and compound assignments.
🎯 Key Takeaway
==, !=, <, > have higher precedence than &, |, ^.
<< and >> have higher precedence than &, |, ^.
Always parenthesize: ((flags & MASK) != 0) — never skip the parentheses.
Endianness and Bitwise Operators — Why Your Network Packet Just Corrupted
Bitwise operators work on the value, not the memory layout. That sounds obvious until your cross-platform serialisation corrupts every packet. Endianness — the order bytes are stored in memory — does not affect &, |, ^, or ~. They operate on the CPU register where the byte order is already resolved. Shift operators? Same. 0x0102 << 8 is always 0x0200, regardless of whether your machine stores the high byte first or last.
The trap: unions and pointer casts. If you overlay a uint32_t over a uint8_t array to extract bytes, you get different results on x86 (little-endian) vs ARM in big-endian mode. Write code that reads bytes using shifts and masks, not pointer arithmetic. Serialisation libraries like Protocol Buffers handle this for you — but when you roll your own packet parser, test on both endiannesses in CI. Production will find the one you didn't.
Hardware registers are another minefield. Many embedded peripherals document bit fields assuming big-endian storage. Your compiler might pack them differently. Always verify with a union and a known pattern like 0xAA55 before trusting bitfield structs.
Network byte order is big-endian. If you memcpy() a struct with bitfields, your parser will fail on x86. Always use ntohl/htonl for wire protocol headers.
🎯 Key Takeaway
Shifts and masks are endian-safe. Union casts and pointer aliasing are not.
Undefined Behaviour in Shifts — The Compiler Will Gaslight You
Shifting signed integers left into the sign bit is undefined behaviour. Shifting by more than the bit-width of the type? Also undefined. The compiler can assume you never do this — and optimise your error handling into a no-op.
Example:int32_t x = 1 << 31. On your machine, this might set the sign bit to 1. But the C standard says this is UB. A different compiler version might silently remove the line, or trap on ARM. Clang's UB sanitizer will catch it. Your production crash dump won't.
Right shift of negative signed integers is implementation-defined: arithmetic shift (sign-extending) vs logical shift (zero-fill). Most compilers do arithmetic shift, but if you rely on it for a signed-to-unsigned conversion trick, you're writing a time bomb. Always cast to unsigned before shifting if you care about the bit pattern.
Another footgun:<< with negative shift count is UB. A junior might write x << (y - offset) where y < offset. That's a hard crash — or worse, silent corruption. Validate your shift counts. Every time.
ShiftUB.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// io.thecodeforge — c-cpp tutorial
#include <cstdio>
#include <cstdint>
#include <climits>
intmain() {
int8_t signed_val = -1;
uint8_t unsigned_val = 0xFF;
// Right shift on signed — implementation-definedprintf("Signed >> 4: %d\n", signed_val >> 4);
printf("Unsigned >> 4: %u\n", unsigned_val >> 4);
// UB: shift too far
int32_t x = 1;
// printf("%d\n", x << 33); // UB. Do not uncomment.// Safe: mask before shift
uint32_t status = 0x80000000;
uint32_t bit_31 = (status >> 31) & 1U; // Always 1, defined behaviourprintf("Bit 31: %u\n", bit_31);
return0;
}
Output
Signed >> 4: -1 // Arithmetic shift, sign-extends
Unsigned >> 4: 15 // Logical shift, zero-fills
Bit 31: 1
🔥Senior Shortcut:
Enable -Wall -Wshift-overflow and -fsanitize=undefined in your CI. Also static-assert that shift counts are within range for all constants.
🎯 Key Takeaway
Cast to unsigned before shifting. Always mask result. Never shift by more than (sizeof(type) * CHAR_BIT) - 1.
Syntax: How Bitwise Operators Appear in Real Code
Bitwise operators in C look deceptively simple: they're single symbols like &, |, ^, ~, <<, and >>. But their syntax carries side effects that catch even senior engineers. The bitwise AND (&) is not the logical AND (&&); mixing them with conditionals silently breaks control flow. The bitwise OR (|) and XOR (^) follow infix notation, meaning they sit between two integer expressions. The NOT operator (~) is unary and flips every bit — but on signed integers, two's complement makes ~0 equal to -1, not 0xFFFFFFFF unless you cast to unsigned. Shift operators (<<, >>) have a gotcha: the right operand must be less than the width of the left operand's type, or the behavior is undefined. Assignments like &=, |=, ^=, <<=, >>= are compound operators that modify the left operand in place. Always parenthesize bitwise expressions inside conditionals: (flags & MASK) != 0, never (flags & MASK) — that assigns truthiness to the result instead of a boolean. Use unsigned types (uint32_t) for portability; signed shifts are implementation-defined for right shifts of negative values.
Writing if (flags & MASK) looks correct but evaluates to the integer result of AND, which could be any non-zero value. Always compare explicitly to 0 or 0UL to get a true boolean.
🎯 Key Takeaway
Always parenthesize bitwise operators in conditions; prefer unsigned types for shift operations.
Examples: Bitwise Mastery Through Compilable Snippets
Theory fades fast without concrete examples that compile and produce output. Below are battle-tested patterns for bit twiddling in C. The first example shows how to toggle a permission bit in a Unix-style mode mask — note that ~ on a signed int flips all bits, including the sign, so always cast to unsigned or use a literal suffix. The second example packs two 16-bit values into a 32-bit integer for network packet headers, demonstrating how shifts avoid manual multiplication. The third example performs a XOR swap without a temp variable; it works only for distinct memory locations — swapping with itself zeroes the value. Each snippet uses uint32_t from <stdint.h> for guaranteed width. Run them to see the output; the compiler will not warn you about sign extension unless you enable -Wsign-conversion. Always test on your target architecture; endianness affects how packed bytes are interpreted. The XOR trick also finds duplicate items in arrays: XOR all elements with the expected range. These examples represent daily-use patterns in embedded systems and protocol parsing.
examples_demo.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// io.thecodeforge — c-cpp tutorial
#include <stdint.h>
#include <stdio.h>
intmain() {
// 1. Toggle permission bit (use unsigned)
uint16_t perms = 0b11011010;
perms ^= 1u << 4; // toggle bit 4printf("perms after toggle: 0x%04X\n", perms);
// 2. Pack two 16-bit into 32-bit
uint32_t header = (18u << 16) | 200u;
printf("packed: 0x%08X\n", header); // 0x001200C8// 3. XOR swap (watch aliasing!)
uint32_t a = 5, b = 3;
a ^= b; b ^= a; a ^= b;
printf("swap: %u, %u\n", a, b);
// 4. Clear lowest set bit (Brian Kernighan)
uint32_t v = 40; // 101000
v &= v - 1;
printf("clear LSB: %u\n", v); // 32 (100000)return0;
}
⚠ Production Trap:
XOR swap on the same variable (e.g., swap(&a, &a)) zeroes it. Use a temp variable or check pointers for equality to avoid undefined behavior.
🎯 Key Takeaway
Bitwise examples must compile; test XOR swap only with distinct variables and use unsigned types to avoid sign extension surprises.
● Production incidentPOST-MORTEMseverity: high
A Permission Flag That Silently Cleared Adjacent Bits
Symptom
After revoking write permission, the user mysteriously lost read and execute permissions as well. The UI showed all permissions reset, but only the write toggle was clicked.
Assumption
The developer assumed that permissions &= ~PERMISSION_WRITE would only affect the write bit because they'd defined PERMISSION_WRITE as 1 << 1. They forgot that ~PERMISSION_WRITE on a 32-bit unsigned int becomes 0xFFFFFFFD, which clears every bit that isn't bit 1.
Root cause
PERMISSION_WRITE was defined as (1 << 1) which is an int constant. When used with unsigned int permissions = ..., the ~ operator promotes the mask to unsigned int but flips all 32 bits, not just the lower 8. The AND operation then clears all bits in positions 2-31, destroying other permissions.
Fix
Always mask the result of NOT to the intended width: permissions &= (~PERMISSION_WRITE) & 0xFF; or use fixed-width types like uint8_t for small flag sets so the mask only has 8 bits.
Key lesson
NOT (~) flips ALL bits of the underlying type — not just the bits you care about.
Always AND the result of NOT with a bitmask of your intended width.
Use uint8_t for byte-sized flag variables so ~ behaves predictably.
Production debug guideSymptom → Action guide for the most common bitwise failures4 entries
Symptom · 01
Setting a flag doesn't persist – the bit stays 0 after OR
→
Fix
Print the mask in hex: printf("mask = 0x%X", mask). Verify the mask has exactly one 1 bit in the expected position. Common cause: mask calculated with (1 << n) where n is larger than the type's bit width (shift count >= width is UB).
Symptom · 02
Clearing a flag also clears unrelated flags
→
Fix
Check the type of the flags variable. If it's wider than the mask, ~mask flips all bits. Use explicit masking: flags &= (~mask) & 0xFF or cast mask to the same type.
Symptom · 03
Right shift gives unexpected large number on signed values
→
Fix
Check if the value is signed int or signed char. Right shift of signed negative is implementation-defined (arithmetic shift fills with 1s). Cast to unsigned before shifting.
Symptom · 04
If condition using bitwise AND behaves like OR
→
Fix
Check operator precedence: if (flags & MASK == 0) is parsed as if (flags & (MASK == 0)). Add parentheses: if ((flags & MASK) == 0). Always parenthesize bitwise expressions in conditions.
★ Quick Debug Cheat Sheet for Bitwise BugsFour commands to diagnose bitwise issues in C code without leaving your terminal
Bit mask looks wrong or shift count too large−
Immediate action
Print the mask and shifted values in hex and binary
Commands
printf("mask = 0x%X\n", mask); printf("shifted = 0x%X\n", value << n);
// Use a helper: see print_binary() from the article; compile with -DDEBUG to enable
Fix now
Ensure n < width of value's type in bits; cast to larger unsigned type if needed.
Flags appear corrupt after toggle or clear+
Immediate action
Print the flags variable before and after operation in hex
SET with |=, CLEAR with &= ~mask, CHECK with & mask — appear in virtually every embedded, systems, and game codebase. Knowing them cold is non-negotiable.
2
Always use fixed-width unsigned types (uint8_t, uint16_t, uint32_t from stdint.h) for bit manipulation. Signed types and variable-width types like int are a minefield of undefined and implementation-defined behaviour.
3
Left shift by N multiplies by 2^N; right shift by N divides by 2^N. But these are only safe and predictable on unsigned values
right-shifting signed negatives is implementation-defined in C.
4
XOR's self-inverse property (a ^ b ^ b == a) enables the unique-element algorithm, simple toggle logic, and basic data obfuscation
and it's a favourite interview topic because the elegant O(1)-space solution is non-obvious until you understand what XOR actually does to bits.
5
Operator precedence in bitwise expressions is counterintuitive
==, !=, <, > bind tighter than &, |, ^. Always parenthesize: if ((flags & MASK) != 0).
Common mistakes to avoid
3 patterns
×
Confusing & (bitwise AND) with && (logical AND)
Symptom
Using & in an if-condition instead of && gives nonsensical results. For example, if (permission_flags & is_logged_in) evaluates a bitwise AND of an integer and a boolean — it may pass or fail for completely wrong reasons.
Fix
Use && for boolean logic (true/false conditions) and & exclusively for bit manipulation. If you're checking a flag, if (flags & MASK) is correct — but don't mix flag-checking with logical conditions in one expression without careful parentheses.
×
Right-shifting a signed negative integer
Symptom
On virtually all modern platforms, right-shifting a signed negative value fills with 1-bits (arithmetic shift), not 0-bits (logical shift). So int value = -8; value >> 1; gives -4, not 2147483644. The C standard calls this implementation-defined behaviour.
Fix
Always use unsigned integers (unsigned int, uint32_t, etc.) for bit manipulation. Add a cast if needed: (unsigned int)signed_value >> shift_amount. This guarantees logical (zero-filling) right shift behaviour.
×
Applying NOT (~) without masking the result
Symptom
~ flips ALL bits including the upper bits of a wider type. If your variable is an unsigned int (32 bits) and you do ~0xFF, you get 0xFFFFFF00 (4294967040), not 0x00 as beginners expect. This is the root cause of hundreds of permission-flag bugs.
Fix
Always AND the result of NOT with a bitmask that limits it to your intended width: (~PERMISSION_WRITE) & 0xFF if working with byte-sized flags, or define your masks and variables as the same fixed-width type from the start.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01SENIOR
How would you check if a given integer is a power of two using bitwise o...
Q02JUNIOR
Given a permissions system where each permission is a bit flag, write th...
Q03SENIOR
What's the difference between arithmetic right shift and logical right s...
Q04SENIOR
Write a function to count the number of set bits (population count) in a...
Q01 of 04SENIOR
How would you check if a given integer is a power of two using bitwise operators? Explain why your solution works at the bit level.
ANSWER
A power of two in binary has exactly one 1-bit set. The expression n & (n - 1) clears the lowest set bit. If the result is zero, there was exactly one set bit. So n > 0 && (n & (n - 1)) == 0 correctly identifies powers of two. The n > 0 check is necessary because zero passes the n & (n - 1) test but is not a power of two.
Q02 of 04JUNIOR
Given a permissions system where each permission is a bit flag, write the code to grant a permission, revoke a permission, and check if a permission is active. Walk me through exactly what happens to the bits in each operation.
ANSWER
Grant: permissions |= mask; — OR forces the target bit to 1, leaving others unchanged.
Revoke: permissions &= ~mask; — AND with inverted mask forces target bit to 0, others unchanged.
Check: if (permissions & mask) — AND isolates the target bit; non-zero means set.
The mask is typically 1 << bit_position. The key insight is that these operations are atomic on the bit level without affecting other bits.
Q03 of 04SENIOR
What's the difference between arithmetic right shift and logical right shift? Which does C guarantee for unsigned types, and what does it say about signed types? Why does this matter in practice?
ANSWER
Arithmetic right shift fills vacated bits with the sign bit (MSB), preserving the sign of negative numbers. Logical right shift fills with zeros. C guarantees logical shift for unsigned types (zero-fill). For signed types, the behavior is implementation-defined — most platforms use arithmetic shift. This matters when extracting bit fields: if you right-shift a signed value expecting zeros, you may get ones instead, corrupting the extracted field. Always cast to unsigned before right shift.
Q04 of 04SENIOR
Write a function to count the number of set bits (population count) in an unsigned integer. Explain the different methods and their trade-offs.
ANSWER
Method 1: Brian Kernighan's algorithm — while (n) { count++; n &= n - 1; } loops once per set bit. Efficient for sparse bits.
Method 2: Lookup table for 8-bit chunks — precompute counts for 0-255, then count += table[byte] for each byte in the int. Fast and constant time.
Method 3: Built-in __builtin_popcount() (GCC/Clang) or __popcnt() (MSVC) — uses CPU instruction (POPCNT) if available, fastest.
Trade-offs: Kernighan's is simple and portable but O(set bits). Lookup table is portable and fast but uses 256 bytes. Built-in is fastest but non-portable.
01
How would you check if a given integer is a power of two using bitwise operators? Explain why your solution works at the bit level.
SENIOR
02
Given a permissions system where each permission is a bit flag, write the code to grant a permission, revoke a permission, and check if a permission is active. Walk me through exactly what happens to the bits in each operation.
JUNIOR
03
What's the difference between arithmetic right shift and logical right shift? Which does C guarantee for unsigned types, and what does it say about signed types? Why does this matter in practice?
SENIOR
04
Write a function to count the number of set bits (population count) in an unsigned integer. Explain the different methods and their trade-offs.
SENIOR
FAQ · 4 QUESTIONS
Frequently Asked Questions
01
What is the difference between bitwise AND (&) and logical AND (&&) in C?
Bitwise AND (&) operates on every individual bit of both operands and produces an integer result. Logical AND (&&) treats both sides as true/false conditions, short-circuits if the left side is false, and always returns 0 or 1. Use & for manipulating bit flags and && for combining boolean conditions in if-statements — mixing them up is a classic source of subtle bugs.
Was this helpful?
02
When should I use bitwise operators instead of regular arithmetic?
Use bitwise operators when you need to pack multiple values into a single integer (like flags, pixel channels, or protocol fields), when you're working with hardware registers or memory-mapped I/O, or when you need the absolute fastest possible multiply/divide by a power of two. For general arithmetic, let the compiler do its job — it will use bit shifts internally where appropriate.
Was this helpful?
03
Why does ~0 give -1 in C instead of 0?
Because integers in C are stored in two's complement representation. The bit pattern for 0 is all zeros. NOT flips every bit, giving all ones. In two's complement, all-ones is the representation of -1. This is why ~0 == -1 for any signed integer type, and why you should mask the result of NOT when working with bit flags: use (~YOUR_FLAG & 0xFF) to stay within your intended bit width.
Was this helpful?
04
How do I print the binary representation of an integer in C for debugging?
The simplest portable method is a loop that shifts and masks each bit: for (int i = 7; i >= 0; i--) printf("%d", (value >> i) & 1); for 8 bits. For wider types, adjust the loop range. Many embedded compilers also support %b format specifier, but it's not standard C. The helper function print_binary shown in the examples works on any platform.