CS140 Lecture notes -- Bit Arithmetic


I am not going to go over things in this level of detail in class, so if class left you a bit fuzzy on bit arithmetic, go over these notes thoroughly.

Bit Arithmetic

C and C++ have operations for bit arithmetic about which students are often unaware. As you know, each data type is a certain number of bytes, and each byte is eight bits. You can kind of see the bit structure of a byte by printing it in hexadecimal or in octal. To truly see the bits, you have write some code.

Look at the program printbits.c:


print_bits(unsigned char c)
{
  ....
}

main()
{
  unsigned char c;
  int i;

  while (1) {
    printf("Enter an integer between 0 and 255: ");
    fflush(stdout);
    if (scanf("%d", &i) != 1) exit(0);
    if (i < 0 || i > 255) {
      printf("Bad value of i\n");
    } else {
      c = (unsigned char) i;
      print_bits(c);
      putchar('\n');
    }
  }
}
This repeatedly takes an integer between 0 and 255, puts it into an unsigned char (i.e. one byte), and then calls print_bits() on it. Print_bits() prints out the bits. Don't worry about how it is implemented yet. The output should be straightforward -- the bits look as you think they should:
UNIX> printbits
Enter an integer between 0 and 255: 0
00000000
Enter an integer between 0 and 255: 1
00000001
Enter an integer between 0 and 255: 2
00000010
Enter an integer between 0 and 255: 3
00000011
Enter an integer between 0 and 255: 4
00000100
Enter an integer between 0 and 255: 5
00000101
Enter an integer between 0 and 255: 255
11111111
Enter an integer between 0 and 255: 254
11111110
Enter an integer between 0 and 255: <CNTL-D>
UNIX>

Bitwise-and

The bit operations in C/C++ operate on these bits. The first operation is bitwise-and. It is the ampersand. The bitwise-and of two numbers simply goes through the bit position and sets that position to 1 if the two bits in that position are 1, and 0 otherwise. Look at and.c. This takes two bytes and prints out their bitwise-and. You'll note, 0 bitwise-and anything is zero. 255 bitwise-and x is x, because all the bits of 255 are one.
UNIX> and                                                          
Enter two integers between 0 and 255: 1 0
00000001 & 00000000 = 00000000
Enter two integers between 0 and 255: 1 1
00000001 & 00000001 = 00000001
Enter two integers between 0 and 255: 1 2
00000001 & 00000010 = 00000000
Enter two integers between 0 and 255: 1 3
00000001 & 00000011 = 00000001
Enter two integers between 0 and 255: 2 3
00000010 & 00000011 = 00000010
Enter two integers between 0 and 255: 0 40
00000000 & 00101000 = 00000000
Enter two integers between 0 and 255: 255 40
11111111 & 00101000 = 00101000
Enter two integers between 0 and 255: 0 89
00000000 & 01011001 = 00000000
Enter two integers between 0 and 255: 255 89
11111111 & 01011001 = 01011001
Enter two integers between 0 and 255: <CNTL-D>
UNIX> 
If a number is all zeros except for a one in the i-th position (this means that the number is 2(i-1)) then that number bitwise-and x is equal to zero or that number, depending on whether x has a bit set in the i-th position. Therefore (4 & 7) equals 4 since 7 has its bit in the third position set.

Similarly, if you are interested in what the lowest i bits of a byte are, you can take that byte bitwise-and (2i)-1. For example, if you want to see what the lowest three bits are of a byte, you take that byte bitwise-and (23)-1 = 7. For example, the lowest three bits of 89 are (89&7)=1:

UNIX> and                                                          
Enter two integers between 0 and 255: 4 7
00000100 & 00000111 = 00000100
Enter two integers between 0 and 255: 4 2
00000100 & 00000010 = 00000000
Enter two integers between 0 and 255: 89 7
01011001 & 00000111 = 00000001
Enter two integers between 0 and 255: 91 7 
01011011 & 00000111 = 00000011
Enter two integers between 0 and 255: <CNTL-D>
UNIX> 

Bitwise-or

Bitwise-or works like bitwise-and, only it sets the bit to one if either of the numbers has its bit in that position set to one. It's operator is the pipe character (|). You'll note that (0|x) = x, and (255|x) = 255.
UNIX> or                                                          
Enter two integers between 0 and 255: 0 1
00000000 | 00000001 = 00000001
Enter two integers between 0 and 255: 1 2
00000001 | 00000010 = 00000011
Enter two integers between 0 and 255: 1 3
00000001 | 00000011 = 00000011
Enter two integers between 0 and 255: 0 40
00000000 | 00101000 = 00101000
Enter two integers between 0 and 255: 255 40
11111111 | 00101000 = 11111111
Enter two integers between 0 and 255: 0 89
00000000 | 01011001 = 01011001
Enter two integers between 0 and 255: 255 89
11111111 | 01011001 = 11111111
Enter two integers between 0 and 255: <CNTL-D>
UNIX> 

Other bitwise operations

There is also bitwise exclusive-or, which returns whether the two bits are different. Its operator is the carat (^). And there is bitwise complement, which is unary, and changes zeros to ones and ones to zero:
UNIX> exor                                                          
Enter two integers between 0 and 255: 1 2
00000001 ^ 00000010 = 00000011
Enter two integers between 0 and 255: 1 3
00000001 ^ 00000011 = 00000010
Enter two integers between 0 and 255: 255 0
11111111 ^ 00000000 = 11111111
Enter two integers between 0 and 255: 255 255
11111111 ^ 11111111 = 00000000
Enter two integers between 0 and 255: 1 1
00000001 ^ 00000001 = 00000000
Enter two integers between 0 and 255: <CNTL-D>
UNIX> complement                                                          
Enter an integer between 0 and 255: 0
~ 00000000 = 11111111
Enter an integer between 0 and 255: 1
~ 00000001 = 11111110
Enter an integer between 0 and 255: 40
~ 00101000 = 11010111
Enter an integer between 0 and 255: 255
~ 11111111 = 00000000
Enter an integer between 0 and 255: <CNTL-D>
UNIX> 

Bit shifting

Finally, there are the bit shift operators: left shift (<<) and right shift (>>). They shift the bits a specified number of positions left and right respectively:
UNIX> leftshift                                                          
Enter an integer and a shift amount: 1 0
00000001 <<  0 = 00000001
Enter an integer and a shift amount: 1 1
00000001 <<  1 = 00000010
Enter an integer and a shift amount: 1 2
00000001 <<  2 = 00000100
Enter an integer and a shift amount: 1 3
00000001 <<  3 = 00001000
Enter an integer and a shift amount: 1 4
00000001 <<  4 = 00010000
Enter an integer and a shift amount: 3 1
00000011 <<  1 = 00000110
Enter an integer and a shift amount: 3 0
00000011 <<  0 = 00000011
Enter an integer and a shift amount: 0 1
00000000 <<  1 = 00000000
Enter an integer and a shift amount: 3 8
00000011 <<  8 = 00000000
Enter an integer and a shift amount: <CNTL-D>
UNIX> rightshift
Enter an integer and a shift amount: 1 0
00000001 >>  0 = 00000001
Enter an integer and a shift amount: 1 1
00000001 >>  1 = 00000000
Enter an integer and a shift amount: 8 1
00001000 >>  1 = 00000100
Enter an integer and a shift amount: 8 2
00001000 >>  2 = 00000010
Enter an integer and a shift amount: 8 3
00001000 >>  3 = 00000001
Enter an integer and a shift amount: 255 1
11111111 >>  1 = 01111111
Enter an integer and a shift amount: 255 7
11111111 >>  7 = 00000001
Enter an integer and a shift amount: <CNTL-D>
UNIX> 
You'll note, left shifting by 1 multiplies by 2, and right shifting by one divides by two. In general, left shifting by i multiplies by 2i, and right shifting by i divides by 2i,

However, a word of caution. Yes, left-shifting by one multiplies by 2 faster than doing x*2. However, do not do this if you want to multiply by two unless your code really has to be optimally fast, or unless it's pretty clear what you're doing. Otherwise, your code becomes extremely hard to read.

Now that you know all this, implementing print_bits() is pretty simple:

print_bits(unsigned char c)
{
  unsigned int i;

  for (i = 1 << 7; i != 0; i >>= 1) {
    if (i&c) putchar('1'); else putchar('0');
  }
}
Yes, I could have done:
  for (i = 1 << 7; i != 0; i >>= 1) {
    putchar('0'+((i&c)>0));
  }
or some such garbage, but again, make your code readable. A good compiler can probably make your readable code as fast as unreadable code.


You can do bit operations on ints, shorts, chars and longs (and long longs in C++). They can be signed or unsigned. Be careful with bit-shifting and long long's. For example, compilers treat constants as ints, so (1 << 60) will equal zero, even if you assign it to a long long (or uint64_t in C).

bit_practice

I include a program here called bit_practice, which lets you practice bit arithmetic. When you run it, it will present you with a bit arithmetic problem, which works on shorts (16-bit numbers). It waits for you to press RETURN, and it will give you the answer. This is really good practice for you, especially to think about the problems in hexadecimal.
UNIX> bit_practice
A is       27  0x001b  00000000 00011011
Operation is ~
<RETURN>
C is    65508  0xffe4  11111111 11100100
----------------
<RETURN>
A is      248  0x00f8  00000000 11111000
Operation is >> 1
<RETURN>
C is      124  0x007c  00000000 01111100
----------------
<RETURN>
A is        0  0x0000  00000000 00000000
B is      124  0x007c  00000000 01111100
Operation is &
<RETURN>
C is        0  0x0000  00000000 00000000
----------------
<RETURN>
A is       41  0x0029  00000000 00101001
Operation is << 4
<RETURN>
C is      656  0x0290  00000010 10010000
----------------
<RETURN>
A is      240  0x00f0  00000000 11110000
Operation is >> 4
<RETURN>
C is       15  0x000f  00000000 00001111
----------------
<RETURN>
A is      138  0x008a  00000000 10001010
Operation is >> 1
<RETURN>
C is       69  0x0045  00000000 01000101
----------------
<RETURN>
A is      122  0x007a  00000000 01111010
Operation is << 1
<RETURN>
C is      244  0x00f4  00000000 11110100
----------------
<CNTL-D>
UNIX>