Why this matters
Everything in a computer's memory is stored as bits: numbers, text, images, your program's machine code. C does not hide this. Its types are defined in terms of bytes, its bugs are often explained in terms of bits (an overflow, a sign that flipped, a character that turned out to be a negative number), and its tools show you memory in hexadecimal. A debugger will happily tell you that a variable holds 0xfffffffe; this lesson makes that readable.
Bits and bytes
A bit is a single binary digit: 0 or 1. Physically it might be a voltage, a charge in a capacitor, or a magnetized spot, but to a programmer it is just one of two states.
One bit can distinguish two things. Two bits give four patterns (00, 01, 10, 11). Every extra bit doubles the count:
| bits | patterns | as unsigned numbers |
|---|---|---|
| 1 | 2 | 0 to 1 |
| 4 | 16 | 0 to 15 |
| 8 | 256 | 0 to 255 |
| 16 | 65,536 | 0 to 65,535 |
| 32 | 4,294,967,296 | 0 to about 4.29 billion |
| 64 | about 1.8 * 10^19 | 0 to about 18 quintillion |
A byte is 8 bits, and it is the smallest unit of memory that has its own address. When the first lesson described memory as a row of numbered boxes, each box was one byte. A group of 4 bits (half a byte) is called a nibble.
Bits have no meaning on their own. The byte 01000001 is the number 65, or the letter A, or part of a machine instruction, or one quarter of a floating-point number, depending entirely on how the program interprets it. In C, the type of a variable is what tells the compiler how to interpret the bytes. That is the theme of this whole module.
Binary: base 2
You already know place value from decimal (base 10): in 352, the 3 is worth 3 * 100, the 5 is worth 5 * 10, the 2 is worth 2 * 1. Each place is worth ten times the place to its right.
Binary (base 2) works the same way with only two digits, and each place is worth twice the place to its right:
place value: 128 64 32 16 8 4 2 1
bit: 0 1 0 0 1 0 1 1
64 8 2 1 = 75
So 01001011 in binary is 64 + 8 + 2 + 1 = 75. The rightmost bit, worth 1, is called the least significant bit; the leftmost is the most significant bit.
To go the other way (decimal to binary), repeatedly take the largest place value that fits:
75: 64 fits -> bit 64 = 1, remainder 11
11: 32? no, 16? no, 8 fits -> bit 8 = 1, remainder 3
3: 4? no, 2 fits -> bit 2 = 1, remainder 1
1: 1 fits -> bit 1 = 1, remainder 0
result: 0100 1011
Notice that a number with all bits set is one less than the next power of two: 1111 1111 is 255 = 256 - 1. That is why the maximum values of types are numbers like 255, 65535 and 4294967295.
Hexadecimal: base 16, a shorthand for binary
Binary is exact but long; 32 bits is a wall of zeros and ones. Decimal is short but does not line up with bits at all. Hexadecimal (hex, base 16) is the compromise programmers use. It needs 16 digits, so after 9 it uses letters:
| hex | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| decimal | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
| binary | 0000 | 0001 | 0010 | 0011 | 0100 | 0101 | 0110 | 0111 | 1000 | 1001 | 1010 | 1011 | 1100 | 1101 | 1110 | 1111 |
Because 16 = 2^4, one hex digit is exactly one nibble, and two hex digits are exactly one byte. Converting is mechanical:
binary: 1011 1110 1110 1111
hex: B E E F -> 0xBEEF
and back: 0x3C is 0011 1100. No arithmetic needed, just the table (which you will soon know by heart). Hex place values are powers of 16, so 0x2A = 2 * 16 + 10 = 42 if you do need decimal.
You will see hex for memory addresses, colors (#FF8800), byte dumps, file permissions' cousins, error codes, and anywhere individual bits matter.
Writing numbers in C source
C lets you write integer literals (numbers typed in the source) in several bases:
| form | base | example | value |
|---|---|---|---|
42 | decimal | 42 | 42 |
0x2A or 0X2a | hexadecimal | 0x2A | 42 |
052 | octal (base 8) | 052 | 42 |
0b101010 | binary (GCC extension, standard only from C23) | 0b101010 | 42 |
The octal form is a famous trap: a leading zero makes a literal octal. 010 is eight, not ten. Octal survives mainly for Unix file permissions (0755). Never pad decimal literals with zeros.
The base is only how you write the number. The compiler stores the same bits for 42, 0x2A and 052. Likewise, printing in a base is only a choice of formatting:
#include <stdio.h>
int main(void)
{
int a = 0x2A;
int b = 052;
int c = 0b101010;
printf("%d %d %d\n", a, b, c);
printf("%x %X %o %#x\n", 255, 255, 8, 255);
printf("%08x\n", 3054);
return 0;
}
42 42 42
ff FF 10 0xff
00000bee
%x prints lowercase hex, %X uppercase, %o octal, and the # flag adds the prefix (0x or 0). %08x pads to 8 hex digits, the width of a 32-bit value, which is how debuggers and hex dumps usually show numbers. printf has no standard binary specifier; you will write your own binary printer once you know loops and bit operators.
Bytes in memory
A number too big for one byte is stored in several consecutive bytes. The number 3054 is 0x00000BEE as a 4-byte value. On x86-64 and almost all ARM systems, the bytes are stored least significant byte first (this is called little-endian):
address: 1000 1001 1002 1003
byte: 0xEE 0x0B 0x00 0x00
You will not need this until the bit-manipulation module, but it explains why memory dumps sometimes look "backwards".
Common mistakes
- Leading zeros in decimal numbers.
int month = 09;does not compile: andint month = 08;fails the same way, while07silently works and010silently means 8.main.c:5:17: error: invalid digit "9" in octal constant - Confusing the digit with the value.
0xFis fifteen, not "F". Letters in hex are digits. - Off-by-one in ranges. 8 bits give 256 values, but the largest is 255.
- Thinking the variable "is in hex". A variable holds bits.
%dand%xare two views of the same bits.
Recap
- A bit is 0 or 1; a byte is 8 bits and is the unit of addressing; n bits give 2^n patterns.
- Binary place values are powers of two; the maximum n-bit unsigned value is 2^n - 1.
- One hex digit is one nibble, two are one byte, so hex converts to binary digit by digit.
- C literals:
42,0x2A, octal052(beware leading zeros), and the GCC extension0b101010. %x,%X,%oand%#xprint the same value in other bases.- Bits mean nothing until a type says how to read them.
Going further: in the sandbox terminal, printf '%x\n' 2026 and echo $((0x7ea)) convert in the shell, and hexdump -C shows any file as hex bytes: try echo -n AB | hexdump -C.