Intermediate 6 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
Plain-English first. Then code. Then the interview question.
About
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

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.

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.

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.

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.

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.

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.

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

  • 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.
  • 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.
  • 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.
  • 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.
  • Operator precedence in bitwise expressions is counterintuitive: ==, !=, <, > bind tighter than &, |, ^. Always parenthesize: if ((flags & MASK) != 0).

Common Mistakes to Avoid

  • 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 Questions on This Topic

  • QHow would you check if a given integer is a power of two using bitwise operators? Explain why your solution works at the bit level.Mid-levelReveal
    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.
  • QGiven a permissions system where each permission is a bit flag, write the code to grant a permission, revoke a permission, and check if a permission is active. Walk me through exactly what happens to the bits in each operation.JuniorReveal
    Grant: permissions |= mask; — OR forces the target bit to 1, leaving others unchanged. Revoke: permissions &= ~mask; — AND with inverted mask forces target bit to 0, others unchanged. Check: if (permissions & mask) — AND isolates the target bit; non-zero means set. The mask is typically 1 << bit_position. The key insight is that these operations are atomic on the bit level without affecting other bits.
  • QWhat's the difference between arithmetic right shift and logical right shift? Which does C guarantee for unsigned types, and what does it say about signed types? Why does this matter in practice?SeniorReveal
    Arithmetic right shift fills vacated bits with the sign bit (MSB), preserving the sign of negative numbers. Logical right shift fills with zeros. C guarantees logical shift for unsigned types (zero-fill). For signed types, the behavior is implementation-defined — most platforms use arithmetic shift. This matters when extracting bit fields: if you right-shift a signed value expecting zeros, you may get ones instead, corrupting the extracted field. Always cast to unsigned before right shift.
  • QWrite a function to count the number of set bits (population count) in an unsigned integer. Explain the different methods and their trade-offs.SeniorReveal
    Method 1: Brian Kernighan's algorithm — while (n) { count++; n &= n - 1; } loops once per set bit. Efficient for sparse bits. Method 2: Lookup table for 8-bit chunks — precompute counts for 0-255, then count += table[byte] for each byte in the int. Fast and constant time. Method 3: Built-in __builtin_popcount() (GCC/Clang) or __popcnt() (MSVC) — uses CPU instruction (POPCNT) if available, fastest. Trade-offs: Kernighan's is simple and portable but O(set bits). Lookup table is portable and fast but uses 256 bytes. Built-in is fastest but non-portable.

Frequently Asked Questions

What is the difference between bitwise AND (&) and logical AND (&&) in C?

Bitwise AND (&) operates on every individual bit of both operands and produces an integer result. Logical AND (&&) treats both sides as true/false conditions, short-circuits if the left side is false, and always returns 0 or 1. Use & for manipulating bit flags and && for combining boolean conditions in if-statements — mixing them up is a classic source of subtle bugs.

When should I use bitwise operators instead of regular arithmetic?

Use bitwise operators when you need to pack multiple values into a single integer (like flags, pixel channels, or protocol fields), when you're working with hardware registers or memory-mapped I/O, or when you need the absolute fastest possible multiply/divide by a power of two. For general arithmetic, let the compiler do its job — it will use bit shifts internally where appropriate.

Why does ~0 give -1 in C instead of 0?

Because integers in C are stored in two's complement representation. The bit pattern for 0 is all zeros. NOT flips every bit, giving all ones. In two's complement, all-ones is the representation of -1. This is why ~0 == -1 for any signed integer type, and why you should mask the result of NOT when working with bit flags: use (~YOUR_FLAG & 0xFF) to stay within your intended bit width.

How do I print the binary representation of an integer in C for debugging?

The simplest portable method is a loop that shifts and masks each bit: for (int i = 7; i >= 0; i--) printf("%d", (value >> i) & 1); for 8 bits. For wider types, adjust the loop range. Many embedded compilers also support %b format specifier, but it's not standard C. The helper function print_binary shown in the examples works on any platform.

🔥

That's C Basics. Mark it forged?

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

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