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 usememcmp, butmemcmpcan 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
strncpywithout null-terminating. If the source string fills the buffer exactly,strncpydoes 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
offsetofandsizeofto inspect layout. - Pass small structs by value, large structs by
constpointer. - Nested structs model hierarchical data. Anonymous structs (C11) flatten access to inner fields.
- Always initialize structs. Never compare them with
==.