4 min read
On this page

Structs

What Is a Struct?

A struct is a composite data type that groups related variables under a single name. Each variable inside a struct is called a field or member.

struct Point {
    double x;
    double y;
};

This defines a new type struct Point with two double fields. You can then declare variables of this type:

struct Point origin;
origin.x = 0.0;
origin.y = 0.0;

Field Access: Dot & Arrow Operators

Use the dot operator (.) to access fields on a struct variable, and the arrow operator (->) to access fields through a pointer to a struct.

#include <stdio.h>

struct Rectangle {
    double width;
    double height;
};

void print_area(const struct Rectangle *r) {
    double area = r->width * r->height;
    printf("Area: %.2f\n", area);
}

int main(void) {
    struct Rectangle box;
    box.width = 5.0;
    box.height = 3.0;

    print_area(&box);

    struct Rectangle *ptr = &box;
    printf("Width via pointer: %.2f\n", ptr->width);

    return 0;
}
Area: 15.00
Width via pointer: 5.00

The arrow operator ptr->width is equivalent to (*ptr).width. The arrow form is preferred because it is easier to read.

Struct Initialization

Basic Initialization

struct Point p1 = {1.0, 2.0};

Fields are assigned in declaration order. If you provide fewer values than fields, the remaining fields are zero-initialized.

Designated Initializers (C99)

Designated initializers let you name the fields explicitly:

struct Point p2 = {.y = 5.0, .x = 3.0};

This is clearer and order-independent. It also makes code resilient to future field additions.

struct Config {
    int timeout_seconds;
    int max_retries;
    int port;
    int verbose;
};

struct Config cfg = {
    .port = 8080,
    .timeout_seconds = 30,
    .max_retries = 3,
    .verbose = 0,
};

Unspecified fields are zero-initialized. This pattern is common in real-world C libraries for configuration structs.

Struct Padding & Alignment

The compiler inserts padding bytes between fields to satisfy alignment requirements. This means sizeof(struct) is often larger than the sum of field sizes.

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

struct Padded {
    char a;     /* 1 byte */
    int b;      /* 4 bytes */
    char c;     /* 1 byte */
};

int main(void) {
    printf("sizeof(struct Padded): %zu\n", sizeof(struct Padded));
    printf("Offset of a: %zu\n", offsetof(struct Padded, a));
    printf("Offset of b: %zu\n", offsetof(struct Padded, b));
    printf("Offset of c: %zu\n", offsetof(struct Padded, c));
    return 0;
}
sizeof(struct Padded): 12
Offset of a: 0
Offset of b: 4
Offset of c: 8

The fields sum to 6 bytes, but the struct occupies 12. The compiler added 3 bytes of padding after a to align b to a 4-byte boundary, and 3 bytes of trailing padding after c so arrays of this struct stay aligned.

Reordering fields by decreasing size reduces padding:

struct Compact {
    int b;      /* 4 bytes */
    char a;     /* 1 byte */
    char c;     /* 1 byte */
};
/* sizeof(struct Compact) is typically 8 */

Packed Structs

When you need the exact layout with no padding, use __attribute__((packed)):

struct __attribute__((packed)) NetworkHeader {
    uint8_t  version;
    uint32_t length;
    uint16_t checksum;
};
/* sizeof is exactly 7 */

Packed structs are useful for matching binary protocols or file formats, but accessing misaligned fields can be slower or cause faults on some architectures.

Passing Structs: By Value vs By Pointer

By Value (Copy)

When you pass a struct to a function by value, the entire struct is copied:

struct Point translate(struct Point p, double dx, double dy) {
    p.x += dx;
    p.y += dy;
    return p;
}

The caller's original struct is not modified. This is safe but expensive for large structs.

By Pointer (Reference)

Passing a pointer avoids the copy:

void translate_in_place(struct Point *p, double dx, double dy) {
    p->x += dx;
    p->y += dy;
}

Use const when the function should not modify the struct:

double distance(const struct Point *a, const struct Point *b) {
    double dx = a->x - b->x;
    double dy = a->y - b->y;
    return sqrt(dx * dx + dy * dy);
}

As a rule of thumb: pass small structs (a few words) by value, and larger structs by pointer.

Nested Structs

Structs can contain other structs:

struct Address {
    char street[100];
    char city[50];
    char state[3];
    int zip;
};

struct Employee {
    char name[100];
    int id;
    struct Address home_address;
    struct Address work_address;
};

void print_employee(const struct Employee *e) {
    printf("Name: %s\n", e->name);
    printf("Home city: %s\n", e->home_address.city);
    printf("Work city: %s\n", e->work_address.city);
}

Access nested fields by chaining the dot or arrow operators. Through a pointer, the first access uses -> and subsequent accesses use ..

Anonymous Structs (C11)

Anonymous structs let you embed fields directly without naming the inner struct:

struct Vector3 {
    union {
        struct {
            float x;
            float y;
            float z;
        };
        float components[3];
    };
};

int main(void) {
    struct Vector3 v = {.x = 1.0f, .y = 2.0f, .z = 3.0f};
    printf("z = %f\n", v.z);
    printf("z = %f\n", v.components[2]);
    return 0;
}

The fields x, y, z are accessible directly on v rather than through an intermediate name. This is commonly used in graphics and math libraries.

Real-World Example: A Simple Database Record

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

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

struct Customer {
    int id;
    char name[128];
    char email[256];
    struct Date signup_date;
    int active;
};

struct Customer new_customer(int id, const char *name, const char *email,
                             struct Date signup) {
    struct Customer c = {
        .id = id,
        .signup_date = signup,
        .active = 1,
    };
    strncpy(c.name, name, sizeof(c.name) - 1);
    strncpy(c.email, email, sizeof(c.email) - 1);
    return c;
}

void print_customer(const struct Customer *c) {
    printf("Customer #%d: %s <%s>\n", c->id, c->name, c->email);
    printf("Signed up: %04d-%02d-%02d\n",
           c->signup_date.year, c->signup_date.month, c->signup_date.day);
    printf("Status: %s\n", c->active ? "active" : "inactive");
}

int main(void) {
    struct Date today = {.year = 2026, .month = 4, .day = 18};
    struct Customer c = new_customer(1001, "Alice", "alice@example.com", today);
    print_customer(&c);
    return 0;
}
Customer #1001: Alice <alice@example.com>
Signed up: 2026-04-18
Status: active

Common Pitfalls

  • Forgetting padding exists. Writing a struct directly to a file or network socket will include padding bytes, which may not match the expected format. Use packed structs or serialize field by field.
  • Comparing structs with ==. C does not support == on structs. You must compare field by field or use memcmp, but memcmp can give false negatives because padding bytes may contain garbage.
  • Returning large structs by value. This copies the entire struct. For structs over a few dozen bytes, prefer passing a pointer to a pre-allocated result.
  • Uninitialized struct fields. Local struct variables without initializers contain garbage. Always initialize with = {0} or designated initializers.
  • Using strncpy without null-terminating. If the source string fills the buffer exactly, strncpy does not add a null terminator. Zero-initialize the struct or manually null-terminate.

Key Takeaways

  • A struct groups related data under one name. Access fields with . (direct) or -> (through a pointer).
  • Designated initializers make initialization clear and order-independent.
  • Compilers add padding for alignment; use offsetof and sizeof to inspect layout.
  • Pass small structs by value, large structs by const pointer.
  • Nested structs model hierarchical data. Anonymous structs (C11) flatten access to inner fields.
  • Always initialize structs. Never compare them with ==.