Table of Contents
KEY TAKEAWAYS
- Structure members are stored in contiguous memory, but the compiler inserts padding bytes to satisfy alignment requirements
- Each data type has a natural alignment — a 4-byte
intmust start at an address divisible by 4 - Padding can significantly increase memory usage — reordering members from largest to smallest minimizes waste
- Use
__attribute__((packed))or#pragma packto eliminate padding, but be aware of performance and portability trade-offs - Understanding struct layout is critical for embedded systems where you share data between MCU and peripherals, or parse communication protocol frames
In the previous post we discussed about structures — what structures are and how they can be useful. We also discussed how to declare and use structures. In this post, we shall look into what the memory layout of a structure looks like, why the compiler adds invisible padding bytes, and how this affects your embedded code.
How Structures Are Stored in Memory
Let us say we have the following declaration for a structure:
struct room
{
int temperature;
float luminosity;
float soundLevel;
int humidity;
};
int main()
{
int i=0;
struct room r[3];
for(i=0;i<3;i++)
{
printf("the address of r[%d] = %d\n",i,&r[i]);
printf("the address of r[%d].temperature = %d\n",i,&r[i].temperature);
printf("the address of r[%d].luminosity = %d\n",i,&r[i].luminosity);
printf("the address of r[%d].soundLevel = %d\n",i,&r[i].soundLevel);
printf("the address of r[%d].humidity = %d\n\n",i,&r[i].humidity);
}
}We have four elements in the structure room and we have created three structure variables — r[0], r[1] and r[2]. The for loop prints the addresses of each structure variable and each element within it.
When we execute this program, we get the following output:
the address of r[0] = 6356700 the address of r[0].temperature = 6356700 the address of r[0].luminosity = 6356704 the address of r[0].soundLevel = 6356708 the address of r[0].humidity = 6356712 the address of r[1] = 6356716 the address of r[1].temperature = 6356716 the address of r[1].luminosity = 6356720 the address of r[1].soundLevel = 6356724 the address of r[1].humidity = 6356728 the address of r[2] = 6356732 the address of r[2].temperature = 6356732 the address of r[2].luminosity = 6356736 the address of r[2].soundLevel = 6356740 the address of r[2].humidity = 6356744
Two important observations from this output:
1. The starting address of a structure variable is the same as the address of its first element.
r[0] starts at 6356700, and r[0].temperature also starts at 6356700. This means a pointer to the structure is also a pointer to its first member.
2. Structure variables in an array are placed one after another, just like basic array elements.
r[0].humidity ends at 6356715 (4 bytes starting from 6356712), and r[1] starts immediately at 6356716. This contiguous arrangement means you can iterate through structure arrays using pointer arithmetic.

What is Structure Padding?
The example above worked out perfectly — all members were 4 bytes (int and float), so no gaps appeared. But what happens when members have different sizes?
struct sensor_data {
char sensor_id; // 1 byte
int reading; // 4 bytes
char status; // 1 byte
};You might expect sizeof(struct sensor_data) to be 6 bytes (1 + 4 + 1). But on a 32-bit system, it is actually 12 bytes. The compiler inserted invisible padding bytes to align each member to its natural boundary.
Here is what the memory layout actually looks like:
Offset 0: sensor_id (1 byte) Offset 1: [padding] (3 bytes) Offset 4: reading (4 bytes) Offset 8: status (1 byte) Offset 9: [padding] (3 bytes) Total: 12 bytes
Why Does the Compiler Add Padding?
CPUs access memory most efficiently when data is naturally aligned — meaning a 4-byte value sits at an address divisible by 4, a 2-byte value at an address divisible by 2, and so on.
On many architectures (especially ARM), accessing a misaligned value causes one of two things:
- Performance penalty: The CPU must perform two memory reads and combine them, taking twice as long
- Hard fault: Some ARM Cortex-M processors trigger a hard fault exception on unaligned access, crashing your program
The compiler adds padding to prevent this. It follows two rules:
- Member alignment: Each member starts at an offset that is a multiple of its size (char = 1, short = 2, int = 4)
- Struct alignment (trailing padding): The total struct size must be a multiple of the largest member’s alignment, so arrays of structs work correctly
How Member Order Affects Size
The same members in a different order can produce a different struct size:
// Bad order: 12 bytes
struct bad_order {
char a; // 1 byte + 3 padding
int b; // 4 bytes
char c; // 1 byte + 3 padding
}; // Total: 12 bytes
// Good order: 8 bytes
struct good_order {
int b; // 4 bytes
char a; // 1 byte
char c; // 1 byte + 2 padding
}; // Total: 8 bytesSame three members, but reordering saves 4 bytes per instance. The rule of thumb: order members from largest to smallest. This minimizes internal padding because each successive smaller member can fit into the alignment gap naturally.
On a desktop application, 4 bytes per struct is nothing. But on an embedded system with 2KB of RAM, if you have an array of 100 sensor readings, that is 400 bytes wasted — 20% of your total memory.
Packed Structures
When you need exact control over layout — such as when mapping a structure to a communication protocol frame or a hardware register block — you can tell the compiler to eliminate all padding:
// GCC / Clang
struct __attribute__((packed)) protocol_frame {
uint8_t header; // 1 byte
uint32_t timestamp; // 4 bytes
uint16_t sensor_val; // 2 bytes
uint8_t checksum; // 1 byte
}; // Total: 8 bytes (no padding)
// MSVC
#pragma pack(push, 1)
struct protocol_frame {
uint8_t header;
uint32_t timestamp;
uint16_t sensor_val;
uint8_t checksum;
};
#pragma pack(pop)Warning: Packed structures have trade-offs:
- Slower access: The compiler generates extra instructions to handle misaligned reads/writes
- Portability risk:
__attribute__((packed))is a GCC extension, not standard C - Fault risk: On strict-alignment architectures (some ARM Cortex-M0), packed access through a pointer can still cause a hard fault
Use packing only when you must match an external format (protocol frame, file format, hardware register). For internal data structures, prefer reordering members instead.
Practical Example: UART Protocol Frame
A common embedded use case is mapping a struct directly onto a received data buffer. Suppose your device receives a 7-byte message over UART:
// Incoming bytes: [0x55] [ID_H] [ID_L] [D0] [D1] [D2] [D3]
struct __attribute__((packed)) sensor_msg {
uint8_t sync_byte; // 0x55
uint16_t sensor_id; // 2 bytes, big-endian
int32_t value; // 4 bytes, big-endian
};
// Parse directly from receive buffer
void process_message(uint8_t *rx_buf) {
struct sensor_msg *msg = (struct sensor_msg *)rx_buf;
if (msg->sync_byte != 0x55) return; // Invalid frame
uint16_t id = ntohs(msg->sensor_id); // Handle endianness
int32_t val = ntohl(msg->value);
// Process the data...
}Without packing, the compiler would insert 1 byte of padding after sync_byte and the fields would not align with the received bytes at all. This is the most common reason embedded developers use packed structs.
How to Check Layout: offsetof and sizeof
C provides two tools to inspect struct layout at compile time:
#include <stddef.h>
#include <stdio.h>
struct example {
char a;
int b;
short c;
};
int main() {
printf("sizeof(struct example) = %zu\n", sizeof(struct example)); // 12
printf("offsetof(a) = %zu\n", offsetof(struct example, a)); // 0
printf("offsetof(b) = %zu\n", offsetof(struct example, b)); // 4
printf("offsetof(c) = %zu\n", offsetof(struct example, c)); // 8
return 0;
}In embedded code, you can use static_assert (C11) to catch layout surprises at compile time instead of discovering them at runtime:
_Static_assert(sizeof(struct sensor_msg) == 7, "sensor_msg must be 7 bytes"); _Static_assert(offsetof(struct sensor_msg, value) == 3, "value must be at offset 3");
If someone changes the struct and breaks the expected layout, the build fails with a clear error message. This is much better than debugging mysterious protocol failures on the target.
Summary
- Structure elements are placed one after another in memory, with padding inserted for alignment
- Structure variables in an array are also placed contiguously
- The starting address of a structure variable is the same as the address of its first element
- Order members from largest to smallest to minimize padding
- Use packed structures only when matching external data formats
- Use
sizeof,offsetof, and_Static_assertto verify layout
The following YouTube video will help you understand better:

Vivek Bhageria — Lead Firmware R&D Engineer, 12+ years. Ex-Bosch (automotive powertrain), MusicTribe (real-time audio), medical devices. M.Tech BITS Pilani. I write at NerdyElectronics — practical, register-level embedded systems for engineers who want to understand what’s actually happening under the hood.

![Memory layout diagram of a C struct room array with 3 elements r[0] r[1] r[2], showing how members temperature luminosity soundLevel and humidity are stored at consecutive memory addresses 700 through 744, with the struct definition containing int and float members](https://nerdyelectronics.com/wp-content/uploads/2020/06/structure_memory_layout_nerdyelectronics-1-e1591528678954-930x620.png)





