Chapter 11: Bitwise Operations: Working at the Bit Level
Up until now, we’ve mostly treated data as whole numbers, characters, or floating-point values. However, at the lowest level, computers store and process all information as sequences of bits (binary digits, 0s and 1s).
Bitwise operations allow you to manipulate these individual bits within integer types. This is a fundamental skill for:
- Low-level programming: Interacting directly with hardware registers.
- Embedded systems: Controlling peripherals, setting flags.
- Data compression and encryption: Efficiently packing/unpacking data.
- Optimized algorithms: Sometimes, bitwise operations can be significantly faster than arithmetic operations.
- Representing sets/flags: Using individual bits to represent a collection of boolean states.
In this chapter, we will explore C’s bitwise operators and learn how to use them effectively.
11.1 Understanding Bits and Bytes
A bit is the smallest unit of digital information, representing a binary value of either 0 or 1.
A byte is typically 8 bits. Data types in C, like char (1 byte), int (typically 4 bytes), long long (8 bytes), are composed of multiple bytes, and thus many bits.
When performing bitwise operations, it’s often helpful to think of numbers in their binary representation.
Decimal to Binary Conversion Examples:
0(decimal) =00000000(binary for 1 byte)1(decimal) =000000015(decimal) =0000010110(decimal) =00001010255(decimal) =11111111
11.2 Bitwise Logical Operators
These operators perform logical operations on corresponding bits of their operands.
11.2.1 Bitwise AND (&)
The bitwise AND operator compares two bits at each position. If both bits are 1, the result is 1; otherwise, it’s 0.
Truth Table:
| Bit A | Bit B | A & B |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 0 |
| 1 | 0 | 0 |
| 1 | 1 | 1 |
Example:
0101 (5 decimal)
& 0011 (3 decimal)
-------
0001 (1 decimal)
Common Uses:
- Checking if a specific bit is set:
if ((number & (1 << bit_position)) != 0) - Clearing a specific bit:
number = number & ~(1 << bit_position);(Using~for NOT) - Masking: Isolating a specific set of bits.
11.2.2 Bitwise OR (|)
The bitwise OR operator compares two bits at each position. If at least one bit is 1, the result is 1; otherwise, it’s 0.
Truth Table:
| Bit A | Bit B | A | B |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 1 |
Example:
0101 (5 decimal)
| 0011 (3 decimal)
-------
0111 (7 decimal)
Common Uses:
- Setting a specific bit:
number = number | (1 << bit_position); - Combining flags: If different features are represented by bits,
A | Bcombines them.
11.2.3 Bitwise XOR (^)
The bitwise XOR (exclusive OR) operator compares two bits at each position. If the bits are different, the result is 1; otherwise, it’s 0.
Truth Table:
| Bit A | Bit B | A ^ B |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
Example:
0101 (5 decimal)
^ 0011 (3 decimal)
-------
0110 (6 decimal)
Common Uses:
- Toggling a specific bit:
number = number ^ (1 << bit_position); - Swapping two numbers without a temporary variable:
a = a ^ b; b = a ^ b; a = a ^ b;(Though not always more efficient or readable). - Detecting changes: If
A ^ Bis non-zero, thenAandBare different.
11.2.4 Bitwise NOT (~)
The bitwise NOT operator (unary operator) inverts all the bits of its operand. 0 becomes 1, and 1 becomes 0.
Truth Table:
| Bit A | ~A |
|---|---|
| 0 | 1 |
| 1 | 0 |
Example (assuming 1-byte integers for simplicity):
~ 00000101 (5 decimal)
-----------
11111010 (250 decimal, if unsigned)
For signed integers, the ~ operator (one’s complement) can be tricky because of two’s complement representation. For signed int x = 5;, ~x would typically be -6. This is because ~x flips all bits, and for two’s complement, ~x is equivalent to (-x - 1).
11.3 Bitwise Shift Operators
These operators move bits to the left or right within an integer.
11.3.1 Left Shift (<<)
The left shift operator shifts bits to the left by a specified number of positions. Empty bit positions on the right are filled with 0s.
Syntax: operand << num_bits
Example:
00000101 (5 decimal)
<< 2 (shift left by 2 bits)
-------
00010100 (20 decimal)
Each left shift by one position is equivalent to multiplying the number by 2.
5 << 1 = 10
5 << 2 = 20
5 << 3 = 40
Common Uses:
- Multiplying by powers of 2:
x << nisx * (2^n). - Creating bitmasks:
1 << bit_positioncreates a mask with only that bit set.
11.3.2 Right Shift (>>)
The right shift operator shifts bits to the right by a specified number of positions.
Syntax: operand >> num_bits
Behavior for Signed vs. Unsigned Integers:
- Unsigned integers: Empty bit positions on the left are filled with
0s (logical right shift). - Signed integers: The behavior for filling the empty bit positions on the left is implementation-defined. Some systems fill with
0s (logical shift), others fill with the sign bit (arithmetic shift) to preserve the sign. It’s safer to use unsigned types for right-shifting if you need predictable behavior.
Example (Unsigned):
00001010 (10 decimal)
>> 1 (shift right by 1 bit)
-------
00000101 (5 decimal)
Example (Signed, typically arithmetic shift):
10000000 (-128, assuming 8-bit signed, two's complement)
>> 1
-------
11000000 (-64) // Sign bit preserved
00001000 (8 decimal)
>> 1
-------
00000100 (4 decimal)
Each right shift by one position is equivalent to dividing the number by 2 (truncating any fractional part).
Common Uses:
- Dividing by powers of 2:
x >> nisx / (2^n). - Extracting specific bits: Shifting bits to the rightmost position, then masking.
11.4 Bitwise Assignment Operators
These are shorthand operators, similar to arithmetic assignment operators.
| Operator | Example | Equivalent to |
|---|---|---|
&= | x &= y | x = x & y |
| ` | =` | x |= y |
^= | x ^= y | x = x ^ y |
<<= | x <<= y | x = x << y |
>>= | x >>= y | x = x >> y |
11.5 Practical Applications and Code Examples
Let’s put these operators into practice.
Code Example: Bitwise Operations
#include <stdio.h>
#include <limits.h> // For CHAR_BIT (number of bits in a char)
// Function to print the binary representation of an integer (for demonstration)
void print_binary(unsigned int n) {
// Assuming 32-bit int, adjust loop for other sizes
// Or, for more general purpose, use sizeof(unsigned int) * CHAR_BIT
for (int i = (sizeof(unsigned int) * CHAR_BIT) - 1; i >= 0; i--) {
printf("%d", (n >> i) & 1); // Shift right, then mask with 1
if (i % 8 == 0 && i != 0) { // Add space every 8 bits for readability
printf(" ");
}
}
printf("\n");
}
int main() {
unsigned int a = 5; // 0000...0101
unsigned int b = 3; // 0000...0011
unsigned int c = 10; // 0000...1010
printf("--- Original Values ---\n");
printf("a = %u (Binary: ", a); print_binary(a); printf(")\n");
printf("b = %u (Binary: ", b); print_binary(b); printf(")\n");
printf("c = %u (Binary: ", c); print_binary(c); printf(")\n");
// Bitwise AND
printf("\n--- Bitwise AND ---\n");
printf("a & b = %u (Binary: ", a & b); print_binary(a & b); printf(")\n"); // 1
// Bitwise OR
printf("\n--- Bitwise OR ---\n");
printf("a | b = %u (Binary: ", a | b); print_binary(a | b); printf(")\n"); // 7
// Bitwise XOR
printf("\n--- Bitwise XOR ---\n");
printf("a ^ b = %u (Binary: ", a ^ b); print_binary(a ^ b); printf(")\n"); // 6
// Bitwise NOT (for unsigned int)
printf("\n--- Bitwise NOT ---\n");
printf("~a = %u (Binary: ", ~a); print_binary(~a); printf(")\n"); // All bits flipped
// Left Shift
printf("\n--- Left Shift ---\n");
printf("a << 1 = %u (Binary: ", a << 1); print_binary(a << 1); printf(")\n"); // 10
printf("c << 2 = %u (Binary: ", c << 2); print_binary(c << 2); printf(")\n"); // 40 (10 * 2 * 2)
// Right Shift
printf("\n--- Right Shift ---\n");
printf("c >> 1 = %u (Binary: ", c >> 1); print_binary(c >> 1); printf(")\n"); // 5
printf("c >> 2 = %u (Binary: ", c >> 2); print_binary(c >> 2); printf(")\n"); // 2
// Setting a bit (e.g., set the 2nd bit (index 1) of 'a')
unsigned int val = 0; // 0000
printf("\n--- Setting/Clearing/Toggling Bits ---\n");
printf("Initial val: %u (Binary: ", val); print_binary(val); printf(")\n");
val |= (1 << 0); // Set bit 0 (rightmost) -> 0001
printf("Set bit 0: %u (Binary: ", val); print_binary(val); printf(")\n");
val |= (1 << 2); // Set bit 2 -> 0101 (5)
printf("Set bit 2: %u (Binary: ", val); print_binary(val); printf(")\n");
// Checking if a bit is set (e.g., check bit 2)
if ((val & (1 << 2)) != 0) {
printf("Bit 2 is SET.\n");
} else {
printf("Bit 2 is NOT set.\n");
}
// Clearing a bit (e.g., clear bit 0)
val &= ~(1 << 0); // Clear bit 0 -> 0100 (4)
printf("Clear bit 0: %u (Binary: ", val); print_binary(val); printf(")\n");
// Toggling a bit (e.g., toggle bit 1)
val ^= (1 << 1); // Toggle bit 1 -> 0110 (6)
printf("Toggle bit 1: %u (Binary: ", val); print_binary(val); printf(")\n");
val ^= (1 << 1); // Toggle bit 1 again -> 0100 (4)
printf("Toggle bit 1 again: %u (Binary: ", val); print_binary(val); printf(")\n");
return 0;
}
Compile and Run:
gcc bitwise_ops.c -o bitwise_ops
./bitwise_ops
Exercise 11.1: Bit Manipulation Functions
Write a C program that defines and uses the following functions:
void print_bits(unsigned char byte): Takes an unsigned character (1 byte) and prints its binary representation.unsigned char set_bit(unsigned char byte, int position): Sets the bit atposition(0-7) to1.unsigned char clear_bit(unsigned char byte, int position): Clears the bit atpositionto0.unsigned char toggle_bit(unsigned char byte, int position): Toggles the bit atposition(flips 0 to 1, or 1 to 0).int check_bit(unsigned char byte, int position): Returns1if the bit atpositionis set,0otherwise.
In your main function, test these functions with various unsigned char values and bit positions.
Example:
unsigned char my_byte = 0b10100110; // 166 decimal
print_bits(my_byte); // Should print 10100110
my_byte = set_bit(my_byte, 1); // Set bit 1 (from right)
print_bits(my_byte); // Should print 10100111
my_byte = clear_bit(my_byte, 7); // Clear bit 7 (leftmost)
print_bits(my_byte); // Should print 00100111
my_byte = toggle_bit(my_byte, 4); // Toggle bit 4
print_bits(my_byte); // Should print 00110111
int is_bit_5_set = check_bit(my_byte, 5); // Should be 0
printf("Is bit 5 set? %d\n", is_bit_5_set);
Exercise 11.2: RGB Color Manipulation (Mini-Challenge)
Imagine a 32-bit integer (unsigned int) represents an RGB color, where each color component (Red, Green, Blue) is stored in 8 bits, and the remaining 8 bits are for Alpha (transparency). The format is typically 0xAARRGGBB (Alpha, Red, Green, Blue).
Write a C program that uses bitwise operations to:
- Declare an
unsigned intvariable for a color, e.g.,0xFF00FF00(Fully opaque, Green). - Extract the individual Red, Green, and Blue components into
unsigned charvariables.- To get Red: Shift right to move Red to the rightmost 8 bits, then mask with
0xFF. - To get Green: Same logic.
- To get Blue: Same logic (it’s already at the rightmost).
- To get Red: Shift right to move Red to the rightmost 8 bits, then mask with
- Modify the color:
- Change the Green component to
0x80(half intensity). - Change the Blue component to
0xFF(full blue). - The Alpha and Red components should remain unchanged.
- To do this: clear the original 8 bits for the component, then OR with the new component shifted into the correct position.
- Change the Green component to
- Print the original color components and the new color’s hexadecimal value.
Example 0xAARRGGBB format:
- Alpha: Bits 24-31
- Red: Bits 16-23
- Green: Bits 8-15
- Blue: Bits 0-7
Hexadecimal to binary:
0xFF=111111110x00=00000000
You’ve now learned how to perform powerful bitwise operations, granting you fine-grained control over individual bits within your data. This is a crucial skill for low-level programming, where understanding the underlying binary representation is key. In the next chapter, we’ll build upon our pointer knowledge with more advanced concepts and introduce function pointers.