Dr. Roger Ianjamasimanana

What are bitwise operations in C?

By Dr. Roger Ianjamasimanana

C offers operators for manipulating bits and bytes directly. By combining these operators with pointers and casts, you can implement low-level functionalities that often belong to assembly language. In many cases, you can maintain portability with careful use of these techniques. However, if you need to interact with specific hardware (for example, embedded devices), you might require non-portable code to drive certain pins or registers. In such cases, it is crucial to control individual bits that correspond to hardware pins.

1. Binary representations

Computers store and manage data in bits (ones and zeros). These bits group into bytes (commonly eight bits per byte). Each byte in memory has its own address. In C, data types specify how many bytes each variable takes, and the compiler arranges memory accordingly. The variable's numeric value is represented as a bit pattern in those allocated bytes.

For instance, consider a 16-bit integer with a decimal value of 22425. Internally, that value appears as:

0101 0111 1001 1001

From right to left, each bit stands for increasing powers of two (20, 21, and so on). We call the right-most bit the least-significant bit (LSB) and the left-most bit the most-significant bit (MSB). For signed types, a 1 in the MSB implies a negative number (assuming two's-complement representation). For example, −22425 might look like:

1010 1000 0110 0111

You can convert a negative decimal to two's-complement binary by:

  1. Subtracting 1 from its absolute value (22425 → 22424).
  2. Converting that result to binary.
  3. Performing a bitwise complement on that binary pattern.

If you use an unsigned type, the MSB also contributes to the magnitude. Thus, an unsigned variable can hold numbers roughly twice as large as a signed variable with the same bit width.

C doesn't directly allow binary literals, but it supports decimal, octal, and hexadecimal. For bitwise tasks, hexadecimal proves more convenient because it groups bits in fours, making them simpler to read compared to long binary sequences.

2. Bitwise operators

C has six bitwise operators: &, |, ^, ~, <<, and >>. These apply only to integers (char, short, int, long) whether signed or unsigned. They don't work with floating-point values. Each operator (except ~) has a compound assignment version (&=, |=, ^=, <<=, >>=).

Remember that these bitwise operators (&, |, and ~) differ from logical operators (&&, ||, and !). They serve different purposes.

2.1 AND, OR, XOR, and NOT

The operators &, |, ^, and ~ offer Boolean logic on single bits. The ~ operator is unary (flip each bit), while the others are binary (compare bits from two operands). Here's a truth table:

x  y  x&y  x|y  x^y
0  0   0     0    0
0  1   0     1    1
1  0   0     1    1
1  1   1     1    0

Consider these unsigned chars:

unsigned char x = 55; /* decimal 55 => hex 0x37 => binary 0011 0111 */
unsigned char y = 25; /* decimal 25 => hex 0x19 => binary 0001 1001 */
unsigned char z;

Below are common operations:

  • AND (&): z = x & y;
    0011 0111 & 0001 1001 => 0001 0001 (decimal 17)
    AND sets a bit in z only if both bits in x and y are 1. Typically used to clear bits or check specific bits.
  • OR (|): z = x | y;
    0011 0117 | 0001 1001 => 0011 1111 (decimal 63)
    OR sets a bit in z if either bit in x or y (or both) is 1. Often used to set bits.
  • XOR (^): z = x ^ y;
    0011 0111 ^ 0001 1001 => 0010 1110 (decimal 46)
    XOR sets a bit in z to 1 if exactly one corresponding bit in x or y is 1. It’s common for toggling bits.
  • NOT (~): z = ~x;
    0011 0111 => 1100 1000 (decimal 200)
    NOT flips each bit, which is helpful for inverting or negating bit patterns.

2.2 Right shift and left shift

The shift operators << and >> move bits in an integer left or right, discarding shifted-out bits and filling the vacated bits with zeros (for left shifts and unsigned right shifts). If you apply x << n, bits in x move n positions left, losing the left-most bits. For x >> n (unsigned), bits move right, losing the right-most bits.

char x = 0x19;      /* 25 decimal => binary 0001 1001 */
char y = x << 2; /* shifts left => 0110 0100 (100 decimal) */

Right-shifting signed integers is implementation-dependent. Some systems perform a logical shift (filling vacated bits with 0), while others do an arithmetic shift (copying the sign bit). If arithmetic shift occurs, negative numbers preserve their sign bit:

signed char x = -75; /* 1011 0101 in binary */
signed char y = x >> 2; 
/* could yield 0010 1101 (45 decimal) or 1110 1101 (−19 decimal) */

Shifting left by n corresponds to multiplying by 2^n, and shifting right by n roughly corresponds to dividing by 2^n (on systems that use arithmetic shift for negatives, this is exact for nonnegative values).

2.3 Operator precedence

Bitwise operators have lower precedence than arithmetic ones, except for the unary ~ which parallels the unary logical ! in precedence. Shifts (<<, >>) rank higher than relational operators (<, >), while among bitwise operators themselves, & precedes ^, which precedes |. All bitwise operators precede logical operators (&&, ||).

You should rely on parentheses for clarity rather than depending heavily on precedence rules.

3. Common bitwise operations in C

Typically, you use bitwise operations either to pack small data into one machine word (for instance, multiple Boolean flags) or to work directly with hardware registers or pins. In both contexts, you target or modify specific bits.

/* Example of enumerating bit masks for four bits: */
enum {
    FIRST = 0x01, /* 0001 */
    SECND = 0x02, /* 0010 */
    THIRD = 0x04, /* 0100 */
    FORTH = 0x08, /* 1000 */
    ALL   = 0x0f  /* 1111 */
};

unsigned flags = 0;
flags |= SECND | THIRD | FORTH;  /* Sets bits 2,3,4 => 1110 */
flags &= ~(FIRST | THIRD);       /* Clears bits 1,3 => 1010 */
flags ^= (THIRD | FORTH);        /* Toggles bits 3,4 => 0110 */

/* Check if bits 1 and 4 are off: */
if ((flags & (FIRST | FORTH)) == 0) {
    flags &= ~ALL; /* Clear all => 0000 */
}

Notice how | combines masks, ~ inverts them, & applies them, and ^ toggles bits. Common patterns include:

  • Set bits: var |= MASK;
  • Clear bits: var &= ~MASK;
  • Toggle bits: var ^= MASK;
  • Check bits: (var & MASK) == 0, etc.

4. Bit-fields in C

C provides bit-fields as an alternative to manual masking. You can declare bit-fields in a struct, indicating how many bits each field occupies. For example:

struct {
    unsigned first : 1;
    unsigned secnd : 1;
    unsigned third : 1;
    unsigned forth : 1;
} flags;

You then set bits using structure syntax:

flags.secnd = flags.third = flags.forth = 1;
flags.first = flags.third = 0;
if (!flags.first && !flags.forth) {
    flags.first = flags.secnd = flags.third = flags.forth = 0;
}

Although this syntax looks simpler, bit-fields often suffer from implementation-dependent details such as maximum field widths, alignment, and byte order. Therefore, using explicit masking with shifts and logical operators is usually more portable and transparent.

If you prefer macros to manipulate bits directly, consider something like:

#define BitSet(arg,posn) ((arg) | (1L << (posn)))
#define BitClr(arg,posn) ((arg) & ~(1L << (posn)))
#define BitFlp(arg,posn) ((arg) ^ (1L << (posn)))
#define BitTst(arg,posn) ((arg) & (1L << (posn)))

enum { FIRST, SECND, THIRD, FORTH };
unsigned flags = 0;
flags = BitSet(flags, FIRST);      /* Set bit 0 */
flags = BitFlp(flags, THIRD);      /* Toggle bit 2 */
if (BitTst(flags, SECND) == 0)     /* Check bit 1 */
    flags = 0;

These macros clarify your intention for setting, clearing, toggling, and testing individual bits.

5. Examples of bitwise operations in C

5.1. Using bitmasks to track feature flags

Use case: imagine you manage an application with several optional features—logging, debug mode, metrics, and so on. You can store each feature as a single bit in an integer, which conserves memory and allows quick testing of multiple features at once.

Program explanation

We will define four bits representing four features. We will then enable or disable these features using bitwise operators and check their states.

#include <stdio.h>
#include <stdlib.h>

/* Define feature flags using bitmasks */
enum {
    LOGGING     = 1 << 0,  /* 0001 */
    DEBUG_MODE  = 1 << 1,  /* 0010 */
    METRICS     = 1 << 2,  /* 0100 */
    EXTRA_CHECK = 1 << 3   /* 1000 */
};

int main(void) {
    unsigned int features = 0; /* Start with all features off */

    /*
     * 1) Turn on LOGGING and METRICS features.
     *    We use the bitwise OR operator to combine flags.
     */
    features |= (LOGGING | METRICS);

    /*
     * 2) Check if DEBUG_MODE is off. If yes, turn it on.
     *    We use bitwise AND to test if DEBUG_MODE bit is off (== 0).
     */
    if ((features & DEBUG_MODE) == 0) {
        features |= DEBUG_MODE;
    }

    /*
     * 3) Disable the METRICS feature (we clear that bit).
     *    We invert (EXTRA_CHECK | METRICS) then AND it with features
     *    to turn off both bits if needed. We'll specifically remove METRICS.
     */
    features &= ~METRICS;

    /*
     * 4) Toggle EXTRA_CHECK (if it was off, switch on; if on, switch off).
     *    The XOR operator toggles bits.
     */
    features ^= EXTRA_CHECK;

    /* Print resulting state in binary-like form for clarity. */
    /* We'll print out bits 0-3 from least-significant to most-significant. */
    printf("Features set: ");
    for (int i = 3; i >= 0; i--) {
        unsigned int mask = 1U << i;
        printf("%d", (features & mask) ? 1 : 0);
    }
    printf(" (in bits)\n");

    /*
     * If we want to see if EXTRA_CHECK is on:
     */
    if ((features & EXTRA_CHECK) != 0) {
        printf("EXTRA_CHECK feature is now ON.\n");
    } else {
        printf("EXTRA_CHECK feature is OFF.\n");
    }

    return 0;
}

How it works:

  • Bitwise OR (|) sets particular bits to 1.
  • Bitwise AND (&) checks bits or clears bits if combined with the inverted mask.
  • Bitwise XOR (^) toggles selected bits.

5.2. Reading hardware pins: simulated input example

Use case: in an embedded system, imagine you read from a hardware register called GPIO that represents multiple input pins in one byte. Each pin indicates a button press, sensor state, or other signals. We can simulate reading that register and then interpret bits to see which pins are active.

Program explanation:

We will define mock pins on a fictional hardware device. Each bit in the GPIO register will correspond to a button or sensor. We'll simulate reading these inputs, then parse them using bitwise operations.

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

/* Define mock pin positions */
enum {
    BUTTON_1   = 1 << 0, /* 0000 0001 */
    BUTTON_2   = 1 << 1, /* 0000 0010 */
    SENSOR_A   = 1 << 2, /* 0000 0100 */
    SENSOR_B   = 1 << 3  /* 0000 1000 */
};

/*
 * Simulate reading from a GPIO register: 
 * We'll pseudo-randomly set some bits to represent active pins.
 */
unsigned char readGPIO(void) {
    /* For demonstration, let's randomize which bits are set */
    unsigned char mockValue = 0;
    mockValue |= (rand() % 2) ? BUTTON_1 : 0;
    mockValue |= (rand() % 2) ? BUTTON_2 : 0;
    mockValue |= (rand() % 2) ? SENSOR_A : 0;
    mockValue |= (rand() % 2) ? SENSOR_B : 0;
    return mockValue;
}

int main(void) {
    srand((unsigned) time(NULL)); /* Seed random generator */

    /* Read the "hardware" */
    unsigned char gpioVal = readGPIO();

    /* Check if each bit is set */
    printf("GPIO Register: 0x%02X\n", gpioVal);
    printf("Button 1 is %s\n", (gpioVal & BUTTON_1) ? "PRESSED" : "not pressed");
    printf("Button 2 is %s\n", (gpioVal & BUTTON_2) ? "PRESSED" : "not pressed");
    printf("Sensor A is %s\n", (gpioVal & SENSOR_A) ? "ACTIVE" : "inactive");
    printf("Sensor B is %s\n", (gpioVal & SENSOR_B) ? "ACTIVE" : "inactive");

    /*
     * Example: If both buttons are pressed, we might toggle sensor bits (just as a demonstration).
     * We'll use XOR to invert the sensor bits.
     */
    if ((gpioVal & BUTTON_1) && (gpioVal & BUTTON_2)) {
        gpioVal ^= (SENSOR_A | SENSOR_B);
        printf("\nBoth buttons pressed! Toggling sensor bits.\n");
        printf("New GPIO Value: 0x%02X\n", gpioVal);
    }

    /*
     * Another example: If sensor A is active, turn it OFF by clearing that bit.
     * We'll use a bitwise AND with the inverted mask for SENSOR_A.
     */
    if (gpioVal & SENSOR_A) {
        gpioVal &= ~SENSOR_A;
        printf("\nSensor A was active. Turning it off.\n");
        printf("New GPIO Value: 0x%02X\n", gpioVal);
    }

    return 0;
}

In this program, we:

  • use readGPIO() to simulate reading a hardware byte that combines multiple pins.
  • mask each pin with (gpioVal & BUTTON_1) or BUTTON_2 or SENSOR_A and so forth to see whether that bit is set.
  • perform various bitwise operations (e.g., toggling sensor bits, clearing sensor A) to illustrate how you'd manipulate hardware states.

This resembles real embedded code where you'd directly read from a hardware register memory address and apply bitwise logic to interpret or alter device states.

feature-top
Readers’ comment
feature-top
Log in to add a comment
🔐 Access