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++
- Đọc danh sách từ
sinh_vien.csv, mỗi dòngma,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. - 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.
- 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.
- 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.
Dữ liệu mẫu, dòng SV04 cố ý có điểm sai:
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::getlinevới dấu phẩy, như bài đọc ghi file. Ba lầngetlinenố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àodongLoi_nguyên văn. KhighiFile, các dòng lỗi được ghi lại ở cuối file. - File không mở được thì ném ngoại lệ,
mainbắ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;
}
themkiểm tra trùng mã bằngtimtrước khi thêm.xoadùng cặpstd::remove_if+erase, gọi là erase-remove idiom:remove_ifdồn các phần tử cần giữ lên đầu và trả về vị trí bắt đầu phần “rác”,erasexóa hẳn phần đó. Đây là cách chuẩn để xóa phần tử theo điều kiện khỏi vector.timtrả về con trỏ tới sinh viên, không có thì trả vềnullptrnhư 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::sortvớ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::leftcăn trái, nhờ vậy bảng thẳng hàng.std::fixedvàstd::setprecision(1)in điểm với đúng 1 chữ số thập phân.inBangvàinThongKenhậnstd::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::accumulatetí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:
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:
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,xepLoaibằngassertđể 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é.





