-
Notifications
You must be signed in to change notification settings - Fork 0
Numeral Systems
The first and most important numeral system to understand as a programmer is binary, or base-2. Binary is the fundamental language of digital systems because it conveniently represents the logical states true or false. Binary numbers use the symbols 0 and 1 to represent individual binary digits, or bits. Binary numbers are also commonly subdivided into two other groupings: the nibble (four bits) and the byte (eight bits, or two nibbles). Bits in the binary numeral system have power-of-2 weights. For example, take an n-bit binary number which has the following positional weights:
Index (Position) | n | ... | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|
Weight (in decimal) | 2n | ... | 23 = 8 | 22 = 4 | 21 = 2 | 20 = 1 |
Counting in binary works exactly like it does in decimal; in fact, this is true for all of the numeral systems described in this section. When we run out of symbols to represent a digit, we add a new digit and start back over from the first symbol. Assuming only positive integers, it looks something like this:
Binary | 000 | 001 | 010 | 011 | 100 | 101 | 110 | 111 | 1000 |
---|---|---|---|---|---|---|---|---|---|
Decimal | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
So, we can represent arbitrary, positive decimal integers with binary. Pretty cool! "What about negative numbers?" you might ask. In modern computing, the dominant system used to represent signed integers in binary is called two's complement. Two's complement prevailed over other systems like signed-magnitude and ones' complement because it has a few particularly useful mathematical properties. First, two's complement has exactly one representation of 0. Second, addition and subtraction over both positive and negative binary numbers represented in two's complement behave how one would expect, unlike in ones' complement and sign-magnitude. As an added bonus, we can use a neat trick to negate any two's complement binary number. In two's complement, the most significant bit (MSB), or left-most bit, is reserved for the sign of the number. Now that I've outlined the basics of two's complement, let's look at an example. Say we have three bits with which to represent a number, reserving the bit in position two (the third bit from the right) for the sign. In two's complement, we can represent all the numbers between -4 and 3 (including 0):
Binary (3-bit) | 000 | 001 | 010 | 011 | 100 | 101 | 110 | 111 |
---|---|---|---|---|---|---|---|---|
Decimal (signed) | 0 | 1 | 2 | 3 | -4 | -3 | -2 | -1 |
Decimal (unsigned) | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
Notice that there is an "overflow" behavior when we reach the largest positive number we can represent. Somewhat confusingly, this means that in the two's complement system, the largest positive number and the largest negative number are effectively adjacent on the number line! For this reason, programmers must be cognizant of the range of values they expect a variable to store so that their programs don't have any undesired overflow/underflow errors during calculations. We'll touch on data types with different widths and signedness in a later section.
That's all for binary! The next two numeral systems are effectively more concise ways to represent binary numbers; as such, the rules of two's complement will still apply when we're dealing with signed numbers.
Octal numbers are base-8, so each octal digit has a power-of-8 positional weight. To represent each octal digit, we use the Arabic numerals from 0 to 7. Let's take an n-digit octal number which has the following positional weights:
Index (Position) | n | ... | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|
Weight (in decimal) | 8n | ... | 83 = 512 | 82 = 64 | 81 = 8 | 80 = 1 |
Hexadecimal numbers are base-16, so this system requires 16 distinct symbols to express each digit. Since the Arabic numerals only go up to 9, hexadecimal supplements them with the first 6 English letters:
Decimal | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Hexadecimal | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F |
In my opinion, hexadecimal is the most convenient numeral system to use when programming, especially when working in an embedded context, because it is a compact way to represent binary literals when working with large bit-width types. For example, each hexadecimal digit represents one nibble of the underlying binary, so byte literals are easily written with only two characters in hex.
So ends the crash course on numeral systems. Next, let's discuss in a little more detail the kinds of mathematical operations we can perform on bit vectors which are useful while programming for an embedded device.