Topic 84 of 100 Structures and Unions in C Interactive Tutorial

Arrays of Structures in C: Managing Collections of Records

12 mins read Last updated: 22 Aug 2026 25,300 views
C Compiler 0.00%

What is Arrays of Structures in C? Core Concept & Direct Summary

Core Objective & Problem Statement

How do arrays of structures store, index, and sort database tables and collections of records in contiguous memory?

Direct Answer Summary

There's a mental model that makes arrays of structures click immediately: an array of structures is a table.

Architecture Diagram
          ┌──────┬────────────────────────────────┬────────────────────┬──────────────┬──────┬────────┐   row 0  │  id  │  title                         │  author            │  category    │ year │ copies │   row 1  │  id  │  title                         │  author            │  category    │ year │ copies │   row 2  │  id  │  title                         │  author            │  category    │ year │ copies │          └──────┴────────────────────────────────┴────────────────────┴──────────────┴──────┴────────┘             ▲             └── one struct = one row;  one member = one column

Each element is a row. Each member is a column. The array gives you the rows in order; the struct gives every row its columns. Once you see it that way, "managing a collection of records" becomes a set of ordinary table operations — find a row, update a cell, remove a row, sort by a column, filter by a value, produce a summary.

That's what this page covers. We'll build a library catalog and take it through the full cycle: load it, look things up, update it, delete a record, insert a new one in the right place, sort it two different ways, filter it, and generate a grouped summary report. Along the way you'll see why arrays of structs beat the older "parallel arrays" technique, and how the records are actually laid out in memory.

(Suggested image: the hero graphic — a labeled table with an arrow pointing to row 1 expanded into its struct form, showing "row = element, column = member")

How Does Arrays of Structures in C Work? In-Depth Mechanics & Architecture

Why Not Parallel Arrays?

Before structures, C programmers stored tabular data using parallel arrays — one array per column:

main.c
Run Online
#define MAX 100
char titles[MAX][60];
/* column 1 */char authors[MAX][40];
/* column 2 */int years[MAX];
/* column 3 */int copies[MAX];
/* column 4 */int count = 0;
/* how many rows are in use */

Row i is "the record" — but it doesn't exist as a single thing. It's an index you have to apply consistently to four separate arrays. That fragility shows up everywhere:

Adding a record — four writes that must stay in sync:

main.c
Run Online
strcpy(titles[count], "Refactoring");
strcpy(authors[count], "Martin Fowler");
years[count] = 1999;
copies[count] = 3;
count++;

Forget one line and the table is permanently desynced — years[2] now describes a different book than titles[2]. There's no error, no warning. Just wrong data forever.

Deleting record 2 — four shifts that must be identical:

main.c
Run Online
for (int i = 2;
i < count - 1;
i++) {
    strcpy(titles[i], titles[i + 1]);
    strcpy(authors[i], authors[i + 1]);
    years[i] = years[i + 1];
    copies[i] = copies[i + 1];
}
count--;

One mistyped bound and the arrays drift apart.

Swapping two records (for sorting) — twelve assignments:

main.c
Run Online
char tmpTitle[60];
strcpy(tmpTitle, titles[j]);
strcpy(titles[j], titles[j+1]);
strcpy(titles[j+1], tmpTitle);
/* ... and the same for authors, years, copies ... */

And you can't pass "a record" to a function — a function would need four parameters per record, or all four arrays plus the index.

Now compare the same operations with an array of structures:

main.c
Run Online
Book catalog[MAX];
catalog[count] = newBook;
/* add: 1 statement */for (int i = idx; i < n - 1; i++) {
    catalog[i] = catalog[i + 1];
    /* delete: 1 shift */
}
Book temp = catalog[i];
/* swap: 3 lines, any columns */catalog[i] = catalog[i + 1];
catalog[i + 1] = temp;
printBook(&catalog[i]);
/* pass a record: 1 argument */
Parallel arraysArray of structures
Add a record4 writes, can desync1 assignment
Delete a record4 shifting loops1 shifting loop
Swap two records12 assignments3 assignments
Pass a record to a function4 arguments1 argument
Risk of desyncHigh — indexes must match everywhereImpossible — fields are bound together
Name for "a record"Doesn't existThe type: Book

That last row is the real point. With parallel arrays, there's no such thing as "a book" in your program. With a struct, Book is a type — and the compiler now helps you keep the columns together.

(Suggested image: the split-columns comparison — four separate arrays above, one array of structs below, with the desync risk marked and then crossed out)


Memory Layout: Records Are Contiguous

An array of structs is one contiguous block of memory, and each record occupies the same number of bytes:

Architecture Diagram
catalog →  ┌─────────────┬─────────────┬─────────────┬─────           │  Book (132B)│  Book (132B)│  Book (132B)│ ...           └─────────────┴─────────────┴─────────────┴─────offset:    0             132           264           396 ...index: [0]           [1]           [2]           [3]

So record i begins at byte offset i × sizeof(Book). That's why indexing is O(1): the address is pure arithmetic, exactly as with an int array.

main.c
Run Online
printf("sizeof(Book)      : %zu bytes\n", sizeof(Book));
/* 132, typically */printf("Record 3 starts at: offset %zu\n", 3 * sizeof(Book));
/* 396 */
Book *p = &catalog[3];
/* same address as: catalog + 3 */printf("%s\n", p->title);
/* pointer + arrow */printf("%s\n",(catalog + 3)->title);
/* identical meaning */

Three equivalent ways to reach the same record — the array form, the pointer form, and the arrow form. They compile to the same address arithmetic. (That equivalence is the subject of the pointers vs arrays tutorial.)

Two size facts worth keeping straight:

main.c
Run Online
sizeof(catalog[0]) /* ONE record — 132 bytes */sizeof(catalog) /* the WHOLE array — 13200 bytes, but ONLY where it's a real array */

Inside a function, catalog decays to a pointer, so sizeof(catalog) there is 8 (the pointer) — never the block. That's why the count travels as a separate parameter.

⚠️ And don't compute record size by adding members.sizeof(Book) is what the compiler says, including any alignment padding. (Here it happens to be exactly 4 + 60 + 40 + 20 + 4 + 4 = 132, but that's luck of the layout — see the struct padding topic.) Always use sizeof.

(Suggested image: the byte-offset ruler under the record block, with catalog[3] at offset 396 and the three equivalent access forms annotated)


Declaring and Initializing the Table

main.c
Run Online
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_BOOKS 100
#define TITLE_LEN 60
#define AUTHOR_LEN 40
#define CATEGORY_LEN 20
typedef struct {
    int id;
    char title[TITLE_LEN];
    char author[AUTHOR_LEN];
    char category[CATEGORY_LEN];
    int year;
    int copies;
}
Book;
Book catalog[MAX_BOOKS];
/* an empty table with room for 100 rows */int count = 0;
/* how many rows are actually used */

count is the essential companion. The array has capacity 100; count says how many rows are real. Every loop uses count, never MAX_BOOKS — otherwise you'd print 94 rows of garbage.

Loading rows with designated initializers

main.c
Run Online
Book catalog[MAX_BOOKS] = {
{
        .id=1, .title="The C Programming Language", .author="Kernighan Ritchie", .category="Programming", .year=1978, .copies=4
    }
    , {
        .id=2, .title="Clean Code", .author="Robert Martin", .category="Programming", .year=2008, .copies=2
    }
    , {
        .id=3, .title="Introduction to Algorithms", .author="Thomas Cormen", .category="Algorithms", .year=2009, .copies=3
    }
    , {
        .id=4, .title="The Pragmatic Programmer", .author="Andrew Hunt", .category="Programming", .year=1999, .copies=5
    }
    , {
        .id=5, .title="Computer Networks", .author="Andrew Tanenbaum", .category="Networking", .year=2010, .copies=1
    }
    , {
        .id=6, .title="Operating System Concepts", .author="Abraham Silberschatz", .category="Systems", .year=2012, .copies=2
    }
}
;
int count = 6;

Each brace group is one row. The named fields make it readable and reorder-proof — and note that a char array member (title) takes a string literal directly in an initializer, even though you can never assign to it later with =:

main.c
Run Online
catalog[0] =(Book) {
    .title = "Direct"
}
;
/* ✓ compound literal, C99 */strcpy(catalog[0].title, "Also fine");
/* ✓ the usual way after declaration *//* catalog[0].title = "Nope";                  ✗ arrays aren't assignable */

Zeroing the whole table is the same one-liner as any array:

main.c
Run Online
Book catalog[MAX_BOOKS] = {
    0
}
;
/* every byte of every record zeroed */

(Suggested image: the initializer braces color-coded — outer braces = rows, inner braces = columns — with one row expanded)


The Table Display Function

Everything downstream needs to print rows, so build that once:

main.c
Run Online
void printHeader(void) {
    printf("%-4s %-30s %-22s %-13s %6s %7s\n", "ID", "Title", "Author", "Category", "Year", "Copies");
    puts("-------------------------------------------------------------------------");
}
void printBook(const Book *b) {
    printf("%-4d %-30s %-22s %-13s %6d %7d\n", b->id, b->title, b->author, b->category, b->year, b->copies);
}
void printTable(const Book books[], int n, const char *heading) {
    printf("\n%s\n", heading);
    printHeader();
    for (int i = 0; i < n; i++) {
        printBook(&books[i]);
        /* pass a pointer to the row */
    }
    printf("Total records: %d\n", n);
}

Two details that matter:

  • printBook takes a const Book *, not a Book. At 132 bytes, passing rows by value in a loop would copy 792 bytes just to display them; a pointer is 8.
  • printTable takes const Book books[] plus n — the array decays to a pointer, so the count is compulsory. And const documents that displaying never modifies the table.

(Suggested image: the printf width specifiers annotated — how %-30s reserves 30 columns for the title so the table lines up)


Lookup: Finding a Row

Two lookups cover most needs — by identity, and by partial match.

main.c
Run Online
int findById(const Book books[], int n, int id) {
    for (int i = 0;
    i < n;
    i++) {
        if (books[i].id == id) return i;
        /* return the ROW INDEX, not the record */
    }
    return -1;
    /* not found */
}
int findByAuthorPart(const Book books[], int n, const char *part, Book matches[], int maxMatches) {
    int count = 0;
    for (int i = 0;
    i < n && count < maxMatches;
    i++) {
        if (strstr(books[i].author, part) != NULL) {
            matches[count++] = books[i];
            /* copy the matching ROW out */
        }
    }
    return count;
}

Return an index, not the record. The index is small, always valid, works as a not-found signal (-1), and lets the caller both read and modify the row. Returning a pointer would work too, but -1 couldn't represent failure then.

matches[count++] = books[i]; is one of the quiet benefits of structs — that's a whole-row copy in a single statement, with the compiler moving all 132 bytes. With parallel arrays, this line would be four separate copies.

And note the comparison types: == for the int id, strstr for substring search on a char array. Never == on strings — that compares addresses.

(Suggested image: a search walking down the author column, with two matching rows highlighted and copied into a separate matches table)


Mutation: Add, Update, Delete, Insert

Four operations, and each is a small lesson in how array-of-struct tables behave.

Add at the end — the easy case

main.c
Run Online
count = insertAt(catalog, count, count, newBook);
/* position == end */

O(1) — no shifting, because nothing comes after the new row.

Update a field

main.c
Run Online
int updateCopies(Book books[], int n, int id, int newCopies) {
    int idx = findById(books, n, id);
    if (idx == -1) return 0;
    books[idx].copies = newCopies;
    /* one cell changes */ return 1;
}

Find the row, write one column. And because the parameter is a plain Book books[] (not const), the change is visible to the caller — same mechanism as any pointer parameter.

Delete a record — close the gap by shifting

This is the operation that separates a real table from a toy example:

main.c
Run Online
int deleteById(Book books[], int n, int id) {
    int idx = findById(books, n, id);
    if (idx == -1) return n;
    /* not found — count unchanged */
    for (int i = idx;
    i < n - 1;
    i++) {
        books[i] = books[i + 1];
        /* shift every later row left one slot */
    }
    return n - 1;
    /* one fewer row */
}

There's no "hole" in an array — so removing row 2 means every row after it slides one slot toward the front, and the count drops. The last slot still holds a stale duplicate, but since every loop respects count, it's simply invisible.

This is the O(n) cost of deletion in an array, and the reason some data structures use linked lists instead. It's also the one operation where the whole-row copy shines: one assignment per shift, no matter how many columns the record has.

Insert at a position — open a gap by shifting

The mirror image:

main.c
Run Online
int insertAt(Book books[], int n, int pos, Book b) {
    if (n >= MAX_BOOKS || pos < 0 || pos > n) return n;
    /* capacity/bounds guard */
    for (int i = n;
    i > pos;
    i--) {
        books[i] = books[i - 1];
        /* shift right to open a slot */
    }
    books[pos] = b;
    return n + 1;
}

Shift backward through the array, from the last row down to the insertion point — because if you shifted forward, you'd overwrite the row you were about to copy. (Same reasoning as the save-next-before-free rule in linked lists.)

And you can use it to keep the table permanently ordered:

main.c
Run Online
int insertSortedByYear(Book books[], int n, Book b) {
    int pos = 0;
    while (pos < n && books[pos].year <= b.year) pos++;
    /* find the slot */ return insertAt(books, n, pos, b);
    /* then open it */
}

Notice the return value protocol throughout: these functions return the new count, and the caller must accept it.

main.c
Run Online
count = deleteById(catalog, count, 5);
/* ✓ count updated */count = insertSortedByYear(catalog, count, newBook);
main.c
Run Online
deleteById(catalog, count, 5);
/* ✗ the row is gone but count is stale —                                                every loop now prints a stale row */

That's the single most common bug with this style of code.

(Suggested image: the delete-by-shift diagram — row 2 flagged for removal, then arrows showing rows 3,4,5 each moving one slot left, with the count marker moving down)


Sorting the Table

Sorting struct arrays has one governing rule:

Compare by one column. Swap the whole row.

Selection sort on the year column:

main.c
Run Online
void sortByYear(Book books[], int n) {
    for (int i = 0;
    i < n - 1;
    i++) {
        int minIdx = i;
        for (int j = i + 1;
        j < n;
        j++) {
            if (books[j].year < books[minIdx].year) {
                /* compare: one column */ minIdx = j;
            }
        }
        if (minIdx != i) {
            Book temp = books[i];
            /* swap: whole rows */ books[i] = books[minIdx];
            books[minIdx] = temp;
        }
    }
}

Selection sort instead of bubble sort this time, for variety — the compare/swap discipline is identical: the comparison touches one member, the swap moves the entire record. If you sorted only the year values and not the rows, the table would be scrambled.

qsort for anything more flexible:

main.c
Run Online
int compareByTitle(const void *a, const void *b) {
    const Book *ba =(const Book *)a;
    const Book *bb =(const Book *)b;
    return strcmp(ba->title, bb->title);
    /* strcmp, not == or < */
}
qsort(catalog, count, sizeof(Book), compareByTitle);

The two arguments that matter:sizeof(Book) tells qsort how big each row is (so it can move whole records), and the comparison function tells it which column and which direction. Change the comparison function and you change the sort — the sort itself never changes.

A multi-key comparison — year, then title as a tiebreak:

main.c
Run Online
int compareByYearThenTitle(const void *a, const void *b) {
    const Book *ba =(const Book *)a;
    const Book *bb =(const Book *)b;
    if (ba->year != bb->year) {
        return ba->year - bb->year;
        /* primary key */
    }
    return strcmp(ba->title, bb->title);
    /* secondary key */
}

This is how real catalogs sort — and it's just a function, so you can add keys indefinitely. (That's the payoff from the function pointers tutorial: the sort is generic, the ordering is yours.)

(Suggested image: the whole-row swap diagram — three record boxes plus a temp box, with the comparison arrow pointing only at the year column)


Filtering and Grouped Reports

Once records are in a table, the interesting questions are aggregates: what's in this category? how much of it do we hold?

Filter — extract matching rows

main.c
Run Online
int filterByCategory(const Book books[], int n, const char *category, Book out[], int maxOut) {
    int count = 0;
    for (int i = 0;
    i < n && count < maxOut;
    i++) {
        if (strcmp(books[i].category, category) == 0) {
            out[count++] = books[i];
            /* copy matching rows into out */
        }
    }
    return count;
}

The function writes matches into a caller-provided output array and returns how many it wrote. That pattern — output parameter plus count — is how C returns collections without allocating. The caller must size the destination and respect the maxOut guard.

main.c
Run Online
Book programming[100];
int pm = filterByCategory(catalog, count, "Programming", programming, 100);
printTable(programming, pm, "===== FILTERED: category == Programming =====");

Grouped report — one row per category

Now the aggregate. Define a second, different struct — a report row, not a catalog row:

main.c
Run Online
typedef struct {
    char name[CATEGORY_LEN];
    int bookCount;
    int copyCount;
}
CategoryStat;

Then group the table:

main.c
Run Online
int buildCategoryStats(const Book books[], int n, CategoryStat stats[], int maxStats) {
    int count = 0;
    for (int i = 0; i < n; i++) {
        int found = -1;
        /* Is this category already in the stats table? */ for (int j = 0; j < count; j++) {
            if (strcmp(stats[j].name, books[i].category) == 0) {
                found = j;
                break;
            }
        }
        /* First time we've seen it — create the row */ if (found == -1) {
            if (count >= maxStats) continue;
            strcpy(stats[count].name, books[i].category);
            stats[count].bookCount = 0;
            stats[count].copyCount = 0;
            found = count++;
        }
        /* Accumulate into the existing or new row */ stats[found].bookCount += 1;
        stats[found].copyCount += books[i].copies;
    }
    return count;
}

This is a GROUP BY written by hand: walk the records, find or create the group row, accumulate. It's the same "search a struct array, then update or append" rhythm you'd use for any aggregation — and it establishes a habit that scales: the output of an aggregation is itself a table of structs, printable with the same rendering approach.

The guard if (count >= maxStats) continue; prevents overflowing the stats array if the data has more distinct categories than you reserved.

(Suggested image: grouping arrows — six catalog rows feeding into three summary rows bucketed by category, with the counts and copy totals accumulating)


📋 Complete Program: A Library Catalog

Here's everything above operating on one table — printed, updated, searched, sorted, deleted from, inserted into, filtered, and summarized.

main.c
Run Online
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_BOOKS 100
#define MAX_FILTERED 100
#define MAX_CATEGORIES 16
#define TITLE_LEN 60
#define AUTHOR_LEN 40
#define CATEGORY_LEN 20
/* One record = one row of the table */typedef struct {
    int id;
    char title[TITLE_LEN];
    char author[AUTHOR_LEN];
    char category[CATEGORY_LEN];
    int year;
    int copies;
}
Book;
/* A row of the summary report */typedef struct {
    char name[CATEGORY_LEN];
    int bookCount;
    int copyCount;
}
CategoryStat;
/* ---------------- Display ---------------- */
void printHeader(void) {
    printf("%-4s %-30s %-22s %-13s %6s %7s\n", "ID", "Title", "Author", "Category", "Year", "Copies");
    puts("-------------------------------------------------------------------------");
}
void printBook(const Book *b) {
    printf("%-4d %-30s %-22s %-13s %6d %7d\n", b->id, b->title, b->author, b->category, b->year, b->copies);
}
void printTable(const Book books[], int n, const char *heading) {
    printf("\n%s\n", heading);
    printHeader();
    for (int i = 0; i < n; i++) {
        printBook(&books[i]);
    }
    printf("Total records: %d\n", n);
}
/* ---------------- Lookup ---------------- */
int findById(const Book books[], int n, int id) {
    for (int i = 0; i < n; i++) {
        if (books[i].id == id) return i;
    }
    return -1;
}
int findByAuthorPart(const Book books[], int n, const char *part, Book matches[], int maxMatches) {
    int count = 0;
    for (int i = 0; i < n && count < maxMatches; i++) {
        if (strstr(books[i].author, part) != NULL) {
            matches[count++] = books[i];
        }
    }
    return count;
}
/* ---------------- Mutation ---------------- */
int updateCopies(Book books[], int n, int id, int newCopies) {
    int idx = findById(books, n, id);
    if (idx == -1) return 0;
    books[idx].copies = newCopies;
    return 1;
}
int deleteById(Book books[], int n, int id) {
    int idx = findById(books, n, id);
    if (idx == -1) return n;
    for (int i = idx; i < n - 1; i++) {
        books[i] = books[i + 1];
        /* shift later rows one slot left */
    }
    return n - 1;
}
int insertAt(Book books[], int n, int pos, Book b) {
    if (n >= MAX_BOOKS || pos < 0 || pos > n) return n;
    for (int i = n; i > pos; i--) {
        books[i] = books[i - 1];
        /* shift right to open a slot */
    }
    books[pos] = b;
    return n + 1;
}
int insertSortedByYear(Book books[], int n, Book b) {
    int pos = 0;
    while (pos < n && books[pos].year <= b.year) pos++;
    return insertAt(books, n, pos, b);
}
/* ---------------- Sorting ---------------- */
void sortByYear(Book books[], int n) {
    for (int i = 0; i < n - 1; i++) {
        int minIdx = i;
        for (int j = i + 1; j < n; j++) {
            if (books[j].year < books[minIdx].year) {
                minIdx = j;
            }
        }
        if (minIdx != i) {
            Book temp = books[i];
            /* swap WHOLE records */ books[i] = books[minIdx];
            books[minIdx] = temp;
        }
    }
}
int compareByTitle(const void *a, const void *b) {
    const Book *ba =(const Book *)a;
    const Book *bb =(const Book *)b;
    return strcmp(ba->title, bb->title);
}
/* ---------------- Reporting ---------------- */
int filterByCategory(const Book books[], int n, const char *category, Book out[], int maxOut) {
    int count = 0;
    for (int i = 0; i < n && count < maxOut; i++) {
        if (strcmp(books[i].category, category) == 0) {
            out[count++] = books[i];
            /* copy the matching record out */
        }
    }
    return count;
}
int buildCategoryStats(const Book books[], int n, CategoryStat stats[], int maxStats) {
    int count = 0;
    for (int i = 0; i < n; i++) {
        int found = -1;
        for (int j = 0; j < count; j++) {
            if (strcmp(stats[j].name, books[i].category) == 0) {
                found = j;
                break;
            }
        }
        if (found == -1) {
            if (count >= maxStats) continue;
            strcpy(stats[count].name, books[i].category);
            stats[count].bookCount = 0;
            stats[count].copyCount = 0;
            found = count++;
        }
        stats[found].bookCount += 1;
        stats[found].copyCount += books[i].copies;
    }
    return count;
}
/* ---------------- Main ---------------- */
int main(void) {
    Book catalog[MAX_BOOKS] = {
{
            .id=1, .title="The C Programming Language", .author="Kernighan Ritchie", .category="Programming", .year=1978, .copies=4
        }
        , {
            .id=2, .title="Clean Code", .author="Robert Martin", .category="Programming", .year=2008, .copies=2
        }
        , {
            .id=3, .title="Introduction to Algorithms", .author="Thomas Cormen", .category="Algorithms", .year=2009, .copies=3
        }
        , {
            .id=4, .title="The Pragmatic Programmer", .author="Andrew Hunt", .category="Programming", .year=1999, .copies=5
        }
        , {
            .id=5, .title="Computer Networks", .author="Andrew Tanenbaum", .category="Networking", .year=2010, .copies=1
        }
        , {
            .id=6, .title="Operating System Concepts", .author="Abraham Silberschatz", .category="Systems", .year=2012, .copies=2
        }
    };
    int count = 6;
    printTable(catalog, count, "===== CATALOG (as loaded) =====");
    printf("\n--- Layout facts ---\n");
    printf("sizeof(Book)       : %zu bytes (typically)\n", sizeof(Book));
    printf("Record index 3 at  : offset %zu (3 x sizeof(Book))\n", 3 * sizeof(Book));
    printf("6 records occupy   : %zu bytes of the block\n", 6 * sizeof(Book));
    /* UPDATE */ if (updateCopies(catalog, count, 2, 7)) {
        printf("\nUpdated id=2 -> copies=7\n");
    }
    /* SEARCH by author substring */ Book matches[MAX_FILTERED];
    int found = findByAuthorPart(catalog, count, "Andrew", matches, MAX_FILTERED);
    printf("\nBooks whose author contains \"Andrew\" (%d found):\n", found);
    printHeader();
    for (int i = 0; i < found; i++) {
        printBook(&matches[i]);
    }
    /* SORT by year */ sortByYear(catalog, count);
    printTable(catalog, count, "===== SORTED BY YEAR (oldest first) =====");
    /* DELETE */ count = deleteById(catalog, count, 5);
    printf("\nDeleted id=5 -> %d records remain\n", count);
    /* INSERT in sorted position */ Book newBook = {
        .id = 7, .title = "Refactoring", .author = "Martin Fowler", .category = "Programming", .year = 1999, .copies = 3
    };
    count = insertSortedByYear(catalog, count, newBook);
    printTable(catalog, count, "===== AFTER INSERTING Refactoring (1999) =====");
    /* SORT by title with qsort */ qsort(catalog, count, sizeof(Book), compareByTitle);
    printTable(catalog, count, "===== SORTED BY TITLE (A-Z) =====");
    /* FILTER */ Book programming[MAX_FILTERED];
    int pm = filterByCategory(catalog, count, "Programming", programming, MAX_FILTERED);
    printTable(programming, pm, "===== FILTERED: category == Programming =====");
    /* GROUPED REPORT */ CategoryStat stats[MAX_CATEGORIES];
    int statCount = buildCategoryStats(catalog, count, stats, MAX_CATEGORIES);
    printf("\n===== CATEGORY REPORT =====\n");
    printf("%-14s %8s %8s\n", "Category", "Titles", "Copies");
    puts("-----------------------------");
    int totalCopies = 0;
    for (int i = 0; i < statCount; i++) {
        printf("%-14s %8d %8d\n", stats[i].name, stats[i].bookCount, stats[i].copyCount);
        totalCopies += stats[i].copyCount;
    }
    puts("-----------------------------");
    printf("%-14s %8d %8d\n", "TOTAL", count, totalCopies);
    return 0;
}

Sample Run (verified)

main.c
Run Online
===== CATALOG(as loaded) =====ID Title Author Category Year Copies-------------------------------------------------------------------------1 The C Programming Language Kernighan Ritchie Programming 1978 42 Clean Code Robert Martin Programming 2008 23 Introduction to Algorithms Thomas Cormen Algorithms 2009 34 The Pragmatic Programmer Andrew Hunt Programming 1999 55 Computer Networks Andrew Tanenbaum Networking 2010 16 Operating System Concepts Abraham Silberschatz Systems 2012 2Total records: 6
--- Layout facts ---sizeof(Book) : 132 bytes(typically)Record index 3 at : offset 396(3 x sizeof(Book))6 records occupy : 792 bytes of the block
Updated id=2 -> copies=7
Books whose author contains "Andrew"(2 found):ID Title Author Category Year Copies-------------------------------------------------------------------------4 The Pragmatic Programmer Andrew Hunt Programming 1999 55 Computer Networks Andrew Tanenbaum Networking 2010 1
===== SORTED BY YEAR(oldest first) =====ID Title Author Category Year Copies-------------------------------------------------------------------------1 The C Programming Language Kernighan Ritchie Programming 1978 44 The Pragmatic Programmer Andrew Hunt Programming 1999 52 Clean Code Robert Martin Programming 2008 73 Introduction to Algorithms Thomas Cormen Algorithms 2009 35 Computer Networks Andrew Tanenbaum Networking 2010 16 Operating System Concepts Abraham Silberschatz Systems 2012 2Total records: 6
Deleted id=5 -> 5 records remain
===== AFTER INSERTING Refactoring(1999) =====ID Title Author Category Year Copies-------------------------------------------------------------------------1 The C Programming Language Kernighan Ritchie Programming 1978 44 The Pragmatic Programmer Andrew Hunt Programming 1999 57 Refactoring Martin Fowler Programming 1999 32 Clean Code Robert Martin Programming 2008 73 Introduction to Algorithms Thomas Cormen Algorithms 2009 36 Operating System Concepts Abraham Silberschatz Systems 2012 2Total records: 6
===== SORTED BY TITLE(A-Z) =====ID Title Author Category Year Copies-------------------------------------------------------------------------2 Clean Code Robert Martin Programming 2008 73 Introduction to Algorithms Thomas Cormen Algorithms 2009 36 Operating System Concepts Abraham Silberschatz Systems 2012 27 Refactoring Martin Fowler Programming 1999 31 The C Programming Language Kernighan Ritchie Programming 1978 44 The Pragmatic Programmer Andrew Hunt Programming 1999 5Total records: 6
===== FILTERED: category == Programming =====ID Title Author Category Year Copies-------------------------------------------------------------------------2 Clean Code Robert Martin Programming 2008 77 Refactoring Martin Fowler Programming 1999 31 The C Programming Language Kernighan Ritchie Programming 1978 44 The Pragmatic Programmer Andrew Hunt Programming 1999 5Total records: 4
===== CATEGORY REPORT =====Category Titles Copies-----------------------------Programming 4 19Algorithms 1 3Systems 1 2-----------------------------TOTAL 6 24

What to notice in the run:

  • The row order changes as operations happen.sortByYear reorders rows; the delete leaves a 5-row table; the sorted insert places Refactoring after the other 1999 book (because the scan uses <=); qsort reorders again. The data stays correct through all of it.
  • Deleted id=5 -> 5 records remain — the count is the return value, and it dropped correctly.
  • The insert landed in the right place. Refactoring (1999) sits after The Pragmatic Programmer (1999) and before Clean Code (2008) — sorted insertion without a full re-sort.
  • Two orderings from one qsort call, differing only in the comparison function.
  • The filtered table kept the source order (Clean Code, Refactoring, C Programming, Pragmatic) — filtering preserves relative order rather than re-sorting.
  • The category report reconciles: Programming 4 titles/19 copies + Algorithms 1/3 + Systems 1/2 = TOTAL 6 titles / 24 copies. Three different group rows produced by hand-written GROUP BY logic.

(Suggested image: the same six-record table rendered three times side by side — by year, by title, filtered — to show that only the row order changes, never the record contents)


Common Mistakes

  1. Losing the returned count — deleteById(catalog, count, 5); without assigning drives every later loop onto stale rows. The count is the API.
  2. Using MAX_BOOKS instead of count in loops — prints uninitialized rows.
  3. Forgetting & in scanf("%d", &catalog[i].id) — scanf needs the member's address.
  4. Assigning a whole array — catalog = other; is impossible. Copy row by row (or memcpy).
  5. catalog[i].title = "x"; — char array members aren't assignable. strcpy, or initialize with braces.
  6. Comparing record fields with the wrong operator — == for numbers, strcmp for strings, never == on char arrays.
  7. Swapping only the sort key instead of the whole row — sorting one column scrambles the table.
  8. sizeof(records) inside a function — it's 8 (the pointer), not the block. Pass the count.
  9. Shifting the wrong direction — delete shifts left/forward (books[i] = books[i+1]), insert shifts right/backward (books[i] = books[i-1], loop descending).
  10. Off-by-one in the shift loop — delete uses i < n - 1; insert starts at i = n and stops at pos.
  11. No capacity guard on insert — writing past MAX_BOOKS corrupts memory. Check n >= MAX_BOOKS first.
  12. Computing record size by adding members — use sizeof(Book); padding applies.
  13. A qsort callback with the wrong signature — it must be int (const void *, const void *), and it must return negative/zero/positive.
  14. Writing to a const table parameter — the compiler catches it, which is the point. Drop const only on functions that genuinely mutate.

(Suggested image: "14 Struct-Array Mistakes" checklist — with #1, #7, and #9 highlighted, since those are the three that silently corrupt data rather than crash)


Final Thoughts

An array of structures is a table you can operate on. One type describes a row; the array gives you the rows. From there, every "record management" feature is a small, teachable pattern:

  • A record is one value — add, copy, swap, and delete rows in single statements, not per-column juggling
  • count is not optional — the array has capacity; count has truth. Return the new count from every operation that changes it
  • Compare one column, move the whole row — the rule that governs every sort
  • Deleting and inserting cost a shift — O(n), and the direction matters: delete shifts left, insert shifts right
  • Aggregates are tables too — a grouped summary is its own struct array, built by find-or-create-and-accumulate

Get those five habits and you can build a catalog, an inventory, a payroll, a log store, or an address book from the same handful of functions. And the ID/record model you've just built is precisely the mental shape of a database table — which is a useful thing to carry into file handling, where the next step is persisting this table to disk and reading it back.

Runnable C Code Example & Practical Implementation

main.c
Run Online
#include <stdio.h>
typedef struct {
    int id;
    char name[20];
    double gpa;
}
Student;
int main() {
    Student classroom[3] = {
{
            101, "Alice", 3.9
        }
        ,
{
            102, "Bob", 3.4
        }
        ,
{
            103, "Charlie", 3.8
        }
    };
    printf("Classroom Roster:\
");
    for (int i = 0; i < 3; i++) {
        printf("ID: %d | Name: %-8s | GPA: %.2f\
",
        classroom[i].id, classroom[i].name, classroom[i].gpa);
    }
    return 0;
}

Key Takeaways, Best Practices & Memory Safety Rules

Best Practice & Key Takeaways
Use arrays of structures (AoS) for general business records, and consider Structure of Arrays (SoA) for high-performance SIMD/game-engine processing.

Frequently Asked Questions & Technical Interview Insights

Technical Interview & Viva Questions

Frequently asked C programming interview questions
Q

Q: What is the memory layout of an array of structures vs a structure of arrays (AoS vs SoA)?

A: Array of Structures (AoS: `[{x,y}, {x,y}]`) stores all fields of an object together (better for OOP record access); Structure of Arrays (SoA: `{x[], y[]}`) stores all similar fields contiguously (better for SIMD vectorization and GPU compute).
Q

Q: How do you dynamically allocate an array of 50 structs?

A: `Student *list = (Student*)malloc(50 * sizeof(Student));`.

Related Tools, Tutorials & Authoritative References

Recommended Interactive Tools

Authoritative Standards & Citations