pointers-memory Lesson 10 18 min read

How Do I Write Programs with Dynamic Memory and Menus?

Putting it all together — a complete menu-driven program with dynamic arrays

Reading: C Text: Ch. 13 (Dynamic Data Structures — complete program patterns)

After this lesson, you will be able to:

  • Write a menu-driven program using calloc, realloc, and free
  • Explain why functions that may realloc need int ** parameters
  • Combine input validation, dynamic allocation, and double pointers in a single program
  • Verify clean Valgrind output for a dynamically allocated program

Everything Comes Together

You’ve learned pointers, pass-by-pointer, dynamic allocation, realloc, and Valgrind — each as separate concepts. Now let’s combine them into a real program: a menu-driven grade manager that dynamically grows as the user adds scores.

This is the kind of program you’ll build for labs: interactive input, dynamic data, clean memory management.


The Grade Manager

Program Structure

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

#define INITIAL_CAPACITY 4

void add_grade(int **grades, int *size, int *capacity);
void display_grades(const int *grades, int size);
double calculate_average(const int *grades, int size);
char get_menu_choice(void);

int main(void)
{
    int *grades = calloc(INITIAL_CAPACITY, sizeof(int));
    if (grades == NULL)
    {
        fprintf(stderr, "Initial allocation failed\n");
        return 1;
    }

    int size = 0;
    int capacity = INITIAL_CAPACITY;
    char choice;

    do
    {
        choice = get_menu_choice();

        switch (choice)
        {
            case 'A':
                add_grade(&grades, &size, &capacity);
                break;
            case 'D':
                display_grades(grades, size);
                break;
            case 'V':
                if (size > 0)
                {
                    printf("Average: %.1f\n", calculate_average(grades, size));
                }
                else
                {
                    printf("No grades entered yet.\n");
                }
                break;
            case 'Q':
                printf("Goodbye!\n");
                break;
        }
    } while (choice != 'Q');

    free(grades);         // Clean up
    grades = NULL;

    return 0;
}

The Add Function (With Dynamic Growth)

Notice int **grades — we need a double pointer because realloc might change where the array lives:

void add_grade(int **grades, int *size, int *capacity)
{
    if (*size >= *capacity)
    {
        *capacity *= 2;
        int *temp = realloc(*grades, *capacity * sizeof(int));
        if (temp == NULL)
        {
            fprintf(stderr, "Failed to grow array\n");
            return;
        }
        *grades = temp;
    }

    int grade;
    do
    {
        printf("Enter grade (0-100): ");
        scanf("%d", &grade);
        while (fgetc(stdin) != '\n') {}
    } while (grade < 0 || grade > 100);

    (*grades)[*size] = grade;
    (*size)++;
    printf("Grade added. (%d/%d capacity used)\n", *size, *capacity);
}

Key Insight: add_grade takes int **grades because realloc might move the memory block. If we only passed int *grades, the function would update its local copy of the pointer, and the caller would still point to the old (now freed) memory. The double pointer lets the function update main’s grades variable directly.

Check Your Understanding
In the grade manager, what would happen if add_grade took int *grades instead of int **grades, and a realloc moved the block?
A main's grades pointer would still point to the old (freed) memory — use-after-free
B The program would work fine — realloc updates all pointers automatically
C Compiler error — realloc requires a double pointer
D Memory leak — the new block would never be freed
Answer: A. With int *grades, the function updates its local copy of the pointer to the new block. But main's grades still points to the old address, which realloc already freed. Any subsequent access through main's pointer is use-after-free. This is exactly why the double pointer is needed — *grades = temp reaches back and updates main's variable.

Display and Average

void display_grades(const int *grades, int size)
{
    if (size == 0)
    {
        printf("No grades to display.\n");
        return;
    }

    printf("Grades (%d total):\n", size);
    for (int i = 0; i < size; i++)
    {
        printf("  %d", grades[i]);
        if ((i + 1) % 10 == 0) printf("\n");
    }
    printf("\n");
}

double calculate_average(const int *grades, int size)
{
    int sum = 0;
    for (int i = 0; i < size; i++)
    {
        sum += grades[i];
    }
    return (double)sum / size;
}

The Menu

char get_menu_choice(void)
{
    char choice;
    do
    {
        printf("\n--- Grade Manager ---\n");
        printf("  A) Add grade\n");
        printf("  D) Display all\n");
        printf("  V) View average\n");
        printf("  Q) Quit\n");
        printf("Choice: ");
        scanf(" %c", &choice);
        while (fgetc(stdin) != '\n') {}
        choice = toupper(choice);
    } while (choice != 'A' && choice != 'D' && choice != 'V' && choice != 'Q');

    return choice;
}

Testing with Valgrind

gcc -Wall -g -o grades grades.c
valgrind --leak-check=full ./grades

With the free(grades) in main, you should see:

All heap blocks were freed -- no leaks are possible
ERROR SUMMARY: 0 errors from 0 contexts
Check Your Understanding
The grade manager calls free(grades); grades = NULL; at the end of main. If you removed grades = NULL;, what would change?
A Memory leak — the memory wouldn't actually be freed
B Nothing visible — but grades would be a dangling pointer, making future maintenance riskier
C The program would crash on the next line
D Valgrind would report a double-free error
Answer: B. The free() still works — the memory is released. But grades now holds a stale address (dangling pointer). In this program, main immediately returns, so it doesn't matter. But in larger programs, code added later might accidentally use the dangling pointer. Setting to NULL is defensive programming — it turns potential silent corruption into a predictable segfault.
Why does this matter?

This program is a template for every lab from here on. Menu loop, dynamic storage, clean Valgrind output. The patterns — double pointer for realloc, null-after-free, input validation — appear in nearly every real C program. Getting comfortable with this structure now saves you from reinventing it every lab.

Design Patterns Used

This program combines every pattern from Series 2 and 3:

Pattern Where
Input validation (do-while) get_menu_choice, add_grade
Buffer clearing (fgetc) After every scanf
Dynamic allocation (calloc) Initial array in main
Double-when-full (realloc) add_grade
Double pointer (int **) add_grade modifies caller’s pointer
const correctness display_grades, calculate_average
Null check After calloc and realloc
Free + NULL End of main
Quick Check: Why does add_grade take int **grades instead of int *grades?

Because realloc might move the memory block to a new address. The function needs to update main’s pointer to point to the new location. With int *grades, the function could only update its local copy. With int **grades, *grades = temp modifies the caller’s variable.

Quick Check: What would happen if you removed free(grades) from main?

The program would still work correctly, but Valgrind would report a memory leak: “definitely lost: N bytes in M blocks.” The OS reclaims all memory when the program exits, but this is bad practice — it masks leaks that matter in long-running programs.

Quick Check: How many patterns from Series 2-3 can you identify in this program?

At least 8: input validation loops, buffer clearing, dynamic allocation, realloc growth, double pointers, const correctness, null checking, and free+NULL cleanup. This is why each pattern was taught individually — so they combine cleanly here.


Series 3 Complete

You’ve gone from “what’s a pointer?” to building a complete dynamic memory program with growable arrays, menu-driven I/O, and clean Valgrind output. That arc — pointers → pass-by-pointer → pointer arithmetic → memory layout → calloc → realloc → Valgrind → synthesis — is the core of C programming.

In Series 4: Advanced C, you’ll build on this foundation with structs (C’s version of classes), file I/O, function pointers, void pointers, and linked lists. Everything from here assumes you’re comfortable with pointers and dynamic memory.

The CTF Challenges for Weeks 6–7 include pointer manipulation and dynamic memory puzzles — exactly the kind of tracing and debugging practice that locks these concepts in.

Big Picture: The skills from this series — manual memory management, pointer manipulation, and debugging with Valgrind — are what separate a C programmer from someone who writes C-shaped Java. Java handles memory for you; C trusts you to handle it yourself. The reward is speed, control, and understanding how software really works at the machine level.