Skip to content

Variables live in memory

Every variable is a run of bytes at an address. Its type sets how many bytes it takes and how they are read.

Helps to know firstA first C program

Helps to know first
This topic

First, the picture

A variable has a name and a value. But where does it live? Watch four variables claim their bytes.

A variable is a box in memory

Three types, three sizes; a copy, then a change

#include <stdio.h> int main(void){    char c = 'A';    int a = 1000;    double d = 2.5;    int b;    b = a;    b = 7;    printf("%zu %zu %zu\n", sizeof c, sizeof a, sizeof d);    printf("a = %d, b = %d\n", a, b);    return 0;}

Output

main has started. Its four boxes are already there, 1, 4, 8 and 4 bytes long; none has a value yet.

0.00 / 12.00 s
Describe this picture

The 14 lines of variables.c with the line about to run marked, beside a picture of memory under the label “Stack”: one card titled main, holding three rows of 8 byte cells, higher addresses on top and rising from left to right within a row. The top row is the bytes 0x7fe8 to 0x7fef: a, an int of 4 bytes at 0x7fe8, fills its left half; the next three cells, 0x7fec to 0x7fee, are empty; c, a char of 1 byte at 0x7fef, is the last cell. The middle row is d, a double of 8 bytes at 0x7fe0. In the bottom row, b, an int at 0x7fdc, fills the right half and the left half is empty. Each box shows its type and name at the top left, its address at the top right, and its value in the middle, ”?” until a line writes one; small ticks along its bottom edge mark its bytes. Under the picture the program’s output appears once it prints, and under everything one sentence on the current step. The clip plays the program once, 9 steps of 1.5 seconds, 12 seconds in all, and rests on the end. c takes 65, a takes 1000, d takes 2.5; b keeps ”?” until b = a; puts 1000 in it, and then b = 7; changes b alone while a keeps 1000. The box the last line wrote is outlined. The output ends as 1 4 8 and a = 1000, b = 7. The transport plays and pauses, steps one line back or forward, and scrubs; a paused step is kept in the page address as mem.step.

Bytes and addresses

Memory is a very long row of bytes. A byte is a small box that holds a whole number from 0 to 255, and every byte has a number of its own, its address. A variable takes a run of bytes next to each other, and its address is the address of its first byte.

The picture draws that row in pieces of 8 bytes, stacked with the higher addresses on top. Within each piece the addresses rise from left to right. So the top row runs from 0x7fe8 on the left to 0x7fef on the right, and the row under it ends at 0x7fe7.

Addresses are written in base 16, after 0x: the digits a to f stand for 10 to 15. Only the last digit matters in this picture. From 0x7fe8 to 0x7fef is 8 bytes, the digits 8, 9, a, b, c, d, e and f.

These are short stand-ins. A real address has about twelve hex digits and changes every time the program runs. The rule that places the boxes in these pictures is one simple rule, the same on every page of this course; a real compiler may put the variables in another order. The sizes of the boxes are the real ones.

Each type takes its own number of bytes

Watch the boxes as the lines run. c is one byte. a is four bytes, and so is b. d is eight bytes. The type of a variable fixes how many bytes it takes, and how those bytes are read as a value.

c holds the letter A, and the picture shows 65. A char is a one-byte number, and C stores each letter as a number. Which number is set by the character table the platform uses, here ASCII: A is 65, B is 66, and so on. printf with %c prints the letter; the picture shows the number in the byte.

In the picture, three cells in the top row, 0x7fec to 0x7fee, belong to no variable. The picture puts a at 0x7fe8, a multiple of its size, 4, and d at 0x7fe0, a multiple of 8: each value starts at a multiple of its own size, which leaves a few bytes unused. The compiler is free to arrange a function’s variables as it likes, and the real build of this program packs all four into 17 bytes with no gap, each still at a multiple of its own size. Structs (1.4) shows why that rule exists.

The code and its cost

sizeof

sizeof gives the size of a variable or a type, in bytes. Line 11 prints sizeof c, sizeof a and sizeof d, and the output is 1 4 8. In printf, %zu is the way to print a size.

The C standard fixes only the size of a char, which is 1. An int is 4 bytes and a double 8 on the platform this course uses, and on most computers today. Size is the first cost of a type: a million doubles take 8,000,000 bytes, twice as much as a million ints.

Copying

Line 9, b = a;, reads the value in a, 1000, and stores it in b. Line 10, b = 7;, then changes b alone. a still holds 1000, and line 12 prints a = 1000, b = 7.

So after b = a; the two are not linked. Changing one never changes the other, because they are two different runs of bytes. Assignment copies a value; it never makes two names for one box.

Key idea

A variable is a run of bytes at an address. b = a copies the value from one run into the other. The two boxes stay separate.

A value not yet written

Line 8, int b;, makes b without giving it a value. The picture shows ”?” in b until line 9. The bytes of b are still there, and they hold whatever an earlier use of that memory left in them. When I stepped this program under a debugger, b held 32767 before line 9.

A new variable does not start at 0. Reading a variable before anything has been written to it is a mistake, and its value can change from run to run. Line 8 also makes no machine instruction at all, which is why the mark never stops on it.

How big a number fits

Four bytes can hold 2³² different patterns, about 4.3 billion. An int uses them for the whole numbers from −2,147,483,648 to 2,147,483,647, that is from −2³¹ to 2³¹ − 1. An unsigned int has no negative numbers and uses them for 0 to 4,294,967,295, that is 2³² − 1. An unsigned char holds 0 to 255 in its one byte.

Unsigned numbers wrap: a value that does not fit is taken modulo 2³² for an unsigned int, or modulo 256 for an unsigned char. In other words, the count goes round like a clock and starts again from 0. For an unsigned char, the wrap happens when the value is stored: C works out u + 10 as an int, and only storing the result into u’s one byte takes it modulo 256.

A signed number that goes past its limit is different. Signed overflow is undefined behaviour: the C standard says nothing about what happens, so a program that does it has a bug, whatever it seems to print. gcc can build a program with checks that catch it, as the worked example shows.

Byte order

How are the four bytes of a filled with 1000? 1000 = 3 × 256 + 232, and 232 is 0xe8 in base 16, so 1000 is 0x3e8. The bytes of a, from its address up, are:

address   0x7fe8  0x7fe9  0x7fea  0x7feb
byte        e8      03      00      00

The lowest byte comes first, at the lowest address. That is called little-endian order, and it is the order on this platform. Other machines put the highest byte first; file formats and network messages say which order they use.

Worked example

Work each of these out on paper before you read the answer.

1. An unsigned char past 255. unsigned char u = 250; u = u + 10; u is promoted to an int, so the sum is 260, an ordinary int. It wraps when it is stored back into u, which has only one byte: 260 modulo 256 is 260 − 256 = 4, so u holds 4.

2. An unsigned int below 0. unsigned int z = 0; z = z - 1; There is no −1 in an unsigned int, so the result wraps to the largest value, 2³² − 1 = 4,294,967,295. The two lines are in wrap.c, which prints 4 4294967295.

3. An int past its limit. int big = 2147483647; big = big + 1; The result, 2,147,483,648, does not fit in an int, so this is signed overflow and undefined. The file is overflow.c. Built with gcc’s checks on, gcc -std=c11 -fsanitize=undefined -g overflow.c -o overflow, it reports the mistake as it runs that line:

overflow.c:6:9: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'

The report names the file and line 6, and says what went wrong. Then the program goes on, and on my machine it printed big = -2147483648, with the checks or without them. Nothing in the standard promises that number, and with optimisation turned on a compiler may do something else entirely.

4. The bytes of 1000. 1000 ÷ 256 is 3, remainder 232. So the high byte is 3 and the low byte is 232, 0xe8. In little-endian order the four bytes are e8 03 00 00.

5. How much room the four variables take. c, a, d and b use 1 + 4 + 8 + 4 = 17 bytes. In the picture, from b’s first byte, 0x7fdc, to c’s byte, 0x7fef, is 20 bytes, because of the three unused cells, and from c’s address to d’s is 0x7fef − 0x7fe0 = 15 bytes. The real build packs them into exactly 17.

Where it’s used

Samples and readings. A sound card stores each audio sample as a fixed-size whole number, often 16 bits, and a sensor sends its readings the same way. Rounding a signal to the values a fixed size can hold is the subject of Quantization.

Counters that wrap on purpose. Network protocols number their packets with fixed-size unsigned counters, and timers in small embedded chips count up and wrap round to 0. The code is written to expect the wrap, so it is never a surprise.

File formats. A file format states the size and the byte order of every field. A program that reads a sound or image file written on another machine reads the bytes in the order the format says, not in its own.

In signal processing · fixed sizes in signal processing

Fixed-size numbers are why digital filters can overflow and why their results carry rounding errors. Finite precision follows those effects through a filter.

Reference card

Code or ideaMeaning
addressthe number of a variable’s first byte; written in base 16, as 0x7fe8
sizeof xthe size of x in bytes; print it with %zu
char1 byte; a letter is stored as its number (‘A’ is 65)
int4 bytes here: −2,147,483,648 to 2,147,483,647
unsigned int4 bytes here: 0 to 4,294,967,295; wraps modulo 2³²
unsigned char1 byte: 0 to 255; wraps modulo 256
double8 bytes here
b = a;copies a’s value into b; later changes to one do not touch the other
int b;b exists, but its value is unknown until written; reading it is a mistake
signed overflowundefined behaviour: a bug, whatever it prints
byte orderlittle-endian here: the lowest byte at the lowest address

Platform: C11, gcc 13 on x86-64 Linux: char 1 byte, int 4, double 8, little-endian. The addresses in the pictures are short stand-ins, by one rule for the whole course; the sizes are the real ones.

End of the lesson

Where to go next.

New lessons by email

Are you 13 or over?

Accounts and email updates are for people aged 13 or over. We keep only your answer, never your date of birth.

Phasorium

Account

Sign inWith Google, GitHub or your email

Subjects

Site

Look