Project: A Student Gradebook
This final project brings the course together in one program: a command-driven gradebook that loads students from a CSV file into a growing array of structs, answers commands typed on standard input, sorts and searches, computes statistics, and saves a report — with careful input parsing, error handling and memory management throughout.
What it does
The program reads students.csv, then processes commands, one per line:
| Command | Effect |
|---|---|
list | Show every student with their average and letter grade |
add NAME S1 S2 S3 | Add a student with three scores |
find NAME | Show one student (case-insensitive) |
top N | The N best students by average |
stats | Class average, best and worst |
save FILE | Write a report file |
quit | Stop |
The data file
TXT students.csv
Amara,88,92,79 Bruno,72,65,80 Chen,95,98,91 Dana,58,71,62 Eli,81,77,85
The program
The complete program is below, followed by a sample session: the commands typed as input, and everything the program printed.
C gradebook.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <ctype.h> #include <stdbool.h> #define NAME_MAX_LEN 32 #define SCORE_COUNT 3 typedef struct { char name[NAME_MAX_LEN]; int scores[SCORE_COUNT]; } Student; typedef struct { Student *items; size_t count; size_t capacity; } Gradebook; /* ---------- the gradebook "class" ---------- */ static bool gradebook_add(Gradebook *gb, const Student *s) { if (gb->count == gb->capacity) { size_t new_cap = gb->capacity ? gb->capacity * 2 : 4; Student *grown = realloc(gb->items, new_cap * sizeof *grown); if (grown == NULL) return false; gb->items = grown; gb->capacity = new_cap; } gb->items[gb->count++] = *s; return true; } static void gradebook_free(Gradebook *gb) { free(gb->items); *gb = (Gradebook) { 0 }; } static double average(const Student *s) { int total = 0; for (int i = 0; i < SCORE_COUNT; i++) total += s->scores[i]; return (double) total / SCORE_COUNT; } static char letter(double avg) { if (avg >= 90) return 'A'; if (avg >= 80) return 'B'; if (avg >= 70) return 'C'; if (avg >= 60) return 'D'; return 'F'; } /* ---------- parsing ---------- */ // Parses "name,s1,s2,s3" or "name s1 s2 s3". Returns false on any problem. static bool parse_student(const char *text, Student *out) { char name[NAME_MAX_LEN]; Student s = { 0 }; int used = 0; if (sscanf(text, " %31[^, ]%*[, ]%d%*[, ]%d%*[, ]%d %n", name, &s.scores[0], &s.scores[1], &s.scores[2], &used) != 4 || text[used] != '\0') { return false; } for (int i = 0; i < SCORE_COUNT; i++) { if (s.scores[i] < 0 || s.scores[i] > 100) return false; } snprintf(s.name, sizeof s.name, "%s", name); *out = s; return true; } static int load_csv(Gradebook *gb, const char *path) { FILE *f = fopen(path, "r"); if (f == NULL) { perror(path); return -1; } char line[128]; int line_no = 0, loaded = 0; while (fgets(line, sizeof line, f) != NULL) { line_no++; line[strcspn(line, "\r\n")] = '\0'; if (line[0] == '\0') continue; Student s; if (!parse_student(line, &s)) { fprintf(stderr, "%s:%d: skipping bad line\n", path, line_no); continue; } if (!gradebook_add(gb, &s)) { fclose(f); return -1; } loaded++; } fclose(f); return loaded; } /* ---------- queries ---------- */ static int by_average_desc(const void *a, const void *b) { double x = average(a), y = average(b); return (x < y) - (x > y); } static bool same_name(const char *a, const char *b) { for (; *a && *b; a++, b++) { if (tolower((unsigned char) *a) != tolower((unsigned char) *b)) return false; } return *a == *b; } static const Student *find(const Gradebook *gb, const char *name) { for (size_t i = 0; i < gb->count; i++) { if (same_name(gb->items[i].name, name)) return &gb->items[i]; } return NULL; } static void print_student(FILE *out, const Student *s) { double avg = average(s); fprintf(out, "%-10s %3d %3d %3d avg %5.1f %c\n", s->name, s->scores[0], s->scores[1], s->scores[2], avg, letter(avg)); } static void print_stats(const Gradebook *gb) { if (gb->count == 0) { printf("no students\n"); return; } const Student *best = &gb->items[0], *worst = &gb->items[0]; double sum = 0; for (size_t i = 0; i < gb->count; i++) { double avg = average(&gb->items[i]); sum += avg; if (avg > average(best)) best = &gb->items[i]; if (avg < average(worst)) worst = &gb->items[i]; } printf("%zu students, class average %.1f\n", gb->count, sum / gb->count); printf("best: %s (%.1f), needs support: %s (%.1f)\n", best->name, average(best), worst->name, average(worst)); } static int save_report(const Gradebook *gb, const char *path) { FILE *f = fopen(path, "w"); if (f == NULL) { perror(path); return -1; } fprintf(f, "Gradebook report (%zu students)\n", gb->count); for (size_t i = 0; i < gb->count; i++) print_student(f, &gb->items[i]); return fclose(f) == 0 ? 0 : -1; } /* ---------- the command loop ---------- */ int main(void) { Gradebook gb = { 0 }; int loaded = load_csv(&gb, "students.csv"); if (loaded < 0) { gradebook_free(&gb); return EXIT_FAILURE; } printf("loaded %d students\n", loaded); char line[128]; while (printf("> "), fgets(line, sizeof line, stdin) != NULL) { line[strcspn(line, "\r\n")] = '\0'; char command[16] = ""; int pos = 0; sscanf(line, "%15s %n", command, &pos); const char *rest = line + pos; printf("%s\n", line); // echo the command if (strcmp(command, "quit") == 0) { break; } else if (strcmp(command, "list") == 0) { for (size_t i = 0; i < gb.count; i++) print_student(stdout, &gb.items[i]); } else if (strcmp(command, "add") == 0) { Student s; if (!parse_student(rest, &s)) { printf("usage: add NAME S1 S2 S3 (scores 0-100)\n"); } else if (find(&gb, s.name) != NULL) { printf("%s is already in the gradebook\n", s.name); } else if (gradebook_add(&gb, &s)) { printf("added %s\n", s.name); } } else if (strcmp(command, "find") == 0) { const Student *s = find(&gb, rest); if (s != NULL) print_student(stdout, s); else printf("no student called \"%s\"\n", rest); } else if (strcmp(command, "top") == 0) { int n = 0; if (sscanf(rest, "%d", &n) != 1 || n <= 0) { printf("usage: top N\n"); continue; } Student *sorted = malloc(gb.count * sizeof *sorted); if (sorted == NULL) break; memcpy(sorted, gb.items, gb.count * sizeof *sorted); qsort(sorted, gb.count, sizeof *sorted, by_average_desc); for (size_t i = 0; i < gb.count && i < (size_t) n; i++) { printf("%zu. ", i + 1); print_student(stdout, &sorted[i]); } free(sorted); } else if (strcmp(command, "stats") == 0) { print_stats(&gb); } else if (strcmp(command, "save") == 0) { if (*rest == '\0') printf("usage: save FILE\n"); else if (save_report(&gb, rest) == 0) printf("report written to %s\n", rest); } else if (command[0] != '\0') { printf("unknown command \"%s\"\n", command); } } gradebook_free(&gb); printf("bye\n"); return EXIT_SUCCESS; }
Input
list add Farah 91 87 94 add Gus 101 50 50 find chen top 3 stats grade everyone save report.txt quit
Output
loaded 5 students > list Amara 88 92 79 avg 86.3 B Bruno 72 65 80 avg 72.3 C Chen 95 98 91 avg 94.7 A Dana 58 71 62 avg 63.7 D Eli 81 77 85 avg 81.0 B > add Farah 91 87 94 added Farah > add Gus 101 50 50 usage: add NAME S1 S2 S3 (scores 0-100) > find chen Chen 95 98 91 avg 94.7 A > top 3 1. Chen 95 98 91 avg 94.7 A 2. Farah 91 87 94 avg 90.7 A 3. Amara 88 92 79 avg 86.3 B > stats 6 students, class average 81.4 best: Chen (94.7), needs support: Dana (63.7) > grade everyone unknown command "grade" > save report.txt report written to report.txt > quit bye
What it uses
| Technique | Lessons |
|---|---|
Structs, typedef and a dynamic array grown with realloc | 37, 40, 44 |
Reading lines with fgets and parsing with sscanf (including %[, %* and %n) | 15, 47 |
File input and output, perror, checking fclose | 47, 50 |
qsort with a comparison function on a copy of the data | 39, 46 |
Case-insensitive comparison with ctype.h | 30 |
Pointers to const data, static helpers | 35, 23 |
| One owner for each allocation, freed on every path | 36, 38 |
Challenges
- Remove students: add
remove NAME, shifting the remaining items down withmemmove. - Persist changes: add
write, which saves the gradebook back tostudents.csvin the same format it reads. - Flexible scores: allow any number of scores per student by storing them in a dynamically allocated array — and freeing each one correctly.
- Split it up: move the gradebook functions into
gradebook.h/gradebook.cand the command loop intomain.c, and write a Makefile (lessons 52–53). - Test it: write a test program with assertions for
parse_student,averageandletter, and run it under-fsanitize=address,undefined.
Where to go next
- Learn C++, which builds on C with classes, templates and a large standard library.
- Explore systems programming: POSIX APIs for processes, files and sockets on Linux and macOS.
- Try embedded development on a microcontroller board, where C is still the main language.
- Read real C code: small, well-written projects such as SQLite, Redis or the Lua interpreter.
- Practise with the exercises and test yourself with the quiz.