Operators: Bitwise and Shift
This chapter covers the bitwise operators (&, |, ^, ~) and the shift operators (<<, >>). These are essential for systems, embedded, and performance work, and their edge cases are a rich source of undefined behavior.
Why This Matters
Bit manipulation is how you talk to hardware, pack data into protocol fields, optimize certain computations, and implement flags and bitsets. Getting the shift rules wrong produces subtle, hard-to-find bugs.
Prerequisites
c.core.9— arithmetic/comparison/logical operators.c.core.3— integer representations.
Core Concept
Bitwise operators
| Operator | Name | Effect | |
|---|---|---|---|
& | bitwise AND | bit is 1 if both corresponding bits are 1 | |
| ` | ` | bitwise OR | bit is 1 if either corresponding bit is 1 |
^ | bitwise XOR | bit is 1 if exactly one corresponding bit is 1 | |
~ | one's complement | flips every bit |
These operate on the integer types after the integer promotions (c.types.promotion).
Shift operators
| Operator | Name | Effect |
|---|---|---|
<< | left shift | shifts bits left, filling with zeros |
>> | right shift | shifts bits right (fill depends on type) |
For unsigned types, x >> n is a logical shift (fills with zeros). For signed non-negative values, x >> n fills with zeros too (because the value is non-negative). For signed negative values, the result of right shift is implementation-defined in C17 and earlier (arithmetic shift fills with ones on most platforms). C23 requires arithmetic shift for negative signed values. STANDARD-VERSION-DEPENDENT
Syntax
unsigned a = 0x0F, b = 0x3C;
a & b; /* 0x0C */
a | b; /* 0x3F */
a ^ b; /* 0x33 */
~a; /* 0xFFFFFFF0 (for 32-bit unsigned int) */
a << 2; /* 0x3C */
b >> 2; /* 0x0F */
Examples
Setting, clearing, toggling, testing bits
#include <stdio.h>
int main(void)
{
unsigned flags = 0;
flags |= (1u << 3); /* set bit 3 */
flags &= ~(1u << 3); /* clear bit 3 */
flags ^= (1u << 3); /* toggle bit 3 */
int is_set = (flags & (1u << 3)) != 0; /* test bit 3 */
printf("%u %d\n", flags, is_set);
return 0;
}
Use 1u (unsigned) for the mask to avoid signed shift issues and to keep the type consistent.
Masking a range of bits
unsigned field = (value >> 4) & 0x3F; /* extract bits 4..9 (6 bits) */
Right shift on signed values
#include <stdio.h>
int main(void)
{
int n = -8;
printf("%d\n", n >> 1); /* implementation-defined for negative */
return 0;
}
On virtually all modern platforms this prints -4 (arithmetic shift), but the C17 standard does not require it. Use unsigned types for shifts whose result you need to be portable.
How It Works
Bitwise operators act on the individual bits of the (promoted) operands. Shift operators move bits by the shift count. Left shift always fills vacated low bits with zeros; right shift fills high bits with zeros for unsigned (and for non-negative signed) values.
Variations
Arithmetic vs. logical shift
- Logical shift: fills with zeros.
- Arithmetic shift: replicates the sign bit (used for signed division by
powers of two).
C guarantees logical behavior for unsigned and non-negative signed right shifts; for negative signed right shifts it is implementation-defined before C23.
Common Mistakes
- Using
1(signed) instead of1uin shift-based masks. - Shifting by a count that is negative or >= the width of the type (UB).
- Confusing
&(bitwise) with&&(logical). - Assuming right shift of a negative signed value is portable.
- Forgetting that
~0is signedint(all ones) unless made unsigned
(~0u).
Undefined Behavior
- Shifting by a negative count, or by a count >= the width of the (promoted)
left operand, is UB. VERIFIED
- Left-shifting a negative signed value is UB (C17); C23 defines it under some
conditions. STANDARD-VERSION-DEPENDENT
- Left-shifting a signed positive value that overflows (produces a value not
representable) is UB.
- Right-shifting a negative signed value is implementation-defined (C17), not
UB, but C23 makes it arithmetic. STANDARD-VERSION-DEPENDENT
Portability
- Use unsigned types for bit manipulation to maximize portability and avoid
UB.
- The number of bits in a type (
CHAR_BIT * sizeof(T)) is implementation-
defined; do not hard-code 32 or 64 for shift bounds.
Under the Hood
Shift instructions (shl, shr, sar, and their ARM equivalents) exist directly in hardware. The compiler emits the appropriate instruction; sar (arithmetic shift right) is used for signed values on x86, while shr (logical) is used for unsigned.
Practical Usage
- Bit flags:
enumwith power-of-two values plus&/|/~. - Bit fields in protocol headers and hardware registers.
- Extracting and packing sub-byte fields.
- Multiplication/division by powers of two (compilers do this automatically;
write the arithmetic, not the shift, for clarity).
Exercises
1. Write functions set_bit, clear_bit, toggle_bit, and test_bit that operate on an unsigned int. Test each. 2. Extract a 4-bit field from a byte and re-insert it, using only masks and shifts. 3. Demonstrate with UBSan what happens when you shift by 32 on a 32-bit int. 4. Write a function that counts set bits (popcount) using a simple loop.
Deep Challenge
Implement a portable popcount and a portable "rotate left" function (rol) that work on unsigned int without relying on the width being 32, and without invoking undefined behavior for any input. Explain each boundary condition you handled.
Related Concepts
c.emb.3— bit manipulation in embedded.c.alg.bit-algo— bit algorithms.c.ds.bitset— bitsets and bitmaps.c.types.promotion— integer promotions.
References
- ISO/IEC 9899:2018 §6.5.7 (shift), §6.5.10–6.5.12 (bitwise).
- GCC/Clang documentation on
-fsanitize=shiftand-fsanitize=undefined.
Verification
- Shift-by-negative or >= width is UB.
VERIFIED - Right shift of negative signed is implementation-defined in C17, arithmetic
in C23. STANDARD-VERSION-DEPENDENT
- Left shift of negative signed is UB in C17.
VERIFIED - No example was executed during generation unless noted. `Execution not
verified.`
Progress
- [ ] Read
- [ ] Understand
- [ ] Complete examples
- [ ] Complete exercises
- [ ] Complete deep challenge
Concept checkboxes
- [ ] Bitwise AND/OR/XOR/NOT
- [ ] Left shift
- [ ] Right shift (logical vs. arithmetic)
- [ ] Set/clear/toggle/test bits
- [ ] Shift UB and portability
Mastery levels
| Concept | Current level (0–8) | Target level |
|---|---|---|
| c.ops.bitwise | 0 | 6 |
| c.ops.shift | 0 | 6 |