Structs are C’s way of grouping related data under one name. They are the foundation of every non-trivial data structure in C — linked lists, trees, hash maps, and more all use structs. Here is everything you need to know.

Declaring and Using a Struct

#include <stdio.h>

struct Point {
    int x;
    int y;
};

int main() {
    struct Point p1;
    p1.x = 10;
    p1.y = 20;

    struct Point p2 = {5, 15};   /* initialiser list */

    printf("p1: (%d, %d)n", p1.x, p1.y);
    printf("p2: (%d, %d)n", p2.x, p2.y);

    return 0;
}

typedef — Remove the struct Keyword

Without typedef, you must write struct Point everywhere. typedef creates an alias so you only write Point:

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

int main() {
    Point p = {10, 20};   /* no 'struct' keyword needed */
    printf("(%d, %d)n", p.x, p.y);
    return 0;
}

For self-referential structs (like a linked list node), you need the tag name:

typedef struct Node {
    int value;
    struct Node *next;   /* can't use 'Node*' here yet — alias not defined */
} Node;

Passing Structs to Functions

By value — a copy is made

#include <stdio.h>

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

void move(Point p, int dx, int dy) {
    p.x += dx;   /* modifies the copy — original unchanged */
    p.y += dy;
}

int main() {
    Point p = {10, 20};
    move(p, 5, 5);
    printf("(%d, %d)n", p.x, p.y);   /* still (10, 20) */
    return 0;
}

By pointer — modifies the original

void move(Point *p, int dx, int dy) {
    p->x += dx;   /* arrow operator: (*p).x */
    p->y += dy;
}

int main() {
    Point p = {10, 20};
    move(&p, 5, 5);
    printf("(%d, %d)n", p.x, p.y);   /* (15, 25) */
    return 0;
}

The arrow operator -> is shorthand for dereferencing and accessing a member: p->x is the same as (*p).x. Pass by pointer when the struct is large (avoids copying) or when you need to modify the original.

Nested Structs

#include <stdio.h>

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

typedef struct {
    Point top_left;
    Point bottom_right;
} Rectangle;

int main() {
    Rectangle r = {{0, 10}, {20, 0}};
    printf("Top-left: (%d, %d)n", r.top_left.x, r.top_left.y);
    printf("Width: %dn", r.bottom_right.x - r.top_left.x);
    return 0;
}

Memory Padding and Alignment

The compiler adds padding bytes between members to ensure each field is aligned to its natural boundary. As covered in our article on sizeof edge cases, this means the struct size is often larger than the sum of member sizes:

#include <stdio.h>

typedef struct {
    char  a;   /* 1 byte, then 3 bytes padding */
    int   b;   /* 4 bytes */
    char  c;   /* 1 byte, then 3 bytes padding */
} BadLayout;   /* sizeof = 12 */

typedef struct {
    int   b;   /* 4 bytes */
    char  a;   /* 1 byte */
    char  c;   /* 1 byte, then 2 bytes padding */
} GoodLayout;  /* sizeof = 8 */

int main() {
    printf("BadLayout:  %zu bytesn", sizeof(BadLayout));
    printf("GoodLayout: %zu bytesn", sizeof(GoodLayout));
    return 0;
}

Order members from largest to smallest type to minimise padding. For embedded systems where size matters critically, use __attribute__((packed)) (GCC-specific) to eliminate all padding — at a performance cost.

Arrays of Structs

#include <stdio.h>
#include <stdlib.h>

typedef struct {
    char name[32];
    int  score;
} Student;

int main() {
    int n = 3;
    Student students[3] = {
        {"Alice", 92},
        {"Bob",   78},
        {"Carol", 85}
    };

    for (int i = 0; i < n; i++) {
        printf("%-10s %dn", students[i].name, students[i].score);
    }

    return 0;
}

For a dynamic number of students, allocate the array on the heap:

Student *students = malloc(n * sizeof(Student));

Initialising Structs

/* Zero-initialise the entire struct */
Student s = {0};

/* Named field initialisation (C99) */
Student s = { .name = "Alice", .score = 92 };

/* Copy one struct to another */
Student copy = s;   /* all fields are copied */

Zero-initialising with = {0} sets all members to their zero value (0 for numbers, NULL for pointers, ” for chars). This is cleaner than calling memset.

TL;DR

  • Use typedef struct { ... } Name; to avoid writing struct Name everywhere
  • Pass by pointer (Point*) when you need to modify the original or when the struct is large
  • Use -> to access members through a pointer: p->x instead of (*p).x
  • Order members largest-to-smallest to reduce padding
  • Use = {0} to zero-initialise a struct safely
  • Self-referential structs (linked list nodes) need the tag name, not the typedef alias