A reading has a 1-byte id, an 8-byte value and a 1-byte flag. How many bytes is it? Guess, then watch.
Why a struct outgrows its fields
One reading: an id, a value and a flag, laid out in memory
#include <stddef.h>#include <stdio.h> struct reading { char id; double value; char flag;}; int main(void){ struct reading r = { 'k', 3.75, 1 }; r.value = r.value * 2; printf("sizeof r = %zu\n", sizeof r); printf("value at %zu, flag at %zu\n", offsetof(struct reading, value), offsetof(struct reading, flag)); return 0;}Output
main has started. r is one block of 24 bytes from 0x7fd8; its fields have no values yet.
Describe this picture
The 18 lines of reading.c with the line about to run marked, beside a picture of memory under the label “Stack”: main’s card, holding the struct r as three rows of 8 byte cells, higher addresses on top. r starts in the bottom row, at 0x7fd8: its first cell is the box id, and the other seven cells of that row are empty. The middle row is the box value, a double of 8 bytes at 0x7fe0. In the top row the first cell, 0x7fe8, is the box flag, and the other seven cells are empty. The empty cells inside r are its padding. The wide box shows its type and name, double r.value, at the top left and its address at the top right; the two 1-byte boxes show only their field names. Each box shows its value in the middle, ”?” until a line writes one. Under the picture are the switch, then the program’s output, and under everything one sentence on the current step. The clip plays the program once, 6 steps of 1.2 seconds, and rests on the end: line 12 fills id with 107 (the letter k), value with 3.75 and flag with 1; line 13 changes value to 7.5; then the output shows sizeof r = 24 and value at 8, flag at 16. The switch, “Field order”, with the positions “as written” and “value first”, swaps the program for reading-reordered.c, whose struct lists value first: r is then two rows from 0x7fe0, value filling the lower row and id and flag the first two cells of the upper row, with six empty cells after them; the output becomes sizeof r = 16 and value at 0, flag at 9. A press while paused keeps the step; a press after the end plays the other program from the start. 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, and the switch as mem.order.
One block, with gaps
The answer is 24 bytes, although the fields add up to only 1 + 8 + 1 = 10. Let’s see where the other 14 go.
The three fields of r sit in one block of memory, in the order the program lists them. The block starts in the bottom row of the picture, at 0x7fd8, and id takes its first byte.
Next comes value, a double. A double must start at an address that is a multiple of 8, and the byte right after id, 0x7fd9, is not. The next multiple of 8 is 0x7fe0, eight bytes from the start of r. So value starts there, and the seven bytes between id and value hold nothing. Bytes inside a struct that belong to no field are called padding.
flag, a char, can start anywhere, so it takes the next byte, 16 bytes in. Then the struct goes on for seven more bytes of padding, to 0x7fef, and ends 24 bytes after it began. The output confirms it: sizeof r = 24, value at 8 and flag at 16.
Now press “value first” and watch the block again. The same three fields, with value listed first, take 16 bytes: value at the start, id 8 bytes in, flag at 9, and six bytes of padding at the end. Only the order changed.
Key idea
A struct’s fields sit in one block, in the order written. Each starts at the next address that suits its type, so the block can be larger than its fields, and the order of the fields decides by how much.
The code and its cost
Declaring a struct and using its fields
Lines 4 to 8 declare a new type, struct reading:
struct reading {
char id;
double value;
char flag;
};
This makes no memory yet. It says what one reading holds: a field for each line, with its type and name. Line 12, struct reading r = { 'k', 3.75, 1 };, makes a variable r of that type and fills its fields in the order they are listed: id gets ‘k’, value 3.75, flag 1. From Variables live in memory (1.2): a char holds a letter as its number, and ‘k’ is 107, which is what the picture shows.
r.value names one field of r, and it works like any variable. Line 13, r.value = r.value * 2;, reads 3.75 from that field, doubles it, and stores 7.5 back. Only those 8 bytes change.
Copying and passing a struct
A struct is copied the way an int is: every byte. Here is a smaller struct, a point with an x and a y, copied once by assignment and once by a call:
struct point {
int x;
int y;
};
void move_right(struct point p)
{
p.x = p.x + 1;
printf("inside: (%d, %d)\n", p.x, p.y);
}
and in main:
struct point a = { 2, 5 };
struct point b = a;
b.y = 9;
move_right(a);
printf("a = (%d, %d), b = (%d, %d)\n", a.x, a.y, b.x, b.y);
b = a copies all 8 bytes of a into b, so b holds (2, 5) too. It does not make b a second name for a: after b.y = 9;, a.y is still 5. The call move_right(a) copies a’s whole block into the parameter p, in move_right’s own frame, as Functions and the call stack (1.3) showed for an int. move_right changes its copy and prints inside: (3, 5); a is untouched, and main prints a = (2, 5), b = (2, 9). The replay shows each copy, field by field.
Copying a struct
b = a copies the whole block; so does a call
#include <stdio.h> struct point { int x; int y;}; void move_right(struct point p){ p.x = p.x + 1; printf("inside: (%d, %d)\n", p.x, p.y);} int main(void){ struct point a = { 2, 5 }; struct point b = a; b.y = 9; move_right(a); printf("a = (%d, %d), b = (%d, %d)\n", a.x, a.y, b.x, b.y); return 0;}Output
main has started. a and b are two blocks of 8 bytes, x then y; no values yet.
Describe this picture
The 22 lines of point.c with the line about to run marked, beside the stack. main’s card has two rows: a, a struct point of 8 bytes at 0x7fe8, on top, its box x in the left half and y in the right half; b at 0x7fe0 below it, the same way. While move_right runs, its card appears below main’s with one row, p at 0x7fc8, also as x and y. Each box shows its type and name, such as int a.x, its address and its value, ”?” until written. The replay runs one line every 1.9 seconds, 10 steps in about 17 seconds, and rests on the end. a takes 2 and 5; b = a writes 2 and 5 into b’s two boxes; b.y = 9 changes b’s y alone. At the call, move_right’s card fades in with p holding 2 and 5; p’s x becomes 3, inside: (3, 5) appears under the picture, and the card fades out. The output ends with a = (2, 5), b = (2, 9). The transport plays and pauses, steps one line back or forward, and scrubs; a paused step is kept in the page address as copy.step.
Copying costs as many bytes as the struct has: 8 for a point, 24 for a reading, whatever the fields hold. For a large struct that adds up on every call, and Passing pointers to functions (2.2) shows how to hand a function the struct’s address instead.
Alignment, padding and sizeof
Two rules decide where each field goes, on this platform:
- Each field starts at a multiple of its alignment: 1 for a
char, 4 for anint, 8 for adouble. If the next free byte is not such a multiple, the bytes up to the next one are padding. - The size of the whole struct is rounded up to a multiple of the largest alignment among its fields.
The second rule explains the padding after flag. In an array of readings, the second reading starts right after the first, sizeof r bytes later. Its value must also start at a multiple of 8, and with a size of 24, it does.
The processor reads and writes a value fastest when the value sits at a multiple of its size, and some processors cannot read it at all otherwise. These rules make sure every field of every struct sits that way.
sizeof r gives the size of the block, padding included. offsetof(struct reading, value), from <stddef.h>, gives how many bytes into the struct a field starts. Line 14 prints the size, and lines 15 and 16 print the offsets.
C keeps the order you write
C never reorders a struct’s fields: they sit in memory in the order the declaration lists them. Programs rely on that. A file format or a network message says that a field is at a given offset, and a struct written in that order matches it byte for byte.
So the order is the programmer’s choice, and its cost is memory. A simple rule keeps the padding small: list the largest fields first. A reading as written takes 24 bytes; with value first, 16. For a million readings, that is 24,000,000 bytes against 16,000,000.
Worked example
Work out each field’s offset and the size of this struct by hand:
struct sample {
char tag;
int count;
char unit;
double total;
};
tag takes byte 0. count needs a multiple of 4, so bytes 1 to 3 are padding and count takes 4 to 7. unit takes byte 8. total needs a multiple of 8: bytes 9 to 15 are padding, and total takes 16 to 23. The largest alignment is 8, and 24 is already a multiple of 8, so sizeof (struct sample) is 24.
| Field | Type | Offset | Bytes |
|---|---|---|---|
| tag | char | 0 | 0 |
| padding | 1 to 3 | ||
| count | int | 4 | 4 to 7 |
| unit | char | 8 | 8 |
| padding | 9 to 15 | ||
| total | double | 16 | 16 to 23 |
Now list the largest first: double total; int count; char tag; char unit;. total takes 0 to 7, count 8 to 11, tag 12, unit 13, and bytes 14 and 15 are padding to reach a multiple of 8. The size is 16.
An array of 1,000 samples takes 24,000 bytes in the first order and 16,000 in the second.
Try it yourself. What is the size of struct { char a; char b; int c; }? a takes byte 0 and b byte 1; c needs a multiple of 4, so bytes 2 and 3 are padding and c takes 4 to 7. The size is 8.
Where it’s used
Records in files. Many file formats are a sequence of records, each with fixed fields at fixed offsets. A program can describe one record as a struct and read it field by field.
Message headers. The header at the front of a network message is a list of fields in a fixed order; the written order is the format. Code that builds or reads one has to place every byte where the format says.
Sensor readings. A small device often sends each reading as a struct like the one on this page: an id, a value, a few flags. Field order decides how many bytes each reading costs to store and send.
Nodes. Every linked structure in this course is built from structs. From Singly linked lists (7.1) on, each node is a struct holding a key and the address of the next node.
Reference card
| Code or idea | Meaning |
|---|---|
struct reading { char id; double value; char flag; }; | declares a type; makes no memory |
struct reading r = { 'k', 3.75, 1 }; | makes r and fills its fields in order |
r.value | one field of r; reads and writes like a variable |
b = a;, f(a) | copy every byte of the struct; the copy is separate |
| alignment | char 1, int 4, double 8 here; each field starts at a multiple of its own |
| padding | bytes of a struct that belong to no field |
sizeof r | the whole block, padding included; a multiple of the largest alignment |
offsetof(struct reading, value) | where the field starts, in bytes; from <stddef.h> |
| order | C keeps the written order; largest fields first keeps the padding small |
Platform: C11, gcc 13 on x86-64 Linux: char 1 byte, int 4, double 8. The addresses in the pictures are short stand-ins, by one rule for the whole course; sizes and offsets are the real ones.