Home C / C++ Bitwise NOT in C — Why ~ Clears Adjacent Permission Bits
Intermediate 11 min · March 06, 2026
Bitwise Operators in C

Bitwise NOT in C — Why ~ Clears Adjacent Permission Bits

The NOT operator flips all 32 bits, not just your flag byte.

N
Naren Founder & Principal Engineer

20+ years shipping performance-critical C and C++ systems. Drawn from code that ran under real load.

Follow
Production
production tested
July 18, 2026
last updated
2,466
articles · all by Naren
Before you start⏱ 25 min
  • Solid grasp of fundamentals
  • Comfortable reading code examples
  • Basic production concepts
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • 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
ChromeFirefoxSafariEdge

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).
bitwise-operators-c Bitwise Operator Hierarchy in C Layered view of operators, precedence, and common pitfalls Application Layer Permission Systems | Data Packing | Cryptography Bitwise Operators & (AND) | | (OR) | ^ (XOR) Precedence Gotchas ~ binds tighter than & | & binds tighter than | | Shifts lower than arithmetic Undefined Behavior Risks Shift by >= width of type | Negative shift count | Signed overflow in shifts Endianness Considerations Network byte order | Bit order vs byte order | Masking across bytes THECODEFORGE.IO
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
void print_binary(const char *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);
}

int main(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            = 46

    printf("=== 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 1
    print_binary("a ^ b",            lights_a ^ lights_b);

    printf("\n=== NOT — flip everything ===\n");
    // Every 0 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 4
    print_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 2
    print_binary("a >> 1",           lights_a >> 1);

    return 0;
}
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.

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 char PermissionSet;

void print_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"   : "-----"
    );
}

int main(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);

    return 0;
}
Output
--- Initial state ---
Permissions: [----] [-----] [-------] [------] [-----]
--- After granting READ and WRITE ---
Permissions: [READ] [WRITE] [-------] [------] [-----]
--- After granting EXECUTE and SHARE ---
Permissions: [READ] [WRITE] [EXECUTE] [------] [SHARE]
--- After revoking WRITE ---
Permissions: [READ] [-----] [EXECUTE] [------] [SHARE]
--- Access check: EXECUTE permission GRANTED ---
--- After toggling SHARE (was ON, now OFF) ---
Permissions: [READ] [-----] [EXECUTE] [------] [-----]
🔥Interview Gold: The Three Bitwise Idioms
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.
🎯 Key Takeaway
SET: flags |= mask; CLEAR: flags &= ~mask; CHECK: flags & mask.
The mask is a power of two: 1 << bit_position.
Always mask the result of NOT to the width of your flags variable.
bitwise-operators-c Bitwise NOT vs Bitwise AND for Clearing Bits Comparing two approaches to clear adjacent permission bits Using ~ (NOT) + & (AND) Using & (AND) with Inverted Ma Operation flags & ~mask flags & (mask ^ 0xFF) (manual inversion) Readability Clear and idiomatic for bit masking Less intuitive, error-prone Portability Works on all integer types, but sign mat Requires knowledge of bit width Performance Single instruction on most CPUs May need extra instruction for XOR Common Mistake Forgetting ~ has higher precedence than Incorrect mask width leading to overflow THECODEFORGE.IO
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 8 bits (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
void demonstrate_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);
}

int main(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();

    return 0;
}
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
void xor_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
void demonstrate_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
int find_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;
}

int main(void) {
    // Demonstrate XOR 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 ID
    int 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);

    return 0;
}
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>

int main(void) {
    unsigned int flags = 0b00001100; // bits 2 and 3 are set
    unsigned int MASK  = 0b00000100; // bit 2

    printf("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-5
    printf("Extracted bits 2-5: 0x%X\n", extract);

    return 0;
}
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.

EndianMistmatch.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
26
27
28
// io.thecodeforge — c-cpp tutorial

#include <cstdio>
#include <cstdint>

int main() {
    uint32_t packet_header = 0x12345678;
    
    // Portable byte extraction — works on every endianness
    uint8_t b0 = (packet_header >> 24) & 0xFF;
    uint8_t b1 = (packet_header >> 16) & 0xFF;
    uint8_t b2 = (packet_header >> 8)  & 0xFF;
    uint8_t b3 = packet_header & 0xFF;
    
    printf("Portable: 0x%02X %02X %02X %02X\n", b0, b1, b2, b3);
    
    // Non-portable — union with uint8_t array
    union {
        uint32_t u32;
        uint8_t  bytes[4];
    } hack;
    hack.u32 = packet_header;
    
    printf("Union:    0x%02X %02X %02X %02X\n", 
           hack.bytes[0], hack.bytes[1], 
           hack.bytes[2], hack.bytes[3]);
    return 0;
}
Output
Portable: 0x12 34 56 78
Union: 0x78 56 34 12 // Only on little-endian x86
⚠ Production Trap:
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>

int main() {
    int8_t  signed_val = -1;
    uint8_t unsigned_val = 0xFF;
    
    // Right shift on signed — implementation-defined
    printf("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 behaviour
    printf("Bit 31: %u\n", bit_31);
    
    return 0;
}
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.

syntax_example.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// io.thecodeforge — c-cpp tutorial
#include <stdint.h>
#include <stdio.h>

int main() {
    uint8_t flags = 0b10101100; // flags
    uint8_t mask  = 0b00001111; // mask lower nibble

    // Correct syntax: parenthesize bitwise in condition
    if ((flags & mask) != 0) {
        printf("Lower nibble has bits set\n");
    }

    // Compound assignment
    flags |= 0b00000011; // set bits 0-1
    printf("flags = 0x%02X\n", flags);

    // Warning: ~ on signed
    int x = 0;
    unsigned int y = ~0U;
    printf("~0 = %d, ~0U = %u\n", ~x, y);
    return 0;
}
⚠ Production Trap:
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 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>

int main() {
    // 1. Toggle permission bit (use unsigned) 
    uint16_t perms = 0b11011010;
    perms ^= 1u << 4;          // toggle bit 4
    printf("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)
    return 0;
}
⚠ 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.

Endianness: How Bitwise Ops Behave Across Architectures

Endianness refers to the byte ordering of multi-byte data types in memory. Little-endian systems (e.g., x86) store the least significant byte first, while big-endian systems (e.g., some network protocols) store the most significant byte first. Bitwise operators operate on the binary representation of values at the abstract machine level, not on the memory layout. Therefore, the result of bitwise operations is independent of endianness when applied to integer values in registers. However, endianness matters when you interpret the same bytes as different types or when you access individual bytes via pointers or unions. For example, consider a 32-bit integer with value 0x12345678. On a little-endian machine, the memory bytes are 0x78, 0x56, 0x34, 0x12; on big-endian, they are 0x12, 0x34, 0x56, 0x78. If you apply a bitwise NOT to the integer, the result is the same on both architectures: ~0x12345678 = 0xEDCBA987. But if you cast the integer to a char* and read the first byte, you get different values. This is critical when serializing data for network packets or file formats. Always use functions like htonl() and ntohl() to convert between host and network byte order. For bitwise operations on multi-byte fields, endianness does not affect the arithmetic result, but it does affect how you interpret raw memory dumps. In practice, avoid relying on the memory layout of integers; use shift and mask operations to extract bytes portably.

endianness.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdint.h>

int main() {
    uint32_t value = 0x12345678;
    uint32_t not_value = ~value;
    printf("value = 0x%08X\n", value);
    printf("~value = 0x%08X\n", not_value);
    
    // Check first byte via pointer
    unsigned char *bytes = (unsigned char *)&value;
    printf("First byte: 0x%02X (endian-dependent)\n", bytes[0]);
    
    // Portable extraction
    uint32_t first_byte = (value >> 0) & 0xFF;  // always least significant byte
    printf("Portable first byte: 0x%02X\n", first_byte);
    return 0;
}
⚠ Endianness Pitfall in Network Protocols
📊 Production Insight
In cross-platform code, avoid type-punning via pointers or unions to inspect bytes. Use portable shift-and-mask operations to extract or set specific bytes.
🎯 Key Takeaway
Bitwise operations on integer values are endian-independent; endianness only affects memory representation and byte-level access.

Bit Fields in Structures: ABI and Portability

Bit fields in C allow you to specify the exact number of bits for a member within a structure. They are useful for packing flags or hardware registers. However, bit fields are highly non-portable due to implementation-defined behavior. The C standard leaves the following aspects unspecified: the allocation order of bit fields within a storage unit (whether they are assigned from most significant bit to least or vice versa), whether a bit field can straddle a storage unit boundary, and the alignment of the storage unit itself. For example, consider a struct with two bit fields: int a:3; int b:5;. On some compilers, a occupies the low-order 3 bits and b the next 5 bits; on others, the order is reversed. This can lead to ABI incompatibilities when libraries compiled with different compilers exchange such structs. Additionally, the signedness of plain int bit fields is implementation-defined (some compilers treat them as signed, others as unsigned). To improve portability, always use unsigned int or explicitly signed/unsigned types. Avoid bit fields for data exchange across compilation units or platforms; use explicit bit masking with shift operators instead. For hardware registers, consult the compiler documentation for the exact layout. In C23, the new header provides standardized bit manipulation functions, but it does not address bit field portability. For production code, prefer manual bit manipulation over bit fields for any external interface.

bitfields.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
#include <stdio.h>
#include <stdint.h>

// Non-portable bit field struct
struct Flags {
    unsigned int read : 1;
    unsigned int write : 1;
    unsigned int exec : 1;
    unsigned int reserved : 5;
};

// Portable alternative using explicit masks
#define FLAG_READ  (1U << 0)
#define FLAG_WRITE (1U << 1)
#define FLAG_EXEC  (1U << 2)

int main() {
    struct Flags f = {1, 0, 1};
    printf("Bit field size: %zu bytes\n", sizeof(f));
    
    uint8_t flags = 0;
    flags |= FLAG_READ;
    flags |= FLAG_EXEC;
    printf("Portable flags: 0x%02X\n", flags);
    return 0;
}
⚠ Bit Fields Are Not Portable
📊 Production Insight
When defining hardware register layouts, rely on compiler-specific attributes like __attribute__((packed)) and verify the layout with static assertions. For portable code, avoid bit fields entirely.
🎯 Key Takeaway
Bit fields are convenient but non-portable; for cross-platform code, use manual bit manipulation with masks and shifts.

Bit Manipulation with in C23

The C23 standard introduced the header, which provides a set of portable functions for common bit manipulation tasks. This includes counting leading zeros (stdc_leading_zeros), counting trailing zeros (stdc_trailing_zeros), counting ones (stdc_popcount), finding the first set bit (stdc_first_leading_one), and many more. These functions are type-generic and work with unsigned integer types up to 64 bits. For example, to count the number of 1 bits in a 32-bit value, you can use stdc_popcount(value). This replaces non-standard compiler builtins like __builtin_popcount or platform-specific intrinsics. The functions are designed to be efficient and may map directly to CPU instructions (e.g., POPCNT on x86). Using improves code portability and readability. However, note that these functions are only available in C23 and later; older compilers may not support them. For production code targeting C23, prefer these standardized functions over manual loops or compiler-specific extensions. The header also includes functions for endian conversion (stdc_byteswap) and bit rotation (stdc_rotl, stdc_rotr). This significantly reduces the need for platform-specific code. When using bitwise NOT in combination with these functions, remember that ~ is still the standard bitwise complement operator. For example, to get the number of zeros in a value, you can compute stdc_popcount(~value). The new standard library functions complement the existing bitwise operators, making bit manipulation safer and more portable.

stdbit_example.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <stdbit.h>

int main() {
    unsigned int value = 0b10110110;
    printf("popcount(%u) = %u\n", value, stdc_popcount(value));
    printf("leading zeros = %u\n", stdc_leading_zeros(value));
    printf("trailing zeros = %u\n", stdc_trailing_zeros(value));
    printf("first leading one = %u\n", stdc_first_leading_one(value));
    
    // Using bitwise NOT with popcount
    printf("popcount(~value) = %u\n", stdc_popcount(~value));
    return 0;
}
🔥C23 Standardizes Bit Manipulation
📊 Production Insight
When migrating to C23, replace compiler-specific intrinsics (e.g., __builtin_popcount) with stdc_popcount and similar functions. This ensures portability across compilers and architectures.
🎯 Key Takeaway
C23's <stdbit.h> offers standardized functions for bit counting, rotation, and endian conversion, reducing platform-specific code.
● 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
Commands
printf("Before: 0x%X\n", flags); printf("After: 0x%X\n", flags);
Verify the mask: printf("mask = 0x%X, ~mask = 0x%X\n", mask, (unsigned int)~mask);
Fix now
Mask the NOT: flags &= (~mask) & 0xFF; // for 8-bit flags
Signed right shift gives unexpected negative or huge positive+
Immediate action
Check the type of the variable being shifted
Commands
printf("value = %d (0x%X)\n", value, value); printf("value >> 1 = %d (0x%X)\n", value >> 1, value >> 1);
printf("(unsigned)value >> 1 = %u\n", (unsigned)value >> 1);
Fix now
Always cast to unsigned before right shifting: ((unsigned)value) >> n
Conditional expression using & yields wrong true/false+
Immediate action
Check parentheses and operator precedence
Commands
// Use explicit parentheses: if ((flags & MASK) != 0)
// Or use logical AND if you mean boolean: if (flags && MASK) — but that's wrong for bit check
Fix now
Always parenthesize: if ((flags & MASK)) or if ((flags & MASK) != 0)
Bitwise Operator Reference
OperatorSymbolWhat It DoesCommon Use CaseResult on 0b1010 & 0b1100
AND&Bit is 1 only if BOTH inputs are 1Masking / checking flags0b1000 (8)
OR|Bit is 1 if EITHER input is 1Setting flags0b1110 (14)
XOR^Bit is 1 if inputs DIFFERToggling flags, finding duplicates0b0110 (6)
NOT~Flips every bitInverting a mask for CLEAR operation~0b1010 = 0b0101 (varies by type)
Left Shift<<Shifts bits left, fills with 0sMultiply by 2^n, building masks0b1010 << 1 = 0b10100 (20)
Right Shift>>Shifts bits rightDivide by 2^n, extracting fields0b1010 >> 1 = 0b0101 (5)
⚙ Quick Reference
12 commands from this guide
FileCommand / CodePurpose
bitwise_basics.cvoid print_binary(const char *label, unsigned int value) {The Six Bitwise Operators and What They Actually Do
permission_flags.ctypedef unsigned char PermissionSet;Bit Flags and Masking
color_packing.cuint32_t pack_rgba(uint8_t red, uint8_t green, uint8_t blue, uint8_t alpha) {Bit Shifting for Fast Math and Data Packing
xor_tricks.cvoid xor_swap(int *value_a, int *value_b) {XOR Tricks and the Swap Without a Temp Variable
precedence_gotchas.cint main(void) {Bitwise Operator Precedence and Common Gotchas
EndianMistmatch.cppint main() {Endianness and Bitwise Operators
ShiftUB.cppint main() {Undefined Behaviour in Shifts
syntax_example.cppint main() {Syntax
examples_demo.cppint main() {Examples
endianness.cint main() {Endianness
bitfields.cstruct Flags {Bit Fields in Structures
stdbit_example.cint main() {Bit Manipulation with in C23

Key takeaways

1
The three bitwise idioms
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).
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.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
What is the difference between bitwise AND (&) and logical AND (&&) in C?
02
When should I use bitwise operators instead of regular arithmetic?
03
Why does ~0 give -1 in C instead of 0?
04
How do I print the binary representation of an integer in C for debugging?
N
Naren Founder & Principal Engineer

20+ years shipping performance-critical C and C++ systems. Drawn from code that ran under real load.

Follow
Verified
production tested
July 18, 2026
last updated
2,466
articles · all by Naren
🔥

That's C Basics. Mark it forged?

11 min read · try the examples if you haven't

Previous
Preprocessor Directives in C
14 / 24 · C Basics
Next
Dynamic Arrays in C