Arrays, Structs, Unions

Years of primitives do not make software — compounds do. C groups values with arrays (same type, indexed), structs (named fields, possibly mixed types), unions (overlapping storage), and enums (named constants). This page builds those types and shows the layout decisions hidden behind them.

Arrays

An array stores N elements of one type in contiguous memory. The index starts at 0, and there is no bounds checking — writing past the end silently corrupts neighboring memory (the classic buffer overflow). Consequence: you pass and return the length yourself, everywhere.

#include <stdio.h>

int main(void) {
    int temps[7] = {15, 17, 16, 19, 21, 20, 18};  // fixed array, 7 ints
    temps[0] = 14;                    // valid: first element
    // temps[7] = 30;                 // BUG: out of bounds, silently corrupts!
    for (int i = 0; i < 7; i++) {
        printf("day %d: %d C\n", i + 1, temps[i]);
    }
    return 0;
}

Fetching the number of elements is a compile-time trick: sizeof(array) / sizeof(array[0]). It works while the array is a real array object (not a pointer — more on that in the pointers page):

int data[100];
size_t n = sizeof(data) / sizeof(data[0]);   // = 100, computed at compile time

Multidimensional Arrays

An int m[rows][cols] is an array of arrays, stored row-major: the entire first row is contiguous, then the second row. Locality matters — iterating in row order is many times faster than column order because of CPU caches.

#include <stdio.h>
#define ROWS 3
#define COLS 4

int main(void) {
    int grid[ROWS][COLS] = {0};        // zero-initialize every cell
    // fill with a nested loop writing in row-major (cache-friendly) order
    for (int r = 0; r < ROWS; r++)
        for (int c = 0; c < COLS; c++)
            grid[r][c] = r * COLS + c; // linear offset within the block
    printf("grid[1][2] = %d\n", grid[1][2]);   // = 6
    return 0;
}

Structs

A struct collects named fields of possibly different types into one object. Fields are accessed with the dot operator, and a whole struct can be assigned and passed by value — a copy of all bytes. Structs are C's only way to define your own data shapes.

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

struct Point {          // define a new type named struct Point
    int x;
    int y;
};

struct Account {        // fields can mix types freely
    char  name[32];     // fixed-size buffer (fixed length in the struct)
    double balance;
    int    active;
};

int main(void) {
    struct Point p = {3, 7};            // positional initialization
    struct Account a = {"ada", 1000.0, 1};
    p.x = 5;                            // mutate a field with '.' 
    a.balance += 250.0;                 // arithmetic on a field
    printf("p=(%d,%d) balance=%.2f\n", p.x, p.y, a.balance);
    return 0;
}

Alignment & Padding

The compiler may insert invisible padding bytes between struct fields so each field sits at an address its type requires (alignment). This makes access fast but means sizeof(struct) is often larger than the sum of field sizes — and field order changes the size. Network protocols and binary files must therefore define their own exact layout rather than relying on compiler layout.

Struct memory layout with alignment padding

Figure 1 — struct {char c; int i;} on a 4-byte-aligned platform: three padding bytes follow the char so the int starts on a multiple of four.

#include <stddef.h>   // offsetof: byte offset of a field
#include <stdio.h>

struct Packed {   // natural order, compiler decides padding
    char  c;
    int   i;
};

struct Manual {   // reordered: big fields first minimizes padding
    int   i;
    char  c;
};

int main(void) {
    printf("offset of i = %zu\n", offsetof(struct Packed, i));  // 4, not 1
    printf("size Packed  = %zu\n", sizeof(struct Packed));      // 8, not 5
    printf("size Manual  = %zu\n", sizeof(struct Manual));      // 8 too
    return 0;
}

Unions — Overlapping Storage

A union stores all its members at the same address: it is as big as its largest member, and writing one member reinterprets the same bytes as the others. That power is dangerous: reading the "wrong" member is technically implementation-defined. The modern, disciplined use is type punning with explicit tagging — and C23 provides a safe tagged variant. For beginners, treat unions as memory-saving devices where only one alternative is live at a time.

#include <stdio.h>

union Number {      // one storage area, many interpretations
    int   i;
    float f;
};

int main(void) {
    union Number n;
    n.i = 42;                 // write the int member
    printf("as int:  %d\n", n.i);     // 42
    n.f = 3.14f;              // same bytes now read as a float
    printf("as float:%f\n", n.f);     // 3.140000 — i's value is gone
    return 0;
}

Enums — Named Constants

An enum gives names to a sequence of integer constants, starting at 0 unless you say otherwise. It turns magic numbers into readable, self-checking code. Keep in mind enums are int-compatible: an enum value is just an int wearing a better name.

enum Color { RED, GREEN, BLUE };        // RED=0, GREEN=1, BLUE=2
enum Status { OK = 0, WARN = 1, ERROR = 2 };

int main(void) {
    enum Color c = BLUE;
    enum Status s = ERROR;
    printf("blue=%d status=%d\n", c, s);
    return 0;
}

Bit-Fields

A bit-field stores an integer member in a controlled number of bits — ideal for flags and packed hardware registers. Syntax is a colon with a width. Layout is compiler-defined, so keep bit-fields inside one struct for register mapping rather than portable files.

struct Flags {
    unsigned read   : 1;   // bit 0
    unsigned write  : 1;   // bit 1
    unsigned mode   : 2;   // bits 2-3
};

int main(void) {
    struct Flags f = {1, 0, 2};    // read=on, write=off, mode=2
    if (f.read) { /* the read flag is set */ }
    return 0;
}

typedef — Names for Types

typedef creates an alias for a type. Use it to shorten verbose spellings and to centralize a type choice that may change later — the code then names concepts, not declarations.

typedef unsigned long ulong;        // short alias
typedef struct Point Point;         // drop the "struct" keyword forever
typedef int (*Comparator)(int, int); // function-pointer type (advanced page)

int main(void) {
    ulong  big = 1UL;
    Point  p = {1, 2};   // same as: struct Point p = {1, 2};
    return 0;
}

Next: pointers & arrays — where C touches the machine directly.