C · Intermediate
Structures in C
In short: A struct groups variables of different types under one name, so a record such as a parcel with a code, a weight and a zone can be stored, copied and passed around as a single unit. Members are accessed with ., or with -> through a pointer; assignment copies every member; and typedef gives the type a shorter name.
One variable for one record
A program that tracks parcels needs, for each parcel, a code, a weight and a delivery zone. Keeping three separate arrays and hoping the indexes stay in step is fragile. A struct declares a new type that holds the three values together, so a parcel becomes one variable, one function argument and one array element.
struct Parcel { char code[8]; double weight_kg; int zone; }; declares the type; struct Parcel a; declares a variable of it. Members are reached with the dot: a.zone = 3. An initialiser in braces fills the members in order, and designated initialisers, { .code = "PK-104", .zone = 3 }, name them explicitly (C99 and later), which reads better and leaves unmentioned members zero. Writing struct in front of the name every time is tedious, so typedef struct { ... } Clock; creates the alias Clock and the keyword can be dropped.
Structs are values. b = a copies every member, including a whole char array inside; afterwards a and b are independent. Passing a struct to a function likewise passes a copy, so a function that receives Clock c can read it freely but cannot change the caller's clock. To change it, pass a pointer, Clock *c, and access members with the arrow, c->minutes, which is shorthand for (*c).minutes. Pointers also avoid copying: a struct holding several arrays may be hundreds of bytes, and copying it on every call is wasteful. A parameter declared const struct Hive *h is the usual choice for read-only access to a large struct.
What structs do not support is ==. Two structs cannot be compared directly, and comparing their bytes with memcmp is unreliable because the padding bytes the compiler may insert between members have unspecified values; compare the members you care about, using strcmp for string members. Assigning a string to a char array member with = is not allowed either, for the same reason it is not allowed for any array; use strcpy or snprintf.
Arrays of structs are the natural shape for a table of records: struct Hive yard[4] is four hives in a row, yard[i].frames reads one field of one hive, and a pointer struct Hive *p = &yard[2] selects a row to work on. Structs can contain other structs, arrays and pointers, which is how linked lists and trees are built once you know dynamic memory. Because of padding, sizeof a struct can be larger than the sum of its members; never assume a particular layout.
Syntax
struct Parcel { /* a new type named "struct Parcel" */
char code[8];
double weight_kg;
int zone;
}; /* the semicolon is required */
struct Parcel a = { "PK-104", 2.5, 3 }; /* positional initialiser */
struct Parcel b = { .code = "PK-105", .zone = 1 }; /* designated; weight_kg becomes 0.0 */
a.zone = 4; /* member access */
b = a; /* copies every member */
typedef struct { int hours; int minutes; } Clock; /* alias: no "struct" keyword needed */
Clock c = { 9, 30 };
void tick(Clock *c) { c->minutes++; } /* -> reaches a member through a pointer */
struct Parcel batch[10]; /* array of structs */
batch[0].zone = 2;p->m is exactly (*p).m. A positional initialiser must follow the declaration order; a designated one may list members in any order and skip some.
Passing a struct to a function
| How | Declaration | Copies the struct? | Can change the caller's struct? |
|---|---|---|---|
| by value | void f(Clock c) | yes, every member | no |
| by pointer | void f(Clock *c) | no, one address | yes, through c-> |
| by pointer to const | void f(const Clock *c) | no | no, and the compiler enforces it |
Declaring, initialising and copying a record
Three parcels: one built with designated initialisers, one copied, one positional.
#include <stdio.h>
#include <string.h>
struct Parcel {
char code[8];
double weight_kg;
int zone;
};
int main(void) {
struct Parcel a = { .code = "PK-104", .weight_kg = 2.5, .zone = 3 };
struct Parcel b = a;
strcpy(b.code, "PK-105");
b.weight_kg += 1.0;
printf("%s %.1f kg zone %d\n", a.code, a.weight_kg, a.zone);
printf("%s %.1f kg zone %d\n", b.code, b.weight_kg, b.zone);
struct Parcel c = { "PK-106", 0.75, 1 };
printf("%s %.2f kg zone %d\n", c.code, c.weight_kg, c.zone);
return 0;
}Output
PK-104 2.5 kg zone 3 PK-105 3.5 kg zone 3 PK-106 0.75 kg zone 1
b = a copies all three members at once, so changing b's code and weight leaves a exactly as it was; the two print differently. The char array member is copied as part of the struct, but it still cannot be assigned with = afterwards, which is why the code is changed with strcpy. The positional initialiser for c lists the members in declaration order; the designated form used for a could list them in any order and leave some out.
Passing by value and by pointer
A clock type that is printed by value and advanced through a pointer.
#include <stdio.h>
typedef struct {
int hours;
int minutes;
} Clock;
void advance(Clock *c, int by_minutes) {
int total = c->hours * 60 + c->minutes + by_minutes;
total %= 24 * 60;
c->hours = total / 60;
c->minutes = total % 60;
}
void print_clock(Clock c) {
printf("%02d:%02d\n", c.hours, c.minutes);
c.minutes = 0; /* changes only this function's copy */
}
int main(void) {
Clock depart = { 23, 40 };
print_clock(depart);
advance(&depart, 35);
print_clock(depart);
advance(&depart, 24 * 60);
print_clock(depart);
return 0;
}Output
23:40 00:15 00:15
print_clock takes a Clock by value; its last line sets the copy's minutes to zero, and that change vanishes when the function returns, so the second print still shows 15 minutes. advance takes a pointer and updates the caller's clock through ->. Adding 35 minutes to 23:40 wraps past midnight because of the %= on the total, and adding a full day changes nothing. Thanks to the typedef, neither function has to write struct anywhere.
An array of structs
Four hives in a table, scanned to compute a total and find the best.
#include <stdio.h>
struct Hive {
char name[12];
int frames;
double honey_kg;
};
int main(void) {
struct Hive yard[4] = {
{ "Alder", 8, 14.2 },
{ "Birch", 10, 21.6 },
{ "Cedar", 6, 9.8 },
{ "Damson", 10, 18.1 }
};
int n = sizeof yard / sizeof yard[0];
double total = 0.0;
int best = 0;
for (int i = 0; i < n; i++) {
total += yard[i].honey_kg;
if (yard[i].honey_kg > yard[best].honey_kg) {
best = i;
}
}
printf("%d hives, %.1f kg in total\n", n, total);
printf("best yield: %s (%.1f kg from %d frames)\n",
yard[best].name, yard[best].honey_kg, yard[best].frames);
struct Hive *p = &yard[2];
p->frames += 2;
printf("%s now has %d frames\n", yard[2].name, yard[2].frames);
return 0;
}Output
4 hives, 63.7 kg in total best yield: Birch (21.6 kg from 10 frames) Cedar now has 8 frames
The array is initialised with one brace group per hive, and sizeof yard / sizeof yard[0] counts them. The loop reads members of yard[i] directly and keeps the index of the best hive, so the final line can print all of its fields. p = &yard[2] points at the third row; p->frames += 2 modifies it in place, which reading yard[2].frames afterwards confirms. Most record-processing programs take this shape: read records into an array of structs, then loop over it.
Common mistakes
Comparing two structs with ==
Why it goes wrong:
if (a == b)does not compile for structs; the language provides no member-by-member comparison, and a byte comparison with memcmp is unreliable because padding bytes are not defined.Fix: Write a small function that compares the members that matter, using strcmp for strings.
C · fixint same_parcel(const struct Parcel *x, const struct Parcel *y) { return strcmp(x->code, y->code) == 0 && x->zone == y->zone; }Expecting a by-value call to change the caller's struct
Why it goes wrong: The function works on a copy. Every assignment inside it is real but is discarded on return, and no error is reported.
Fix: Take a pointer parameter and use
->, then call with&variable.Assigning a string to a char array member
Why it goes wrong:
a.code = "PK-200"is a compile error: arrays cannot be assigned after initialisation, inside a struct or not.Fix: Use
strcpy(a.code, "PK-200"), orsnprintf(a.code, sizeof a.code, "%s", text)when the source length is not known.Using . on a pointer or -> on a struct
Why it goes wrong:
p.frameswhenpis a pointer, oryard[0]->frameswhenyard[0]is a struct, are both compile errors about the operand type.Fix: Use
.on a struct value and->on a pointer to a struct.(*p).framesis also valid but harder to read.
Where you use this
Any input that arrives as records maps directly onto an array of structs. A stock list with one line per item, name quantity unit_price, is read with scanf into struct Item items[50], and the rest of the program works with items[i].quantity instead of three parallel arrays. Functions such as line_total take a pointer to const so they neither copy nor modify the record. The same struct can later be sorted with qsort by any member, as the sorting lesson shows, or written out with fprintf, each time handled as one unit.
struct Item {
char name[20];
int quantity;
double unit_price;
};
double line_total(const struct Item *it) {
return it->quantity * it->unit_price;
}
struct Item items[50];
int n = 0;
while (n < 50 && scanf("%19s %d %lf", items[n].name, &items[n].quantity, &items[n].unit_price) == 3) {
n++;
}Key points
- A struct declares a type whose variables hold several named members of different types.
- Access members with
.; through a pointer use->. - Assignment copies the whole struct, and passing by value copies it too.
- Pass a pointer when the function must modify the struct or the struct is large; add
constwhen it only reads. - Structs cannot be compared with
==; compare their members. typedef struct { ... } Name;removes the need to writestructeverywhere.
Try it yourself
Complete area and perimeter for the struct Rect type: the area is width times height and the perimeter is twice their sum. With the input 4 7 the program prints area 28 and perimeter 22.
#include <stdio.h>
struct Rect {
int width;
int height;
};
int area(struct Rect r) {
return 0; /* replace */
}
int perimeter(struct Rect r) {
return 0; /* replace */
}
int main(void) {
struct Rect plot;
if (scanf("%d %d", &plot.width, &plot.height) != 2) {
return 1;
}
printf("area %d\n", area(plot));
printf("perimeter %d\n", perimeter(plot));
return 0;
}4 7area 28
perimeter 22#include <stdio.h>
struct Rect {
int width;
int height;
};
int area(struct Rect r) {
return r.width * r.height;
}
int perimeter(struct Rect r) {
return 2 * (r.width + r.height);
}
int main(void) {
struct Rect plot;
if (scanf("%d %d", &plot.width, &plot.height) != 2) {
return 1;
}
printf("area %d\n", area(plot));
printf("perimeter %d\n", perimeter(plot));
return 0;
}Practise this
Exercises for this lesson are in the C practice set.
Related lessons
- Arrays in CHow C arrays store same-typed values by zero-based index: initialiser lists, the sizeof length idiom, passing arrays to functions and out-of-range dangers.10 min
- Pointers in CWhat a C pointer is, how & and * work, why functions take addresses to change their caller's variables, and how pointer arithmetic walks an array.10 min
- Dynamic Memory in CRequesting memory at run time in C with malloc, calloc and realloc, checking for NULL, sizing allocations correctly, and freeing every block exactly once.10 min
- Functions in CHow to define and call C functions: prototypes before the first call, arguments copied by value, void functions, early returns and local variables.10 min
Frequently asked questions
Should I pass a struct by value or by pointer in C?
Pass a pointer when the function must change the struct, or when the struct is more than a few words in size, because passing by value copies every byte on each call. Pass by value for small structs such as a pair of ints when you want the function to be unable to affect the caller. A pointer to const gives the efficiency of a pointer with the safety of a value.
What does typedef struct do?
It gives the struct type a one-word alias. typedef struct { int x; int y; } Point; lets you write Point p; instead of struct Point p;. You can also name the tag and the alias together, typedef struct Node { ... } Node;, which is required when the struct contains a pointer to its own type, because inside the braces the alias does not exist yet.
Can a struct contain an array or another struct?
Yes. A member can be an array of any type, another struct, or a pointer, and nested members are reached by chaining operators: order.customer.name[0]. Copying the outer struct copies nested arrays and structs with it, but copies only the address held by a pointer member, so both copies then share the pointed-at data.
How this page was checked. Every program on it was run with GCC 13 (C11) at build time by the publishing checks, and the output shown is what it printed. Running C inside the browser is not available yet, so the Run button is absent rather than pretending; copy the code and run it with GCC 13 (C11) locally.