┌──────┬────────────────────────────────┬────────────────────┬──────────────┬──────┬────────┐ 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 columnArrays of Structures in C: Managing Collections of Records
What is Arrays of Structures in C? Core Concept & Direct Summary
How do arrays of structures store, index, and sort database tables and collections of records in contiguous memory?
There's a mental model that makes arrays of structures click immediately: an array of structures is a table.
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:
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:
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:
One mistyped bound and the arrays drift apart.
Swapping two records (for sorting) — twelve assignments:
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:
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:
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.
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:
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
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
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 =:
Zeroing the whole table is the same one-liner as any array:
(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:
Two details that matter:
printBooktakes aconst Book *, not aBook. At 132 bytes, passing rows by value in a loop would copy 792 bytes just to display them; a pointer is 8.printTabletakesconst Book books[]plusn— the array decays to a pointer, so the count is compulsory. Andconstdocuments 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.
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
O(1) — no shifting, because nothing comes after the new row.
Update a field
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:
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:
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:
Notice the return value protocol throughout: these functions return the new count, and the caller must accept it.
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:
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:
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:
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
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.
Grouped report — one row per category
Now the aggregate. Define a second, different struct — a report row, not a catalog row:
Then group the table:
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.
Sample Run (verified)
What to notice in the run:
- The row order changes as operations happen.
sortByYearreorders rows; the delete leaves a 5-row table; the sorted insert places Refactoring after the other 1999 book (because the scan uses<=);qsortreorders 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
qsortcall, 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
- Losing the returned count —
deleteById(catalog, count, 5);without assigning drives every later loop onto stale rows. The count is the API. - Using
MAX_BOOKSinstead ofcountin loops — prints uninitialized rows. - Forgetting
&inscanf("%d", &catalog[i].id)— scanf needs the member's address. - Assigning a whole array —
catalog = other;is impossible. Copy row by row (ormemcpy). catalog[i].title = "x";—chararray members aren't assignable.strcpy, or initialize with braces.- Comparing record fields with the wrong operator —
==for numbers,strcmpfor strings, never==onchararrays. - Swapping only the sort key instead of the whole row — sorting one column scrambles the table.
sizeof(records)inside a function — it's 8 (the pointer), not the block. Pass the count.- Shifting the wrong direction — delete shifts left/forward (
books[i] = books[i+1]), insert shifts right/backward (books[i] = books[i-1], loop descending). - Off-by-one in the shift loop — delete uses
i < n - 1; insert starts ati = nand stops atpos. - No capacity guard on insert — writing past
MAX_BOOKScorrupts memory. Checkn >= MAX_BOOKSfirst. - Computing record size by adding members — use
sizeof(Book); padding applies. - A
qsortcallback with the wrong signature — it must beint (const void *, const void *), and it must return negative/zero/positive. - Writing to a
consttable parameter — the compiler catches it, which is the point. Dropconstonly 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
countis not optional — the array has capacity;counthas 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
#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
Frequently Asked Questions & Technical Interview Insights
Technical Interview & Viva Questions
Frequently asked C programming interview questionsQ: What is the memory layout of an array of structures vs a structure of arrays (AoS vs SoA)?
Q: How do you dynamically allocate an array of 50 structs?
Related Tools, Tutorials & Authoritative References
Recommended Interactive Tools
- Online C Compiler & Runner — Interactive online GCC sandbox to execute C code in real time.
- Online C Code Editor & IDE — Multi-file C programming environment with code formatting.
- C++ Programming Tutorial — Transition from procedural C to Object-Oriented C++ with STL.
- JavaScript Programming Tutorial — Master modern JavaScript fundamentals, DOM APIs, and async patterns.
Authoritative Standards & Citations
- ISO/IEC 9899 (C Language Standard) — Official international C specification by ISO/IEC JTC 1/SC 22/WG 14.
- GNU GCC Documentation — Official reference manual for the GNU Compiler Collection and C libraries.
- The C Programming Language (K&R) — Definitive classic work by Brian Kernighan & Dennis Ritchie at Bell Labs.
- C Reference Documentation (cppreference.com) — Verified reference covering C89 through C23 language features.