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:

CommandEffect
listShow every student with their average and letter grade
add NAME S1 S2 S3Add a student with three scores
find NAMEShow one student (case-insensitive)
top NThe N best students by average
statsClass average, best and worst
save FILEWrite a report file
quitStop

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

TechniqueLessons
Structs, typedef and a dynamic array grown with realloc37, 40, 44
Reading lines with fgets and parsing with sscanf (including %[, %* and %n)15, 47
File input and output, perror, checking fclose47, 50
qsort with a comparison function on a copy of the data39, 46
Case-insensitive comparison with ctype.h30
Pointers to const data, static helpers35, 23
One owner for each allocation, freed on every path36, 38

Challenges

  1. Remove students: add remove NAME, shifting the remaining items down with memmove.
  2. Persist changes: add write, which saves the gradebook back to students.csv in the same format it reads.
  3. Flexible scores: allow any number of scores per student by storing them in a dynamically allocated array — and freeing each one correctly.
  4. Split it up: move the gradebook functions into gradebook.h/gradebook.c and the command loop into main.c, and write a Makefile (lessons 52–53).
  5. Test it: write a test program with assertions for parse_student, average and letter, 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.