Skip to Content
Course content

181: Bit Shifting Operators

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I remember a project a few years back where I was writing a driver for a cheap temperature sensor. The sensor sent back a 12-bit reading, but because the hardware communicated in 8-bit chunks, that reading was split across two different registers. I spent an hour scratching my head because the numbers were coming back as complete nonsense, only to realize I was shifting the high byte in the wrong direction. It was a classic "face-palm" moment, but it's exactly why you need to be comfortable with bit shifting; when you're dealing with hardware or network protocols, the data rarely arrives in neat, pre-packaged integers.

Moving Bits to the Left and Right

At its core, bit shifting is just moving the binary digits of a number to the left or right. We use two operators for this: << for left shift and >> for right shift. When you left shift a value, you're pushing zeros in from the right and pushing the most significant bits off the left edge. For example, if you have the binary value 00000101 (which is 5) and you shift it left by two positions (5 << 2), it becomes 00010100 (which is 20).

You'll notice a pattern here: shifting left by one is mathematically equivalent to multiplying by 2. Shifting right (>>) is the opposite; it moves bits to the right, effectively dividing the number by 2 and discarding any remainder. I often use shifts instead of multiplication or division when I'm working in tight loops on embedded systems because, while modern compilers are great at optimizing, shifting is the most "honest" way to tell the CPU exactly what you want it to do with the bits.

unsigned char value = 3; // Binary: 00000011
unsigned char left = value << 2;  // Binary: 00001100 (Decimal 12)
unsigned char right = value >> 1; // Binary: 00000001 (Decimal 1)

The Danger of Signed Shifts

Here is where things get tricky, and where many developers shoot themselves in the foot. There is a huge difference between shifting an unsigned int and a signed int. When you right-shift an unsigned value, the CPU always fills the empty spots on the left with zeros. This is called a logical shift.

However, with signed integers, C allows for "arithmetic shifts." To preserve the sign of a negative number, the CPU might fill those empty left spots with ones instead of zeros. If you shift a negative number right, it stays negative. This behavior isn't actually guaranteed by the C standard—it's implementation-defined—which is a fancy way of saying "it depends on your compiler and CPU." My advice? If you are doing bit manipulation, always use unsigned types. It eliminates the ambiguity and saves you from a debugging nightmare.

Packing and Unpacking Data

The real power of shifting comes when you combine it with the bitwise OR (|) operator. This allows you to "pack" multiple small values into one larger variable. Imagine you have three different settings, each only needing 4 bits, and you want to store them in a single 12-bit space within a 16-bit integer. You shift the first value to the far left, the second value to the middle, and leave the third at the end.

To get those values back out, you do the reverse: shift the target bits back to the right and then use a "mask" (a bitwise AND) to clear out any leftover data from the other fields. This is how virtually every network packet and image file format works under the hood.

// Packing two 8-bit values into one 16-bit integer
unsigned char high_byte = 0xAB;
unsigned char low_byte = 0xCD;
unsigned short packed = (high_byte << 8) | low_byte; 
// Result: 0xABCD



📋 Practical Task

Exercise: RGB Color Packer

In many graphics systems, a single 32-bit integer is used to store a color. This integer is composed of four 8-bit components: Red, Green, Blue, and Alpha (transparency). Each component ranges from 0 to 255.

Write a program that does the following:

  • Defines four unsigned char variables for Red, Green, Blue, and Alpha.
  • Packs these four values into a single unsigned int called packedColor using the following layout:
    • Alpha: Bits 24-31
    • Red: Bits 16-23
    • Green: Bits 8-15
    • Blue: Bits 0-7
  • Prints the final packedColor in hexadecimal format.
  • Demonstrates "unpacking" by extracting the Green component back out of the packedColor and printing it as a decimal value to verify it matches the original.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.