Structures, Unions, and Bit Fields in C

Structures

A struct is a user-defined composite data type that groups together variables of different types under a single name. Unlike arrays—which store elements of the same type—structures allow heterogeneous data to be logically associated.

Definition and Declaration

struct Point {
    int x;
    int y;
};

This defines a new type struct Point. A forward declaration like struct Point; creates an incomplete type; it’s useful for pointer declarations before the full definition is available.

Initialization and Member Access

Variables of struct type can be declared alongside the definition or atferward:

struct Time {
    short hour, minute, second;
    short year, month, day;
} t1 = { .year = 2024, .month = 5, .day = 1 };

Members are accessed using the dot operator (.). Uninitialized members are zero-initialized if the struct has static storage duration or is explicitly initialized with {}.

Compound literals offer another initialization method:

struct Time t2 = (struct Time){ .hour = 9, .minute = 30 };

Memory Layout and Padding

Struct members are laid out in memory in declaration order, but padding may be inserted between members (and at the end) to satisfy alignment requirements. For example:

struct Example {
    char a;   // offset 0
    int b;    // offset 4 (after 3 bytes padding)
};            // total size: 8 bytes (aligned to int boundary)

The size of a struct is at least the sum of its members’ sizes, but often larger due to alignment.

Flexible Array Members

A struct may end with an incomplete array (a "flexible array member") if it has at least one named member:

struct Buffer {
    size_t len;
    char data[];
};

Such structs cannot be members of other structs or arrays. Memory must be dynamically allocated to accommodate the trailing array:

struct Buffer *buf = malloc(sizeof(struct Buffer) + 100);
buf->len = 100;
strcpy(buf->data, "Hello");

Arrays of Structures

Struct arrays are declared like any other array:

struct Student students[3] = {
    {1, "Alice", 95.5f},
    {2, "Bob", 88.0f},
    {3, "Charlie", 92.0f}
};

Access uses array indexing and the dot operator: students[i].name.

Structs and Functions

Structs can be passed by value or returned from functions:

struct Result compute() {
    struct Result r = {1, 42};
    return r;
}

Passing large structs by value is inefficient; pointers are prefererd for modification or performance.

Pointers to Structs

Use the arrow operator (->) to access members via a pointer:

struct Student *p = &students[0];
printf("%s\n", p->name);

A pointer to the first member of a struct can be safely cast to a pointer to the struct itself—a technique used in low-level programming.

Unions

A union allows storing different data types in the same memory location. Only one member is active at a time.

union Data {
    int i;
    float f;
    char str[20];
};

The size of a union equals the size of its largest member (plus possible padding).

Memory Sharing

All members share the same starting address:

union Value {
    uint32_t raw;
    struct { uint8_t b0, b1, b2, b3; } bytes;
};

This enables type punning—interpreting the same bits as different types. Note: accessing inactive union members is undefined behavior in strict ISO C, though widely supported.

Pointers to a union and to any of its member are interchangeable via casting:

union Value v = { .raw = 0x12345678 };
uint8_t *b = &v.bytes.b0; // valid
union Value *u = (union Value*)b; // also valid

Practical Use Case

Unions are common in system APIs for multi-view data access, such as IPv4 address representation:

struct in_addr {
    union {
        struct { uint8_t s_b1, s_b2, s_b3, s_b4; } S_un_b;
        uint16_t S_un_w[2];
        uint32_t S_addr;
    } S_un;
};

Bit Fields

Bit fields allow packing multiple small integer values into a single storage unit.

struct Flags {
    unsigned int readable : 1;
    unsigned int writable : 1;
    unsigned int executable : 1;
    unsigned int reserved : 5;
};

Each field specifies the number of bits it occupies. The underlying type must be an integer type (e.g., int, unsigned int, _Bool).

Limitations

  • Bit fields cannot have their address taken—no pointers to bit fields.
  • They cannot be used with sizeof or _Alignas.
  • Layout is implementation-defined (endianness, allocation direction).

Usage with Pointers

Access bit fields through struct pointers normally:

struct Flags *fp = ...;
fp->readable = 1;

But direct pointer arithmetic on bit fields is impossible.

Alignment and Packing

Compilers insert padding to align data according to hardware requirements. Rules include:

  • Each member is aligned to its natural boundary (e.g., int on 4-byte boundary).
  • The entire struct is padded to align with its largest member.

Example:

struct Packed {
    char a;   // offset 0
    int b;    // offset 4 → 3 bytes padding after 'a'
};            // size = 8

Controlling Alignment

GCC provides attributes to override default alignment:

struct __attribute__((packed)) Tight {
    char a;
    int b;
}; // size = 5 (no padding)

Use #pragma pack(n) or __attribute__((aligned(n))) for custom alignment, but beware of performance penalties on unaligned accesses.

Typedef for Simplicity

typedef creates aliases for complex types:

typedef struct {
    double x, y;
} Point;

typedef Point* PointPtr;

This avoids repetitive struct keywords and simplifies declarations:

Point origin = {0.0, 0.0};
PointPtr p = &origin;

typedef can also alias incomplete types or VLAs (since C99):

void func(int n) {
    typedef int Array[n];
    Array v; // v is int[n]
}

It does not create a new type—only a synonym—so typedef names are compatible with their underlying types.

Tags: C Struct UNION bit-field alignment

Posted on Thu, 29 Oct 2026 16:55:01 +0000 by shinyo