C Language Structures: From Beginner to Advanced

C Language Structures (struct) From Beginner to Advanced

1) What is a Structure?

A structure (<span>struct</span>) “packages” different types of logically related data into a whole, making it easier to pass, store, and manage together. Unlike arrays that can only hold “elements of the same type”, structures can hold “members of different types”.

2) Definition, Declaration, and Initialization

2.1 Basic Definition and Usage

#include <stdio.h>

struct Point {
    int x;
    int y;
};

int main(void) {
    struct Point p1;         // Declare variable
    p1.x = 10;
    p1.y = 20;
    printf("p1 = (%d, %d)\n", p1.x, p1.y);

    // Compound literal initialization (since C99)
    struct Point p2 = { .x = 3, .y = 4 }; // Designated initializer
    printf("p2 = (%d, %d)\n", p2.x, p2.y);
    return 0;
}

2.2 <span>typedef</span> Simplifying Syntax

typedef struct Point {
    int x;
    int y;
} Point; // Now you can use Point directly without writing struct Point

Point p = {1, 2};

Convention: Keep the <span>struct</span> tag name in public APIs (for forward declaration), while also providing a <span>typedef</span> alias.

2.3 Nested Structures

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

typedef struct {
    char name[32];
    Date birthday;   // Nested
} Person;

Person a = { "Alice", {1990, 5, 21} };

3) Member Access: Dot and Arrow Operators

  • Access with variables using <span>.</span>
  • Access with pointers using <span>-></span>
typedef struct { int x, y; } Point;

void move(Point *p, int dx, int dy) {
    p->x += dx;   // Pointer → use ->
    p->y += dy;
}

4) Structures as Function Parameters/Return Values

4.1 Pass by Value (Copies a Copy)

typedef struct { int w, h; } Size;

int area(Size s) { return s.w * s.h; } // s is a copy

Suitable for small structures (like 2-3 scalars). Passing large structures by value frequently incurscopy overhead.

4.2 Pass by Pointer (No Copy, Can Modify Actual Parameter)

void normalize(Size *s) {
    if (s->w < 0) s->w = -s->w;
    if (s->h < 0) s->h = -s->h;
}

4.3 Return Structures (Allowed in C)

typedef struct { int x, y; } Point;
Point make_point(int x, int y) { return (Point){x, y}; }

Modern compiler optimizations (RVO) are mature, returning small structures is usually efficient.

5) Arrays and Pointers in Structures

5.1 Fixed Size Array Members

typedef struct {
    char name[32]; // Fixed upper limit, simple and safe
    int  score;
} Student;

5.2 Pointer Members (Require Separate Allocation and Deallocation)

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

typedef struct {
    char *name; // Points to a string on the heap
    int score;
} Student2;

Student2 make_student2(const char *s, int score) {
    Student2 st = {0};
    st.score = score;
    st.name = malloc(strlen(s) + 1);
    strcpy(st.name, s);
    return st; // Returns a shallow copy (pointer value is copied)
}

void free_student2(Student2 *st) {
    free(st->name);
    st->name = NULL;
}

Shallow Copy vs Deep Copy: Assigning structures by value only copies the “pointer value”, not the content pointed to by the pointer. When deep copy is needed,allocate and copy yourself.

6) Alignment and Memory Layout: <span>sizeof</span>, Padding

The compiler will insert padding bytes between members to meet platform alignment requirements. This will affect <span>sizeof</span> and binary read/write.

#include <stdio.h>
typedef struct {
    char  c; // 1 byte
    int   i; // 4 bytes (may need alignment)
    char  d; // 1 byte
} S;

int main(void) {
    printf("sizeof(S) = %zu\n", sizeof(S)); // May be 12 (not 6)
}

6.1 Member Reordering to Reduce Memory Waste

Place large, highly aligned members first, which usually reduces padding:

typedef struct {
    int   i;
    char  c;
    char  d;
} S2; // Commonly more compact than S

It is not recommended to directly use <span>fwrite/fread</span> to dump structures across platforms/networks/files: affected by alignment, padding, and endianness. A protocol format should be defined and fields serialized.

7) Designated Initializers (C99)

typedef struct { int r, g, b; } RGB;
RGB red = {.r = 255, .g = 0, .b = 0};

High readability; safer when field order changes.

8) Anonymous Structures and Compound Literals

8.1 Anonymous Structures (Available in some compilers/standards, C11 supports anonymous members in union nested scenarios)

More commonly, one-time use compound literals:

typedef struct { int x, y; } Point;

void print_point(Point p);

print_point((Point){.x = 7, .y = 9}); // Compound literal

9) Bit-Fields: Save Flag Bits/Hardware Register Mapping

#include <stdint.h>
typedef struct {
    unsigned ready : 1;
    unsigned error : 1;
    unsigned mode  : 2; // 0~3
    unsigned resv  : 4; // Reserved bits to fill 1 byte
} Status8;

Note:

  • The layout of bit-fields (starting from high/low bits) is implementation-dependent, different compilers/platforms may vary.
  • Not suitable for cross-platform persistence or network protocols; more suitable for in-process space saving or mapping specific compiler + architecture registers.

10) Structures + Unions: Parsing Different Views of the Same Memory Segment

#include <stdint.h>

typedef union {
    uint32_t u32;
    struct {
        unsigned a : 3;
        unsigned b : 5;
        unsigned c : 8;
        unsigned d : 16;
    } bits; // View different fields in the same 32 bits
} Reg;

Still remember: Bit-field layout and endianness issues make it non-portable. For parsing protocols, it is recommended to manually shift and mask.

11) Flexible Array Members (C99)

The official way for variable-length structures: Define the last member as an indefinite length array <span>[]</span>, then allocate “structure header + data” at once.

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

typedef struct {
    size_t len;
    char   data[]; // Flexible array member, must be the last
} Blob;

Blob* blob_new(const void *src, size_t n) {
    Blob *b = malloc(sizeof(Blob) + n);
    if (!b) return NULL;
    b->len = n;
    memcpy(b->data, src, n);
    return b;
}

int main(void) {
    const char *msg = "hello";
    Blob *b = blob_new(msg, 6); // Includes '\0'
    printf("len=%zu, data=%s\n", b->len, b->data);
    free(b);
}

12) Opaque Handles: Hiding Implementation, Stable ABI

Header File (api.h)

// Forward declaration: Do not expose internal members
typedef struct Image Image;

Image* image_create(int w, int h);
void   image_destroy(Image*);
int    image_width(const Image*);
int    image_height(const Image*);

Implementation File (image.c)

#include <stdlib.h>
struct Image {
    int w, h;
    unsigned char *pixels; // Private implementation details
};

Image* image_create(int w, int h) {
    Image *img = malloc(sizeof *img);
    img->w = w; img->h = h;
    img->pixels = calloc(w*h, 1);
    return img;
}
void image_destroy(Image* img){ free(img->pixels); free(img); }
int  image_width(const Image* img){ return img->w; }
int  image_height(const Image* img){ return img->h; }

Using the forward declaration + exposing only pointers method, the caller receives a “handle”, allowing the implementation to be modified while keeping the binary interface stable.

13) Function Pointer Members: Simulating “Methods” or Callbacks

#include <stdio.h>

typedef struct {
    int  value;
    void (*on_change)(int new_value); // Callback
} Gauge;

void gauge_set(Gauge *g, int v) {
    g->value = v;
    if (g->on_change) g->on_change(v);
}

void handler(int v) { printf("value changed: %d\n", v); }

int main(void) {
    Gauge g = {.value=0, .on_change=handler};
    gauge_set(&g, 42);
}

14) Linked Lists/Trees: Organizing Complex Data Structures with Structures

14.1 Singly Linked List

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

typedef struct Node {
    int value;
    struct Node *next; // Self-reference
} Node;

Node* push_front(Node* head, int v) {
    Node *n = malloc(sizeof *n);
    n->value = v;
    n->next  = head;
    return n;
}

void free_list(Node* head){
    while (head) { Node* t = head; head = head->next; free(t); }
}

int main(void) {
    Node *head = NULL;
    head = push_front(head, 3);
    head = push_front(head, 2);
    head = push_front(head, 1);
    for (Node* p=head; p; p=p->next) printf("%d ", p->value);
    free_list(head);
}

15) <span>const</span> and Read-Only Semantics

  • <span>const Struct *p</span>: The data pointed to is read-only, but the pointer itself can be changed to point elsewhere.
  • <span>Struct * const p</span>: The pointer cannot be changed, but the data it points to can be modified.
  • <span>const Struct * const p</span>: Neither can be changed.
typedef struct { int x, y; } Point;
void foo(const Point *p) { /* p->x = 1; // ❌ Cannot modify */ }

16) Comparing Structures: Cannot Use <span>==</span>

typedef struct { int x,y; } P;

int equal_p(P a, P b) {
    return a.x == b.x && a.y == b.y;
}

Do not use <span>memcmp(&a, &b, sizeof a)</span> to compare: because padding’s uninitialized bytes may differ.

17) <span>offsetof</span> and “Container Structure” Technique

Common in kernels/embedded: Reverse the member address to infer the outer structure address.

#include <stddef.h> // offsetof

typedef struct {
    int   id;
    char  name[32];
} User;

size_t off = offsetof(User, name); // Offset of name relative to the start of the structure

Combined with custom macros, it can achieve the technique of “restoring the parent structure pointer from the member pointer” (container_of).

18) <span>static</span>, <span>inline</span> and Structures (Brief)

  • <span>static</span> modifies global structure variables → only visible in the current translation unit (internal linkage).
  • <span>inline</span> functionsoperating on structures are common in performance-sensitive paths, reducing call overhead (whether truly inlined is determined by the compiler).

19) Interaction with Files/Networks (Serialization)

Do not directly <span>fwrite(&obj, sizeof obj, 1, fp)</span> to exchange between different platforms/compilers. The correct approach: Define fixed sizes and endianness for each field, and encode/decode field by field.

20) Practical Example Collection

20.1 Geometry: Vectors and Rectangles

#include <stdio.h>

typedef struct {double x, y; } Vec2;
typedef struct { Vec2 min, max; } Rect;

Vec2 vec2_add(Vec2 a, Vec2 b){ return (Vec2){a.x+b.x, a.y+b.y}; }
double rect_area(Rect r){
    double w = r.max.x - r.min.x, h = r.max.y - r.min.y;
    return (w>0 && h>0) ? w*h : 0.0;
}

int main(void){
    Rect r = {{0,0},{3,4}};
    printf("area = %.1f\n", rect_area(r));
}

20.2 Status Flags (Bit-Fields)

#include <stdio.h>
typedef struct {
    unsigned connected : 1;
    unsigned busy      : 1;
    unsigned error     : 1;
    unsigned retry     : 3; // 0..7
    unsigned reserved  : 2;
} Flags;

int main(void){
    Flags f = { .connected=1, .busy=0, .error=0, .retry=5 };
    printf("connected=%u retry=%u\n", f.connected, f.retry);
}

20.3 Using Flexible Array Members to Pack Messages

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

typedef struct {
    uint16_t type;
    uint16_t len;   // Length of data
    uint8_t  data[]; // Variable length
} Packet;

Packet* packet_new(uint16_t type, const void *payload, uint16_t len){
    Packet *p = malloc(sizeof *p + len);
    p->type = type; p->len = len;
    memcpy(p->data, payload, len);
    return p;
}

int main(void){
    const char *msg = "PING";
    Packet *p = packet_new(1, msg, 5);
    printf("type=%u, len=%u, first=%c\n", p->type, p->len, p->data[0]);
    free(p);
}

20.4 Function Pointers for Polymorphism (Mini “Interface”)

#include <stdio.h>

typedef struct Shape Shape;

struct Shape {
    double (*area)(const Shape*);
};

typedef struct {
    Shape base; // “Inheritance”
    double r;
} Circle;

double circle_area(const Shape *s){
    const Circle *c = (const Circle*)s;
    return 3.14159 * c->r * c->r;
}

int main(void){
    Circle c = { .base = { .area = circle_area }, .r = 2.0 };
    printf("circle area = %.2f\n", c.base.area((Shape*)&c));
}

21) Common Pitfalls & Best Practices Checklist

  1. Initialization: Reading uninitialized local structure variables is undefined behavior. Use designated initializers or zeroing:<span>Struct s = {0};</span>
  2. Copy: When containing pointer members, “assigning by value” is just a shallow copy, do not forget deep copy or custom copy functions.
  3. Comparison: Do not use <span>memcmp</span> to compare structures (padding issues), compare field by field.
  4. Alignment/Packing: <span>#pragma pack</span>/<span>__attribute__((packed))</span> should only be used when truly necessary and for external binary compatibility, as it may bring performance/unaligned access risks.
  5. ABI Stability: Structures exported by libraries should maintain binary compatibility; it is more recommended to use opaque handles to hide implementations.
  6. Cross-Platform Serialization: Do not directly write the entire struct; clarify endianness, bit width, and encode/decode field by field.
  7. Flexible Array Members must be the last field, and the allocated size should include “header + data”.
  8. Bit-Fields are non-portable, use sparingly for persistence/network protocols.
  9. Thread Safety: Pack related state into a struct, manage it with mutexes (you can also put the lock in the struct).
  10. Const Correctness: If the interface does not modify input parameters, use <span>const Struct*</span> to enhance readability and safety.

22) Comprehensive Example: Simple Student Table (Add/Retrieve/Save)

Demonstration: Fixed array members + pointer array + file text serialization (to avoid binary layout issues).

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

#define MAX_NAME 32
#define INIT_CAP 16

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

typedef struct {
    Student *data;
    size_t   size;
    size_t   cap;
} StudentTable;

void st_init(StudentTable *t){
    t->data = malloc(sizeof(Student) * INIT_CAP);
    t->size = 0;
    t->cap  = INIT_CAP;
}

void st_free(StudentTable *t){ free(t->data); t->data=NULL; t->size=t->cap=0; }

void st_push(StudentTable *t, Student s){
    if (t->size == t->cap){
        t->cap *= 2;
        t->data = realloc(t->data, sizeof(Student) * t->cap);
    }
    t->data[t->size++] = s; // Copy by value (safe: no heap pointers)
}

Student* st_find_by_id(StudentTable *t, int id){
    for (size_t i=0;i<t->size;i++) if (t->data[i].id==id) return &t->data[i];
    return NULL;
}

int st_save_txt(StudentTable *t, const char *path){
    FILE *fp = fopen(path, "w");
    if (!fp) return -1;
    for (size_t i=0;i<t->size;i++){
        // Text serialization: clear fields, delimiters
        fprintf(fp, "%d,%s,%d\n", t->data[i].id, t->data[i].name, t->data[i].score);
    }
    fclose(fp);
    return 0;
}

int main(void){
    StudentTable t; st_init(&t);
    st_push(&t, (Student){1,"Alice", 95});
    st_push(&t, (Student){2,"Bob",   88});

    Student *p = st_find_by_id(&t, 2);
    if (p) p->score = 90;

    st_save_txt(&t, "students.csv");
    st_free(&t);
}

Leave a Comment