Structs: Custom Data Types

So far we’ve worked with single values. An int here, a char there.

Real programs need to group related information together. That’s what structs do.

Think about a student record. You need a name, an age, and a grade. You could use three separate variables:

char student_name[50];
int student_age;
double student_grade;

But that’s messy. What if you have 30 students? That’s 90 variables. And how do you keep track of which name goes with which age?

A struct lets you bundle related data into one package.

Your First Struct

struct Student {
    char name[50];
    int age;
    double grade;
};

This creates a new type called struct Student. It contains three pieces of data. Think of it like creating a form - it says what information belongs together.

Using a Struct

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

struct Student {
    char name[50];
    int age;
    double grade;
};

int main(void) {
    struct Student alice;

    // Use the dot (.) to access each piece
    strcpy(alice.name, "Alice Smith");  // We'll explain strcpy below
    alice.age = 15;
    alice.grade = 92.5;

    printf("Name: %s\n", alice.name);
    printf("Age: %d\n", alice.age);
    printf("Grade: %.1f\n", alice.grade);

    return 0;
}

The dot . means “get this piece from the struct.” alice.age means “the age part of alice.”

Note: To copy text into a character array, we use strcpy() from <string.h>. You can’t use = to copy strings in C - that only works for numbers.

Filling Everything at Once

Instead of setting each field separately, you can fill everything at once:

struct Student alice = {"Alice Smith", 15, 92.5};

Or if you want to be extra clear:

struct Student alice = {
    .name = "Alice Smith",
    .age = 15,
    .grade = 92.5
};

The second way is longer but easier to read. You can see exactly which value goes where.

The typedef Shortcut

Typing struct Student everywhere gets old. The typedef keyword creates a shorter name:

typedef struct {
    char name[50];
    int age;
    double grade;
} Student;  // Now "Student" is a type, not "struct Student"

int main(void) {
    Student alice = {"Alice Smith", 15, 92.5};  // Much cleaner!
    printf("%s is %d years old\n", alice.name, alice.age);
    return 0;
}

This is the pattern you’ll see in most C code.

Arrays of Structs

Just like you can have an array of integers, you can have an array of structs:

#include <stdio.h>

typedef struct {
    char name[50];
    int score;
} Player;

int main(void) {
    Player players[] = {
        {"Alice", 2500},
        {"Bob", 1800},
        {"Charlie", 3200}
    };
    int count = sizeof(players) / sizeof(players[0]);

    // Find the highest score
    int best = 0;
    for (int i = 1; i < count; i++) {
        if (players[i].score > players[best].score) {
            best = i;
        }
    }

    printf("Winner: %s with %d points!\n",
           players[best].name, players[best].score);

    return 0;
}

Structs and Functions

You can pass structs to functions just like any other type:

#include <stdio.h>

typedef struct {
    double x;
    double y;
} Point;

// Function that takes a struct
void print_point(Point p) {
    printf("(%.1f, %.1f)\n", p.x, p.y);
}

// Function that returns a struct
Point create_point(double x, double y) {
    Point p = {x, y};
    return p;
}

int main(void) {
    Point p = create_point(3.0, 4.0);
    print_point(p);  // (3.0, 4.0)
    return 0;
}

Pass by Value (Structs Are Copied)

Just like with regular variables, when you pass a struct to a function, the function gets a copy:

void try_to_change(Point p) {
    p.x = 999;  // Only changes the copy
}

int main(void) {
    Point p = {10, 20};
    try_to_change(p);
    printf("x = %.1f\n", p.x);  // Still 10!
    return 0;
}

Pointers to Structs

To actually change a struct in a function, pass a pointer to it:

void actually_change(Point *p) {
    p->x = 999;  // The arrow -> accesses fields through a pointer
}

int main(void) {
    Point p = {10, 20};
    actually_change(&p);
    printf("x = %.1f\n", p.x);  // Now it's 999!
    return 0;
}

The Arrow Operator ->

When you have a pointer to a struct, use -> instead of . to access fields:

  • p.x - “the x field of p” (p is a struct)
  • p->x - “the x field of what p points to” (p is a pointer to a struct)

The arrow -> is actually shorthand for (*p).x - “follow the pointer, then get the field.” The arrow is just easier to read.

Nested Structs

Structs can contain other structs:

typedef struct {
    int day;
    int month;
    int year;
} Date;

typedef struct {
    char name[50];
    Date birthday;
    Date enrollment;
} Student;

int main(void) {
    Student alice = {
        "Alice Smith",
        {15, 6, 2009},    // birthday: June 15, 2009
        {1, 9, 2024}      // enrollment: September 1, 2024
    };

    printf("%s was born on %d/%d/%d\n",
           alice.name,
           alice.birthday.month,
           alice.birthday.day,
           alice.birthday.year);

    return 0;
}

A Practical Example: Student Grades

Let’s build something useful:

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

#define MAX_STUDENTS 30

typedef struct {
    char name[50];
    int scores[5];
    double average;
} Student;

void calculate_average(Student *s) {
    int total = 0;
    for (int i = 0; i < 5; i++) {
        total += s->scores[i];
    }
    s->average = total / 5.0;
}

void print_student(Student s) {
    printf("%-20s ", s.name);
    for (int i = 0; i < 5; i++) {
        printf("%3d ", s.scores[i]);
    }
    printf("  Avg: %.1f\n", s.average);
}

int main(void) {
    Student class[] = {
        {"Alice", {92, 88, 95, 90, 87}, 0},
        {"Bob", {78, 82, 75, 80, 85}, 0},
        {"Charlie", {95, 98, 92, 96, 94}, 0}
    };
    int count = sizeof(class) / sizeof(class[0]);

    // Calculate averages
    for (int i = 0; i < count; i++) {
        calculate_average(&class[i]);
    }

    // Print everyone
    printf("%-20s  Test Scores          Average\n", "Name");
    printf("------------------------------------------------\n");
    for (int i = 0; i < count; i++) {
        print_student(class[i]);
    }

    // Find top student
    int top = 0;
    for (int i = 1; i < count; i++) {
        if (class[i].average > class[top].average) {
            top = i;
        }
    }
    printf("\nTop student: %s with %.1f average\n",
           class[top].name, class[top].average);

    return 0;
}

Try It Yourself

  1. Create a Rectangle struct with width and height. Write functions to calculate area and perimeter.
  2. Create a Time struct (hours, minutes, seconds). Write a function that adds two times together.
  3. Create a Book struct (title, author, year, price). Write a program that stores 5 books and finds the cheapest one.

Common Mistakes

  • Forgetting the semicolon after the struct definition (the } needs a ; after it)
  • Using . when you should use -> (and vice versa)
  • Trying to use = to copy strings into struct fields (use strcpy() instead)

Next Up

In Part 14, we’ll learn about dynamic structs - creating structs at runtime and understanding how they’re stored in memory.


Enjoyed This?

If this helped something click, subscribe to my YouTube channel. More content like this, same approach - making things stick without insulting your intelligence. It’s free, it helps more people find this stuff, and it tells me what’s worth making more of.