Recently, a fan asked me: “Why is the size of the structure I defined always larger than the sum of the sizes of its member variables?” Don’t worry, this isn’t the compiler “stealing memory”; it hides an important concept—byte alignment. Today, we will explain this using the logic of “arranging books on a shelf” so clearly that you will never be confused by “memory size” again!1. What is byte alignment? Let’s use the “bookshelf” analogy.First, let’s set aside complex concepts and imagine memory as a “bookshelf”, where each “slot” represents 1 byte, with addresses arranged sequentially from 0, 1, 2, 3… The data types in C language, such as char, int, and float, are like books of different thicknesses:
- char is a thin book: occupies 1 slot (1 Byte)
- short is a medium-thick book: occupies 2 slots (2 Bytes)
- int is a thick book: occupies 4 slots (4 Bytes)
- double is a very thick book: occupies 8 slots (8 Bytes)
The rules of byte alignment are like the “shelving requirements” of a library: a certain “book” must be placed in a slot that is a multiple of its own thickness. For example, a short (2 bytes) must be placed at addresses 0, 2, 4…, while an int (4 bytes) must be placed at 0, 4, 8…. If not placed according to the rules, the CPU will encounter a “bottleneck” when reading data—this is the core logic of byte alignment.
2. Why is alignment necessary? The CPU and hardware are “forcing” you to do it.
You might ask: “Can’t we just place them next to each other? It would save memory!” Actually, no. This is a “hard requirement” from the CPU and hardware, mainly for two reasons:
1. Improve memory access efficiency: The CPU “reads quickly” is the key.
The CPU does not read memory “byte by byte”; instead, it reads in blocks.
For example, a 32-bit CPU can read 4 bytes at a time (like moving 4 thin books at once); a 64-bit CPU can read 8 bytes at a time.
Counterexample: If you want to read an int (4 bytes), but it is not aligned—say it starts at address 1 (occupying slots 1, 2, 3, 4). In this case, the CPU has to read twice:
The first read is from slots 0-3, only getting 3 bytes from slots 1-3; the second read is from slots 4-7, only getting 1 byte from slot 4. Finally, these two parts have to be “stitched together” to be used.
But if the int is aligned to address 4 (occupying slots 4-7), the CPU can read it all at once—efficiency doubles! In embedded development, “speed” is often a matter of life and death (for example, in real-time control), so alignment cannot be taken lightly.
2. Hardware requirements: Some chips “crash if not aligned”.
Even more stringent than CPU efficiency are hardware limitations. For example, many ARM processors (common in embedded systems) will trigger a “bus error” if accessing 32-bit data (int/float) at an address that is not a multiple of 4—causing the program to crash immediately, leaving no chance for debugging!
It’s like trying to fit a 4-slot thick book into slot 1; the bookshelf collapses…
3. Core rules of byte alignment: Understand how to calculate size in 3 minutes.
To understand alignment, remember 3 key rules. We will calculate step by step with examples, so you will never have to “guess sizes” again.
1. Single data type: Its “alignment value” is its “thickness”.
Each data type has its own “alignment value” (by default):
| Data Type | Size in Slots | Alignment Value | Must Be Placed At Address |
| char | 1 | 1 | Any address (0, 1, 2) |
| short | 2 | 2 | 0, 2, 4, 6… |
| int | 4 | 4 | 0, 4, 8, 12… |
| float | 4 | 4 | 0, 4, 8, 12… |
| double | 8 | 8 | 0, 8, 16… |
Remember: Data must be placed at addresses that are multiples of their alignment value—this is the foundation of foundations.
2. Struct alignment: Align members first, then align the whole (key point!).
The size of a structure is not simply the “sum of its members”; it must be calculated in two steps: first align each member, then align the structure as a whole. Let’s compare two examples, and you’ll understand immediately (default 4-byte alignment, the most common scenario in embedded systems).
Example 1: The confusing “char + int” structure
struct Test1{char a; //1 byte, alignment 1int b; //4 bytes, alignment 4};
Let’s look at the memory layout step by step using the “bookshelf” logic:
-
First, place a (char, 1 byte): starting from address 0, occupying slot 0 (aligned, since 1 can be placed anywhere);
-
Next, place b (int, 4 bytes): its alignment value is 4, so it must be placed at an address that is a multiple of 4 (0, 4, 8…). However, a has already occupied slot 0, so the next multiple of 4 is 4—therefore, slots 1, 2, and 3 must be left empty (this is the “memory hole”), and b starts from 4, occupying slots 4-7;
-
Finally, align the structure as a whole: the structure’s “overall alignment value” is the largest alignment value among its members (which is 4), so the total size of the structure must be a multiple of 4. The current members occupy 0-7 (total of 8 bytes), and 8 is a multiple of 4, so sizeof(struct Test1) = 8.
Here’s the key point: the answer to your previous question about how many slots are empty between a and b is 3 slots (addresses 1, 2, 3)—because b must align to an address that is a multiple of 4, so between 0 and 4, aside from a occupying 0, the remaining 3 must be left empty.
| a | address 0 | 1 byte |
| / | address 1 | 1 byte (empty) |
| / | address 2 | 1 byte (empty) |
| / | address 3 | 1 byte (empty) |
| b | address 4 | 4 bytes |
| address 5 | 1 byte | |
| address 6 | 1 byte | |
| address 7 | 1 byte |
Example 2: Will changing the order change the size?
struct Test2{int b; //4 byteschar a; //1 byte};
Let’s calculate again:
-
First, place b: starting from 0, occupying slots 0-3 (aligned to 4);
-
Next, place a: alignment value 1, can be placed anywhere, then b occupies slot 4;
-
Overall alignment: the maximum alignment value is still 4, the total size must be a multiple of 4. Currently occupying 0-4 (total of 5 bytes), the closest multiple of 4 is 8— so addresses5, 6, 7 must be empty, total size sizeof(struct Test2) = 8.
But what if we add a short? For example:
struct Test3{int b; //4 byteschar a; //1 byteshort c; //2 bytes};
Calculating:
b occupies slots 0-3;
a occupies slot 4;
c aligns to 2, the next multiple of 2 is 6, so leave 5 empty, c occupies slots 6-7.
Overall alignment 4, currently total occupied 0-7, exactly a multiple of 4. So sizeof(struct Test3) is also 8.
But if we swap the order of a and b in Test3:
struct Test4{char a; //1 byteint b; //4 byteshort c; //2 bytes};
sizeof(struct Test4) = 12.
See, the order of structure members directly affects how much memory is wasted—in embedded development, memory is precious, and reasonable ordering can save a lot of space.
3. Arrays and unions: much simpler.
-
Arrays: calculated based on the “alignment value of a single element”. For example, char arr[5], each char aligns to 1, so the entire array occupies 5 bytes; int arr[3], each int aligns to 4, starting from 0, occupying 0-11 (3*4=12 bytes), alignment is fine.
-
Unions: calculated based on the “maximum member’s alignment value”. The size of a union is the size of the largest member (also needs to be a multiple of the maximum alignment value). For example:
union Test5{char a;int b;};
The largest member is b (4 bytes), so sizeof(union Test5)=4, a and b share addresses 0-3.
4. Manually controlling byte alignment
#pragma pack(n): specifies the alignment boundary as n (n can be 1, 2, 4, 8, 16), #pragma pack() cancels the custom setting and restores the default.
For example, the earlier struct Test1, which by default occupies 8 bytes, can be forced to 1 byte alignment (no holes) using pack(1):
Using #pragma pack
#pragma pack(1) // Start 1 byte alignmentstruct Test1{char a;int b;};// Calculate size: 1+4 = 5 bytes, 1 byte alignment does not require extra space
5. “Alignment pitfalls” in embedded development: watch out for these!
In embedded development, there are many “pitfalls” with byte alignment that can take you half a day to debug. Remember these two points:
1. Different compilers/platforms: default alignment may vary.
32-bit ARM defaults to 4-byte alignment, while 64-bit ARM defaults to 8-byte alignment;
When developing cross-platform, for example, if you port code from STM32 (32-bit ARM) to Linux (64-bit), you must manually specify the alignment method (e.g., using pack(4)), otherwise the structure size will change, and data transmission will be incorrect.
2. Alignment is a “trade-off”: efficiency versus memory.
-
If you want speed: use default alignment (e.g., 4 bytes), the CPU reads data quickly, but there will be memory holes;
-
If you want to save: use tight alignment (pack(1)), no memory waste, but the CPU may need to read data multiple times, reducing efficiency.
In embedded scenarios, such as sensor data transmission (memory is tight), you can use pack(1); but for core control logic (which requires speed), you must use default alignment—choose based on needs, and don’t blindly try to “save memory”.
6. Conclusion: Remember these 3 sentences, and alignment will no longer be difficult.
1. Byte alignment is a “requirement of the CPU and hardware”, not something the compiler is messing up;
2. Structure size = total sum of aligned members + holes added by overall alignment, order affects hole size;
3. Manual control using #pragma pack or __attribute__, must be used for cross-platform.
In essence, byte alignment is about “trading a small amount of memory holes for access efficiency”—in embedded development, you need to conserve memory while maintaining efficiency, mastering alignment is a “basic skill”. Next time you encounter an incorrect structure size, think about the logic of “arranging books on a shelf”, and you will surely find the problem!
If you have further questions, such as “how to calculate the size of a certain structure”, feel free to leave a comment, and we can work together to save memory~
Click the following card to follow this public account:If you like it, remember to “share” and “recommend”, “like” or “leave a message”!