thecodex.expert · The Codex Family of Knowledge
C

Structs and Arrays

C's only data grouping mechanism and its raw sequence type — the foundation of every complex data structure in systems software.

C17 / C23 Zero overhead Last verified:
Canonical Definition

A C struct groups named fields of different types into a single composite type. Arrays are fixed-size contiguous sequences of a single type, and decay to a pointer to their first element in most expressions. Strings in C are null-terminated char arrays — the length is not stored, just a sentinel zero byte. Function pointers enable callbacks and polymorphism-like dispatch patterns.

The central idea

C has no classes. When you need to group related data — a person's name and age, a network packet's header and payload — you use a struct. When you need a sequence of the same type, you use an array. These two constructs, combined with pointers, build every data structure in C: linked lists, trees, hash tables, queues.

Structs: definition and usage

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

/* struct definition */
typedef struct {
    char name[64];
    int age;
    double balance;
} Account;   /* typedef lets us use Account instead of struct Account */

/* Functions that operate on structs receive a pointer — avoids copying */
void deposit(Account *acc, double amount) {
    acc->balance += amount;   /* -> dereferences and accesses field */
    /* equivalent to: (*acc).balance += amount */
}

void print_account(const Account *acc) {   /* const: promise not to modify */
    printf("Name: %s, Age: %d, Balance: %.2f\n",
           acc->name, acc->age, acc->balance);
}

int main(void) {
    /* Stack allocation: brace initialiser */
    Account alice = {"Alice", 30, 1000.00};
    deposit(&alice, 500.0);
    print_account(&alice);   /* Name: Alice, Age: 30, Balance: 1500.00 */

    /* Heap allocation */
    Account *bob = malloc(sizeof(Account));
    if (!bob) return 1;
    strncpy(bob->name, "Bob", sizeof(bob->name) - 1);
    bob->name[sizeof(bob->name) - 1] = '\0';   /* ensure null termination */
    bob->age = 25;
    bob->balance = 200.0;
    print_account(bob);
    free(bob);

    return 0;
}

Arrays and strings

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

int main(void) {
    /* Fixed-size array: size must be a compile-time constant (or C99 VLA) */
    int nums[5] = {10, 20, 30, 40, 50};
    printf("sizeof(nums) = %zu bytes\n", sizeof(nums));    /* 20 (5 * 4) */
    printf("element count = %zu\n", sizeof(nums) / sizeof(nums[0]));  /* 5 */

    /* Strings: null-terminated char arrays */
    char greeting[32] = "Hello";   /* {'H','e','l','l','o','\0', ...} */
    printf("strlen = %zu\n", strlen(greeting));   /* 5 — does NOT count '\0' */
    printf("sizeof = %zu\n", sizeof(greeting));   /* 32 — total buffer size */

    /* String functions — always use strncpy, not strcpy (bounds safety) */
    char buf[8];
    strncpy(buf, "Codex", sizeof(buf) - 1);
    buf[sizeof(buf) - 1] = '\0';   /* manual null termination */

    /* 2D array: row-major storage */
    int matrix[3][4] = {
        {1,  2,  3,  4},
        {5,  6,  7,  8},
        {9, 10, 11, 12}
    };
    printf("matrix[1][2] = %d\n", matrix[1][2]);  /* 7 */

    /* Array decay: passes as pointer — sizeof DOES NOT work in the function */
    /* Always pass the size separately: void process(int *arr, size_t n) */

    return 0;
}

Function pointers: callbacks and dispatch

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

/* Function pointer type: pointer to a function taking two ints, returning int */
typedef int (*CompareFunc)(const void *, const void *);

int compare_int_asc(const void *a, const void *b) {
    return (*(int*)a - *(int*)b);
}

int compare_int_desc(const void *a, const void *b) {
    return (*(int*)b - *(int*)a);
}

int main(void) {
    int arr[] = {5, 2, 8, 1, 9, 3};
    int n = sizeof(arr) / sizeof(arr[0]);

    /* qsort accepts a function pointer — the classic C callback */
    qsort(arr, n, sizeof(int), compare_int_asc);
    for (int i = 0; i < n; i++) printf("%d ", arr[i]);
    printf("\n");   /* 1 2 3 5 8 9 */

    qsort(arr, n, sizeof(int), compare_int_desc);
    for (int i = 0; i < n; i++) printf("%d ", arr[i]);
    printf("\n");   /* 9 8 5 3 2 1 */

    /* Storing a function pointer in a variable */
    CompareFunc cmp = compare_int_asc;
    int a = 3, b = 5;
    printf("cmp result: %d\n", cmp(&a, &b));   /* negative: a < b */

    return 0;
}

Struct padding and memory layout

The C compiler inserts padding bytes between struct fields to ensure each field is aligned to its natural alignment (typically its size). A double (8 bytes) must start at an 8-byte boundary; an int (4 bytes) at a 4-byte boundary. The total struct size is padded to a multiple of its largest field's alignment. This means field order affects struct size — reordering fields to put large types first can significantly reduce padding.

Cpadding.c
#include <stdio.h>

/* Inefficient: 24 bytes due to padding */
struct Padded {
    char  a;      /* 1 byte + 7 padding */
    double b;     /* 8 bytes */
    char  c;      /* 1 byte + 7 padding */
};                /* total: 24 bytes */

/* Efficient: 16 bytes — largest fields first */
struct Packed {
    double b;     /* 8 bytes */
    char   a;     /* 1 byte */
    char   c;     /* 1 byte + 6 padding */
};                /* total: 16 bytes */

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

Linked lists: structs with self-referential pointers

C's only way to build dynamic data structures is through structs containing pointers to the same struct type. This self-referential pattern underlies linked lists, trees, graphs, and hash tables in C.

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

typedef struct Node {
    int value;
    struct Node *next;   /* must use struct Node here — typedef not yet complete */
} Node;

Node *node_new(int value) {
    Node *n = malloc(sizeof(Node));
    if (n) { n->value = value; n->next = NULL; }
    return n;
}

void list_print(const Node *head) {
    for (const Node *n = head; n != NULL; n = n->next)
        printf("%d -> ", n->value);
    printf("NULL\n");
}

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

int main(void) {
    Node *head = node_new(1);
    head->next = node_new(2);
    head->next->next = node_new(3);
    list_print(head);   /* 1 -> 2 -> 3 -> NULL */
    list_free(head);
    return 0;
}
Commonly confused
sizeof(array) vs sizeof(pointer) — the decay trap. Inside the function that declares int arr[5], sizeof(arr) is 20 (correct). Pass that array to another function and inside that function, sizeof(arr) is 4 or 8 (the pointer size — wrong). Arrays decay to pointers when passed to functions. Always pass the array length as a separate parameter: void process(int *arr, size_t n).
C strings have no length field — null terminator IS the length marker. strlen walks the string byte by byte counting until it hits \0. This makes it O(n). If you modify a string and forget the null terminator, strlen and most string functions will walk past the end of your buffer — buffer overread, undefined behaviour, potential crash or exploit. Always ensure \0 at the end when building strings manually.
struct assignment copies the entire struct. Account b = a copies every byte of a into b. This is a shallow copy — if the struct contains pointers, the pointers are copied but not the pointed-to data. Both a.name and b.name now point to the same memory (if name were a pointer). For structs with pointer fields, deep copy requires manually duplicating the pointed-to data.
How this connects
Requires first

Flexible array members and designated initialisers (C99+)

C99 introduced flexible array members — a struct's last field can be an array of unspecified size: struct Buffer { size_t len; char data[]; };. You allocate sizeof(Buffer) + n bytes and the data array uses the extra space. This is the pattern used for variable-length protocol messages in the Linux kernel and network stack. C99 also introduced designated initialisers: Account a = { .name = "Alice", .age = 30 } — field order in the initialiser doesn't need to match declaration order, and unspecified fields are zero-initialised. C23 further refined these.

vtable pattern: function pointer tables for polymorphism

C++'s vtable is just a struct of function pointers. The same pattern is used extensively in C: a struct Operations containing function pointers serves as a virtual dispatch table. The Linux kernel uses this pattern throughout — struct file_operations for the VFS, struct net_device_ops for network drivers. Each driver provides its own file_operations struct with its implementation of open, read, write, ioctl. The VFS calls through the function pointer — a callback that dispatches to the right driver implementation. This is C's mechanism for runtime polymorphism without classes.

Specification reference

ISO/IEC 9899:2018 (C17). Sections 6.2.5 (types), 6.7.2.1 (struct/union), 6.7.9 (initialisation). Kernighan, B. W. & Ritchie, D. M. (1988). The C Programming Language (2nd ed.). Chapter 6 (Structures). cppreference.com/w/c/language/struct.

Sources

1
ISO/IEC 9899:2018. C17 Standard. §6.7.2.1 Struct and union specifiers.
2
Kernighan, B. W. & Ritchie, D. M. (1988). The C Programming Language (2nd ed.). Chapter 6.
3
cppreference.com. C struct declaration. en.cppreference.com/w/c/language/struct.
4
Corbet, J. et al. Linux Device Drivers (3rd ed.). O'Reilly. — vtable pattern in the Linux kernel.
5
Seacord, R. C. (2013). Secure Coding in C and C++ (2nd ed.). Addison-Wesley. — Array and string safety.
Source confidence: High Last verified: Primary source: ISO/IEC 9899:2018 — C17 standard