Computers can only store 0s and 1s. There is no minus sign or decimal point inside a memory cell. So before we can understand how C stores numbers in a variable, we need to understand how numbers are written using only bits. Everything in Data Types And Variables builds on top of this note.
Bits, Nibbles and Bytes
A bit is a single binary digit: either 0 or 1.
A nibble is a group of 4 bits. It corresponds to exactly one hexadecimal digit.
A byte is a group of 8 bits (two nibbles), and it is the smallest unit of memory a C program can address.
1 byte=8 bits
Since each bit can hold two values, a byte can hold 28=256 distinct patterns. That single fact is where every data type range in C comes from.
Place Values (Positional Notation)
In the decimal system we use every day, each position is worth a power of 10. Binary works exactly the same way, except each position is worth a power of 2:
Bit position
7
6
5
4
3
2
1
0
Weight
128
64
32
16
8
4
2
1
Power of 2
27
26
25
24
23
22
21
20
To read a binary number, multiply each bit by its weight and add everything up:
v=i=0∑n−1bi⋅2i=bn−1⋅2n−1+⋯+b1⋅2+b0
where bi is the bit at position i.
Follow Along: Reading 0101 1010
Let’s decode the byte 0101 1010 bit by bit, using the table above. Write the byte under the weights, and multiply each column:
Notice the trick: a 0 bit contributes nothing, so you can simply add up the weights of the columns that contain a 1. You don’t even need to know which bit position each 1 is in. The weight is written right above it.
Your turn
Try 0010 0101 yourself before reading on.
The 1s sit under the weights 32, 4 and 1:
001001012=32+4+1=3710
If you got 37, you can already read binary.
MSB and LSB
The MSB (Most Significant Bit) is the leftmost bit. It has the largest weight (27 in a byte). In 0101 1010 the MSB is the first 0 (worth 128, and here it contributes nothing).
The LSB (Least Significant Bit) is the rightmost bit. It has the weight 1. In 0101 1010 the LSB is the final 0.
A quick sanity check: flipping only the LSB can change the value by at most 1, while flipping only the MSB can change it by 128. That’s why they’re called most and least significant.
Converting Decimal to Binary
Method 1: Subtract the largest power of 2 (place-value method)
Find the biggest weight that fits, mark that bit 1, subtract, and repeat with the remainder.
Binary strings are long and easy to misread, so we often group bits into nibbles and give each nibble one hexadecimal (base-16) digit:
What Is a Base-16 Digit?
A base answers one question: how many distinct symbols do we have before we run out and carry to the next position?
Base 10 (decimal): ten symbols, 0 to 9. We’ve used this since childhood, so it feels natural, but the choice of ten is arbitrary (probably just because we have ten fingers).
Base 2 (binary): two symbols, 0 and 1. One bit per position.
Base 16 (hexadecimal): sixteen symbols per position. But our digits stop at 9, so we’re six symbols short. So hexadecimal invents six extra digits by recycling the first letters of the alphabet:
Because 16=24: one hex digit replaces exactly 4 bits, always. A byte is two hex digits, and a 16-bit value is four. The conversion is pure substitution, no arithmetic needed. Decimal has no clean relationship to powers of 2, which is why decimal and binary never line up.
Binary
Decimal
Hex
Binary
Decimal
Hex
0000
0
0
1000
8
8
0001
1
1
1001
9
9
0010
2
2
1010
10
A
0011
3
3
1011
11
B
0100
4
4
1100
12
C
0101
5
5
1101
13
D
0110
6
6
1110
14
E
0111
7
7
1111
15
F
In C, hexadecimal literals are written with a 0x prefix, e.g. 0x19. Each hex digit covers 4 bits, so one byte is always exactly two hex digits:
What Is the 0x?
The 0x is not part of the value. It’s a marker that tells the compiler to read the digits that follow in base 16. This matters because the digits themselves can be ambiguous:
19 → is this nineteen? or 1·16 + 9 = 25?
A bare 19 is decimal (nineteen) by default, so we need a way to switch bases. C’s rule is: the first digit must always be one of 0 to 9 (so the compiler can still recognize it as a number, not a variable name), and then a prefix selects the base:
Literal
Prefix
Base
Value (decimal)
25
(none)
10
25
0x19
0x
16
1⋅16+9=25
031
leading 0
8 (octal)
3⋅8+1=25
0b11001
0b
2 (GCC extension)
16+8+1=25
So 0x simply means: “hex follows”. The x itself stands for hexadecimal (in older literature you’ll also see it called hex, and the prefix is sometimes written 0X, where uppercase is identical in meaning).
The value doesn't change, only the notation
25, 0x19, 031 and 0b11001 all produce the same number in memory. The compiler converts them to identical bits. The prefix only changes how you wrote it down:
char a = 25; // decimalchar b = 0x19; // hexadecimal, same value as achar c = 0b11001; // binary (GCC extension), also the same
Beware the leading-zero octal trap
A plain leading 0 (not 0x) means octal (base 8) in C. Writing 019 is not nineteen. It’s actually a compile error, since 9 is not a valid octal digit. This is why you should never pad decimal literals with zeros.
0001 1001
↓ ↓
1 9 → 0x19
And reading a hex number back into decimal uses powers of 16:
0x19=1⋅161+9⋅160=16+9=2510
Quick check
0xE7 is 1110 0111 in binary, because E = 1110 and 7 = 0111. As a plain (unsigned) binary number that is 128+64+32+4+2+1=231. Why this same byte can also mean −25 is explained in One’s and Two’s Complement.
Counting and the All Ones Trick
Two patterns show up constantly in C:
All zeros is the smallest value a plain binary number can hold: n00…0=0
All ones is the largest value a plain binary number can hold:
n bits11…1=2n−1
Why 2n−1? The odometer argument
Think of a car odometer with 3 digits. It can show 000 through 999. That’s 103=1000 different readings. The highest reading is not 1000 (it can’t show four digits), it’s 999=103−1. The count is always one more than the highest value, because counting starts at zero.
Binary behaves identically. With 3 bits there are 23=8 patterns, and counting runs from 0 up to the maximum:
Eight patterns, highest value 7, not 8. The pattern 000 spent one slot on zero. In general: n bits give 2n patterns, and since counting starts at 0, the largest is 2n−1. All-ones is simply the pattern that represents it.
You can see the same thing from the rollover: adding 1 to 111 cascades a carry through every bit and pushes out a 4th digit:
1 1 1
+ 1
-------
1 0 0 0 ← = 8, but it needs a 4th bit
The result 1000 (23) doesn’t fit in 3 bits. So the largest number that does fit must be one less: 23−1=7. For a byte, the same argument with n=8 gives 28−1=255.
Key Formulas
Here is everything from this note on one card. The third row is the one that trips everyone up. It’s not a new idea, it’s just the Place Values rule from earlier written in symbols:
i=0∑n−1bi⋅2i
Read it one piece at a time, right to left:
2i is the weight of position i (position 0 is worth 1, position 1 is worth 2, position 2 is worth 4, and so on).
bi⋅2i: take the bit at that position and multiply it by its weight. If the bit is 0, the column is worth nothing. If it’s 1, the column is worth exactly its weight.
∑i=0n−1: add up all those columns, from position 0 up to position n−1 (the leftmost bit).
So the whole monster formula just says: each bit times its weight, all added up. That is the exact thing you did in Follow Along: Reading 0101 1010.
Question
Formula (for n bits)
In plain words
Byte (n=8)
Number of distinct patterns
2n
Every added bit doubles the count
256
Largest plain binary value
2n−1
All bits set to 1
255
Value of a pattern
∑i=0n−1bi⋅2i
Multiply each bit by its weight and add, which means add the weights of the 1 bits