Senior 10 min · March 06, 2026

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
May 23, 2026
last updated
1,554
articles · all by Naren
 ● 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.

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 NOT in C — Adjacent Permission Bits THECODEFORGE.IO Bitwise NOT in C — Adjacent Permission Bits How ~ clears bits and the pitfalls of operator precedence Bitwise NOT (~) Operation Flips all bits: 0→1, 1→0 Bit Flags & Masking Use AND/OR to set/clear specific bits Clearing Adjacent Bits ~mask clears multiple permission bits Precedence Gotcha ~ binds tighter than &, |, <<, >> Undefined Shift Behavior Shift by >= width or negative is UB Endianness & Networking Bitwise ops are endian-agnostic; shifts aren't ⚠ ~ has higher precedence than & and | Always parenthesize: flags &= ~(BIT_A | BIT_B) THECODEFORGE.IO
thecodeforge.io
Bitwise NOT in C — Adjacent Permission Bits
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.

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 <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>

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.
● 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)

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).

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.
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
May 23, 2026
last updated
1,554
articles · all by Naren
🔥

That's C Basics. Mark it forged?

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

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