A variable is bytes at an address
A variable in C is a name for a few bytes at one place in memory. Knowing how many bytes it takes and where it sits explains most of the surprises in C programs.
A game keeps a player's score in an int. The score lives in four bytes side by side, and the first of them has an address. Every later idea in this module builds on that picture.
By the end of this lesson
Note
I can say how big a variable is and where it sits in memory.
Key words
| Word | What it means | Example |
|---|---|---|
| Byte | eight bits, the smallest piece of memory with an address | |
| Address | the number that says where a byte sits in memory | |
| sizeof | an operator that gives a size in bytes | sizeof(int) |
| Struct | a group of named fields stored in one block | |
| Alignment | the rule that a value starts at a multiple of its size | |
| Padding | unused bytes the compiler adds to keep alignment |
The cells students fill in are left blank in this preview.
Every type has a size in bytes
Memory is a long row of bytes, and each byte has an address, a number that counts from the start. A variable's type says how many bytes it takes. The operator sizeof gives that number.
The table shows the common sizes on 64-bit Linux, the system in the lab. They are the same on an Intel and an Arm processor.
Sizes of the common types
| Type | Bytes | Holds |
|---|---|---|
| char | 1 | One letter or a small whole number |
| short | 2 | A small whole number |
| int | 4 | A whole number |
| long | 8 | A large whole number |
| double | 8 | A number with a fraction |
| pointer | 8 | An address |
The cells students fill in are left blank in this preview.
Write and build a size checker
printf '%s\n' '#include <stdio.h>' 'int main(void) {' ' printf("char %zu\n", sizeof(char));' ' printf("short %zu\n", sizeof(short));' ' printf("int %zu\n", sizeof(int));' ' printf("long %zu\n", sizeof(long));' ' printf("double %zu\n", sizeof(double));' ' printf("pointer %zu\n", sizeof(int *));' ' return 0;' '}' | gcc -Wall -x c - -o sizesShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Print the size of each type
./sizesShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Every byte has its own address
Neighbouring bytes have neighbouring addresses. An int takes four bytes, so the next int in an array starts four addresses later. The address of a value is the address of its first byte.
The bytes of one number also have an order. On the lab's processors the smallest part comes first, which is called little-endian.
Worked example: 258 in four bytes
| Byte | Value | Each unit is worth |
|---|---|---|
| Byte 0 | 2 | 1 |
| Byte 1 | 1 | 256 |
| Byte 2 | 0 | 65,536 |
| Byte 3 | 0 | 16,777,216 |
| Total | 2 + 256 = 258 |
The cells students fill in are left blank in this preview.
Look at the bytes of one number
#include <stdio.h>
int main(void) {
int n = 258;
// &n is the address of n. This reads the same four bytes one at a time.
unsigned char *p = (unsigned char *)&n;
for (int i = 0; i < 4; i++) printf("byte %d is %d\n", i, p[i]);
int row[3] = {7, 8, 9};
// How many bytes apart are two neighbours?
printf("gap %ld\n", (char *)&row[1] - (char *)&row[0]);
return 0;
}
Write and build the byte viewer
printf '%s\n' '#include <stdio.h>' 'int main(void) {' ' int n = 258;' ' unsigned char *p = (unsigned char *)&n;' ' for (int i = 0; i < 4; i++) printf("byte %d is %d\n", i, p[i]);' ' int row[3] = {7, 8, 9};' ' printf("gap %ld\n", (char *)&row[1] - (char *)&row[0]);' ' return 0;' '}' | gcc -Wall -x c - -o bytesShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Show the four bytes and the gap
./bytesShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Padding keeps values aligned
A processor reads a four-byte int fastest when its address is a multiple of four. So the compiler follows an alignment rule: a value starts at a multiple of its own size.
When a small field comes before a big one in a struct, the compiler leaves padding, unused bytes, to keep the rule. It also pads the end so every struct in an array stays aligned.
Worked example: three fields, twelve bytes
| Bytes | What is there |
|---|---|
| 0 | Field a, one char |
| 1 to 3 | Padding, so that b can start at 4 |
| 4 to 7 | Field b, one int |
| 8 | Field c, one char |
| 9 to 11 | Padding, so the whole struct is a multiple of 4 |
The cells students fill in are left blank in this preview.
The same fields in two orders
struct loose { char a; int b; char c; };
struct tight { int b; char a; char c; };
// loose is 12 bytes. tight holds the same fields in 8.
Write and build the struct test
printf '%s\n' '#include <stdio.h>' '#include <stddef.h>' 'struct loose { char a; int b; char c; };' 'struct tight { int b; char a; char c; };' 'int main(void) {' ' printf("loose %zu, offset of b %zu, offset of c %zu\n", sizeof(struct loose), offsetof(struct loose, b), offsetof(struct loose, c));' ' printf("tight %zu\n", sizeof(struct tight));' ' return 0;' '}' | gcc -Wall -x c - -o padShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Print both sizes and two offsets
./padShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
A common mistake
Warning
Many people think a struct is as big as its fields added up. It is often bigger. The compiler adds padding to keep each field aligned. Putting the biggest fields first usually removes most of it.