19. Project: Student Management System

19. Project: Student Management System

Learning Objectives

  • Apply STL containers (vector, map, set) in a real-world application
  • Design classes with encapsulation and data validation
  • Implement file I/O with serialization and deserialization
  • Use exception handling for robust error management
  • Leverage smart pointers for automatic memory management
  • Build a menu-driven CLI interface
  • Practice modern C++ best practices

Table of Contents

  1. Project Overview
  2. Requirements
  3. Class Design
  4. Student Class
  5. Database Class
  6. File I/O and Serialization
  7. Exception Handling
  8. Menu Interface
  9. Complete Implementation
  10. Testing and Usage

1. Project Overview

We'll build a Student Management System that allows users to: - Add, remove, and update student records - Search and filter students by name, ID, or GPA - Calculate statistics (average GPA, top students) - Save/load data to/from files - Handle errors gracefully

┌─────────────────────────────────────────────────────────────┐
│           Student Management System Architecture            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌───────────────┐                                          │
│  │     main()    │  (Menu-driven CLI)                       │
│  └───────┬───────┘                                          │
│          │                                                   │
│          ▼                                                   │
│  ┌───────────────┐       ┌─────────────────┐                │
│  │   Database    │◄──────┤ StudentDatabase │                │
│  │   (singleton) │       │ (manages all    │                │
│  └───────┬───────┘       │  students)      │                │
│          │               └─────────────────┘                │
│          │                                                   │
│          ▼                                                   │
│  ┌───────────────┐                                          │
│  │    Student    │  (Data class)                            │
│  │  - id, name   │                                          │
│  │  - age, gpa   │                                          │
│  └───────────────┘                                          │
│                                                             │
│  STL Usage:                                                 │
│    - map<int, Student>  (ID → Student lookup)               │
│    - vector<Student>    (sorted results)                    │
│    - set<string>        (unique names)                      │
│                                                             │
│  File I/O:                                                  │
│    - Save to text file (CSV-like format)                    │
│    - Load from file (deserialization)                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘

2. Requirements

Functional Requirements

  1. CRUD Operations: Create, Read, Update, Delete student records
  2. Search: Find students by ID, name, or GPA range
  3. Statistics: Compute average GPA, find top N students
  4. Persistence: Save/load data from a text file
  5. Validation: Ensure valid IDs, GPAs (0.0–4.0), ages (> 0)

Technical Requirements

  • Use STL containers (map, vector, set)
  • Use smart pointers (shared_ptr or unique_ptr where applicable)
  • Implement exception handling (custom exceptions)
  • Follow RAII principles
  • Use const correctness
  • Implement operator overloading (for Student comparison)

3. Class Design

3.1 Student Class

Represents a single student with attributes: - id (int, unique) - name (string) - age (int) - gpa (double, 0.0–4.0)

Methods: - Constructor with validation - Getters/setters - Operator overloading (<, ==) for sorting and searching - Friend function for I/O

3.2 StudentDatabase Class

Manages the collection of students: - std::map<int, Student> for O(log N) lookup by ID - Methods: add, remove, update, search, save, load - Exception handling for invalid operations


4. Student Class

4.1 Header File (Student.h)

#ifndef STUDENT_H
#define STUDENT_H

#include <string>
#include <iostream>
#include <stdexcept>

class Student {
private:
    int id;
    std::string name;
    int age;
    double gpa;

public:
    // Constructor with validation
    Student(int id, const std::string& name, int age, double gpa);

    // Default constructor (for map insertion)
    Student() : id(0), name(""), age(0), gpa(0.0) {}

    // Getters
    int getId() const { return id; }
    std::string getName() const { return name; }
    int getAge() const { return age; }
    double getGpa() const { return gpa; }

    // Setters (with validation)
    void setName(const std::string& newName);
    void setAge(int newAge);
    void setGpa(double newGpa);

    // Operator overloading
    bool operator<(const Student& other) const;  // Compare by GPA (descending)
    bool operator==(const Student& other) const; // Compare by ID

    // Friend function for output
    friend std::ostream& operator<<(std::ostream& os, const Student& s);

    // Serialization
    std::string serialize() const;
    static Student deserialize(const std::string& line);
};

#endif // STUDENT_H

4.2 Implementation File (Student.cpp)

#include "Student.h"
#include <sstream>
#include <iomanip>

Student::Student(int id, const std::string& name, int age, double gpa)
    : id(id), name(name), age(age), gpa(gpa) {
    // Validation
    if (id <= 0) {
        throw std::invalid_argument("ID must be positive");
    }
    if (name.empty()) {
        throw std::invalid_argument("Name cannot be empty");
    }
    if (age <= 0 || age > 120) {
        throw std::invalid_argument("Age must be between 1 and 120");
    }
    if (gpa < 0.0 || gpa > 4.0) {
        throw std::invalid_argument("GPA must be between 0.0 and 4.0");
    }
}

void Student::setName(const std::string& newName) {
    if (newName.empty()) {
        throw std::invalid_argument("Name cannot be empty");
    }
    name = newName;
}

void Student::setAge(int newAge) {
    if (newAge <= 0 || newAge > 120) {
        throw std::invalid_argument("Age must be between 1 and 120");
    }
    age = newAge;
}

void Student::setGpa(double newGpa) {
    if (newGpa < 0.0 || newGpa > 4.0) {
        throw std::invalid_argument("GPA must be between 0.0 and 4.0");
    }
    gpa = newGpa;
}

bool Student::operator<(const Student& other) const {
    // Sort by GPA descending
    return gpa > other.gpa;
}

bool Student::operator==(const Student& other) const {
    return id == other.id;
}

std::ostream& operator<<(std::ostream& os, const Student& s) {
    os << "ID: " << std::setw(5) << s.id
       << " | Name: " << std::setw(20) << std::left << s.name
       << " | Age: " << std::setw(3) << s.age
       << " | GPA: " << std::fixed << std::setprecision(2) << s.gpa;
    return os;
}

std::string Student::serialize() const {
    std::ostringstream oss;
    oss << id << "," << name << "," << age << "," << std::fixed << std::setprecision(2) << gpa;
    return oss.str();
}

Student Student::deserialize(const std::string& line) {
    std::istringstream iss(line);
    std::string token;
    int id, age;
    double gpa;
    std::string name;

    // Parse CSV: id,name,age,gpa
    std::getline(iss, token, ',');
    id = std::stoi(token);

    std::getline(iss, name, ',');

    std::getline(iss, token, ',');
    age = std::stoi(token);

    std::getline(iss, token, ',');
    gpa = std::stod(token);

    return Student(id, name, age, gpa);
}

5. Database Class

5.1 Header File (StudentDatabase.h)

#ifndef STUDENTDATABASE_H
#define STUDENTDATABASE_H

#include "Student.h"
#include <map>
#include <vector>
#include <memory>
#include <fstream>

class StudentDatabase {
private:
    std::map<int, Student> students;  // ID → Student
    std::string filename;

public:
    StudentDatabase(const std::string& filename = "students.csv")
        : filename(filename) {}

    // CRUD operations
    void addStudent(const Student& student);
    void removeStudent(int id);
    void updateStudent(int id, const Student& updatedStudent);
    Student getStudent(int id) const;

    // Search and filter
    std::vector<Student> searchByName(const std::string& name) const;
    std::vector<Student> filterByGpa(double minGpa, double maxGpa) const;

    // Statistics
    double averageGpa() const;
    std::vector<Student> topNStudents(int n) const;

    // Display
    void displayAll() const;

    // File I/O
    void saveToFile() const;
    void loadFromFile();

    // Utility
    size_t size() const { return students.size(); }
    bool empty() const { return students.empty(); }
};

#endif // STUDENTDATABASE_H

5.2 Implementation File (StudentDatabase.cpp)

#include "StudentDatabase.h"
#include <algorithm>
#include <iostream>
#include <iomanip>

void StudentDatabase::addStudent(const Student& student) {
    int id = student.getId();
    if (students.find(id) != students.end()) {
        throw std::runtime_error("Student with ID " + std::to_string(id) + " already exists");
    }
    students[id] = student;
    std::cout << "Student added successfully.\n";
}

void StudentDatabase::removeStudent(int id) {
    auto it = students.find(id);
    if (it == students.end()) {
        throw std::runtime_error("Student with ID " + std::to_string(id) + " not found");
    }
    students.erase(it);
    std::cout << "Student removed successfully.\n";
}

void StudentDatabase::updateStudent(int id, const Student& updatedStudent) {
    auto it = students.find(id);
    if (it == students.end()) {
        throw std::runtime_error("Student with ID " + std::to_string(id) + " not found");
    }
    it->second = updatedStudent;
    std::cout << "Student updated successfully.\n";
}

Student StudentDatabase::getStudent(int id) const {
    auto it = students.find(id);
    if (it == students.end()) {
        throw std::runtime_error("Student with ID " + std::to_string(id) + " not found");
    }
    return it->second;
}

std::vector<Student> StudentDatabase::searchByName(const std::string& name) const {
    std::vector<Student> results;
    for (const auto& [id, student] : students) {
        if (student.getName().find(name) != std::string::npos) {
            results.push_back(student);
        }
    }
    return results;
}

std::vector<Student> StudentDatabase::filterByGpa(double minGpa, double maxGpa) const {
    std::vector<Student> results;
    for (const auto& [id, student] : students) {
        double gpa = student.getGpa();
        if (gpa >= minGpa && gpa <= maxGpa) {
            results.push_back(student);
        }
    }
    return results;
}

double StudentDatabase::averageGpa() const {
    if (students.empty()) {
        return 0.0;
    }
    double sum = 0.0;
    for (const auto& [id, student] : students) {
        sum += student.getGpa();
    }
    return sum / students.size();
}

std::vector<Student> StudentDatabase::topNStudents(int n) const {
    std::vector<Student> all;
    for (const auto& [id, student] : students) {
        all.push_back(student);
    }

    // Sort by GPA descending (using operator<)
    std::sort(all.begin(), all.end());

    // Return top N
    if (n > static_cast<int>(all.size())) {
        n = all.size();
    }
    return std::vector<Student>(all.begin(), all.begin() + n);
}

void StudentDatabase::displayAll() const {
    if (students.empty()) {
        std::cout << "No students in database.\n";
        return;
    }

    std::cout << "\n" << std::string(70, '=') << "\n";
    std::cout << "Total Students: " << students.size() << "\n";
    std::cout << std::string(70, '=') << "\n";
    for (const auto& [id, student] : students) {
        std::cout << student << "\n";
    }
    std::cout << std::string(70, '=') << "\n\n";
}

void StudentDatabase::saveToFile() const {
    std::ofstream ofs(filename);
    if (!ofs) {
        throw std::runtime_error("Failed to open file for writing: " + filename);
    }

    for (const auto& [id, student] : students) {
        ofs << student.serialize() << "\n";
    }

    std::cout << "Data saved to " << filename << "\n";
}

void StudentDatabase::loadFromFile() {
    std::ifstream ifs(filename);
    if (!ifs) {
        std::cout << "No existing file found. Starting with empty database.\n";
        return;
    }

    students.clear();
    std::string line;
    int count = 0;

    while (std::getline(ifs, line)) {
        if (line.empty()) continue;
        try {
            Student s = Student::deserialize(line);
            students[s.getId()] = s;
            count++;
        } catch (const std::exception& e) {
            std::cerr << "Error parsing line: " << line << " (" << e.what() << ")\n";
        }
    }

    std::cout << "Loaded " << count << " students from " << filename << "\n";
}

6. File I/O and Serialization

Data is saved in CSV format:

1,Alice Johnson,20,3.85
2,Bob Smith,22,3.20
3,Carol Lee,19,3.95

Each line: id,name,age,gpa

The serialize() and deserialize() methods handle conversion.


7. Exception Handling

// Example usage with exception handling
try {
    Student s(101, "John Doe", 21, 3.75);
    db.addStudent(s);
} catch (const std::invalid_argument& e) {
    std::cerr << "Validation error: " << e.what() << "\n";
} catch (const std::runtime_error& e) {
    std::cerr << "Runtime error: " << e.what() << "\n";
} catch (const std::exception& e) {
    std::cerr << "Unexpected error: " << e.what() << "\n";
}

Custom exceptions could be defined for more granular error handling:

class DatabaseException : public std::runtime_error {
public:
    explicit DatabaseException(const std::string& msg) : std::runtime_error(msg) {}
};

8. Menu Interface

8.1 Menu Display

void displayMenu() {
    std::cout << "\n========== Student Management System ==========\n";
    std::cout << "1. Add Student\n";
    std::cout << "2. Remove Student\n";
    std::cout << "3. Update Student\n";
    std::cout << "4. Display All Students\n";
    std::cout << "5. Search by Name\n";
    std::cout << "6. Filter by GPA Range\n";
    std::cout << "7. Show Average GPA\n";
    std::cout << "8. Show Top N Students\n";
    std::cout << "9. Save to File\n";
    std::cout << "10. Load from File\n";
    std::cout << "0. Exit\n";
    std::cout << "===============================================\n";
    std::cout << "Enter choice: ";
}

8.2 Input Helper Functions

int getIntInput(const std::string& prompt) {
    int value;
    std::cout << prompt;
    while (!(std::cin >> value)) {
        std::cin.clear();
        std::cin.ignore(10000, '\n');
        std::cout << "Invalid input. " << prompt;
    }
    std::cin.ignore(10000, '\n');
    return value;
}

double getDoubleInput(const std::string& prompt) {
    double value;
    std::cout << prompt;
    while (!(std::cin >> value)) {
        std::cin.clear();
        std::cin.ignore(10000, '\n');
        std::cout << "Invalid input. " << prompt;
    }
    std::cin.ignore(10000, '\n');
    return value;
}

std::string getStringInput(const std::string& prompt) {
    std::string value;
    std::cout << prompt;
    std::getline(std::cin, value);
    return value;
}

9. Complete Implementation

9.1 Main Program (main.cpp)

#include "Student.h"
#include "StudentDatabase.h"
#include <iostream>
#include <limits>

// Input helper functions
int getIntInput(const std::string& prompt);
double getDoubleInput(const std::string& prompt);
std::string getStringInput(const std::string& prompt);
void displayMenu();

int main() {
    StudentDatabase db("students.csv");

    // Load existing data
    try {
        db.loadFromFile();
    } catch (const std::exception& e) {
        std::cerr << "Error loading file: " << e.what() << "\n";
    }

    int choice;
    do {
        displayMenu();
        choice = getIntInput("");

        try {
            switch (choice) {
                case 1: { // Add Student
                    int id = getIntInput("Enter ID: ");
                    std::string name = getStringInput("Enter Name: ");
                    int age = getIntInput("Enter Age: ");
                    double gpa = getDoubleInput("Enter GPA: ");

                    Student s(id, name, age, gpa);
                    db.addStudent(s);
                    break;
                }
                case 2: { // Remove Student
                    int id = getIntInput("Enter ID to remove: ");
                    db.removeStudent(id);
                    break;
                }
                case 3: { // Update Student
                    int id = getIntInput("Enter ID to update: ");
                    Student oldStudent = db.getStudent(id);
                    std::cout << "Current record:\n" << oldStudent << "\n";

                    std::string name = getStringInput("Enter new Name (or press Enter to keep): ");
                    if (name.empty()) name = oldStudent.getName();

                    int age = getIntInput("Enter new Age (or 0 to keep): ");
                    if (age == 0) age = oldStudent.getAge();

                    double gpa = getDoubleInput("Enter new GPA (or -1 to keep): ");
                    if (gpa < 0) gpa = oldStudent.getGpa();

                    Student updatedStudent(id, name, age, gpa);
                    db.updateStudent(id, updatedStudent);
                    break;
                }
                case 4: { // Display All
                    db.displayAll();
                    break;
                }
                case 5: { // Search by Name
                    std::string name = getStringInput("Enter name to search: ");
                    auto results = db.searchByName(name);
                    std::cout << "\nFound " << results.size() << " student(s):\n";
                    for (const auto& s : results) {
                        std::cout << s << "\n";
                    }
                    break;
                }
                case 6: { // Filter by GPA
                    double minGpa = getDoubleInput("Enter minimum GPA: ");
                    double maxGpa = getDoubleInput("Enter maximum GPA: ");
                    auto results = db.filterByGpa(minGpa, maxGpa);
                    std::cout << "\nFound " << results.size() << " student(s):\n";
                    for (const auto& s : results) {
                        std::cout << s << "\n";
                    }
                    break;
                }
                case 7: { // Average GPA
                    double avg = db.averageGpa();
                    std::cout << "\nAverage GPA: " << std::fixed << std::setprecision(2) << avg << "\n";
                    break;
                }
                case 8: { // Top N Students
                    int n = getIntInput("Enter number of top students: ");
                    auto top = db.topNStudents(n);
                    std::cout << "\nTop " << top.size() << " student(s):\n";
                    for (const auto& s : top) {
                        std::cout << s << "\n";
                    }
                    break;
                }
                case 9: { // Save
                    db.saveToFile();
                    break;
                }
                case 10: { // Load
                    db.loadFromFile();
                    break;
                }
                case 0: { // Exit
                    std::cout << "Exiting...\n";
                    break;
                }
                default:
                    std::cout << "Invalid choice. Try again.\n";
            }
        } catch (const std::invalid_argument& e) {
            std::cerr << "Validation error: " << e.what() << "\n";
        } catch (const std::runtime_error& e) {
            std::cerr << "Runtime error: " << e.what() << "\n";
        } catch (const std::exception& e) {
            std::cerr << "Error: " << e.what() << "\n";
        }

    } while (choice != 0);

    // Auto-save on exit
    try {
        db.saveToFile();
    } catch (const std::exception& e) {
        std::cerr << "Failed to save data: " << e.what() << "\n";
    }

    return 0;
}

// Input helper implementations
int getIntInput(const std::string& prompt) {
    int value;
    std::cout << prompt;
    while (!(std::cin >> value)) {
        std::cin.clear();
        std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
        std::cout << "Invalid input. " << prompt;
    }
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
    return value;
}

double getDoubleInput(const std::string& prompt) {
    double value;
    std::cout << prompt;
    while (!(std::cin >> value)) {
        std::cin.clear();
        std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
        std::cout << "Invalid input. " << prompt;
    }
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
    return value;
}

std::string getStringInput(const std::string& prompt) {
    std::string value;
    std::cout << prompt;
    std::getline(std::cin, value);
    return value;
}

void displayMenu() {
    std::cout << "\n========== Student Management System ==========\n";
    std::cout << "1. Add Student\n";
    std::cout << "2. Remove Student\n";
    std::cout << "3. Update Student\n";
    std::cout << "4. Display All Students\n";
    std::cout << "5. Search by Name\n";
    std::cout << "6. Filter by GPA Range\n";
    std::cout << "7. Show Average GPA\n";
    std::cout << "8. Show Top N Students\n";
    std::cout << "9. Save to File\n";
    std::cout << "10. Load from File\n";
    std::cout << "0. Exit\n";
    std::cout << "===============================================\n";
    std::cout << "Enter choice: ";
}

9.2 Makefile

CXX = g++
CXXFLAGS = -std=c++17 -Wall -Wextra -O2
TARGET = student_mgmt
OBJS = main.o Student.o StudentDatabase.o

all: $(TARGET)

$(TARGET): $(OBJS)
    $(CXX) $(CXXFLAGS) -o $(TARGET) $(OBJS)

main.o: main.cpp Student.h StudentDatabase.h
    $(CXX) $(CXXFLAGS) -c main.cpp

Student.o: Student.cpp Student.h
    $(CXX) $(CXXFLAGS) -c Student.cpp

StudentDatabase.o: StudentDatabase.cpp StudentDatabase.h Student.h
    $(CXX) $(CXXFLAGS) -c StudentDatabase.cpp

clean:
    rm -f $(OBJS) $(TARGET)

.PHONY: all clean

10. Testing and Usage

10.1 Compilation

make

10.2 Sample Usage

$ ./student_mgmt

========== Student Management System ==========
1. Add Student
2. Remove Student
...
0. Exit
===============================================
Enter choice: 1
Enter ID: 101
Enter Name: Alice Johnson
Enter Age: 20
Enter GPA: 3.85
Student added successfully.

Enter choice: 1
Enter ID: 102
Enter Name: Bob Smith
Enter Age: 22
Enter GPA: 3.20
Student added successfully.

Enter choice: 4

======================================================================
Total Students: 2
======================================================================
ID:   101 | Name: Alice Johnson       | Age:  20 | GPA: 3.85
ID:   102 | Name: Bob Smith           | Age:  22 | GPA: 3.20
======================================================================

Enter choice: 7
Average GPA: 3.52

Enter choice: 8
Enter number of top students: 1
Top 1 student(s):
ID:   101 | Name: Alice Johnson       | Age:  20 | GPA: 3.85

Enter choice: 9
Data saved to students.csv

Enter choice: 0
Exiting...
Data saved to students.csv

10.3 File Contents (students.csv)

101,Alice Johnson,20,3.85
102,Bob Smith,22,3.20

to navigate between lessons