Skip to main content
C intermediate Lesson 12 of 23

Structs in C

Learn struct definition, typedef, nested structs, struct pointers, and bit fields for efficient data packing.

Defining and Using Structs

A struct groups related variables of different types under a single name. This is how C creates custom data types — instead of passing five separate variables representing a point, you define a Point struct and pass one. Structs make code self-documenting and easier to maintain.

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

/* Struct definition — creates a new type */
struct Point {
    double x;
    double y;
};

/* Nested struct — a Rectangle is defined in terms of two Points */
struct Rectangle {
    struct Point top_left;
    struct Point bottom_right;
};

double rect_area(struct Rectangle r) {
    double width  = r.bottom_right.x - r.top_left.x;
    double height = r.bottom_right.y - r.top_left.y;
    return width * height;
}

int main(void) {
    /* Initialize with brace syntax — members in declaration order */
    struct Point p = {3.0, 4.0};
    printf("Point: (%.1f, %.1f)\n", p.x, p.y);

    /* Designated initializers (C99) — by member name, order doesn't matter */
    struct Rectangle rect = {
        .top_left     = {.x = 0.0, .y = 10.0},
        .bottom_right = {.x = 5.0, .y = 0.0}
    };
    printf("Area: %.1f\n", rect_area(rect));   /* 50.0 */

    /* Assign members individually after declaration */
    struct Point q;
    q.x = 7.0;
    q.y = -2.0;

    return 0;
}

typedef with Structs

typedef creates an alias for a type, eliminating the need to write struct before every use. This is common in application code and makes struct-heavy code much more readable. For self-referential structs (like linked list nodes), you must keep the tag name so the struct can refer to itself.

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

/* Define the struct and typedef in one step — no tag needed */
typedef struct {
    char   name[64];
    int    age;
    double salary;
} Employee;

/* Keep the tag for self-referential structs — 'Node' isn't fully defined yet */
typedef struct Node {
    int          value;
    struct Node *next;   /* must use 'struct Node' here, not just 'Node' */
} Node;

Employee make_employee(const char *name, int age, double salary) {
    Employee e;
    strncpy(e.name, name, sizeof(e.name) - 1);
    e.name[sizeof(e.name) - 1] = '\0';
    e.age    = age;
    e.salary = salary;
    return e;
}

void print_employee(const Employee *e) {
    printf("Name: %-20s Age: %3d Salary: $%.2f\n",
           e->name, e->age, e->salary);
}

int main(void) {
    Employee staff[] = {
        make_employee("Alice Johnson", 32, 95000.0),
        make_employee("Bob Smith",     28, 72000.0),
        make_employee("Carol Davis",   45, 120000.0),
    };

    int n = sizeof(staff) / sizeof(staff[0]);
    for (int i = 0; i < n; i++) {
        print_employee(&staff[i]);
    }

    return 0;
}

Struct Pointers and the Arrow Operator

Passing large structs by value copies all their bytes — expensive for big structs. Instead, pass a pointer to the struct. The arrow operator -> is syntactic sugar for dereferencing a pointer and accessing a member: p->name means exactly the same as (*p).name, but is much cleaner to read.

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

typedef struct {
    char  title[128];
    char  author[64];
    int   year;
    float rating;
} Book;

/* Pointer parameter: no copy, and the function can modify the struct */
void update_rating(Book *b, float new_rating) {
    b->rating = new_rating;   /* equivalent to (*b).rating = new_rating */
}

/* Allocate a Book on the heap and return a pointer to it */
Book *create_book(const char *title, const char *author, int year) {
    Book *b = malloc(sizeof(Book));
    if (!b) return NULL;

    strncpy(b->title,  title,  sizeof(b->title)  - 1);
    strncpy(b->author, author, sizeof(b->author) - 1);
    b->title[sizeof(b->title) - 1]   = '\0';
    b->author[sizeof(b->author) - 1] = '\0';
    b->year   = year;
    b->rating = 0.0f;
    return b;
}

int main(void) {
    Book *b = create_book("The C Programming Language", "Kernighan & Ritchie", 1978);
    if (!b) return 1;

    update_rating(b, 4.9f);
    printf("'%s' by %s (%d) — Rating: %.1f\n",
           b->title, b->author, b->year, b->rating);

    free(b);
    return 0;
}

Nested Structs and Arrays of Structs

Structs can contain other structs, and you can create arrays of structs. These compose naturally and let you model real-world entities like a person with a name, a birthday, and contact details.

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

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

typedef struct {
    char first[32];
    char last[32];
} Name;

typedef struct {
    Name   name;
    Date   birthday;
    char   email[128];
} Person;

void print_person(const Person *p) {
    printf("%s %s | Born: %02d/%02d/%d | %s\n",
           p->name.first, p->name.last,
           p->birthday.day, p->birthday.month, p->birthday.year,
           p->email);
}

int main(void) {
    Person people[] = {
        {{"Alice", "Smith"},   {15, 3, 1990}, "alice@example.com"},
        {{"Bob",   "Johnson"}, {22, 7, 1985}, "bob@example.com"},
    };

    for (size_t i = 0; i < sizeof(people)/sizeof(people[0]); i++) {
        print_person(&people[i]);
    }

    return 0;
}

Struct Padding and Size

The compiler inserts invisible padding bytes between struct members to satisfy alignment requirements — a 4-byte int must start at a 4-byte-aligned address. This means sizeof(struct) can be larger than the sum of its member sizes. Ordering members from largest to smallest alignment minimizes padding and reduces memory usage.

#include <stdio.h>
#include <stddef.h>

/* Poorly ordered — wastes memory due to padding */
struct Padded {
    char   a;     /* 1 byte */
    /* 3 bytes padding — int must be 4-byte aligned */
    int    b;     /* 4 bytes */
    char   c;     /* 1 byte */
    /* 7 bytes padding — double must be 8-byte aligned */
    double d;     /* 8 bytes */
};  /* total: 24 bytes — wastes 10 bytes */

/* Well ordered — largest-first minimizes padding */
struct Packed {
    double d;     /* 8 bytes */
    int    b;     /* 4 bytes */
    char   a;     /* 1 byte */
    char   c;     /* 1 byte */
    /* 2 bytes padding — struct size must be multiple of largest alignment */
};  /* total: 16 bytes — saves 8 bytes */

int main(void) {
    printf("Padded: %zu bytes\n", sizeof(struct Padded));   /* 24 */
    printf("Packed: %zu bytes\n", sizeof(struct Packed));   /* 16 */

    /* offsetof — exact byte offset of a member within the struct */
    printf("Offset of b in Padded: %zu\n", offsetof(struct Padded, b));  /* 4 */
    printf("Offset of d in Padded: %zu\n", offsetof(struct Padded, d));  /* 16 */

    return 0;
}

Order struct members from largest to smallest alignment to minimize padding. This matters when you have millions of struct instances.

Bit Fields

Bit fields let you pack multiple small integer values into a single word, specifying exactly how many bits each field occupies. They are commonly used for hardware register mapping and network protocol headers where every bit has a defined meaning.

#include <stdio.h>

/* Hardware status register mapped to exactly 1 byte */
typedef struct {
    unsigned ready   : 1;   /* 1 bit — device ready */
    unsigned busy    : 1;   /* 1 bit — device busy */
    unsigned error   : 1;   /* 1 bit — error occurred */
    unsigned timeout : 1;   /* 1 bit — operation timed out */
    unsigned mode    : 2;   /* 2 bits — operating mode (values 0-3) */
    unsigned         : 2;   /* 2 bits padding/reserved — unnamed */
} StatusReg;

/* IPv4 header fields (simplified) */
typedef struct {
    unsigned version  : 4;   /* IP version (always 4) */
    unsigned ihl      : 4;   /* header length in 32-bit words */
    unsigned dscp     : 6;   /* differentiated services code point */
    unsigned ecn      : 2;   /* explicit congestion notification */
    unsigned          : 16;  /* total length (use uint16_t in practice) */
} IPHeader;

int main(void) {
    StatusReg reg = {0};
    reg.ready = 1;
    reg.mode  = 3;   /* binary 11 — maximum mode value for a 2-bit field */

    printf("ready=%u, busy=%u, mode=%u\n",
           reg.ready, reg.busy, reg.mode);
    printf("sizeof StatusReg: %zu bytes\n", sizeof(StatusReg));  /* typically 1 */

    return 0;
}

Bit fields are commonly used for hardware register mapping and protocol headers. Note that the exact layout (endianness, padding) is implementation-defined, so they are not always portable across compilers or architectures.

Frequently Asked Questions

What is struct padding and why does it matter?
Compilers insert padding bytes between struct members to align them on natural boundaries (e.g., a 4-byte int is placed at a 4-byte-aligned address). This means sizeof(struct) can be larger than the sum of its members. Order members from largest to smallest to minimize wasted space.
Should I use typedef with structs?
It's a matter of style. typedef hides the 'struct' keyword, making code shorter. The Linux kernel avoids typedef for structs to keep types explicit. Many application codebases use typedef. Be consistent within a project.
What is the -> operator?
The arrow operator dereferences a pointer to a struct and accesses a member. p->member is exactly equivalent to (*p).member but more readable.