Learn C Programming

Lesson 1 of 9 · Structures, Unions and Enums

Module 12 · Structures, Unions and Enums

struct: Grouping Fields That Belong Together

FreeReading

In this lesson

  • Define a struct with fields of mixed types, and initialise a variable of it by position or by field name.
  • Read, write and scanf fields with the dot, copy a record with =, and compare two records with your own function.
  • Draw a struct in memory, its fields in declaration order, and check its size with sizeof.

Maria keeps her shop's products in three arrays: names, prices and stock. Box 0 of each is rice, box 1 is oil, and only Maria's care keeps them in step.

One evening she sorts the names into alphabetical order and forgets to move the prices. The list now says oil costs 65 taka, and the first customer who buys oil gets a very good day.

This lesson gives Maria one variable per product, holding all three values, so they can only move together.

A struct keeps one record's fields together

Here are the three arrays before and after her sort, one row per index.

IndexBefore: name, price, stockAfter sorting the names only
0rice, 65, 40oil, 65, 40
1oil, 180, 12rice, 180, 12
2salt, 40, 30salt, 40, 30

Nothing in C ties names[1] to prices[1]; they sit side by side only because Maria keeps them there. A name, a price and a stock count for one product are one record. A record should be one variable.

C gives you that with a structure, written struct. It is a type you design, grouping several values that each have a name and a type. Each value inside is a field; the C standard calls it a member.

A struct definition

struct product {
    char name[NAME_LEN];
    int price;
    int stock;
};
  • struct product is the new type's name. product, the word after struct, is its tag.
  • Each line in the braces declares one field, a type and then a name. Fields may have different types.
  • char name[NAME_LEN]; is a char array field: a word of up to 19 letters and its zero (Module 10).
  • The ; after } ends the definition. Leave it out and the error lands on the next line.
#include <stdio.h>

#define NAME_LEN 20

struct product {
    char name[NAME_LEN];
    int price;
    int stock;
};

int main(void)
{
    struct product rice = { "rice", 65, 40 };

    printf("%s: %d taka, %d in stock\n", rice.name, rice.price, rice.stock);
    return 0;
}
rice: 65 taka, 40 in stock

The definition sits above main, so every function in the file can use the type. It makes no variable and sets aside no memory. It describes a shape, like a blank form with three labelled boxes.

The line in main makes a variable rice of type struct product; the type's name is both words. The braces hold an initialiser, the starting values of the fields.

To reach one field, write the variable, a dot and the field's name. This is the dot operator: rice.price reads aloud as "the price field of rice", and it is an ordinary int.

So a struct definition describes the shape once, and each variable of that type is one whole record.

Initialisers: by position, by name, and the zero rule

The braces in { "rice", 65, 40 } fill the fields in the order they were declared. That is a positional initialiser: short, but you must remember the order.

A designated initialiser names each field with a dot: { .price = 180, .name = "oil" }. The order stops mattering, and a reader sees which value goes where. Module 9 lesson 1 did the same for an array's boxes.

The zero rule carries over from arrays too: a field you leave out starts at zero. A number becomes 0, and a char array an empty string. This track writes = { 0 } to start a whole record at zero. Maria tries all four forms.

#include <stdio.h>

#define NAME_LEN 20

struct product {
    char name[NAME_LEN];
    int price;
    int stock;
};

int main(void)
{
    struct product rice = { "rice", 65, 40 };
    struct product oil = { .price = 180, .name = "oil" };
    struct product salt = { "salt" };
    struct product blank = { 0 };

    printf("%s %d %d\n", rice.name, rice.price, rice.stock);
    printf("%s %d %d\n", oil.name, oil.price, oil.stock);
    printf("%s %d %d\n", salt.name, salt.price, salt.stock);
    printf("[%s] %d %d\n", blank.name, blank.price, blank.stock);
    return 0;
}
rice 65 40
oil 180 0
salt 0 0
[] 0 0

oil got no stock, so its stock is 0. salt got only a name, so its price and stock are 0. The brackets frame the empty name of blank.

No message on the Playground or with -Wall. -Wextra warns about salt only: warning: missing initializer for field 'price' of 'struct product' [-Wmissing-field-initializers]. A short positional list may be a slip, so GCC asks. { 0 } and the designated oil say what they mean, and get no warning.

So a field you leave out starts at zero, and a designated initialiser names the fields in any order.

The dot writes fields, and scanf fills them

The dot also works on the left of =: item.price = item.price + 5; reads one field, adds 5 and writes it back. The other fields do not move.

scanf needs the address of every box it fills (Module 3, lesson 4). For an int field that is &item.price: the dot goes first, so & takes the address of that one field.

The name field takes no &, because an array's name in an expression is already an address (Module 11, lesson 3). The 19 in %19s leaves room for the zero (Module 10, lesson 3).

#include <stdio.h>

#define NAME_LEN 20

struct product {
    char name[NAME_LEN];
    int price;
    int stock;
};

int main(void)
{
    struct product item = { 0 };

    scanf("%19s %d %d", item.name, &item.price, &item.stock);
    item.price = item.price + 5;
    printf("%s now costs %d taka, %d in stock\n", item.name, item.price, item.stock);
    return 0;
}
lentils now costs 125 taka, 25 in stock

That output is for the input lentils 120 25.

So item.price is a box like any other int: read it, write it, or hand its address to scanf.

Assignment copies the whole record

Maria wants a weekend offer: the same rice at 60 taka, under its own name. She copies the record with one =, then changes the copy.

#include <stdio.h>
#include <string.h>

#define NAME_LEN 20

struct product {
    char name[NAME_LEN];
    int price;
    int stock;
};

int main(void)
{
    struct product rice = { "rice", 65, 40 };
    struct product offer = { 0 };

    offer = rice;
    strcpy(offer.name, "rice offer");
    offer.price = 60;

    printf("%s: %d taka, %d in stock\n", rice.name, rice.price, rice.stock);
    printf("%s: %d taka, %d in stock\n", offer.name, offer.price, offer.stock);
    return 0;
}

Each row shows both records after that line has run.

After the linericeoffer
struct product offer = { 0 };rice, 65, 40(empty), 0, 0
offer = rice;rice, 65, 40rice, 65, 40
strcpy(offer.name, "rice offer");rice, 65, 40rice offer, 65, 40
offer.price = 60;rice, 65, 40rice offer, 60, 40
rice: 65 taka, 40 in stock
rice offer: 60 taka, 40 in stock

One = copied all three fields, the name's 20 char boxes included. From then on the records are separate: changing offer never touches rice, just as with two int variables (Module 2, lesson 1).

An array alone cannot be assigned with = (Module 11, lesson 3). Yet the array inside this struct was copied. A struct is one value, and = copies a value whole, whatever is inside.

The field alone is still an array, though. Bob renames the offer with offer.name = "rice offer";. An error on every command line, the Playground included: error: assignment to expression with array type, under the badge Compile error. Put text into a char array field with strcpy from <string.h> (Module 10, lesson 4), as Maria did.

So = copies every field of a struct at once, but a char array field alone still needs strcpy.

No == for structs: write the comparison yourself

If = copies a whole record, surely == compares two? Maria writes if (rice == offer) to check her copy. An error on every command line, the Playground included: error: invalid operands to binary == (have 'struct product' and 'struct product').

An operand is a value an operator works on, and a binary operator takes two. GCC is saying == means nothing for two values of type struct product. C leaves it out on purpose: only you know what equal means for your type. Lesson 6 adds a second reason, found in the bytes between fields.

So you write the comparison as a function. Zara checks that the shelf matches Maria's stock book, field by field. Numbers compare with ==; names compare with strcmp, which returns 0 when two strings match.

#include <stdio.h>
#include <string.h>

#define NAME_LEN 20

struct product {
    char name[NAME_LEN];
    int price;
    int stock;
};

int equal_products(struct product a, struct product b);

int main(void)
{
    struct product shelf = { "oil", 180, 12 };
    struct product book = { "oil", 180, 12 };
    struct product label = { "oil", 185, 12 };

    printf("shelf and book: %d\n", equal_products(shelf, book));
    printf("shelf and label: %d\n", equal_products(shelf, label));
    return 0;
}

int equal_products(struct product a, struct product b)
{
    return strcmp(a.name, b.name) == 0
        && a.price == b.price
        && a.stock == b.stock;
}
shelf and book: 1
shelf and label: 0

The function returns 1 when all three fields match, and 0 otherwise; the shelf label says 185 taka. If Maria later decides stock does not count, she changes one function, not every if in her program.

So C gives structs = but not ==, and "equal" means whatever your function says.

A function that takes a struct

Maria prints products in many places, and copying the same printf everywhere is how typos get in. So Alice writes it once, as a function. A struct type can be a parameter, just like int.

#include <stdio.h>

#define NAME_LEN 20

struct product {
    char name[NAME_LEN];
    int price;
    int stock;
};

void print_product(struct product p);

int main(void)
{
    struct product rice = { "rice", 65, 40 };
    struct product oil = { "oil", 180, 12 };
    struct product lentils = { "lentils", 120, 25 };

    print_product(rice);
    print_product(oil);
    print_product(lentils);
    return 0;
}

void print_product(struct product p)
{
    printf("%-8s %4d taka %4d in stock\n", p.name, p.price, p.stock);
}
rice       65 taka   40 in stock
oil       180 taka   12 in stock
lentils   120 taka   25 in stock

The prototype comes after the struct definition, because it uses the type. %-8s pads each name to 8 places and %4d gives each number 4, so the columns line up (Module 3, lesson 3).

A parameter is a fresh box holding a copy of the argument (Module 7, lesson 3). A struct is no exception: p is a new record with a copy of every field. That lesson promised copies get expensive for structures. Lesson 2 measures the cost, and lesson 3 avoids it.

So a function can take a whole record as one parameter, and it works on its own copy.

The struct in memory

A struct variable is one block of memory. C17 promises that its fields sit in the order you declared them, at increasing addresses. The first one starts where the struct starts. Kenji asks sizeof (Module 2, lesson 5), and adds a small record of his own.

#include <stdio.h>

#define NAME_LEN 20

struct product {
    char name[NAME_LEN];
    int price;
    int stock;
};

struct result {
    char grade;
    int marks;
};

int main(void)
{
    struct product rice = { "rice", 65, 40 };
    struct result kenji = { 'A', 91 };

    printf("name %zu + price %zu + stock %zu\n",
           sizeof rice.name, sizeof rice.price, sizeof rice.stock);
    printf("sizeof rice: %zu\n", sizeof rice);
    printf("grade %zu + marks %zu\n", sizeof kenji.grade, sizeof kenji.marks);
    printf("sizeof kenji: %zu\n", sizeof kenji);
    return 0;
}
name 20 + price 4 + stock 4
sizeof rice: 28
grade 1 + marks 4
sizeof kenji: 8
rice and kenji in memory: fields in declaration order, and 3 bytes of padding in kenji struct product rice: 28 bytes, fields in the order declared name 20 bytes price 4 bytes stock 4 bytes r i c e \0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 65 40 0 20 24 28 Offsets count bytes from the start of rice; addresses increase to the right. 20 + 4 + 4 = 28 = sizeof rice: no gap between these fields. struct result kenji: 8 bytes, not 5 grade 1 byte padding 3 bytes marks 4 bytes 'A' unused 91 0 1 4 8 1 + 4 = 5 bytes of fields, but sizeof kenji is 8: 3 unused bytes after grade. Unused bytes between fields are called padding. The compiler adds them; lesson 6 measures where, and explains why.

For rice the sizes add up: 20 + 4 + 4 = 28. A field's offset is how many bytes it sits from the start of the struct. Measured on Compiler Explorer's GCC 12 at the Playground's -O2, name is at 0, price at 20 and stock at 24.

Kenji's record does not add up: a char is 1 byte and an int is 4, yet sizeof kenji is 8. The compiler left 3 unused bytes after grade, so marks starts at offset 4. Such bytes are called padding, and lesson 6 explains them.

So the fields sit in declaration order, and a struct's sizeof is at least the sum of its fields, sometimes more.

Example 1: the smallest struct, a point on a map

A point on Kenji's map has two whole-number coordinates, x and y. The smallest struct worth writing holds exactly those two.

#include <stdio.h>

struct point {
    int x;
    int y;
};

int main(void)
{
    struct point shop = { 3, 4 };

    printf("the shop is at (%d, %d)\n", shop.x, shop.y);
    shop.x = 5;
    printf("after the move: (%d, %d)\n", shop.x, shop.y);
    return 0;
}
the shop is at (3, 4)
after the move: (5, 4)

Two fields of one type still earn a struct: one name, shop, stands for the whole point. Writing shop.x left shop.y alone.

Run in Compiler
Example 2: Kenji's halfway point

Kenji's drone flies from the depot to Maria's shop and needs a battery stop halfway. The halfway point's x is the average of the two x fields; the same goes for y.

#include <stdio.h>

struct point {
    int x;
    int y;
};

void print_point(struct point p);

int main(void)
{
    struct point depot = { -40, 10 };
    struct point shop = { 120, 70 };
    struct point halfway = { 0 };

    halfway.x = (depot.x + shop.x) / 2;
    halfway.y = (depot.y + shop.y) / 2;

    printf("depot:   ");
    print_point(depot);
    printf("shop:    ");
    print_point(shop);
    printf("halfway: ");
    print_point(halfway);
    return 0;
}

void print_point(struct point p)
{
    printf("(%d, %d)\n", p.x, p.y);
}
depot:   (-40, 10)
shop:    (120, 70)
halfway: (40, 40)

A third struct point holds the answer, filled one field at a time. Both sums are even, so / 2 loses nothing; an odd sum would drop the half (Module 4).

Run in Compiler
Example 3: a sale at the till, before and after

Maria types one line at the till: the product, its price, its stock, and how many she sold. The program prints the record before and after the sale, then the money taken.

#include <stdio.h>

#define NAME_LEN 20

struct product {
    char name[NAME_LEN];
    int price;
    int stock;
};

void print_product(struct product p);

int main(void)
{
    struct product item = { 0 };
    int sold = 0;

    scanf("%19s %d %d %d", item.name, &item.price, &item.stock, &sold);

    printf("before: ");
    print_product(item);
    item.stock = item.stock - sold;
    printf("after:  ");
    print_product(item);
    printf("takings: %d taka\n", sold * item.price);
    return 0;
}

void print_product(struct product p)
{
    printf("%s, %d taka, %d in stock\n", p.name, p.price, p.stock);
}
before: tea, 120 taka, 30 in stock
after:  tea, 120 taka, 26 in stock
takings: 480 taka

That output is for the input tea 120 30 4. Only the stock changed between the two lines. Zara tries 0 sold first, and the two lines come out the same.

Run in Compiler

Where this is used

  • C's own calendar. <time.h> holds a date and time in a struct tm. Its fields include tm_mday (the day of the month), tm_mon (0 to 11) and tm_year (years since 1900).
  • C's own division. div(17, 5) from <stdlib.h> returns one div_t with two fields: quot is 3 and rem is 2.
  • File details. POSIX stat fills a struct stat with a file's size in st_size, the time it was last modified in st_mtime, and more. ls -l shows those fields as its size and date columns.
  • Games. SDL2, a C library for games and graphics, describes a rectangle as an SDL_Rect with int fields x, y, w and h. SDL_RenderFillRect draws a filled box from one.

Common mistakes

1. Bob forgets the semicolon after the closing brace.

struct product {
    char name[NAME_LEN];
    int price;
    int stock;
}

void print_product(struct product p);

An error on every command line, the Playground included: error: expected ';', identifier or '(' before 'void'. GCC points at the prototype, the line after the struct. A definition may declare variables before its ;, so GCC reads on. End every struct definition with };. You will forget it because a function's closing brace needs none.

2. A positional initialiser in the wrong order.

struct product oil = { "oil", 12, 180 };

No message at any command line. Bob meant 12 in stock at 180 taka, but the fields go name, price, stock, so the program prints oil: 12 taka, 180 in stock. Write { .name = "oil", .price = 180, .stock = 12 } and the order stops mattering. You will slip because both numbers look right on their own.

3. Printing a whole struct with one conversion.

struct point shop = { 3, 4 };

printf("%d\n", shop);

Silent on the Playground. A local gcc -Wall on GCC 12 says warning: format '%d' expects argument of type 'int', but argument 2 has type 'struct point' [-Wformat=]. printf has no conversion for a struct, so this is undefined behaviour and whatever it prints means nothing. Print each field with its own conversion, or write a print function. You will try it because %d has printed everything else so far.

Brain teaser

Kenji writes a struct with no tag at all, and declares both of its variables in one declaration.

#include <stdio.h>

int main(void)
{
    struct { int a; } x = { 1 }, y = { 2 };

    x = y;
    printf("x.a is %d\n", x.a);
    if (x == y) {
        printf("equal\n");
    }
    return 0;
}

What does x = y; copy? GCC 12 refuses one line of this program: which line, and what type does its message name? You never wrote that type's name anywhere.

Reread what GCC said about rice == offer. Then ask how a compiler can name a struct type that has no tag.

Exercise 1Easy

Maria prints a card for every shelf. Write void print_product(struct product p), which prints one product on one line.

Input. One line: a name, a price and a stock count, separated by spaces. The name is one word.

Output. One line: name: <name>, price: <price> taka, stock: <stock>.

Constraints. The name has 1 to 19 letters; 1 <= price <= 100000; 0 <= stock <= 100000.

Sample. Input rice 65 40 gives name: rice, price: 65 taka, stock: 40.

#include <stdio.h>

#define NAME_LEN 20

struct product {
    char name[NAME_LEN];
    int price;
    int stock;
};

void print_product(struct product p);

int main(void)
{
    struct product item = { 0 };

    scanf("%19s %d %d", item.name, &item.price, &item.stock);
    print_product(item);
    return 0;
}

void print_product(struct product p)
{
    /* Print the three fields of p on one line, in the exact words
       of the Output section: the name, the price, then the stock. */
}

Not graded on its own. A judge cannot see the struct holding the three values, and Module 3's problems already print such lines.

Run in Compiler
Exercise 2Medium

Kenji's delivery drone flies straight between two points on a city map, in metres. Write double distance(struct point a, struct point b). sqrt from <math.h> gives the square root (Module 7, lesson 2).

Input. Lines of four integers x1 y1 x2 y2, to the end of input. There is at least one line, and at most 1000.

Output. For each line, the straight-line distance from (x1, y1) to (x2, y2), with exactly two digits after the point.

Constraints. -10000 <= every coordinate <= 10000.

Sample. Input 0 0 3 4 gives 5.00.

#include <stdio.h>
#include <math.h>

struct point {
    int x;
    int y;
};

double distance(struct point a, struct point b);

int main(void)
{
    struct point a = { 0 };
    struct point b = { 0 };

    while (scanf("%d %d %d %d", &a.x, &a.y, &b.x, &b.y) == 4) {
        printf("%.2f\n", distance(a, b));
    }
    return 0;
}

double distance(struct point a, struct point b)
{
    /* How far apart are the two x fields, and the two y fields?
       Pythagoras joins them: the square root of the sum of their squares. */
    return 0.0;
}

Graded as point-distance. The hidden tests include the same point twice, which must print 0.00, and the far corners -10000 -10000 10000 10000, which are 28284.27 apart.

Run in Compiler
Exercise 3Hard

Amara's booking form keeps a date as one record, not three loose numbers. Write int is_valid(struct date d), which returns 1 if the date exists and 0 if it does not. Module 5's calendar rules hold. April, June, September and November have 30 days. February has 28, or 29 in a leap year, and every other month has 31. A leap year divides by 4 and not by 100, or divides by 400.

Input. One line with three integers: day, month and year.

Output. One line: valid if the date exists, otherwise invalid.

Constraints. -100 <= day <= 100, -100 <= month <= 100, and 1 <= year <= 9999.

Sample. Input 29 2 2024 gives valid. Input 31 4 2025 gives invalid.

#include <stdio.h>

struct date {
    int day;
    int month;
    int year;
};

int is_valid(struct date d);

int main(void)
{
    struct date d = { 0 };

    scanf("%d %d %d", &d.day, &d.month, &d.year);
    if (is_valid(d)) {
        printf("valid\n");
    } else {
        printf("invalid\n");
    }
    return 0;
}

int is_valid(struct date d)
{
    /* First the month: is d.month between 1 and 12?
       Then the day: how many days does that month have in d.year? */
    return 0;
}

Not graded on its own. Module 5's date-check grades this same output; here the point is the struct and the function that takes it.

Run in Compiler

Common doubts

  • Why does a struct definition end with a semicolon, when a function does not?

    A struct definition is a declaration, and a declaration ends with ;. Between } and ; you may even declare variables of the type, as the brain teaser does. A function body is not a declaration, so it ends at its brace.

  • Where should the struct definition go?

    Above the prototypes and main, outside every function. A type is known from its definition to the end of the file (Module 7, lesson 4). Defined inside main, no other function could use it.

  • Can a field be an array of numbers, or another struct?

    Yes to both. int marks[3]; is a fine field. Lesson 4 puts a date inside a record, and the dots chain: loan.due.day.

  • Why can = copy the array inside a struct, but not an array on its own?

    A struct is one value, and = copies a value whole. An array's name in an expression becomes the address of its first box (Module 11, lesson 3). So there is no whole array value to copy. That is why offer.name = "rice offer"; fails.

Key takeaways

  • A struct groups fields of different types under one type name; its definition ends with }; and makes no variable.
  • Initialise by position, { "rice", 65, 40 }, or by name, { .price = 180, .name = "oil" }; a field left out starts at zero.
  • The dot reads and writes one field; scanf takes &item.price, but item.name with no &.
  • = copies a whole struct, char array fields included; one char array field alone needs strcpy.
  • == does not compile for structs: compare field by field in a function, with strcmp for names.
  • Fields sit in declaration order. struct product is 20 + 4 + 4 = 28 bytes, but Kenji's char and int take 8, with 3 bytes of padding.

Next, Amara keeps a whole class of these records in one array, and passes them to functions.

End of lesson 1

Mark it done, and your progress moves with you.

Next: Arrays of Structs, and Structs in Functions