An array holds many values of one type. A struct does the opposite: it holds a few values of different types and treats them as one thing. A point is an x and a y. An employee is a name, an id, and a salary. Without structs you would carry those around as three loose variables and hope you never mixed up whose salary was whose.
Declaring a Struct Type
A struct declaration names a tag and lists the members inside braces:
struct Point {
int x;
int y;
};
The semicolon after the closing brace is required - forgetting it is one of the most confusing error messages a C beginner will meet, because the compiler blames the next line.
This declares a type, not a variable. In C the type's full name is struct Point, with the keyword included:
Members are read and written with the dot operator, p.x. A struct variable is an ordinary variable: it lives on the stack, it can be assigned to another struct of the same type with =, and it disappears at the end of its scope.
Writing struct Point everywhere gets tiring, which is why almost all real C code wraps the declaration in a typedef so the type is simply Point.
Initializing a Struct
You can fill a struct at the moment you declare it. The positional form lists values in declaration order:
struct Point p = {3, 4}; // x = 3, y = 4
The designated initializer names each member instead, which is clearer and safer:
Three things worth noticing:
- The designated form can list members in any order, so adding or reordering a member later does not silently shift values into the wrong slots.
- Any member you leave out is zero-initialized -
{0}is the idiomatic way to zero an entire struct. - A
char name[32]member is a real array inside the struct, so the string is stored in the struct itself, not somewhere else. That also meansa.name = "Ada";after the fact is illegal; you needstrcpy(see string functions).
A struct with no initializer is uninitialized, and reading its members before writing them is undefined behavior. = {0} costs nothing and removes the whole category of bug.
Assigning and Comparing
Struct assignment copies every member:
a is untouched: b got its own copy. But note what C does not give you - there is no == for structs. if (a == b) will not compile. Comparing means comparing members:
if (a.x == b.x && a.y == b.y) { /* equal */ }
Do not reach for memcmp as a shortcut. Compilers insert invisible padding bytes between members for alignment, and those bytes can hold garbage, so two structs with identical members can compare unequal byte-for-byte.
Arrays of Structs
Because a struct is a type like any other, you can make an array of them - the standard way to hold a table of records.
staff[i].salary reads as "element i, then its salary member" - indexing binds tighter than the dot, so no parentheses are needed.
Structs Are Copied Into Functions
This is the rule that catches people. When you pass a struct to a function, C copies it. The function works on its own private copy:
p is still (3, 4). Reading a struct by value, as distance_from_origin does, is perfectly fine and often the clearest choice for small structs. But if the function must change the caller's struct - or if the struct is large enough that copying it is wasteful - pass a pointer instead. That is the subject of structs and pointers.
Returning a struct by value works the same way and is completely legal:
struct Point make_point(int x, int y) {
struct Point p = {x, y};
return p;
}
Nested Structs
A struct member can itself be a struct. Chain the dots to reach inside:
b.published.year walks in one level at a time. Nesting is how you build real data models in C - and the inner struct is stored inside the outer one, not pointed at, so the whole Book is one contiguous block of memory.
Size, Padding, and Member Order
sizeof tells you how big a struct is, and the answer is often larger than the sum of its members:
On a typical machine Wasteful is 12 bytes and Tidy is 8, even though both hold the same three members. The compiler inserts padding so each member sits at an address its type likes - an int usually wants a multiple of 4. Grouping larger members before smaller ones tends to pack better.
This matters rarely (a million-element array, a network packet layout) and never for correctness of your own code. What it does explain is why sizeof surprises you, and why memcmp on structs is a bad idea.
Frequently Asked Questions
How do you declare a struct in C?
Write struct followed by a tag name and a braced list of members: struct Point { int x; int y; };. That declares a type, not a variable. To make a variable you then write struct Point p; - the word struct is part of the type name in C, which is why most code pairs it with a typedef.
What is the difference between . and -> for structs?
. and -> for structs?Use . when you have the struct itself (p.x) and -> when you have a pointer to it (ptr->x). The arrow is shorthand for (*ptr).x. See structs and pointers for the details.
Are structs passed by value or by reference in C?
By value, always. Passing a struct to a function copies every member, so changes inside the function do not affect the caller's variable. To mutate the original - or to avoid copying a large struct - pass a pointer instead.
How do I initialize a struct in C?
Either positionally, struct Point p = {3, 4};, or with designated initializers, struct Point p = {.y = 4, .x = 3};. The designated form names each member, so it survives someone reordering the struct later and leaves unnamed members zeroed.