Types and how data lives in memory · about 40 min

Bits, bytes, binary and hexadecimal

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:

bitspatternsas unsigned numbers
120 to 1
4160 to 15
82560 to 255
1665,5360 to 65,535
324,294,967,2960 to about 4.29 billion
64about 1.8 * 10^190 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:

hex0123456789ABCDEF
decimal0123456789101112131415
binary0000000100100011010001010110011110001001101010111100110111101111

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:

formbaseexamplevalue
42decimal4242
0x2A or 0X2ahexadecimal0x2A42
052octal (base 8)05242
0b101010binary (GCC extension, standard only from C23)0b10101042

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

Recap

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.

Exercises

Exercise 1 · quiz

Binary to decimal

What is the binary number 1011 0110 in decimal? Type just the number.

Answer

Add the place values of the 1 bits: 128 + 32 + 16 + 4 + 2 = 182. A quick check with hex: the two nibbles are 1011 = B (11) and 0110 = 6, so the byte is 0xB6 = 11 * 16 + 6 = 182.

Exercise 2 · quiz

Hex and nibbles

Which of these is the binary form of 0x3F?

Answer
  • 0011 1111

Each hex digit is exactly one nibble (4 bits): 3 = 0011 and F = 1111, so 0x3F = 0011 1111, which is 63 in decimal. This one-digit-per-nibble property is the whole reason programmers use hexadecimal: you can convert to and from binary in your head, digit by digit.

Exercise 3 · quiz

How many values?

How many different values can be stored in 2 bytes (16 bits)?

Answer
  • 65536

Each extra bit doubles the number of patterns, so n bits give 2 to the power n patterns: 2^16 = 65536. If those patterns represent non-negative integers, the range is 0 to 65535. The largest value is always one less than the number of values, because 0 uses one of the patterns.

Exercise 4 · write code

One number, three spellings

The starter stores the number 2026 in a variable. Print it in decimal, hexadecimal (lowercase, with the 0x prefix) and octal, exactly like this:

decimal: 2026
hex: 0x7ea
octal: 3752

Use format specifiers (%d, %x or %#x, %o) on the variable n; do not type the converted digits yourself.

File: main.c

Starting point

#include <stdio.h>

int main(void)
{
    int n = 2026;
    printf("decimal: %d\n", n);
    /* print the hex and octal lines */
    return 0;
}

Example

expected output

decimal: 2026
hex: 0x7ea
octal: 3752
Hint 1

%x prints hexadecimal, %o octal.

Hint 2

%#x adds the 0x prefix for you; 0x%x works too.

Reference solution
#include <stdio.h>

int main(void)
{
    int n = 2026;
    printf("decimal: %d\n", n);
    printf("hex: %#x\n", n);
    printf("octal: %o\n", n);
    return 0;
}

Exercise 5 · write code

The leading-zero trap

A colleague stored some room numbers with leading zeros to make them line up. The program prints the wrong numbers for two of them, and one line does not even compile if you uncomment it. Fix the literals so the program prints exactly:

room 101
room 042
room 010
room 009

Keep the %03d format, which already adds leading zeros when printing. The fix is in how the numbers are written in the source.

File: main.c

Starting point

#include <stdio.h>

int main(void)
{
    printf("room %03d\n", 101);
    printf("room %03d\n", 042);
    printf("room %03d\n", 010);
    /* printf("room %03d\n", 009); */
    return 0;
}

Example

expected output

room 101
room 042
room 010
room 009
Hint 1

Run it: 042 prints as 34 and 010 as 8. Why would a leading zero change the value?

Hint 2

In C, a literal that starts with 0 is octal (base 8). 042 is 4*8 + 2 = 34, and 009 is invalid because 9 is not an octal digit.

Hint 3

Write the numbers without leading zeros: 42, 10, 9. The %03d format adds zeros on output.

Reference solution
#include <stdio.h>

int main(void)
{
    printf("room %03d\n", 101);
    printf("room %03d\n", 42);
    printf("room %03d\n", 10);
    printf("room %03d\n", 9);
    return 0;
}