malloc · realloc · sizeof · Struct Pointers — 2 Examples
0%
Intermediate C  ·  Dynamic Memory

malloc, realloc &
Struct Pointers

Two focused intermediate programs — a dynamic integer array that grows with realloc, and a heap-allocated struct array that builds a live student database — both with full memory diagrams and free patterns.

1
Dynamic Int Array + realloc
2
Struct Malloc Student DB

⚡ Dynamic Memory — The Core Idea

Stack memory is fast but fixed-size and automatically reclaimed. Heap memory lets you request exactly how many bytes you need at runtime and keep them alive as long as you want — you control when they are freed. The four heap functions live in <stdlib.h>:

malloc
void* malloc(size_t n)
Allocate n bytes. Contents are uninitialised (garbage). Returns NULL on failure. Always check.
calloc
void* calloc(n, size)
Allocate n × size bytes, zero-filled. Safer default than malloc when you need clean memory.
realloc
void* realloc(ptr, n)
Resize an existing block to n bytes. May move to a new address. Old data is preserved up to the smaller of old/new size.
free
void free(void* ptr)
Return the block to the heap. After free, set the pointer to NULL — accessing freed memory is undefined behaviour.

📐 sizeof and typeof

sizeof(type) returns the number of bytes a type occupies at compile time — sizeof(int) is typically 4, sizeof(double) is 8. Always use sizeof instead of hard-coded byte counts — it makes your code portable across 32-bit and 64-bit systems and across different struct layouts.

The idiomatic malloc pattern is T *p = malloc(n * sizeof *p) — using sizeof *p (size of what the pointer points to) rather than sizeof(T). This way, if you ever change the type of p, the malloc automatically stays correct without a separate edit.

typeof(expr) is a GCC/Clang extension (standardised in C23) that yields the type of an expression. It is most useful in macros: #define SWAP(a,b) do { typeof(a) _t=(a);(a)=(b);(b)=_t; } while(0) — works for any type without needing a separate type argument.

heap vs stack — where each lives and who frees it
Stack variable
int arr[5]
← auto-freed when function returns, fixed size
Heap block
malloc(5*sizeof(int))
← lives until you call free(), size chosen at runtime
Pointer on stack
int *p
heap block
← pointer is 8 bytes on stack; data is on heap
After free(p)
int *p
freed! ☠
← set p = NULL immediately after free
example 1
1
📈 Dynamic Integer Array — grow with realloc
Start with capacity 4, double it whenever full — classic dynamic array (vector) growth pattern using sizeof *ptr
malloc · realloc · sizeof
A dynamic array starts small and doubles its capacity whenever it runs out of room. This keeps the amortised cost of appending at O(1) — the same as a C++ vector or Java ArrayList. The key ingredients: a capacity counter (slots allocated), a size counter (elements actually used), and a realloc call whenever size == capacity. We use sizeof *arr throughout so the code works even if you change the element type. After every realloc, we verify the return value is not NULL before replacing the old pointer — otherwise a failed realloc would leak the original block.
dynamic_array.c
C
#include <stdio.h>
#include <stdlib.h>

/* ── typeof-style SWAP macro — works for any type ── */
#define SWAP(a, b) do {         \
    __typeof__(a) _t = (a);     \
    (a) = (b); (b) = _t;        \
} while (0)

/* ── Dynamic array descriptor ── */
typedef struct {
    int  *data;      /* pointer to heap block     */
    int   size;      /* elements currently stored */
    int   capacity;  /* slots allocated on heap   */
} DynArr;

/* ── Initialise with starting capacity ── */
void da_init(DynArr *da, int cap) {
    da->data     = malloc(cap * sizeof *da->data);
    if (!da->data) { fprintf(stderr, "malloc failed\n"); exit(1); }
    da->size     = 0;
    da->capacity = cap;
    printf("  init: capacity=%d  heap bytes=%zu\n",
           cap, cap * sizeof *da->data);
}

/* ── Append — doubles capacity when full ── */
void da_push(DynArr *da, int val) {
    if (da->size == da->capacity) {
        int  newCap = da->capacity * 2;
        int *tmp    = realloc(da->data, newCap * sizeof *da->data);
        if (!tmp) { fprintf(stderr, "realloc failed\n"); exit(1); }
        da->data     = tmp;
        da->capacity = newCap;
        printf("  GROW  capacity %d -> %d  (realloc)\n",
               da->capacity / 2, newCap);
    }
    da->data[da->size++] = val;
}

/* ── Pop last element ── */
int da_pop(DynArr *da) {
    if (da->size == 0) { printf("  pop: empty!\n"); return -1; }
    return da->data[--da->size];
}

/* ── Sort using pointer walk + SWAP macro ── */
void da_sort(DynArr *da) {
    for (int i = 0; i < da->size - 1; i++)
        for (int j = 0; j < da->size - i - 1; j++)
            if (da->data[j] > da->data[j+1])
                SWAP(da->data[j], da->data[j+1]);
}

/* ── Print contents ── */
void da_print(const DynArr *da, const char *label) {
    printf("  %-14s[ ", label);
    for (int *p = da->data; p < da->data + da->size; p++)
        printf("%d ", *p);
    printf("]  size=%d  cap=%d\n", da->size, da->capacity);
}

/* ── Free heap memory ── */
void da_free(DynArr *da) {
    free(da->data);
    da->data = NULL;
    da->size = da->capacity = 0;
    printf("  free: heap block released, pointer nulled\n");
}

int main() {
    DynArr da;
    printf("=== Dynamic Array (start cap=4) ===\n");
    da_init(&da, 4);

    printf("\n--- Push 10 elements (watch it grow) ---\n");
    int vals[] = { 42, 17, 8, 95, 33, 61, 4, 78, 50, 29 };
    for (int i = 0; i < 10; i++) da_push(&da, vals[i]);
    da_print(&da, "After push:");

    printf("\n--- sizeof info ---\n");
    printf("  sizeof(int)       = %zu bytes\n", sizeof(int));
    printf("  sizeof(*da.data)  = %zu bytes\n", sizeof *da.data);
    printf("  sizeof(DynArr)    = %zu bytes\n", sizeof(DynArr));
    printf("  heap used by data = %zu bytes\n",
           da.capacity * sizeof *da.data);

    printf("\n--- Sort + pop 2 ---\n");
    da_sort(&da);
    da_print(&da, "Sorted:");
    printf("  pop() -> %d\n", da_pop(&da));
    printf("  pop() -> %d\n", da_pop(&da));
    da_print(&da, "After pop:");

    printf("\n--- Cleanup ---\n");
    da_free(&da);
    return 0;
}
output
=== Dynamic Array (start cap=4) ===
  init: capacity=4  heap bytes=16

--- Push 10 elements (watch it grow) ---
  GROW  capacity 4 -> 8  (realloc)
  GROW  capacity 8 -> 16  (realloc)
  After push:    [ 42 17 8 95 33 61 4 78 50 29 ]  size=10  cap=16

--- sizeof info ---
  sizeof(int)       = 4 bytes
  sizeof(*da.data)  = 4 bytes
  sizeof(DynArr)    = 16 bytes
  heap used by data = 64 bytes

--- Sort + pop 2 ---
  Sorted:        [ 4 8 17 29 33 42 50 61 78 95 ]  size=10  cap=16
  pop() -> 95
  pop() -> 78
  After pop:     [ 4 8 17 29 33 42 50 61 ]  size=8  cap=16

--- Cleanup ---
  free: heap block released, pointer nulled
capacity doubling — realloc triggers at size == capacity
init cap=4
42
17
8
95
← 4 slots, 16 bytes on heap
push 5th → GROW
42
17
8
95
33
·
·
·
← realloc to 8 slots, 32 bytes
push 9th → GROW
42
17
8
95
33
61
4
78
50
·
·
·
← realloc to 16 slots, 64 bytes
sizeof *p idiom
malloc(n * sizeof *da->data)
← type-safe: auto-correct if type changes
Never use the original pointer after realloc. Always assign to a temp: int *tmp = realloc(da->data, ...); if (!tmp) { /* handle */ } da->data = tmp;. If you write da->data = realloc(da->data, ...) and realloc returns NULL, you've lost the original pointer — that's a memory leak with no recovery.
sizeof *ptr is safer than sizeof(Type). Writing malloc(n * sizeof *da->data) means if you ever change data from int* to long*, the malloc size updates automatically. With sizeof(int), you'd have to hunt down every malloc call manually.
example 2
2
🎓 Struct malloc — Dynamic Student Database
Heap-allocate an array of structs, add students, search by roll number, sort by marks — all via struct pointers
struct · malloc · realloc
The real power of heap allocation is with arrays of structs. We define a Student struct, then malloc a block large enough for N students: Student *db = malloc(n * sizeof *db). Each element is accessed exactly like a stack array — db[i].name, db[i].marks. When we need more room, realloc resizes the block without changing how we access elements. The program demonstrates adding students, searching by roll number using a pointer walk, sorting by marks with a pointer-based comparison, and computing statistics — then frees everything cleanly.
student_db.c
C
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* ── Student struct ── */
typedef struct {
    int   roll;
    char  name[30];
    float marks;
    char  grade;
} Student;

/* ── Database: array of Students on the heap ── */
typedef struct {
    Student *data;
    int      count;
    int      cap;
} StudentDB;

/* assign grade from marks */
char calcGrade(float m) {
    if (m >= 90) return 'A';
    if (m >= 75) return 'B';
    if (m >= 60) return 'C';
    if (m >= 45) return 'D';
    return 'F';
}

/* init DB with initial capacity */
void db_init(StudentDB *db, int cap) {
    db->data  = malloc(cap * sizeof *db->data);
    if (!db->data) { fprintf(stderr,"malloc failed\n"); exit(1); }
    db->count = 0;
    db->cap   = cap;
    printf("  DB init: cap=%d  sizeof(Student)=%zu  "
           "heap=%zu bytes\n",
           cap, sizeof(Student),
           cap * sizeof(Student));
}

/* add student — realloc if full */
void db_add(StudentDB *db, int roll,
           const char *name, float marks) {
    if (db->count == db->cap) {
        int      nc  = db->cap * 2;
        Student *tmp = realloc(db->data, nc * sizeof *db->data);
        if (!tmp) { fprintf(stderr,"realloc failed\n"); exit(1); }
        db->data = tmp;
        db->cap  = nc;
        printf("  GROW DB cap -> %d\n", nc);
    }
    Student *s = &db->data[db->count++];
    s->roll  = roll;
    s->marks = marks;
    s->grade = calcGrade(marks);
    strncpy(s->name, name, 29);
}

/* search by roll using pointer walk */
Student* db_find(StudentDB *db, int roll) {
    Student *end = db->data + db->count;
    for (Student *p = db->data; p < end; p++)
        if (p->roll == roll) return p;
    return NULL;
}

/* sort by marks descending (bubble via pointer) */
void db_sort(StudentDB *db) {
    int n = db->count;
    for (int i = 0; i < n-1; i++)
        for (Student *p = db->data; p < db->data + n-i-1; p++)
            if (p->marks < (p+1)->marks) {
                Student tmp = *p; *p = *(p+1); *(p+1) = tmp;
            }
}

/* print full roster */
void db_print(const StudentDB *db) {
    printf("  %-5s %-14s %6s  Grade\n",
           "Roll", "Name", "Marks");
    printf("  %s\n", "-------------------------------");
    for (int i = 0; i < db->count; i++) {
        Student *s = &db->data[i];
        printf("  %-5d %-14s %6.1f  %c\n",
               s->roll, s->name, s->marks, s->grade);
    }
}

/* stats using pointer walk */
void db_stats(const StudentDB *db) {
    float   sum = 0, best = db->data[0].marks;
    Student *top = db->data;
    Student *end = db->data + db->count;
    for (Student *p = db->data; p < end; p++) {
        sum += p->marks;
        if (p->marks > best) { best = p->marks; top = p; }
    }
    printf("  Average : %.2f\n", sum / db->count);
    printf("  Topper  : %s (%.1f)\n", top->name, top->marks);
}

int main() {
    StudentDB db;
    printf("=== Student Database (Dynamic Struct Array) ===\n\n");
    db_init(&db, 3);

    printf("\n--- Add 6 students (triggers realloc at 4th) ---\n");
    db_add(&db, 101, "Ananya",  88.5f);
    db_add(&db, 102, "Rohan",   74.0f);
    db_add(&db, 103, "Priya",   95.5f);
    db_add(&db, 104, "Karan",   61.0f);
    db_add(&db, 105, "Sunita",  82.5f);
    db_add(&db, 106, "Arjun",   47.0f);

    printf("\n--- Roster (insertion order) ---\n");
    db_print(&db);

    printf("\n--- Search roll=104 ---\n");
    Student *found = db_find(&db, 104);
    if (found)
        printf("  Found: %s  marks=%.1f  grade=%c"
               "  addr=%p\n",
               found->name, found->marks,
               found->grade, (void*)found);

    printf("\n--- Sort by marks (descending) ---\n");
    db_sort(&db);
    db_print(&db);

    printf("\n--- Statistics ---\n");
    db_stats(&db);

    printf("\n--- sizeof breakdown ---\n");
    printf("  sizeof(Student) = %zu bytes\n", sizeof(Student));
    printf("  heap used        = %zu bytes (%d slots)\n",
           db.cap * sizeof(Student), db.cap);
    printf("  data actually    = %zu bytes (%d students)\n",
           db.count * sizeof(Student), db.count);

    free(db.data);
    db.data = NULL;
    printf("\n  free(db.data) — done.\n");
    return 0;
}
output
=== Student Database (Dynamic Struct Array) ===

  DB init: cap=3  sizeof(Student)=40  heap=120 bytes

--- Add 6 students (triggers realloc at 4th) ---
  GROW DB cap -> 6

--- Roster (insertion order) ---
  Roll  Name            Marks  Grade
  -------------------------------
  101   Ananya           88.5  B
  102   Rohan            74.0  B
  103   Priya            95.5  A
  104   Karan            61.0  C
  105   Sunita           82.5  B
  106   Arjun            47.0  D

--- Search roll=104 ---
  Found: Karan  marks=61.0  grade=C  addr=0x55a3f2c01090

--- Sort by marks (descending) ---
  Roll  Name            Marks  Grade
  -------------------------------
  103   Priya            95.5  A
  101   Ananya           88.5  B
  105   Sunita           82.5  B
  102   Rohan            74.0  B
  104   Karan            61.0  C
  106   Arjun            47.0  D

--- Statistics ---
  Average : 74.75
  Topper  : Priya (95.5)

--- sizeof breakdown ---
  sizeof(Student) = 40 bytes
  heap used        = 240 bytes (6 slots)
  data actually    = 240 bytes (6 students)

  free(db.data) — done.
struct array on heap — each slot is sizeof(Student) = 40 bytes
db.data →
101·Ananya
102·Rohan
103·Priya
← initial 3 slots, 120 bytes
after realloc →
101
102
103
104
105
106
← 6 slots, 240 bytes, data preserved
access pattern
db.data[i].marks
← exactly like stack array
pointer walk
for(Student *p = db.data; p < end; p++)
← p++ advances 40 bytes
Sorting a struct array swaps entire structs. Student tmp = *p; *p = *(p+1); *(p+1) = tmp; copies all 40 bytes of the struct in each swap. For large structs this is slow — a better approach is to sort an array of pointers to structs (Student **) and swap the pointers (8 bytes each) instead of the structs themselves.
After free(), always null the pointer. free(db.data); db.data = NULL; — this prevents accidental use-after-free bugs. Calling free(NULL) is safe and does nothing, so nulling the pointer also makes double-free calls harmless.
checklist
  • malloc / calloc — allocate on the heap. Use sizeof *ptr not sizeof(Type). Always check for NULL. calloc zero-fills; malloc leaves garbage.
  • realloc — always assign to a temp pointer. If realloc returns NULL, the original block is still valid; assigning directly would leak it. Double capacity each time for O(1) amortised append.
  • typeof / __typeof__ — yields the type of an expression at compile time. Makes SWAP macros type-generic. Standardised in C23; use __typeof__ for older GCC/Clang.
  • Struct mallocStudent *db = malloc(n * sizeof *db). Access exactly like a stack array: db[i].marks. Walk with Student *p = db; p < db+n; p++ — each p++ advances by sizeof(Student) bytes.
  • free discipline — free every malloc. After free, set pointer to NULL. Never access freed memory. For large structs, sort pointer arrays not struct arrays to avoid copying full structs on each swap.