Before you begin
Why this matters
Binary and hexadecimal appear in file contents, in hardware registers, and in the debugger. When you can read them by hand, you can check a value instead of guessing. You will also recognize two common bugs: adding a char where you meant its digit, and a leading zero that makes a literal octal.
What this helps you reason about
- Say the decimal value of a binary or hexadecimal number.
- Print a number in hexadecimal with printf, and in binary with a loop.
- Turn a string of digits into a number, and spot code that adds a character code instead of a digit.
The number eleven can be written as 11, as 1011, or as b, depending on the base. This lesson shows how a row of bits stands for a number, how C writes and prints numbers in hexadecimal and binary, and how to turn a string of digits into a number yourself.
Already know this? The short version
- In base 2 each place is worth twice the place to its right, in base 16 sixteen times; 0xb and 11 are one number, and one hexadecimal digit is four bits.
- C11 has hexadecimal (0x) and octal (a leading 0, so 010 is 8) constants; 0b is a GCC and Clang extension until C23 adds it.
- unsigned char holds 0 to UCHAR_MAX, 255 when CHAR_BIT is 8. printf has %x, %X, %#x, %02x, and %o; C11 has no binary conversion (C23 adds %b), so print bits with a loop over the places.
- A char is a small integer: text[i] - '0' is the digit; adding text[i] adds its code. strtol(text, &end, 2) stops silently at the first character that is not a digit of the base, so check end.
Words in this lesson (8)
- bit
- A single binary digit: 0 or 1.
- place value
- What a digit is worth because of where it stands. In decimal, each place is worth ten times the place to its right; in binary, two times; in hexadecimal, sixteen times.
- base
- The number of different digits a way of writing numbers uses: 10 for decimal, 2 for binary, 16 for hexadecimal. Each place is worth the base times the place to its right.
- binary
- Writing numbers in base 2, with the digits 0 and 1. Binary 1011 is 8 + 2 + 1 = 11.
- hexadecimal
- Writing numbers in base 16, with the digits 0 to 9 and a to f, where a is 10 and f is 15. One hexadecimal digit stands for exactly four bits.
- literal
- A value written directly in the code, such as 11 or 0xb.
- byte
- The smallest piece of memory a C program can address. It has CHAR_BIT bits, which is 8 on almost every machine; then one byte can hold a number from 0 to 255.
- character code
- The number a program stores for a character. The digit characters 0 to 9 have the codes 48 to 57, so the character 7 is stored as 55, not 7.
Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.
01 / Place value
How can a row of bits stand for a number?
A bit is a single binary digit: 0 or 1. A row of bits can stand for a number, in the same way that a row of decimal digits does.
In the decimal number 345, the 3 is worth 300 because of where it stands. Each place is worth ten times the place to its right: ones, tens, hundreds. This is called place value. The number of different digits is the base, so decimal is base 10. Binary is base 2: it has the digits 0 and 1, and each place is worth two times the place to its right: 1, 2, 4, 8, and so on. So binary 1011 is 1 × 8 + 0 × 4 + 1 × 2 + 1 × 1 = 11.
You can also read the digits from left to right and keep a value so far. For each digit, multiply the value so far by 2, then add the digit. The loop below does that. int bits[] = {1, 0, 1, 1} is an array of four ints, and the for loop runs its block with i equal to 0, 1, 2, and 3; i++ adds 1 to i.
int bits[] = {1, 0, 1, 1};
int value = 0;
for (int i = 0; i < 4; i++) {
value = value * 2 + bits[i];
}
printf("%d\n", value); /* 11 */ Each digit is worth the digit times its place. Reading from the left, multiply the value so far by the base and add the next digit.
Precisely: why multiplying works
After the last digit, the first digit has been multiplied by the base once for every digit after it, which is exactly its place value. With 10 instead of 2, the same steps read decimal digits: 3, then 34, then 345.
Experiment
Build a number one bit at a time
Multiplying by 2 moves every bit already read one place to the left, which doubles what it is worth. Adding the next bit fills the new ones place. After four bits, the first 1 has been doubled three times, so it is worth 8.
Why it matters The same digits mean different numbers in different bases. The base is the number you multiply by.
Predict the four printed values. Then change value * 2 to value * 10 and predict again.
#include <stdio.h>
int main(void) {
setvbuf(stdout, NULL, _IOLBF, 0); // line by line, as in a terminal
int bits[] = {1, 0, 1, 1};
int value = 0;
for (int i = 0; i < 4; i++) {
value = value * 2 + bits[i];
printf("%d\n", value);
}
return 0;
}
What to look for
Step through the loop and watch i and value. The panel shows the array as bits = {1, 0, 1, 1}. value goes 0, 1, 2, 5, 11, and the run prints 1, 2, 5, 11, one per line. After the edit, value goes 1, 10, 101, 1011: the same digits read as a decimal number.
Expected output
1
2
5
1102 / Literals
How do you write a number in binary or hexadecimal?
A value written directly in the code is a literal. C calls a number written in the code an integer constant. 0xb is a hexadecimal integer constant: it is the number 11, and so is the decimal constant 11. A constant that starts with 0 alone is octal, base 8: 017 is 15, and 010 is 8, not 10. C11 has no binary constants. The base changes how a number is written, not the number.
Hexadecimal is base 16. It needs sixteen digits, so after 0 to 9 it uses the letters a to f: a is 10, b is 11, and f is 15. Capital letters A to F mean the same. Each place is worth sixteen times the place to its right, so hexadecimal 2a is 2 × 16 + 10 = 42.
Programmers use hexadecimal because one hexadecimal digit stands for exactly four bits: four bits have 16 patterns. Hexadecimal b is binary 1011, and hexadecimal 2a is binary 0010 1010: the 2 gives the left four bits and the a gives the right four.
int decimal = 11;
int hex = 0xb;
printf("%d\n", decimal == hex); /* 1 */
printf("%d\n", 0x2a); /* 42 */
printf("%d\n", 010); /* 8 */ A binary or hexadecimal literal is another way to write the same number. One hexadecimal digit is four bits.
Coming from Python: Python 3 rejects 010; C reads it as octal, 8, without a warning.
Precisely: 0b before and after C23
GCC and Clang accept 0b1011 as an extension, and the runner’s gcc 12 compiles it with -std=c11. With -pedantic, GCC warns that binary constants are an extension. C23 makes 0b part of the standard and adds the digit separator ', as in 0b1011'0110.
03 / Bytes
What is a byte, and what can it hold?
In C, a byte has CHAR_BIT bits, from <limits.h>. CHAR_BIT is 8 on almost every machine, and this lesson assumes 8. Memory and files are measured in bytes.
Each bit has two possible values, so 8 bits have 2 × 2 × 2 × 2 × 2 × 2 × 2 × 2 = 256 different patterns. Read as a number, they run from 00000000, which is 0, to 11111111, which is 255. In hexadecimal, a byte is always two digits, from 00 to ff. That is why tools often show bytes as pairs of hexadecimal digits, such as 48 69 ff.
In C, a char is one byte, and unsigned char holds the numbers 0 to UCHAR_MAX, from <limits.h>, which is 255 when a byte is 8 bits. What happens to a value that does not fit, and how negative numbers are stored, is the subject of a later lesson, Integers, signs, and overflow.
unsigned char top = 0xff;
printf("%d\n", top); /* 255 */
printf("%d\n", UCHAR_MAX); /* 255 */ A byte is almost always 8 bits: 256 patterns, read as 0 to 255, or two hexadecimal digits.
Precisely: CHAR_BIT
The C standard says a byte has CHAR_BIT bits, at least 8 (C11 5.2.4.2.1). POSIX requires exactly 8, and so does almost every machine today. A number wider than one byte, such as an int, is stored in several bytes, and the machine chooses their order (its byte order, or endianness). Code that reads such a number from a file or a network must put the bytes together in the order the data was written. Whether plain char can hold negative numbers depends on the platform, which is one reason to use unsigned char for bytes. The stepper shows an unsigned char as a number followed by the character in quotes, such as 255 '\377'.
04 / Printing
How do you print a number in binary or hexadecimal?
printf shows a number in decimal with %d. %x prints hexadecimal digits with small letters, %X with capital letters, and %o octal. None of them adds a prefix; %#x adds 0x to every value except 0, which it prints as 0.
A number between % and the letter is a width, the smallest number of characters to print. A 0 in front of it asks for zeros instead of spaces, so %02x prints a byte as two hexadecimal digits. The width never removes digits.
C11 has no conversion for binary. To print bits, visit the places from 128 down to 1. value / place % 2 is the digit at that place: / divides and drops the remainder, and % 2 keeps the remainder after dividing by 2.
int value = 42;
printf("%x %X %#x\n", value, value, value); /* 2a 2A 0x2a */
printf("%02x\n", 10); /* 0a */
for (int place = 128; place >= 1; place = place / 2) {
printf("%d", value / place % 2);
}
printf("\n"); /* the loop printed 00101010 */ %x and %X print hexadecimal digits and %02x pads to two; binary needs a loop over the places.
Precisely: %b in C23
C23 adds %b to printf. The runner’s C library, glibc, already prints 1011 for printf("%b", 11), but a C11 program cannot rely on it.
Experiment
Print one number in hexadecimal and binary
printf has %x for hexadecimal, and %02x pads to 2 digits with zeros. C11 has no conversion for binary, so the loop prints one digit per place: value / place % 2 is the digit at that place. The loop visits 8 places, from 128 down to 1.
Why it matters A loop over 8 places prints a byte. A larger number needs more places, or its higher bits are silently left out.
Predict the three printed lines. Then change 11 to 300 and predict again. Which place does the loop never visit?
#include <stdio.h>
int main(void) {
setvbuf(stdout, NULL, _IOLBF, 0); // line by line, as in a terminal
int value = 11;
printf("%x\n", value);
printf("%02x\n", value);
for (int place = 128; place >= 1; place = place / 2) {
printf("%d", value / place % 2);
}
printf("\n");
return 0;
}
What to look for
Step through the loop and watch place: it goes 128, 64, 32, 16, 8, 4, 2, 1. The binary digits appear in the output all at once, when printf("\n") ends the line, because output is sent line by line. The run prints b, then 0b, then 00001011. After the edit, it prints 12c twice, because %02x adds no zeros to 3 digits, then 00101100: the loop starts at 128, so the 256 place of 300 is never printed.
Expected output
b
0b
0000101105 / Digits in text
How do you turn a string of digits into a number?
Text such as "101" is not a number. It is a string of three characters. A program stores each character as a number of its own, called its character code. The digit characters 0 to 9 have the codes 48 to 57, in order. So the character 1 is stored as 49, not as 1, and the character 7 as 55.
To get the value of a digit character, subtract the code of the character 0: 49 - 48 is 1. Then use the loop from the first section. In C, a char is a small integer that holds the character's code, and '0' is a character constant, the code 48. So text[i] - '0' is the digit's value. The loop stops at the character '\0' that ends every C string.
strtol(text, &end, 2), from <stdlib.h>, does the same work for you. It stops at the first character that is not a digit of the base and returns what it has read so far, with no error: strtol("1021", &end, 2) is 2. Afterwards, end points to where it stopped, so *end != '\0' tells you that characters were left over.
const char *text = "101";
int value = 0;
for (int i = 0; text[i] != '\0'; i++) {
value = value * 2 + (text[i] - '0');
}
printf("%d\n", value); /* 5 */ Turn each digit character into its digit, then multiply the value so far by the base and add.
Coming from Python: text[i] is a number, not a one-character string: text[i] + 1 is a code plus 1, and '7' + 1 is 56.
Precisely: what C promises about codes
The values 48 to 57 come from ASCII, which almost every system uses. C itself promises less, and enough: the codes of '0' to '9' are consecutive and increasing (C11 5.2.1), so c - '0' works with any character set. It does not promise that 'a' to 'f' are consecutive.
Experiment
Add the digits of a binary string
A char is a small integer, and in arithmetic it counts as its code. The character 1 is stored as its code, 49, and the character 0 as 48. Adding the code instead of the digit gives 49, then 49 × 2 + 48 = 146, then 146 × 2 + 49 = 341. Subtracting the code of 0 turns each code into its digit: 49 - 48 is 1.
Why it matters A digit character is not its digit. Subtract the code of the character 0 before you multiply and add.
The loop is meant to give 5 for "101". Predict what it prints. Then change + digit; to + (digit - '0'); and predict again.
#include <stdio.h>
int main(void) {
setvbuf(stdout, NULL, _IOLBF, 0); // line by line, as in a terminal
const char *text = "101";
int value = 0;
for (int i = 0; i < 3; i++) {
char digit = text[i];
value = value * 2 + digit;
}
printf("%d\n", value);
return 0;
}
What to look for
Step through the loop and watch digit and value. text shows an address and the string "101". digit shows 49 '1': a char holds the code 49 of the character 1. value goes 49, 146, 341, and the run prints 341. After the edit, value goes 1, 2, 5, and the run prints 5.
Expected output
34106 / Try it
Can you explain a fresh example?
Work out what each literal is worth, then how each number is printed. The next lesson, Function calls, follows the values that functions receive and return.
#include <stdio.h>
int main(void) {
int a = 0x1f;
int b = 010;
char digit = '7';
printf("%d %x %d\n", a + b, a + b, digit);
return 0;
}
What is printed?
Reveal the reasoning
39 27 55. 0x1f is 1 × 16 + 15 = 31, and 010 is octal, so it is 8; a + b is 39. In hexadecimal, 39 is 2 × 16 + 7, which prints as 27: the same digits as decimal twenty-seven, but a different number. digit holds the code of the character 7, and %d prints that code, 55.
Practice tickets
- BITS-101 · The pill counter shows 3 for a tray of 6 pills → Free
- BITS-102 · Heart rates saved with a letter come out wrong → Sign in · Pro
- BITS-103 · Impossible hearing aid settings are saved as other values → Sign in · Pro