Bitwise Operations and Unions
Now that we know how numeric data is represented as a sequence of bits, we will find it useful to manipulate these bits in several different ways; several bitwise operations help us in this regard. As in Scheme, sometimes we need a modest amount of flexibility with respect to the data type used in particular circumstances; the union type accomplishes this. You will learn about both in the following textbook readings.
The reading below makes reference to
the struct feature of the C
programming language, which we will cover in detail after the next
module. For now, you may infer what you can about them from the
reading, but you should not worry about understanding all the details.
- King: Sections 16.4, 20.1
Bitwise Operations
The C programming language contains six bitwise operations that may be
applied to any integer type, including char, int,
and long. We know each of these types
consists of some number of bits, which are individually binary values,
so it should not come as a surprise to know that the logical operators
we've learned so far, such as AND and OR, can be applied to each
positionally aligned pair of bits. For example, whereas a && b evaluates to true (really 1) if both a and
b are non-zero, using the bitwise operator a & b compares the corresponding bits of
a and b, yielding a 1 in positions where both operands' bits are
1 (i.e., true), and a 0 (i.e., false) in all other bit positions.
| Operation | Meaning |
|---|---|
& | Bitwise AND |
| | Bitwise OR |
^ | Bitwise Exclusive OR (XOR) |
~ | Bitwise NOT |
<< | Shift Left |
>> | Shift Right |
In considering the bitwise operators, we think of a binary bit 1 as
"true" and the binary bit value 0 as "false." . The so-called
"exclusive OR", often called "XOR" is true when either one or the other
of the operands is true, but is false when both are true or both are
false. The negation operator simply flips (or inverts) all of the bits
in the value. Both the shift left (<<) and shift right
(>>) operators move all bits by a specified number of
places. Bits moved beyond the end of storage are lost, and bits
introduced at the start of the shift become 0.
Suppose num1 is the 8-bit integer 10010110,
and num2 is the 8-bit integer 00001111. When
these integers are used with the bitwise operations, each bit of
num1 is paired with the corresponding bit of
num2. The result is an 8-bit integer, where the resulting
bits are derived from the given bitwise operation for each bit.
| Expression | Result |
|---|---|
num1 & num2 |
00000110 |
num1 | num2 |
10011111 |
num1 ^ num2 |
10011001 |
~num1 |
01101001 |
~num2 |
11110000 |
num1<<2 |
01011000 |
num2<<3 |
01111000 |
num1>>2 |
00100101 |
num2>>3 |
00000001 |
Bit shifting deserves a bit more discussion. Shifting left always produces zeros to fill in the spaces left vacant by the bits that have moved. However, shifting to the right may either produce zeros (called a logical shift) or ones IF the leading bit is a one (called an arithmetic shift, since it preserves the sign and remains the equivalent of dividing by 2, even when the operand is negative). The difference stems from the fact that both operations are often desirable but must be supported by the underlying hardware. C's designers have left this behavior "compiler dependent", meaning the programmer won't be able to predict what the outcome of such an expression will be when the leading bit is 1 in a right shift.
Bit Manipulation
Although many applications work with variables at the level of numbers, characters, or strings, some processing may benefit by working directly at a low level with specific bits. Two examples of this processing follow.
Multiplication and Division by 2i
The reading on the representation of integers observed that the bits in an integer increased by a factor of two in moving from right to left. Thus, given the representation for a positive integer n, the representation for 2n can be obtained by shifting all bits for n to the left 1 position—inserting 0 as the rightmost bit. Similarly, shifting the bits of n two places to the left (inserting two zeros on the right) yields the bit representation for the number 4n.
More generally, shifting the bit representation of the number n to the left by i bits (by inserting i bits at the right) yields the binary representation of the number n × 2i.
Similarly, shifting the binary representation of the number n to the right by 1 place (and dropping the rightmost bit) has the effect of dividing the number by 2 (and ignoring any remainder).
Altogether, shifting left or right is a particularly efficient mechanism to multiply or divide numbers by powers of 2.
Examples
-
The number 21 (decimal) has the bit pattern
00010101, and the number 42 (twice 21) has the bit pattern00101010. Each bit in the bit representation for 21 is shifted left one place in the representation for 42 (with a 0 inserted at the right). -
Similarly, the number 10 (decimal) has the bit pattern
00001010— the result obtained from 21 by dropping the rightmost bit and prepending a 0 at the left. That is, the binary representation of the number 10 (decimal) can be considered as the result of the binary representation of 21 (decimal) shifted right 1 place: as decimal numbers, 10 = 21>>1.
Highest Set Bit
Given a non-negative integer n, let us number its bits from right to
left, starting at 0. Thus, according to this counting, the bit 1
appears in the number 0000010 at bit 1. (For this reason,
we discourage you from saying things like "the first bit", because that
can be ambiguously interpreted as either bit 0 or bit 1. We prefer that
nomenclature to the ordinals "first", "second", etc.)
With this counting, we write a code segment that determines the highest numbered bit that contains a 1 for the non-negative integer n.
To approach this problem, we consider shifting the bits of the number one bit to the right successively until the resulting number is 0. The relevant code is below. (Note that it will produce -1 if no bits are set.)
/* Identify the highest set bit of a given number */
#include
#ifndef VALUE
#define VALUE 136
#endif
int
main(void)
{
int count = -1;
int n = VALUE;
/* Find location of the highest set bit */
while (n != 0) {
count++;
n = n >> 1; /* Note: we could have written: n>>=1 */
}
printf("Highest set bit in %d: %d\n",VALUE,count);
return 0;
} // main
Printing a non-negative value in binary
Before we introduce the next example, we should say that
Depending on the computer architecture, a byte may consist of 8 or more
bits, the exact number provided as CHAR_BIT.
[cppreference.com]
The platform-specific value CHAR_BIT is defined in
limits.h.
Recall that the sizeof operator gives the number of bytes
required for a data type. Therefore, to place a one in the left-most
bit but one of an int, as we do in the next example, we shift a single
1 by the appropriate amount:
unsigned mask = 1 << (int_size - 2); // 0100...000
Note that we will want to place the one in the leftmost-but-one slot because we will proceed by right shifting mask, and C does not define what values get prepended at the left when the leading bit is 1.
To approach this problem, consider the integer with bit representation
1000...000, which we might call mask. For
this integer, the left-most bit, in bit i, is 1, and all
of the rest are 0. Next, for a non-negative integer, consider applying
the bitwise AND operation (&) to the
number n and mask:
-
In bit
i, the mask is 1. Ifnalso has 1 in biti, then the result in bitiis(1 & 1), which is 1. Ifnhas a 0 in biti, then the result in bitiis(1 & 0), which is 0. Altogether, the result of bitwise and for(mask & n)has the same value in bitias inn. -
For all bits other than bit
i, themaskis 0, and the result of a bitwise AND with 0 is always 0.
Putting the pieces together, the result of mask & n is to extract the ith
bit of n and set all other bits to 0. With this
observation, we can print the ith bit of n
with the code:
if ((mask & n) == 0)
printf ("0");
else
printf ("1");
We might express this a bit more concisely using the ternary conditional operator:
printf( (mask & n) ? "1" : "0" );
A similar approach can be used for each bit of n, if we
successively shift the mask variable right by 1 bit each
time.
/* Print the binary representation of a non-negative integer */
#include
#include
#ifndef VALUE
#define VALUE 42
#endif
int
main(void)
{
int n = VALUE;
if (n<0) {
printf("Error: n must be non-negative");
return 1;
}
printf ("number of bits in int: %ld\n", sizeof(int)*CHAR_BIT);
printf ("bit pattern for n: ");
int int_size = CHAR_BIT*sizeof(int);
// initialize mask with a 1 in the leftmost-but-one position
unsigned int mask = 1 << (int_size - 2);
printf ("0"); // print a leading zero
for (int i = 0; i < int_size-1; i++) { // for each bit:
printf( (mask & n) ? "1" : "0" ); // print the bit
mask = mask >> 1; // shift the mask rightward
}
printf ("\n");
return 0;
} // main
