When managing complex data records like student databases, network interfaces, or sensor telemetry in C, keeping separate scalar arrays (int id[100], char name[100][32], float temperature[100]) is an architectural nightmare. Sorting or swapping one record requires manually shuffling elements across three distinct arrays. An Array of Structures (struct Device devices[100]) solves this by grouping related fields into a single contiguous record block in memory.

Memory Architecture: Contiguous Struct Elements & Padding

In memory, an array of structures is stored as a contiguous sequence of struct blocks. However, because CPUs require data types to be aligned on natural word boundaries (e.g. 4-byte alignment for 32-bit integers), the compiler inserts invisible padding bytes between struct members:

RAM Memory Diagram: Array of 2 Struct Recordstext
+---------------------------------------------------------------------------------+
| RAM LAYOUT FOR struct StudentData students[2]                                   |
+---------------------------------------------------------------------------------+
| ELEMENT 0: students[0] (Total 20 Bytes with Padding)                            |
| Address: 0x1000 | id (4 Bytes)     : 0x00000065                                 |
| Address: 0x1004 | name (10 Bytes)  : "Alice\0\0\0\0"                              |
| Address: 0x100E | PADDING (2 Bytes): [ 0x00 0x00 ] (Aligns gpa to 4-byte boundary) |
| Address: 0x1010 | gpa (4 Bytes)    : 3.92                                       |
+---------------------------------------------------------------------------------+
| ELEMENT 1: students[1] (Total 20 Bytes with Padding)                            |
| Address: 0x1014 | id (4 Bytes)     : 0x00000066                                 |
| Address: 0x1018 | name (10 Bytes)  : "Bob\0\0\0\0\0\0"                            |
| Address: 0x1022 | PADDING (2 Bytes): [ 0x00 0x00 ]                                |
| Address: 0x1024 | gpa (4 Bytes)    : 3.75                                       |
+---------------------------------------------------------------------------------+
 
Address Arithmetic: &students[1] = Base_Addr (0x1000) + (1 * sizeof(struct StudentData))
                                 = 0x1000 + (1 * 20) = 0x1014

1. Production Implementation: Static Struct Array & qsort()

Here is a production C program declaring a static array of structures, populating data, and sorting records by GPA using POSIX qsort():

struct_array_qsort.cc
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
#define MAX_STUDENTS 3
 
typedef struct {
    int id;
    char name[16];
    float gpa;
} Student;
 
// Comparator callback function for qsort (Descending by GPA)
int compare_by_gpa(const void *a, const void *b) {
    const Student *s1 = (const Student *)a;
    const Student *s2 = (const Student *)b;
    if (s2->gpa > s1->gpa) return 1;
    if (s2->gpa < s1->gpa) return -1;
    return 0;
}
 
int main(void) {
    // Compile-time struct array initialization
    Student roster[MAX_STUDENTS] = {
        {101, "Alice", 3.85f},
        {102, "Bob",   3.50f},
        {103, "Charlie", 3.95f}
    };
 
    printf("=== Original Roster ===\n");
    for (size_t i = 0; i < MAX_STUDENTS; i++) {
        printf("ID: %d | Name: %-10s | GPA: %.2f\n", roster[i].id, roster[i].name, roster[i].gpa);
    }
 
    // Sort struct array in-place
    qsort(roster, MAX_STUDENTS, sizeof(Student), compare_by_gpa);
 
    printf("\n=== Sorted Roster (Highest GPA First) ===\n");
    for (size_t i = 0; i < MAX_STUDENTS; i++) {
        printf("Rank %zu: %-10s (GPA: %.2f)\n", i + 1, roster[i].name, roster[i].gpa);
    }
 
    return 0;
}

Key Insights from Struct Array Sorting:

  • Dot Operator (`.`) Access: Elements are accessed via roster[i].field. roster[i] yields the struct value at index i.

  • Atomic In-Place Swaps: When qsort() swaps two elements, it copies the entire sizeof(Student) byte block (24 bytes), automatically keeping id, name, and gpa synchronized.

2. Dynamic Allocation: Heap-Allocated Array of Structures

When the number of records is unknown until runtime, allocate the struct array dynamically on the Heap using malloc():

dynamic_struct_array.cc
#include <stdio.h>
#include <stdlib.h>
 
typedef struct {
    int device_id;
    int status_code;
} DeviceRecord;
 
int main(void) {
    size_t count = 5;
 
    // Allocate array of 5 DeviceRecord structs on Heap
    DeviceRecord *devices = malloc(count * sizeof(DeviceRecord));
    if (devices == NULL) {
        perror("Allocation failed");
        return 1;
    }
 
    // Access using pointer arrow notation vs array subscript notation
    for (size_t i = 0; i < count; i++) {
        (devices + i)->device_id = 1000 + (int)i; // Pointer arithmetic with ->
        devices[i].status_code   = 200;           // Subscript notation with .
    }
 
    printf("Device 2 ID: %d | Status: %d\n", devices[2].device_id, devices[2].status_code);
 
    free(devices); // Always release heap memory!
    return 0;
}

Pointer Arithmetic Equivalence:

  • `devices[i].field` == `(devices + i)->field`: Both expressions compute the exact same target RAM address: (char *)base + i * sizeof(DeviceRecord).

AoS vs SoA: High-Performance Computing Considerations

In game engines, SIMD vectorization, and GPU compute shaders, traditional Array of Structures (AoS) can hinder cache locality if algorithms only read one field across millions of records. Modern high-performance systems often prefer Structure of Arrays (SoA):

AoS vs SoA Memory Comparisontext
+---------------------------------------------------------------------------------+
| Array of Structures (AoS) - Best for OOP & Record Lookup                        |
| Layout: [x0, y0, z0] [x1, y1, z1] [x2, y2, z2]                                  |
+---------------------------------------------------------------------------------+
 
+---------------------------------------------------------------------------------+
| Structure of Arrays (SoA) - Best for SIMD Vectorization & GPU Shaders           |
| Layout: X_Array: [x0, x1, x2] | Y_Array: [y0, y1, y2] | Z_Array: [z0, z1, z2]   |
+---------------------------------------------------------------------------------+

Troubleshooting & Common Pitfalls Checklist

  • Uninitialized Struct Pointers - Writing DeviceRecord *dev; dev[0].id = 5; without malloc() triggers a Segmentation Fault.

  • Forgetting `sizeof(struct)` in Allocation - Allocating malloc(N) instead of malloc(N * sizeof(struct)) under-allocates memory and causes buffer overflow corruptions.

Arrays of structures provide a clean, type-safe mechanism for managing relational records in C while maintaining total control over hardware memory alignment.