Bitwise Operations in C/C++
Why store a bool when 8 of them fit in a byte?
In C, bitwise operators are used to perform operations directly on the binary representations of numbers. These operators work by manipulating individual bits (0s and 1s) in a number.
The following 6 operators are bitwise operators (also known as bit operators as they work at the bit-level). They are used to perform bitwise operations in C.
- The
&(bitwise AND) in C takes two numbers as operands and does AND on every bit of two numbers. The result of AND is 1 only if both bits are 1. - The
|(bitwise OR) in C takes two numbers as operands and does OR on every bit of two numbers. The result of OR is 1 if any of the two bits is 1. - The
^(bitwise XOR) in C takes two numbers as operands and does XOR on every bit of two numbers. The result of XOR is 1 if the two bits are different. - The
<<(left shift) in C takes two numbers, the left shifts the bits of the first operand, and the second operand decides the number of places to shift. - The
>>(right shift) in C takes two numbers, right shifts the bits of the first operand, and the second operand decides the number of places to shift. - The
~(bitwise NOT) in C takes one number and inverts all bits of it.
An example:
int main()
{
// a = 5 (00000101 in 8-bit binary)
// b = 9 (00001001 in 8-bit binary)
unsigned int a = 5, b = 9;
// The result is 00000001 :
printf("a & b = %u\n", a & b);
// The result is 00001101 :
printf("a|b = %u\n", a | b);
// The result is 00001100
printf("a^b = %u\n", a ^ b);
// The result is 11111111111111111111111111111010 (assuming 32-bit unsigned int)
printf("~a = %u\n", a = ~a);
// The result is 00010010
printf("b << 1 = %u\n", b << 1);
// The result is 00000100
printf("b >> 1 = %u\n", b >> 1);
return 0;
}
Output
a & b = 1 a|b = 13 a^b = 12 ~a = 4294967290 b<<1 = 18 b>>1 = 4
Some Noteworthy Facts
Shift Operators: Left-shift (<<) and right-shift (>>) should not be used with negative numbers. Shifting by a negative number or more than the size of the integer leads to undefined behavior. No shift occurs if the number of shifts is 0.
Bitwise OR (|): The OR of two numbers is like adding them if no carry occurs. If there is a carry, the sum is calculated as a | b + a & b.
-
Bitwise XOR (^): Very useful in programming problems. For example, finding the odd occurring number in a set where all other numbers occur even times can be done efficiently using XOR.
#include <stdio.h> int main(void) { int arr[] = {12, 12, 14, 90, 14, 14, 14}; int n = sizeof(arr) / sizeof(arr[0]); int res = 0, i; for (int i = 0; i < n; i++) res ^= arr[i]; printf("%d", res); return 0; }Output
90
-
Logical vs Bitwise: Bitwise operators should not replace logical operators. Logical operators (&&, ||, !) return 0 or 1, while bitwise operators return an integer value.
#include <stdio.h> int main() { int x = 2, y = 5; (x & y) ? printf("True ") : printf("False "); (x && y) ? printf("True ") : printf("False "); return 0; }Output
False True
-
Shift and Arithmetic: Left-shift (<<) is equivalent to multiplication by 2, and right-shift (>>) is equivalent to division by 2 for positive numbers.
#include <stdio.h> int main() { int x = 19; printf("x << 1 = %d\n", x << 1); printf("x >> 1 = %d", x >> 1); return 0; }Output
x << 1 = 38 x >> 1 = 9
-
Check Odd/Even: The AND operator (&) can quickly check if a number is odd or even. (x & 1) is non-zero if x is odd, and 0 if even.
#include <stdio.h> int main() { int x = 19; (x & 1) ? printf("Odd") : printf("Even"); return 0; }Output
Odd
-
Bitwise NOT (~): Should be used carefully. Applying ~ can produce large numbers for unsigned variables or negative numbers for signed variables due to 2’s complement representation.
#include <stdio.h> int main() { unsigned int x = 1; printf("Signed Result %d \n", ~x); printf("Unsigned Result %u", ~x); return 0; }Output
Signed Result -2 Unsigned Result 4294967294
Note The output of the above program is compiler dependent.
Bitmasks
A bitmask is a binary pattern used to select, set, clear, or toggle specific bits in a variable — usually with the help of bitwise operators.
Here's a refresher in C/C++ style syntax:
#define BIT0 (1 << 0) // 00000001 #define BIT1 (1 << 1) // 00000010 #define BIT2 (1 << 2) // 00000100 ...
If you want to store multiple binary flags in one byte, you can use each bit as a switch:
uint8_t flags = 0;
flags |= BIT2; // turn on bit 2
flags &= ~BIT1; // turn off bit 1
if (flags & BIT2) {
// bit 2 is on
}
Bitmasks in Embedded Engineers
Whether you're toggling an LED or decoding a TCP header, bitmasks are a secret weapon that give you both power and performance. In the world of embedded systems, every byte counts — but the beauty of bit-level thinking goes far beyond microcontrollers.
Some Real Use Cases in Embedded Systems:
- GPIO State Control
-
In firmware for microcontrollers (like STM32, ESP32, etc.), you'll often manipulate GPIO ports directly:
GPIO_PORT |= (1 << 5); // Set pin 5 high GPIO_PORT &= ~(1 << 5); // Set pin 5 low
Why? Because hardware registers expose bit fields — one bit per pin. You can't waste time or RAM with structs or variables.
- Flags in Event Loops
-
Instead of polling multiple boolean variables, embedded loops often use a single byte (or
uint32_t, or...) for flag storage:#define EVENT_BUTTON_PRESS (1 << 0) #define EVENT_TIMEOUT (1 << 1) #define EVENT_ERROR (1 << 2) if (event_flags & EVENT_BUTTON_PRESS) { handle_button(); event_flags &= ~EVENT_BUTTON_PRESS; }It’s fast, memory-efficient, and avoids branching where unnecessary.