Numbers stored on a computer are stored in binary, but are usually input and output in decimal format. The mathematical operations such as addition, subtraction, multiplication and division are independent of what base a number is stored in. Bitwise operators, however, only make sense when thinking of number in binary.
The simplest bitwise operators are & and | which work like the logical && and || operators, except they work bit by bit instead of treating their arguments as one value.
For instance 5 & 6 would be calculated as:
101 & 110 --- 100
And be equal to 4.
5 | 6 would be calculated as:
101 | 110 --- 111
And be equal to 7.
Another bitwise operator is the ^ operator which means "exclusive or" or "xor". This operator compares bits and results in a 1 when exactly one of the two bits is set:
101 ^ 110 --- 011
So 5 ^ 6 = 3.
These three operators are summarized in the following truth table:
A | B | A & B | A | B | A ^ B |
---|---|---|---|---|
0 | 0 | 0 | 0 | 0 |
0 | 1 | 0 | 1 | 1 |
1 | 0 | 0 | 1 | 1 |
1 | 1 | 1 | 1 | 0 |
The ! operator is used for logical negation. The bitwise operator for negation is ~. The result of a bitwise negation very much depends on the size of the values being negated. For example ~5 as a single byte:
~ 00000101 ---------- 11111010 = 250
However, if we are using 4 byte integers:
~ 00000000000000000000000000000101 ---------------------------------- 11111111111111111111111111111010 = 4294967290
Another thing we can do with bits is shift them left or right. For example, if we shift 5 left three spots:
101 << 3 ------ 101000 = 40
Shifting left by 3 is the same thing as multiplying by 8. In general, shifting left by $N$ is the same as multiplying by $2^N$.
We can also shift right using the >> operator:
10010 = 18 >> 2 ----- 100 = 4
When shifting right, the bits on the right are lost. So the rightmost 10 are shifted off of the number.
Notice that shifting right by 2 is the same as dividing by 4 (and dropping any remainder. In general, shifting right by $N$ is the same as dividing by $2^N$.
This program tests all of the bitwise operators. Notice it uses unsigned chars as the data type which can store numbers from 0 - 255. The %hhu format specifier is for unsigned chars:
#include <stdio.h>
int main() {
unsigned char a, b;
printf("Enter two numbers [0 - 255]: ");
scanf("%hhu %hhu", &a, &b);
printf("%hhu & %hhu = %hhu\n", a, b, a & b);
printf("%hhu | %hhu = %hhu\n", a, b, a | b);
printf("%hhu ^ %hhu = %hhu\n", a, b, a ^ b);
printf("%hhu << %hhu = %hhu\n", a, b, a << b);
printf("%hhu >> %hhu = %hhu\n", a, b, a >> b);
printf("~%hhu = %hhu\n", a, ~a);
printf("~%hhu = %hhu\n", b, ~b);
return 0;
}
One thing we will want to do with the bitwise operators is treat a large value as a several individual values packed together. For instance, we might view a 32-bit integer as eight 4-bit values, 32 one-bit values, or any other combination.
You can only directly read or write an entire byte, but with the bitwise operators, we can read and write bits.
How could we write a function that tells us if a single bit in a value is set or not?
With such a function, we could write a program to print a number in binary:
/* return whether a given bit is 1 or not */
int test_bit(unsigned int value, int bit_number) {
int mask = 1 << bit_number;
if ((value & mask) == 0) {
return 0;
} else {
return 1;
}
}
Likewise, can we write a function to set a given bit in a number, and make it 1?
/* sets a bit in a number to 1 */
void set_bit(unsigned int* value, int bit_number) {
int mask = 1 << bit_number;
*value = *value | mask;
}
How could we clear a bit which would set it to zero?
/* clear a bit in a number to 0 */
void clear_bit(unsigned int* value, int bit_number) {
int mask = ~(1 << bit_number);
*value = *value & mask;
}
Here we want to flip, or invert a single bit in a number. How can we do that?
/* invert a given bit in a number */
void flip_bit(unsigned int* value, int bit_number) {
int mask = 1 << bit_number;
*value = *value ^ mask;
}
These functions can be found in bits.c.
We have seen the printf and scanf functions for doing I/O in C. There are versions of these functions call fprintf and fscanf which work similarly except that they work with files instead of standard input and output.
To use them, we must create a FILE* object, which is a pointer to a FILE structure.
To create a file, we use the fopen function which takes the name of the file as the first parameter. The second parameter is the mode string which is one of the following:
Mode | Meaning |
r | Open for reading. |
r+ | Open for reading and writing, file is not overwritten. |
w | Open for writing. |
w+ | Open for reading and writing, but file is overwritten if exists. |
a | Open for appending (writes at end of file). |
a+ | Open for reading and appending. |
To read a number from an input file, we could use:
#include <stdio.h>
int main() {
FILE* file = fopen("data.txt", "r");
int number;
fscanf(file, "%d", &number);
printf("Read %d\n", number);
fclose(file);
return 0;
}
C also has functions for reading and writing binary data. The fread function takes the following parameters:
void* ptr
This is a pointer into memory where the data should be read into. A void* is a pointer that can point to any data type. fread can be used to read any data type (including structs).
int size
The size of each data element to be read. Because fread works for any type, we must tell it how big each element is.
int num
The number of elements to read. If greater than 1, fread will read in multiple elements at once (as in a whole array).
FILE* file
The file to read from (which must already be open).
fread returns the number of elements which were successfully read. You should check that this is equal to the number you attempted to read.
The following example shows how we could read in an array of 5 numbers stored in binary:
int array[5];
if (fread(array, sizeof(int), 5, file) != 5) {
/* there was an error */
}
The analogous function for output is fwrite, which takes exactly the same parameters as fread, except that it writes the data to a file instead of reading it.
An example which would write the binary values above would look like:
int array[5];
if (fwrite(array, sizeof(int), 5, file) != 5) {
/* there was an error */
}
This programs reads 5 ints from the user and writes them to a file in binary. Notice that the file will not be human-readable afterwards! You can use the hexdump -C command to view the file.
Copyright © 2024 Ian Finlayson | Licensed under a Creative Commons BY-NC-SA 4.0 License.