How Do I Write Programs with Dynamic Memory and Menus?
Putting it all together — a complete menu-driven program with dynamic arrays
After this lesson, you will be able to:
- Write a menu-driven program using
calloc,realloc, andfree - Explain why functions that may
reallocneedint **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_gradetakesint **gradesbecausereallocmight move the memory block. If we only passedint *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 updatemain’sgradesvariable directly.
add_grade took int *grades instead of int **grades, and a realloc moved the block?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
free(grades); grades = NULL; at the end of main. If you removed grades = NULL;, what would change?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.