C Bitwise Operators
Bitwise operators let C programs work with the individual bits inside integer values. They matter when you store many yes/no settings in one number, read hardware-style flags, build masks, pack data, or need fast powers-of-two arithmetic.
Unlike logical operators such as && and ||, bitwise operators do not ask whether a whole value is true or false. They compare, change, or move each bit position independently.
Overview: How Bitwise Operators Work
Every integer in C is stored as a fixed number of bits. An unsigned int might have 32 bits on a common system, so the value 5 is stored with the low bits 0101: a one in the fours place, a zero in the twos place, and a one in the ones place. Bitwise operators operate on those bit patterns directly.
The main operators are bitwise AND &, bitwise OR |, bitwise XOR ^, bitwise NOT ~, left shift <<, and right shift >>. They work only with integer operands, not float or double. Before the operation, small integer types such as char and short are promoted, usually to int. That promotion is one reason bitwise work is often clearest with unsigned int or fixed-width unsigned types from <stdint.h>.
& keeps a bit only when both operands have a one in that position. | sets a bit when either operand has a one. ^ sets a bit when the two operands differ. ~ flips every bit in its promoted operand. << moves bits left and fills the low bits with zeros. For unsigned values, >> moves bits right and fills high bits with zeros.
Bitwise operators are commonly used with masks. A mask is a value chosen because certain bit positions are one. For example, 1u << 2 creates a mask for bit 2, which has the value 4. You can turn that bit on with value | mask, test it with value & mask, toggle it with value ^ mask, and clear it with value & ~mask.
Syntax
The common forms are:
| Operator | Form | Meaning |
|---|---|---|
& |
a & b |
Bitwise AND: keep bits that are one in both operands. |
| |
a | b |
Bitwise OR: set bits that are one in either operand. |
^ |
a ^ b |
Bitwise XOR: set bits that are different between operands. |
~ |
~a |
Bitwise complement: flip every bit in the promoted integer. |
<< |
a << n |
Shift bits left by n positions. |
>> |
a >> n |
Shift bits right by n positions. |
| Compound forms | x &= m, x |= m, x ^= m, x <<= n, x >>= n |
Compute the bitwise operation and store the result back in x. |
Shift counts must be valid. Shifting by a negative amount or by an amount greater than or equal to the width of the promoted left operand has undefined behavior. For a 32-bit unsigned int, x << 31 can be valid, but x << 32 is not.
Examples
Combining and Comparing Bits
#include <stdio.h>
int main(void) {
unsigned int a = 12; /* binary 1100 */
unsigned int b = 10; /* binary 1010 */
printf("a & b = %u\n", a & b);
printf("a | b = %u\n", a | b);
printf("a ^ b = %u\n", a ^ b);
printf("~a low 4 bits = %u\n", (~a) & 15u);
return 0;
}
Output:
a & b = 8
a | b = 14
a ^ b = 6
~a low 4 bits = 3
12 is 1100 in the low four bits, and 10 is 1010. AND keeps only 1000, OR produces 1110, and XOR produces 0110. The complement flips all bits in the full unsigned int, so the example masks with 15u to display only the low four bits.
Using Flags in One Integer
#include <stdio.h>
int main(void) {
const unsigned int READ = 1u << 0;
const unsigned int WRITE = 1u << 1;
const unsigned int EXECUTE = 1u << 2;
const unsigned int ARCHIVE = 1u << 3;
unsigned int permissions = 0;
permissions |= READ;
permissions |= WRITE;
permissions |= ARCHIVE;
printf("permissions = %u\n", permissions);
printf("can write = %u\n", (permissions & WRITE) != 0u);
printf("can execute = %u\n", (permissions & EXECUTE) != 0u);
permissions &= ~WRITE;
permissions ^= EXECUTE;
printf("after changes = %u\n", permissions);
printf("can write = %u\n", (permissions & WRITE) != 0u);
printf("can execute = %u\n", (permissions & EXECUTE) != 0u);
return 0;
}
Output:
permissions = 11
can write = 1
can execute = 0
after changes = 13
can write = 0
can execute = 1
Each named constant owns one bit. |= turns flags on without disturbing the other bits. &= ~WRITE clears the write bit because ~WRITE has zeros at that bit position and ones elsewhere. ^= EXECUTE toggles execute: if it was off, it becomes on; if it was on, it would become off.
Shifting to Pack and Unpack Values
#include <stdio.h>
#include <stdint.h>
int main(void) {
uint32_t red = 120u;
uint32_t green = 45u;
uint32_t blue = 200u;
uint32_t color = (red << 16) | (green << 8) | blue;
printf("packed color = 0x%06X\n", color);
printf("red = %u\n", (color >> 16) & 0xFFu);
printf("green = %u\n", (color >> 8) & 0xFFu);
printf("blue = %u\n", color & 0xFFu);
return 0;
}
Output:
packed color = 0x782DC8
red = 120
green = 45
blue = 200
This program packs three 8-bit color channels into one 24-bit pattern inside a uint32_t. Red is shifted into bits 16 through 23, green into bits 8 through 15, and blue stays in bits 0 through 7. To unpack, the program shifts the desired channel down to the low bits and masks with 0xFFu.
How It Works Step by Step
Consider permissions &= ~WRITE. First, WRITE is a mask with one bit set. Next, ~WRITE flips that mask, creating a value with a zero where the write bit is and ones in the other positions. Then permissions & ~WRITE keeps every permission bit except the write bit. Finally, &= stores the result back into permissions.
For color = (red << 16) | (green << 8) | blue, each channel is first moved to its assigned bit range. OR then combines the ranges. Because the ranges do not overlap, no channel overwrites another. The parentheses are not just decorative; they make the intended grouping clear when shifts and OR appear together.
Under the hood, a CPU can often execute these operations with direct machine instructions. That makes bitwise operators useful in systems programming, compression, networking, graphics, embedded programming, and performance-sensitive code. Even so, clarity matters more than cleverness. A named mask such as CAN_WRITE is much easier to maintain than a bare number such as 2.
Common Mistakes
Using & When You Mean &&
& is bitwise AND. && is logical AND. The expression (age >= 18) && has_id is a condition. The expression permissions & WRITE is a bit test. Although (a > 0) & (b > 0) may appear to work because comparisons produce 0 or 1, it does not short-circuit, so the right side is always evaluated.
Forgetting Parentheses in Bit Tests
Write (permissions & WRITE) != 0u, not permissions & WRITE != 0u. The second form is easy to misread because comparison and bitwise precedence interact. Parentheses make the test explicit: first isolate the bit, then compare the result with zero.
Shifting Signed or Negative Values
Prefer unsigned operands for shifts. Left-shifting a negative signed value has undefined behavior, and right-shifting a negative signed value is implementation-defined. Use 1u << bit for masks instead of 1 << bit, and store bit patterns in unsigned types when possible.
Assuming ~x Only Flips the Bits You Can See
If x is 12, ~x does not simply become 3. It flips every bit in the promoted integer width. To work with only a field of bits, mask the result: (~x) & 15u keeps only the low four bits.
Best Practices
- Use unsigned integer types for masks, flags, and shifts.
- Create masks with
1u << bitso the left operand is unsigned. - Give every mask a meaningful name, such as
READorHAS_ERROR. - Use
(value & mask) != 0uwhen converting a bit test to a true-or-false result. - Check that shift counts are nonnegative and smaller than the width of the promoted left operand.
- Use hexadecimal constants such as
0xFFufor byte-sized masks because they line up naturally with groups of four bits. - Add parentheses around shifts, masks, and comparisons when the expression would otherwise require the reader to remember precedence rules.
Practice Exercises
- Define three flags named
IS_VISIBLE,IS_SELECTED, andIS_LOCKED. Turn on visible and selected, print each flag test, then clear selected. - Write a program that stores an 8-bit value in an
unsigned intand prints the high nibble and low nibble. Hint: use>> 4and& 0xFu. - Pack two numbers from
0to255into oneunsigned int, then unpack and print them again.
Summary
- Bitwise operators work on individual integer bits, not whole truth values.
&,|, and^combine bit patterns position by position.~flips all bits in the promoted operand, so masks are often needed afterward.<<and>>move bits left or right, but invalid shift counts cause undefined behavior.- Flags use one bit per setting and are commonly turned on with OR, tested with AND, cleared with AND plus complement, and toggled with XOR.
- Unsigned types make bitwise code more predictable, especially for shifts.
- Clear names and parentheses are essential because bitwise expressions can become dense quickly.
