A struct that looks like it holds 14 bytes of data can occupy 24. Reorder the same four members and it drops to 16 — a third smaller, with nothing added or removed. Most struct tutorials never mention this, which is why most struct tutorials stop being useful the moment you care about memory, file formats, or comparing two records correctly.
This guide covers the syntax first and then the parts that matter in production: how the compiler lays members out, why memcmp() reports two identical records as different, and what pass-by-value actually costs. Every program below was compiled and run for this article on Ubuntu 24.04 with GCC 13.3 under -std=c17 with -Wall -Wextra. Sizes come from sizeof and offsetof at runtime, the padding bytes are printed as raw hex, and the pass-by-value figures are timed over ten million calls with inlining disabled. All output is captured verbatim.
Table of Contents
- What Is a Structure in C?
- Declaring and Initializing a Structure
- Memory Layout: Padding and Alignment
- Why memcmp() Cannot Compare Structs
- Passing Structures to Functions
- Nested Structures and Arrays of Structures
- Flexible Array Members
- Structures vs Unions
- Real-World Example: An Inventory Record
- Key Takeaways
- Frequently Asked Questions
- Conclusion
What Is a Structure in C?
A structure in C is a user-defined type that groups related variables of different types under one name. Each variable inside it is called a member, and members are stored in a single contiguous block of memory in declaration order. You declare a structure with the struct keyword, access its members with the dot operator (.) on a value or the arrow operator (->) through a pointer, and can copy an entire structure with a single assignment.
Structures are how C represents records: a file header, a network packet, a row from a database, a point in 3D space. Unlike an array, the members can be of different types; unlike separate variables, they travel together as one unit that can be assigned, passed, and returned.
For the normative rules on structure declarations and member layout, cppreference’s struct page is the reference to keep alongside this one.
Declaring and Initializing a Structure
The declaration defines a new type. Creating a variable of that type is a separate step:
#include <stdio.h>
struct Item {
int id;
char name[32];
float price;
int qty;
};
int main(void) {
struct Item widget; /* note: 'struct Item', not just 'Item' */
widget.id = 101;
widget.qty = 5;
printf("%d\n", widget.id);
return 0;
}
The struct keyword is required in C. Writing Item widget; without it is a compile error — and a common one, because it is valid in C++:
error: unknown type name 'Item'; use 'struct' keyword to refer to the type
10 | Item widget;
| ^~~~
| struct
GCC names the fix in the diagnostic. If you want the shorter spelling in C, typedef gives it to you:
typedef struct {
int id;
char name[32];
float price;
int qty;
} Item;
Item widget; /* now valid C */
This is the idiom most production C uses. The trade-off is that an anonymous typedef’d struct cannot refer to itself — for a linked list node you need a tag:
typedef struct Node {
int value;
struct Node *next; /* the tag makes self-reference possible */
} Node;
Designated initializers
C99 added designated initializers, and they are strictly better than positional ones for anything with more than two members:
Item a = { .name = "Widget", .price = 9.99f, .id = 101 };
id=101 name=Widget price=9.99 qty=0 (unset member zeroed)
Order does not matter, the reader can see which value goes where, and any member you leave out is zero-initialized. Positional initialization — {101, "Widget", 9.99f, 0} — breaks silently the day someone inserts a member in the middle. Prefer designated initializers.
A compound literal creates an anonymous struct value in place, which is useful for passing a one-off to a function:
Item b = (Item){ .id = 102, .name = "Gadget", .price = 24.50f, .qty = 3 };
Assignment copies everything
b.id=102 c.id=103 (independent), c.name=Gadget
Item c = b; copies every member, including the whole 32-byte name array. The two variables are then completely independent. This is one of the few places C does a deep-ish copy for you — but note that if a member is a pointer, only the pointer is copied, not what it points to.
Memory Layout: Padding and Alignment
Here is the part that separates a working understanding from a superficial one.
The compiler does not pack members tightly. Each member must sit at an offset that is a multiple of its own alignment requirement, so the compiler inserts padding bytes to make that happen. Measured with sizeof and offsetof:
struct Bad { char flag; int id; char grade; double score; }
sum of members = 14 bytes
sizeof(struct Bad) = 24 bytes <-- 10 bytes of padding
offsets: flag=0 id=4 grade=8 score=16
alignment requirement: 8 bytes
struct Good { double score; int id; char flag; char grade; }
same members, reordered largest-first
sizeof(struct Good) = 16 bytes <-- 2 bytes of padding
offsets: score=0 id=8 flag=12 grade=13
alignment requirement: 8 bytes
Saving: 8 bytes per struct (33%)
Across an array of 1,000,000: 22 MB vs 15 MB
Identical members. Identical data. A third less memory, purely from declaration order.
The rule that produces this: a double requires an 8-byte boundary, so after char grade at offset 8 the compiler must skip to offset 16, wasting seven bytes. Declaring members from largest alignment to smallest packs them naturally.
Two caveats before you go reordering every struct in your codebase. First, this matters when you have many instances — an array of a million records, a cache-sensitive hot loop — and is irrelevant for a handful of configuration structs, where readability wins. Second, never reorder members of a struct that maps to a file format, a network packet, or a hardware register, because the layout is the contract.
You can inspect any struct’s layout yourself with offsetof from <stddef.h>, and query alignment with alignof from <stdalign.h>.
Why memcmp() Cannot Compare Structs
A widely repeated shortcut says you can compare two structs with memcmp(). Here is what actually happens. Two records with every named member set identically:
sizeof(struct Rec) = 12 (members total 9, so 3 bytes are padding)
Every named member is identical:
a.n=42 a.c='X' a.m=99
b.n=42 b.c='X' b.m=99
Member-by-member comparison : EQUAL
memcmp(&a, &b, sizeof a) : NOT EQUAL <-- the trap
raw bytes of a: 2a 00 00 00 58 ff ff ff 63 00 00 00
raw bytes of b: 2a 00 00 00 58 00 00 00 63 00 00 00
^^^^^^^^ padding differs
The raw bytes show the cause exactly. Both structs hold 42, 'X' and 99 in their members — but the three padding bytes after c contain ff ff ff in one and 00 00 00 in the other, because padding holds whatever was previously in that memory. The C standard leaves padding contents unspecified, so memcmp() is comparing garbage alongside your data.
Compare structs member by member. Write a function that does it explicitly:
#include <string.h>
int item_equal(const Item *a, const Item *b) {
return a->id == b->id
&& a->qty == b->qty
&& a->price == b->price /* see note on float comparison */
&& strcmp(a->name, b->name) == 0;
}
Two details worth flagging. Comparing float members with == carries the usual floating-point caveat — for computed values, compare against a tolerance. And for char array members use strcmp, not strcpy: a comparison function built on strcpy overwrites its first argument and always reports “not equal”, since strcpy returns a pointer that is never null:
before: a="one" b="two"
after : a="two" b="two"
!strcpy(a,b) evaluated to 0
Zeroing a struct with memset before filling it makes memcmp appear to work, because the padding then matches. Do not rely on it: the compiler is free to leave padding untouched by member assignment, and the technique breaks silently the moment a struct is copied or passed by value.
Passing Structures to Functions
C passes structs by value — the function receives a complete copy. That is often what you want for small structs, and it costs real time for large ones.
Measured over ten million calls, with inlining disabled and the callee reading the entire struct:
sizeof(Big) = 516 bytes, 10000000 calls, callee reads the whole struct
run 1 by value 238 ms
run 1 by pointer 59 ms
run 2 by value 196 ms
run 2 by pointer 59 ms
Roughly 3–4× slower by value for a 516-byte struct. But an honest caveat from the same benchmark session: when the callee only read a single member, or when the struct was small (44 bytes), the difference vanished entirely — modern optimizers elide copies they can prove are unnecessary. The cost is real, but it appears when the copy actually has to happen.
The practical guidance:
void print_item(const Item *it); /* read-only: const pointer */
void update_qty(Item *it, int delta); /* needs to modify the caller's data */
Item make_item(int id, const char *name); /* returning by value is fine */
constpointer for read-only access to anything beyond a few words. It documents intent and avoids the copy.- Non-const pointer when the function must modify the original. Passing by value and expecting the caller to see changes is a classic bug — the callee is modifying its own copy.
- Returning by value is fine for small structs; compilers elide the copy in most cases.
Nested Structures and Arrays of Structures
Structs compose. A struct member can itself be a struct:
struct Address {
char city[40];
char postcode[12];
};
struct Employee {
char name[50];
struct Address address; /* nested */
int salary;
};
struct Employee e = {
.name = "Ada",
.address = { .city = "London", .postcode = "SW1A 1AA" },
.salary = 75000
};
printf("%s\n", e.address.city); /* chain the dots */
Access chains from outermost to innermost. Partial chains are compile errors: e.address alone is a struct value, not a string, and e.city does not exist.
An array of structures is the standard way to hold a table of records:
struct Item inventory[3] = {
{ .id = 101, .name = "Widget", .price = 9.99f, .qty = 5 },
{ .id = 102, .name = "Gadget", .price = 24.50f, .qty = 3 },
{ .id = 103, .name = "Doohickey", .price = 4.75f, .qty = 12 }
};
for (size_t i = 0; i < sizeof inventory / sizeof inventory[0]; i++)
printf("%-10s %6.2f x%d\n",
inventory[i].name, inventory[i].price, inventory[i].qty);
Note sizeof inventory / sizeof inventory[0] for the element count — it stays correct if the array grows. This only works where the array is visible; passed to a function the array decays to a pointer and sizeof gives the pointer size instead.
Flexible Array Members
C99 added a way to allocate a header and a variable-length payload in a single block:
typedef struct {
size_t count;
Item items[]; /* flexible array member; must be last */
} Inventory;
size_t n = 3;
Inventory *inv = malloc(sizeof *inv + n * sizeof *inv->items);
inv->count = n;
4. Flexible array member: one allocation, 140 bytes
sizeof(Inventory) alone = 8 (the array contributes 0)
The flexible member contributes nothing to sizeof, which is why the allocation adds the payload size explicitly. One malloc and one free instead of two, with the header and data adjacent in memory — good for cache behaviour and impossible to leak half of.
Rules: the flexible array must be the last member, and the struct must have at least one other member before it.
Structures vs Unions
Both group members under one name. The difference is memory:
| Aspect | Structure | Union |
|---|---|---|
| Memory | Each member gets its own storage | All members share the same storage |
| Size | Sum of members plus padding | Size of the largest member |
| Valid members at once | All of them | One — the last one written |
| Typical use | Records: a thing with several attributes | Variant data: one thing that could be several types |
| Reading a member you did not write | Fine | Type punning; rules apply |
A struct holding an int and a double occupies at least 16 bytes; a union of the same two occupies 8, because they overlap. Unions are the right tool for tagged variants — a value that is either an integer or a float — usually paired with a struct holding the tag. The dedicated guide to unions in C covers type punning and the struct-with-tag pattern in depth.
Real-World Example: An Inventory Record
Putting it together — the pattern most production C uses for record handling:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
float price; /* largest alignment first */
int id;
int qty;
char name[32];
} Item;
static int item_equal(const Item *a, const Item *b) {
return a->id == b->id && a->qty == b->qty
&& a->price == b->price && strcmp(a->name, b->name) == 0;
}
static void item_print(const Item *it) {
printf(" #%-4d %-12s %7.2f x%d\n", it->id, it->name, it->price, it->qty);
}
static void restock(Item *it, int amount) { /* modifies the caller's item */
it->qty += amount;
}
int main(void) {
Item stock[] = {
{ .id = 101, .name = "Widget", .price = 9.99f, .qty = 5 },
{ .id = 102, .name = "Gadget", .price = 24.50f, .qty = 3 },
{ .id = 103, .name = "Doohickey", .price = 4.75f, .qty = 12 },
};
size_t n = sizeof stock / sizeof stock[0];
puts("Inventory:");
for (size_t i = 0; i < n; i++) item_print(&stock[i]);
restock(&stock[1], 10);
puts("After restocking Gadget:");
item_print(&stock[1]);
Item copy = stock[0];
printf("copy equals original: %s\n", item_equal(©, &stock[0]) ? "yes" : "no");
printf("sizeof(Item) = %zu bytes\n", sizeof(Item));
return 0;
}
Every choice here is deliberate: members ordered by alignment, const pointers for read-only functions, a non-const pointer where mutation is intended, designated initializers, member-wise comparison rather than memcmp, and sizeof stock / sizeof stock[0] for the count.
Key Takeaways
structis required in C when declaring variables —Item x;without atypedefis a compile error, though it is valid C++.- Padding is not optional. The same four members reordered went from 24 bytes to 16 — 33% smaller, measured with
sizeofandoffsetof. - Never reorder a struct that maps to a file format, packet, or hardware register. The layout is the contract.
memcmp()cannot compare structs. Two records with identical members compared NOT EQUAL because their padding bytes differed. Compare member by member.- Pass large structs by
constpointer. A 516-byte struct measured 3–4× slower by value when the copy could not be elided. - Use designated initializers. Unlisted members are zeroed, and inserting a member later does not silently break existing initializations.
- Flexible array members put a header and variable-length payload in one allocation.
Frequently Asked Questions
Conclusion
The syntax of structures is a morning’s work. What takes longer is the mental shift from thinking of a struct as a bag of named fields to thinking of it as a specific arrangement of bytes — because that is what it is, and every interesting property follows from it. Padding exists because of alignment. memcmp fails because of padding. Pass-by-value costs because the bytes get copied. Flexible array members work because the header and payload are adjacent.
Once structs are laid out the way you intend, the next questions are how to get them onto disk and back — where layout stops being an optimization detail and becomes a compatibility contract. The C programming section covers file handling, memory allocation, and the data structures that structs make possible.



