C lesson 8 of 8
Structs and typedef in C
How to group related values into one type with struct, reach members with the dot and arrow operators, shorten the name with typedef, and build and search an array of structs.
Published · Every example on this page was run before it was published.
An array holds many values of one type. A struct holds several values of different types, under
one name, as one thing. That is the tool that turns a pile of loose variables - code, quantity,
unit_price, repeated for every part in a warehouse - into a single Part you can copy, pass to a
function, return, and put in an array. It is the last piece of plain C's vocabulary, and the one that
decides how readable a program of any size is going to be.
Declaring a Struct and Reaching Its Members
A struct declaration lists members, each with its own type. Creating one and initialising it uses
the same brace syntax as an array, filling the members in declaration order. The . operator reads and
writes a member.
#include <stdio.h>
struct Point {
int x;
int y;
};
int main(void) {
struct Point origin = {0, 0};
struct Point target = {3, 4};
printf("origin: (%d, %d)\n", origin.x, origin.y);
printf("target: (%d, %d)\n", target.x, target.y);
target.x = 10;
printf("moved: (%d, %d)\n", target.x, target.y);
return 0;
}origin: (0, 0)
target: (3, 4)
moved: (10, 4)Two details of the syntax catch people out. The declaration ends with a semicolon after the closing
brace, unlike a function. And the type's full name is struct Point, two words - the struct keyword is
part of it every time you use it, which gets tiresome fast and is the reason typedef exists.
Members can be given names by hand rather than by position, which is clearer for anything with more than two or three of them:
#include <stdio.h>
struct Reading {
int sensor;
double value;
char unit;
};
int main(void) {
struct Reading first = {.sensor = 3, .value = 21.5, .unit = 'C'};
struct Reading blank = {0};
printf("sensor %d read %.1f%c\n", first.sensor, first.value, first.unit);
printf("blank sensor: %d, value: %.1f\n", blank.sensor, blank.value);
return 0;
}sensor 3 read 21.5C
blank sensor: 0, value: 0.0Those .name = value forms are designated initialisers, and they may be written in any order. Any
member you leave out is set to zero, which is what makes = {0} a complete, correct way to zero a whole
struct whatever it contains.
typedef Gives the Type a Single-Word Name
typedef creates an alias for a type. Applied to a struct, it lets you drop the keyword everywhere
afterwards, and the usual idiom declares and names in one go:
#include <stdio.h>
typedef struct {
char code[8];
int quantity;
double unit_price;
} Item;
int main(void) {
Item bolt = {"BLT-10", 250, 0.75};
printf("%s\n", bolt.code);
printf("quantity: %d\n", bolt.quantity);
printf("unit price: %.2f\n", bolt.unit_price);
printf("line total: %.2f\n", bolt.quantity * bolt.unit_price);
return 0;
}BLT-10
quantity: 250
unit price: 0.75
line total: 187.50The struct itself has no name here - it is anonymous, and Item is the name of the type. From that point
on Item is used exactly like int would be: as a variable's type, as a parameter's type, as a return
type, as an array's element type.
Note char code[8]: a struct member may be an array, and it is stored inside the struct rather than
pointed at from it. Eight bytes are reserved whatever you put there, and "BLT-10" needs seven of them
with its terminator, so it fits. A member that is a char * instead would store only an address, and
whatever it pointed at would have to outlive the struct - a real difference, and a decision worth making
consciously.
bolt.quantity * bolt.unit_price mixes an int member with a double member; ordinary conversion rules
apply, so the result is a double.
A Struct Is a Value: It Gets Copied
Assigning one struct to another copies every member. Passing one to a function copies it too, exactly as
an int would be copied - the pass-by-value rule from the functions lesson applies unchanged, no matter
how big the struct is.
#include <stdio.h>
typedef struct {
int x;
int y;
} Point;
void move_by_value(Point p) {
p.x = p.x + 100;
}
void move_by_pointer(Point *p) {
p->x = p->x + 100;
}
int main(void) {
Point here = {1, 2};
Point copy = here;
copy.y = 99;
printf("here: (%d, %d)\n", here.x, here.y);
printf("copy: (%d, %d)\n", copy.x, copy.y);
move_by_value(here);
printf("after passing by value: (%d, %d)\n", here.x, here.y);
move_by_pointer(&here);
printf("after passing a pointer: (%d, %d)\n", here.x, here.y);
return 0;
}here: (1, 2)
copy: (1, 99)
after passing by value: (1, 2)
after passing a pointer: (101, 2)Point copy = here; produced a genuinely independent struct, which is why setting copy.y left here
alone. Note that this is assignment working on a compound value - and it is the one case where it does,
which is why a = b between two structs copies the data while a = b between two arrays does not.
move_by_value changed its own copy and achieved nothing. move_by_pointer received an address and
reached the original.
The Arrow Operator
p->x is shorthand for (*p).x: dereference the pointer, then take the member. The parentheses in the
long form are not optional, because . binds tighter than *, so *p.x would try to take the member
first. Nobody writes the long form.
#include <stdio.h>
typedef struct {
char name[16];
int score;
} Player;
void award(Player *player, int points) {
player->score += points;
}
int main(void) {
Player p = {"Nadia", 0};
award(&p, 30);
award(&p, 12);
printf("%s has %d points\n", p.name, p.score);
printf("the same member the long way: %d\n", (*(&p)).score);
return 0;
}Nadia has 42 points
the same member the long way: 42The rule is short: . when you have the struct, -> when you have a pointer to it.
Taking a pointer is also how you avoid copying a large struct on every call. A function that only reads
should take a const Type *, which both avoids the copy and states that nothing will be modified.
Structs In and Out of Functions
A function can take structs and return them, which is the cleanest way to give back several related values at once - better than the output-pointer version from the pointers lesson when the values belong together.
#include <stdio.h>
typedef struct {
int quotient;
int remainder;
} DivisionResult;
DivisionResult divide(int numerator, int denominator) {
DivisionResult result = {numerator / denominator, numerator % denominator};
return result;
}
int main(void) {
DivisionResult a = divide(47, 5);
DivisionResult b = divide(100, 7);
printf("47 / 5 = %d remainder %d\n", a.quotient, a.remainder);
printf("100 / 7 = %d remainder %d\n", b.quotient, b.remainder);
return 0;
}47 / 5 = 9 remainder 2
100 / 7 = 14 remainder 2result is a local, and returning it copies it out before it ceases to exist - which is why returning a
struct is safe while returning a pointer to a local is not.
Structs Inside Structs
A member may itself be a struct, and members are reached by chaining the dots.
#include <stdio.h>
typedef struct {
int day;
int month;
int year;
} Date;
typedef struct {
char title[24];
Date due;
int done;
} Task;
int main(void) {
Task t = {"Reorder washers", {4, 11, 2026}, 0};
printf("%s\n", t.title);
printf("due %02d/%02d/%d\n", t.due.day, t.due.month, t.due.year);
printf("done: %s\n", t.done ? "yes" : "no");
t.done = 1;
printf("done: %s\n", t.done ? "yes" : "no");
return 0;
}Reorder washers
due 04/11/2026
done: no
done: yesThe Date must be declared before the Task that contains one, because the compiler has to know how big
a Date is in order to lay out a Task. The nested braces in the initialiser match the nesting of the
types, and t.due.day reads as "the task's due date, then that date's day".
%02d is a width of two with zero padding, which is why day 4 printed as 04 while the year printed
in full with a plain %d.
The done member is an int used as a flag, which is the usual C way before <stdbool.h> gets
involved. The conditional operator turns it into a word for display; the struct itself stores 0 or 1.
Arrays of Structs
This is where structs earn their place. An array of structs is a table: one row per item, one member per column.
#include <stdio.h>
typedef struct {
char code[8];
int quantity;
} Part;
int main(void) {
Part parts[3] = {
{"BLT-10", 250},
{"NUT-06", 400},
{"WSH-04", 120},
};
for (int i = 0; i < 3; i++) {
printf("%s x%d\n", parts[i].code, parts[i].quantity);
}
parts[1].quantity -= 50;
printf("after a withdrawal: %s x%d\n", parts[1].code, parts[1].quantity);
printf("rows: %zu\n", sizeof parts / sizeof parts[0]);
return 0;
}BLT-10 x250
NUT-06 x400
WSH-04 x120
after a withdrawal: NUT-06 x350
rows: 3parts[i].code reads as "element i, then its code member", and the two operators combine in exactly
that order. The sizeof idiom from the arrays lesson works here unchanged, because the array's
declaration is still in scope.
What sizeof Says About a Struct
A struct's size is not necessarily the sum of its members' sizes. The compiler may insert unused padding bytes so that each member begins at an address its type requires.
#include <stdio.h>
struct Tagged {
char tag;
int value;
};
int main(void) {
printf("a char plus an int is %zu bytes of data\n", sizeof(char) + sizeof(int));
printf("the struct occupies %zu bytes\n", sizeof(struct Tagged));
return 0;
}a char plus an int is 5 bytes of data
the struct occupies 8 bytesThose eight bytes are what this site's target produces: one byte for the char, three unused, then four
for the int at an address divisible by four. The exact figure is decided by the platform's alignment
rules, so treat it as an illustration of the principle rather than a number to memorise - and use
sizeof whenever the size is needed, exactly as with every other type.
The practical consequences are two. Ordering members from largest to smallest tends to waste less space, which matters when you have millions of them and not at all when you have twelve. And you cannot compare two structs by comparing their bytes, because the padding is not required to hold anything in particular
- compare member by member instead. For the same reason there is no
==for structs: C will copy one but refuses to guess what comparing two should mean.
A Worked Example
A small stock table built from an array of structs, with a function that works on one row, a loop that aggregates, and a search for the largest line.
#include <stdio.h>
typedef struct {
char code[8];
int quantity;
double unit_price;
} Part;
double line_total(const Part *part) {
return part->quantity * part->unit_price;
}
int main(void) {
Part parts[4] = {
{"BLT-10", 250, 0.75},
{"NUT-06", 400, 0.25},
{"WSH-04", 120, 1.50},
{"BRK-02", 15, 4.00},
};
int count = (int) (sizeof parts / sizeof parts[0]);
double total = 0.0;
int best = 0;
int low_stock = 0;
printf("%-8s %5s %8s\n", "code", "qty", "value");
for (int i = 0; i < count; i++) {
double line = line_total(&parts[i]);
total += line;
if (line > line_total(&parts[best])) {
best = i;
}
if (parts[i].quantity < 100) {
low_stock++;
}
printf("%-8s %5d %8.2f\n", parts[i].code, parts[i].quantity, line);
}
printf("rows: %d\n", count);
printf("inventory value: %.2f\n", total);
printf("largest line: %s at %.2f\n", parts[best].code, line_total(&parts[best]));
printf("parts below 100 units: %d\n", low_stock);
return 0;
}code qty value
BLT-10 250 187.50
NUT-06 400 100.00
WSH-04 120 180.00
BRK-02 15 60.00
rows: 4
inventory value: 527.50
largest line: BLT-10 at 187.50
parts below 100 units: 1line_total takes a const Part * rather than a Part. Either would work, and the pointer version
avoids copying the struct on every one of the calls in the loop while promising not to modify the row. It
uses -> because it holds a pointer; main uses . because it holds the array.
The best index is the whole search: it starts at row zero and moves whenever a strictly larger line is
found, so it ends up holding the position of the first maximum. Storing the index rather than a copy of
the row means parts[best] always reflects the current contents, which matters if anything in the loop
modifies a row.
count comes from the sizeof idiom rather than from a literal 4, so adding a fifth part to the
initialiser list requires no other change - a small thing that prevents a real class of bug.
The two printf calls with width specifiers are what make the table line up. %-8s is a string in a
field eight wide, padded on the right; %5d is an integer right-aligned in five; %8.2f is a number
with two decimals right-aligned in eight. Every price in that table is a multiple of a quarter, so the
figures are exact rather than rounded approximations - the variables lesson explains why that is worth
arranging in an example.
Common Mistakes
Forgetting the semicolon after the closing brace. }; ends a struct declaration. Leaving it off
produces a confusing message about whatever follows.
Using . on a pointer, or -> on a struct. p.x where p is a Point * is an error, as is
here->x where here is a Point. . for the struct, -> for the pointer to one.
Writing *p.x and expecting p->x. . binds tighter than *, so that reads as *(p.x). The long
form needs parentheses: (*p).x.
Leaving struct off the type name. Without a typedef, the type is struct Point and Point alone
is not a type at all.
Comparing two structs with ==. Not allowed, and would not mean what you want even if it were,
because of padding. Compare the members you care about.
Assuming sizeof a struct is the sum of its members. Padding makes it at least that and often more.
Never compute a struct's size by hand.
Assigning a char array member from a string. item.code = "BLT-10"; is an error - the member is an
array, and arrays are not assignable. Use strcpy(item.code, "BLT-10"), after checking the text fits in
the member's size, or initialise it at declaration where the brace form does the copy for you.
Returning a pointer to a local struct. Returning the struct itself copies it out and is safe.
Returning &result hands back the address of something that has just ceased to exist.
Storing a char * in a struct and letting the target die first. A pointer member does not own what it
points at. If the struct is going to outlive the original text, give it a char array, or allocate a copy
and record who frees it.
Next Steps
You now have all of plain C's vocabulary: types, control flow, functions, arrays, strings, pointers, dynamic memory and structs. Everything beyond this track - linked lists, trees, file handling, whole libraries - is those pieces combined, and a struct containing a pointer to its own type is the doorway to most of it.
In the C playground, define a struct with three members of three different types
and an array of four of them, then write one function that prints a single row and another that returns
the index of the row with the largest value of some member. When that feels routine, add a Part *
member pointing at another element of the same array and print a chain of two - the first data structure
you build rather than declare.
Write it yourself
Reading about code and writing it are different skills. These exercises practise exactly what this lesson covered; they run in this tab and need no account.