Project quản lý sinh viên C++: thêm, sửa, xóa, thống kê và lưu file CSV

Project quản lý sinh viên C++: thêm, sửa, xóa, thống kê và lưu file CSV - Khuê Nguyễn Creator

Trong bài này chúng ta sẽ làm project quản lý sinh viên C++: chương trình đọc danh sách sinh viên từ file CSV, cho thêm, sửa điểm, xóa, sắp xếp, thống kê theo xếp loại, xuất báo cáo và lưu lại khi thoát. Đây là đề bài tập lớn kinh điển của môn lập trình, và là dịp để luyện kỹ phần đọc ghi file, thuật toán STL và xử lý dữ liệu sai.

Bài 37 trong Serie Lập trình C++ từ A tới Z

Yêu cầu của chương trình quản lý sinh viên C++

  1. Đọc danh sách từ sinh_vien.csv, mỗi dòng ma,ho ten,diem. Dòng sai (điểm không phải số, ngoài 0–10) thì bỏ qua khi xử lý nhưng không được làm mất khi lưu lại.
  2. Menu: xem bảng, thêm, sửa điểm, xóa, sắp xếp theo điểm, thống kê, xuất báo cáo ra file, lưu và thoát.
  3. Không cho thêm 2 sinh viên trùng mã. Điểm nhập vào phải là số trong khoảng 0–10.
  4. Thống kê điểm trung bình và số sinh viên theo xếp loại Giỏi, Khá, Trung bình, Yếu.

Rút kinh nghiệm từ project quản lý thư viện ở bài trước, lần này dòng lỗi được giữ lại nguyên văn khi lưu, để người dùng tự mở file ra sửa.

Sơ đồ luồng dữ liệu chương trình quản lý sinh viên C++: đọc CSV, xử lý bằng vector và STL, ghi file và báo cáo

Dữ liệu mẫu, dòng SV04 cố ý có điểm sai:

File sinh_vien.csv ban đầu có một dòng điểm lỗi và lệnh biên dịch chương trình

Kiểm tra dữ liệu và xếp loại

struct SinhVien
{
    std::string ma;
    std::string hoTen;
    double diem;
};

// Doi chuoi sang diem, nem ngoai le neu khong phai so hoac ngoai khoang 0-10
double docDiem(const std::string& chuoi)
{
    double diem = std::stod(chuoi);
    if (diem < 0 || diem > 10)
        throw std::out_of_range("diem ngoai khoang 0-10");
    return diem;
}

std::string xepLoai(double diem)
{
    if (diem >= 8.0) return "Gioi";
    if (diem >= 6.5) return "Kha";
    if (diem >= 5.0) return "Trung binh";
    return "Yeu";
}

Hàm docDiem gom toàn bộ việc kiểm tra điểm vào một chỗ: std::stod ném std::invalid_argument nếu không phải số, còn điểm ngoài khoảng thì tự ném std::out_of_range. Mọi chỗ nhập điểm (đọc file, thêm mới, sửa điểm) đều gọi hàm này, nên không chỗ nào quên kiểm tra. Cách dùng ngoại lệ này lấy từ bài xử lý ngoại lệ trong C++.

Class QuanLySinhVien

Đọc và ghi file

class QuanLySinhVien
{
public:
    // Doc file CSV: ma,ho ten,diem. Tra ve so dong loi
    int docFile(const std::string& tenFile)
    {
        std::ifstream file(tenFile);
        if (!file)
            throw std::runtime_error("khong mo duoc file " + tenFile);
        std::string dong;
        int soDongLoi = 0;
        while (std::getline(file, dong))
        {
            std::stringstream ss(dong);
            SinhVien sv;
            std::string diem;
            if (std::getline(ss, sv.ma, ',') && std::getline(ss, sv.hoTen, ',') && std::getline(ss, diem))
            {
                try
                {
                    sv.diem = docDiem(diem);
                    ds_.push_back(sv);
                    continue;
                }
                catch (const std::exception&) {}
            }
            dongLoi_.push_back(dong);          // giu lai dong loi, khong lam mat du lieu
            soDongLoi++;
        }
        return soDongLoi;
    }

    void ghiFile(const std::string& tenFile) const
    {
        std::ofstream file(tenFile);
        for (const SinhVien& sv : ds_)
            file << sv.ma << ',' << sv.hoTen << ',' << sv.diem << '\n';
        for (const std::string& dong : dongLoi_)       // ghi lai nguyen van de nguoi dung tu sua
            file << dong << '\n';
    }
  • Mỗi dòng được tách bằng std::getline với dấu phẩy, như bài đọc ghi file. Ba lần getline nối bằng &&: thiếu trường nào là cả điều kiện sai.
  • Dòng hợp lệ vào ds_, dòng lỗi vào dongLoi_ nguyên văn. Khi ghiFile, các dòng lỗi được ghi lại ở cuối file.
  • File không mở được thì ném ngoại lệ, main bắt và bắt đầu với danh sách rỗng thay vì dừng chương trình.

Thêm, xóa, tìm

    bool them(const SinhVien& sv)
    {
        if (tim(sv.ma) != nullptr)
            return false;                          // trung ma
        ds_.push_back(sv);
        return true;
    }

    bool xoa(const std::string& ma)
    {
        auto it = std::remove_if(ds_.begin(), ds_.end(), [&ma](const SinhVien& sv) { return sv.ma == ma; });
        if (it == ds_.end())
            return false;
        ds_.erase(it, ds_.end());
        return true;
    }

    SinhVien* tim(const std::string& ma)
    {
        auto it = std::find_if(ds_.begin(), ds_.end(), [&ma](const SinhVien& sv) { return sv.ma == ma; });
        return it == ds_.end() ? nullptr : &*it;
    }
  • them kiểm tra trùng mã bằng tim trước khi thêm.
  • xoa dùng cặp std::remove_if + erase, gọi là erase-remove idiom: remove_if dồn các phần tử cần giữ lên đầu và trả về vị trí bắt đầu phần “rác”, erase xóa hẳn phần đó. Đây là cách chuẩn để xóa phần tử theo điều kiện khỏi vector.
  • tim trả về con trỏ tới sinh viên, không có thì trả về nullptr như bài mảng và con trỏ. Con trỏ cho phép sửa trực tiếp điểm của sinh viên tìm được.

Lưu ý: Con trỏ trả về từ tim trỏ vào bên trong vector. Sau khi thêm hoặc xóa phần tử, vector có thể chuyển dữ liệu sang vùng nhớ mới và con trỏ cũ trở thành con trỏ treo. Vì vậy main chỉ dùng con trỏ ngay sau khi tìm, không giữ lại để dùng sau một lần thêm, xóa.

Sắp xếp, in bảng và thống kê

    void sapXepTheoDiem()
    {
        std::sort(ds_.begin(), ds_.end(), [](const SinhVien& a, const SinhVien& b) { return a.diem > b.diem; });
    }

    void inBang(std::ostream& os) const
    {
        os << std::left << std::setw(8) << "Ma" << std::setw(22) << "Ho ten" << std::setw(6) << "Diem" << "Xep loai\n";
        for (const SinhVien& sv : ds_)
            os << std::setw(8) << sv.ma << std::setw(22) << sv.hoTen
               << std::setw(6) << std::fixed << std::setprecision(1) << sv.diem << xepLoai(sv.diem) << '\n';
    }

    void inThongKe(std::ostream& os) const
    {
        if (ds_.empty())
        {
            os << "Danh sach rong\n";
            return;
        }
        double tong = std::accumulate(ds_.begin(), ds_.end(), 0.0,
                                      [](double t, const SinhVien& sv) { return t + sv.diem; });
        os << "So sinh vien: " << ds_.size() << ", diem trung binh: "
           << std::fixed << std::setprecision(2) << tong / ds_.size() << '\n';
        for (const char* loai : {"Gioi", "Kha", "Trung binh", "Yeu"})
        {
            auto dem = std::count_if(ds_.begin(), ds_.end(), [loai](const SinhVien& sv) { return xepLoai(sv.diem) == loai; });
            os << "  " << loai << ": " << dem << '\n';
        }
    }
  • std::sort với lambda sắp theo điểm giảm dần.
  • std::setw(n) trong thư viện <iomanip> đặt độ rộng cột, std::left căn trái, nhờ vậy bảng thẳng hàng. std::fixed và std::setprecision(1) in điểm với đúng 1 chữ số thập phân.
  • inBang và inThongKe nhận std::ostream&, nên cùng một hàm in được ra màn hình (std::cout) lẫn ra file (std::ofstream). Đây chính là lợi ích của việc file và màn hình dùng chung kiểu stream.
  • Thống kê dùng std::accumulate tính tổng điểm và std::count_if đếm theo xếp loại, như bài STL.

Hàm main và menu

std::string nhap(const std::string& loiNhac)
{
    std::cout << loiNhac;
    std::string s;
    std::getline(std::cin, s);
    return s;
}

int main()
{
    const std::string FILE_DU_LIEU = "sinh_vien.csv";
    QuanLySinhVien ql;
    try
    {
        int loi = ql.docFile(FILE_DU_LIEU);
        std::cout << "Da doc " << FILE_DU_LIEU << (loi ? ", bo qua " + std::to_string(loi) + " dong loi" : "") << '\n';
    }
    catch (const std::exception& e)
    {
        std::cout << "Bat dau voi danh sach rong (" << e.what() << ")\n";
    }

    std::string chon;
    do
    {
        chon = nhap("\n1.Xem 2.Them 3.Sua diem 4.Xoa 5.Sap xep 6.Thong ke 7.Xuat bao cao 0.Luu va thoat\nChon: ");
        if (chon == "1")
            ql.inBang(std::cout);
        else if (chon == "2")
        {
            SinhVien sv;
            sv.ma = nhap("Ma: ");
            sv.hoTen = nhap("Ho ten: ");
            try
            {
                sv.diem = docDiem(nhap("Diem: "));
                std::cout << (ql.them(sv) ? "  Da them\n" : "  Trung ma sinh vien\n");
            }
            catch (const std::exception&)
            {
                std::cout << "  Diem khong hop le\n";
            }
        }
        else if (chon == "3")
        {
            SinhVien* sv = ql.tim(nhap("Ma: "));
            if (sv == nullptr)
                std::cout << "  Khong tim thay\n";
            else
            {
                try
                {
                    sv->diem = docDiem(nhap("Diem moi: "));
                    std::cout << "  Da sua diem cua " << sv->hoTen << '\n';
                }
                catch (const std::exception&)
                {
                    std::cout << "  Diem khong hop le\n";
                }
            }
        }
        else if (chon == "4")
            std::cout << (ql.xoa(nhap("Ma: ")) ? "  Da xoa\n" : "  Khong tim thay\n");
        else if (chon == "5")
        {
            ql.sapXepTheoDiem();
            std::cout << "  Da sap xep theo diem giam dan\n";
        }
        else if (chon == "6")
            ql.inThongKe(std::cout);
        else if (chon == "7")
        {
            std::ofstream baoCao("bao_cao.txt");
            ql.inBang(baoCao);
            ql.inThongKe(baoCao);
            std::cout << "  Da xuat bao_cao.txt\n";
        }
    } while (chon != "0");

    ql.ghiFile(FILE_DU_LIEU);
    std::cout << "Da luu " << FILE_DU_LIEU << '\n';
    return 0;
}

Lựa chọn menu đọc bằng std::getline thành chuỗi và so sánh chuỗi, nên gõ chữ hay gõ số lạ cũng không làm chương trình chạy sai. Phần thêm, sửa điểm bọc trong try để bắt lỗi từ docDiem.

Code đầy đủ

Đây là full code, các bạn copy vào file main.cpp rồi biên dịch bằng g++ là chạy được:

#include <algorithm>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>

struct SinhVien
{
    std::string ma;
    std::string hoTen;
    double diem;
};

// Doi chuoi sang diem, nem ngoai le neu khong phai so hoac ngoai khoang 0-10
double docDiem(const std::string& chuoi)
{
    double diem = std::stod(chuoi);
    if (diem < 0 || diem > 10)
        throw std::out_of_range("diem ngoai khoang 0-10");
    return diem;
}

std::string xepLoai(double diem)
{
    if (diem >= 8.0) return "Gioi";
    if (diem >= 6.5) return "Kha";
    if (diem >= 5.0) return "Trung binh";
    return "Yeu";
}

class QuanLySinhVien
{
public:
    // Doc file CSV: ma,ho ten,diem. Tra ve so dong loi
    int docFile(const std::string& tenFile)
    {
        std::ifstream file(tenFile);
        if (!file)
            throw std::runtime_error("khong mo duoc file " + tenFile);
        std::string dong;
        int soDongLoi = 0;
        while (std::getline(file, dong))
        {
            std::stringstream ss(dong);
            SinhVien sv;
            std::string diem;
            if (std::getline(ss, sv.ma, ',') && std::getline(ss, sv.hoTen, ',') && std::getline(ss, diem))
            {
                try
                {
                    sv.diem = docDiem(diem);
                    ds_.push_back(sv);
                    continue;
                }
                catch (const std::exception&) {}
            }
            dongLoi_.push_back(dong);          // giu lai dong loi, khong lam mat du lieu
            soDongLoi++;
        }
        return soDongLoi;
    }

    void ghiFile(const std::string& tenFile) const
    {
        std::ofstream file(tenFile);
        for (const SinhVien& sv : ds_)
            file << sv.ma << ',' << sv.hoTen << ',' << sv.diem << '\n';
        for (const std::string& dong : dongLoi_)       // ghi lai nguyen van de nguoi dung tu sua
            file << dong << '\n';
    }

    bool them(const SinhVien& sv)
    {
        if (tim(sv.ma) != nullptr)
            return false;                          // trung ma
        ds_.push_back(sv);
        return true;
    }

    bool xoa(const std::string& ma)
    {
        auto it = std::remove_if(ds_.begin(), ds_.end(), [&ma](const SinhVien& sv) { return sv.ma == ma; });
        if (it == ds_.end())
            return false;
        ds_.erase(it, ds_.end());
        return true;
    }

    SinhVien* tim(const std::string& ma)
    {
        auto it = std::find_if(ds_.begin(), ds_.end(), [&ma](const SinhVien& sv) { return sv.ma == ma; });
        return it == ds_.end() ? nullptr : &*it;
    }

    void sapXepTheoDiem()
    {
        std::sort(ds_.begin(), ds_.end(), [](const SinhVien& a, const SinhVien& b) { return a.diem > b.diem; });
    }

    void inBang(std::ostream& os) const
    {
        os << std::left << std::setw(8) << "Ma" << std::setw(22) << "Ho ten" << std::setw(6) << "Diem" << "Xep loai\n";
        for (const SinhVien& sv : ds_)
            os << std::setw(8) << sv.ma << std::setw(22) << sv.hoTen
               << std::setw(6) << std::fixed << std::setprecision(1) << sv.diem << xepLoai(sv.diem) << '\n';
    }

    void inThongKe(std::ostream& os) const
    {
        if (ds_.empty())
        {
            os << "Danh sach rong\n";
            return;
        }
        double tong = std::accumulate(ds_.begin(), ds_.end(), 0.0,
                                      [](double t, const SinhVien& sv) { return t + sv.diem; });
        os << "So sinh vien: " << ds_.size() << ", diem trung binh: "
           << std::fixed << std::setprecision(2) << tong / ds_.size() << '\n';
        for (const char* loai : {"Gioi", "Kha", "Trung binh", "Yeu"})
        {
            auto dem = std::count_if(ds_.begin(), ds_.end(), [loai](const SinhVien& sv) { return xepLoai(sv.diem) == loai; });
            os << "  " << loai << ": " << dem << '\n';
        }
    }

private:
    std::vector<SinhVien> ds_;
    std::vector<std::string> dongLoi_;
};

std::string nhap(const std::string& loiNhac)
{
    std::cout << loiNhac;
    std::string s;
    std::getline(std::cin, s);
    return s;
}

int main()
{
    const std::string FILE_DU_LIEU = "sinh_vien.csv";
    QuanLySinhVien ql;
    try
    {
        int loi = ql.docFile(FILE_DU_LIEU);
        std::cout << "Da doc " << FILE_DU_LIEU << (loi ? ", bo qua " + std::to_string(loi) + " dong loi" : "") << '\n';
    }
    catch (const std::exception& e)
    {
        std::cout << "Bat dau voi danh sach rong (" << e.what() << ")\n";
    }

    std::string chon;
    do
    {
        chon = nhap("\n1.Xem 2.Them 3.Sua diem 4.Xoa 5.Sap xep 6.Thong ke 7.Xuat bao cao 0.Luu va thoat\nChon: ");
        if (chon == "1")
            ql.inBang(std::cout);
        else if (chon == "2")
        {
            SinhVien sv;
            sv.ma = nhap("Ma: ");
            sv.hoTen = nhap("Ho ten: ");
            try
            {
                sv.diem = docDiem(nhap("Diem: "));
                std::cout << (ql.them(sv) ? "  Da them\n" : "  Trung ma sinh vien\n");
            }
            catch (const std::exception&)
            {
                std::cout << "  Diem khong hop le\n";
            }
        }
        else if (chon == "3")
        {
            SinhVien* sv = ql.tim(nhap("Ma: "));
            if (sv == nullptr)
                std::cout << "  Khong tim thay\n";
            else
            {
                try
                {
                    sv->diem = docDiem(nhap("Diem moi: "));
                    std::cout << "  Da sua diem cua " << sv->hoTen << '\n';
                }
                catch (const std::exception&)
                {
                    std::cout << "  Diem khong hop le\n";
                }
            }
        }
        else if (chon == "4")
            std::cout << (ql.xoa(nhap("Ma: ")) ? "  Da xoa\n" : "  Khong tim thay\n");
        else if (chon == "5")
        {
            ql.sapXepTheoDiem();
            std::cout << "  Da sap xep theo diem giam dan\n";
        }
        else if (chon == "6")
            ql.inThongKe(std::cout);
        else if (chon == "7")
        {
            std::ofstream baoCao("bao_cao.txt");
            ql.inBang(baoCao);
            ql.inThongKe(baoCao);
            std::cout << "  Da xuat bao_cao.txt\n";
        }
    } while (chon != "0");

    ql.ghiFile(FILE_DU_LIEU);
    std::cout << "Da luu " << FILE_DU_LIEU << '\n';
    return 0;
}

Kết quả chạy thử

Thêm sinh viên mới, sửa điểm sinh viên SV03, sắp xếp rồi xem bảng:

Chạy chương trình quản lý sinh viên: thêm, sửa điểm, sắp xếp theo điểm và xem bảng xếp loại

Lần chạy thứ hai: thống kê, thử nhập điểm 12 bị từ chối, xuất báo cáo, thoát. Dòng SV04 có điểm lỗi vẫn còn nguyên trong file sau khi lưu:

Thống kê xếp loại, từ chối điểm 12, xuất báo cáo và dòng lỗi vẫn giữ lại trong file

Hướng mở rộng

  • Tách code thành nhiều file: sinh_vien.h, quan_ly_sinh_vien.h/.cpp, main.cpp, như project thư viện.
  • Lưu điểm nhiều môn bằng std::map<std::string, double>, tính điểm trung bình có trọng số theo số tín chỉ.
  • Tìm kiếm theo tên gần đúng, không phân biệt hoa thường.
  • Viết các hàm kiểm thử nhỏ cho docDiem, xepLoai bằng assert để chạy tự động mỗi khi sửa code.

Kết

Project quản lý sinh viên C++ luyện đúng những kỹ năng mà phần mềm thật cần nhất: đọc ghi file an toàn, kiểm tra dữ liệu ở một chỗ duy nhất, không làm mất dữ liệu khi gặp dòng lỗi, và dùng thuật toán STL thay cho vòng lặp tự viết. Thiết kế hàm in nhận std::ostream& là một mẹo nhỏ nhưng rất đáng nhớ: viết một lần, in ra được cả màn hình lẫn file.

Muốn tra cứu chi tiết về std::sort và std::fstream, các bạn xem thêm tài liệu chuẩn trên cppreference.

Bài tiếp theo chúng ta chuyển sang một project vui hơn: làm game rắn săn mồi có đồ họa với thư viện SFML. Nếu thấy bài viết có ích, hãy chia sẻ cho bạn bè đang làm bài tập lớn nhé.

Rate this post

Để lại một bình luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *