Skip to content
Home » Embedded Systems » Embedded C » Memory Layout of a C Structure: Padding, Alignment, and sizeof

Memory Layout of a C Structure: Padding, Alignment, and sizeof

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
Embedded Systems Learning Path
Part 38 of 129View Full Path →

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 int must 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 pack to 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:

  1. Member alignment: Each member starts at an offset that is a multiple of its size (char = 1, short = 2, int = 4)
  2. 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 bytes

Same 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

  1. Structure elements are placed one after another in memory, with padding inserted for alignment
  2. Structure variables in an array are also placed contiguously
  3. The starting address of a structure variable is the same as the address of its first element
  4. Order members from largest to smallest to minimize padding
  5. Use packed structures only when matching external data formats
  6. Use sizeof, offsetof, and _Static_assert to verify layout

The following YouTube video will help you understand better:

Leave a Reply

Your email address will not be published. Required fields are marked *