Chapter 11: Bitwise Operations: Working at the Bit Level

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) = 00000001
  • 5 (decimal) = 00000101
  • 10 (decimal) = 00001010
  • 255 (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 ABit BA & B
000
010
100
111

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 ABit BA | B
000
011
101
111

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 | B combines 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 ABit BA ^ B
000
011
101
110

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 ^ B is non-zero, then A and B are 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
01
10

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 << n is x * (2^n).
  • Creating bitmasks: 1 << bit_position creates 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 >> n is x / (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.

OperatorExampleEquivalent to
&=x &= yx = x & y
`=`x |= y
^=x ^= yx = x ^ y
<<=x <<= yx = x << y
>>=x >>= yx = 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:

  1. void print_bits(unsigned char byte): Takes an unsigned character (1 byte) and prints its binary representation.
  2. unsigned char set_bit(unsigned char byte, int position): Sets the bit at position (0-7) to 1.
  3. unsigned char clear_bit(unsigned char byte, int position): Clears the bit at position to 0.
  4. unsigned char toggle_bit(unsigned char byte, int position): Toggles the bit at position (flips 0 to 1, or 1 to 0).
  5. int check_bit(unsigned char byte, int position): Returns 1 if the bit at position is set, 0 otherwise.

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:

  1. Declare an unsigned int variable for a color, e.g., 0xFF00FF00 (Fully opaque, Green).
  2. Extract the individual Red, Green, and Blue components into unsigned char variables.
    • 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).
  3. 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.
  4. 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 = 11111111
  • 0x00 = 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.