Bài 18.1130 phút đọc
Project: thiết kế hệ quản lý sinh viên
Sau bài này bạn sẽ làm được
- Chia dự án thành năm tầng có ranh giới rõ ràng
- Thiết kế struct sinh viên và bộ quản lý mảng động
- Viết tầng lưu trữ hỗ trợ cả tệp nhị phân và CSV
- Đặt quy ước mã lỗi dùng chung cho cả dự án
Đây là dự án lớn đầu tiên của khóa học, và nó dùng gần như mọi thứ từ Phần 0 tới Phần 8. Bài này thiết kế, bài sau viết chức năng. Hãy đọc hết thiết kế trước khi gõ dòng mã đầu tiên.
#Đề bài
terminal
./ql-sinhvien du-lieu.svdb
========= QUAN LY SINH VIEN =========
1. Them sinh vien 9. Thong ke theo lop
2. Xoa sinh vien 10. Thong ke theo xep loai
3. Sua thong tin 11. Top N diem cao nhat
4. Tim kiem 12. Sinh vien bi canh bao
5. Hien thi (co phan trang) 13. Luu file
6. Sap xep theo MSSV 14. Tai file
7. Sap xep theo diem TB 15. Xuat CSV
8. Sap xep theo ten 16. Nhap CSV
0. Thoat
Da tai 247 sinh vien tu du-lieu.svdb
Chon: | Yêu cầu | Dùng kiến thức của bài nào |
|---|---|
| Mảng động, không giới hạn cứng số sinh viên | Bài 14.8 realloc |
| Lưu nhị phân có phần đầu và mã kiểm tra | Bài 18.5 và 18.8 |
| Xuất và nhập CSV | Bài 18.9 |
| Ghi an toàn bằng tệp tạm rồi đổi tên | Bài 18.10 |
| Tìm tuyến tính theo tên, nhị phân theo mã | Bài 26.1 và 26.2 |
| Sắp xếp nhiều tiêu chí bằng qsort | Bài 13.5 con trỏ hàm |
| Kiểm tra mọi dữ liệu vào | Bài 11.3 và Bài 18.4 |
| Không rò rỉ bộ nhớ, chạy sạch Valgrind | Bài 14.10 |
| Chia nhiều tệp nguồn | Chuẩn bị cho Chương 20 |
#Năm tầng
main.c chi phan tich tham so dong lenh va goi vong lap menu
|
ui.c hien thi menu, doc lua chon, doc du lieu tu nguoi dung
| KHONG biet du lieu duoc luu the nao
quanly.c mang dong, them, xoa, sua, tim, sap xep, thong ke
| KHONG biet gi ve tep, KHONG in ra man hinh
luutru.c doc ghi nhi phan
csv.c doc ghi CSV
| KHONG biet gi ve mang dong, chi lam viec voi mang thuong
sinhvien.c mot sinh vien: kiem tra hop le, so sanh, dinh dang
KHONG phu thuoc gi ca| Tệp | Được gọi bởi | Được phép dùng | Bị cấm |
|---|---|---|---|
| sinhvien.c | Mọi tầng | string.h, ctype.h | Không in ra màn hình, không đụng tệp |
| quanly.c | ui.c | sinhvien.h, stdlib.h | Không đụng tệp, không in ra màn hình |
| luutru.c, csv.c | ui.c | sinhvien.h, stdio.h | Không biết QuanLy, chỉ nhận mảng và số lượng |
| ui.c | main.c | Tất cả | Không chứa logic nghiệp vụ |
| main.c | Hệ điều hành | ui.h | Không quá năm mươi dòng |
loi.h, dùng chung cho cả dự án
#ifndef LOI_H
#define LOI_H
typedef enum {
QL_OK = 0,
/* Du lieu vao */
QL_MSSV_SAI,
QL_TEN_RONG,
QL_DIEM_NGOAI_KHOANG,
QL_NAM_SINH_SAI,
QL_TRUNG_MSSV,
QL_KHONG_TIM_THAY,
/* Bo nho */
QL_HET_BO_NHO,
/* Tep */
QL_LOI_MO_TEP,
QL_LOI_GHI,
QL_LOI_DOC,
QL_TEP_HONG,
QL_SAI_PHIEN_BAN,
} MaLoi;
const char *ql_mo_ta_loi(MaLoi m);
#endif#Cấu trúc dữ liệu
sinhvien.h
#ifndef SINHVIEN_H
#define SINHVIEN_H
#include <stdint.h>
#include "loi.h"
#define SV_MSSV_CO 16
#define SV_TEN_CO 64
#define SV_LOP_CO 16
#define SV_SO_MON 5
typedef struct {
char mssv[SV_MSSV_CO];
char ho_ten[SV_TEN_CO];
char lop[SV_LOP_CO];
uint16_t nam_sinh;
char gioi_tinh; /* 'M' hoac 'F' */
uint16_t diem_x100[SV_SO_MON]; /* 8.25 luu la 825 */
uint8_t con_hoat_dong; /* 0 la da xoa mem */
} SinhVien;
/* Tinh tu diem_x100, khong luu vao tep */
uint16_t sv_diem_tb_x100(const SinhVien *sv);
const char *sv_xep_loai(const SinhVien *sv);
/* Kiem tra hop le, tra ve QL_OK hoac ma loi cu the */
MaLoi sv_kiem_mssv(const char *s);
MaLoi sv_kiem_ten(const char *s);
MaLoi sv_kiem_diem(uint16_t diem_x100);
MaLoi sv_kiem_nam_sinh(uint16_t nam);
MaLoi sv_kiem_tat_ca(const SinhVien *sv);
#endifsinhvien.c, phần tính điểm
#include "sinhvien.h"
uint16_t sv_diem_tb_x100(const SinhVien *sv) {
uint32_t tong = 0;
for (int i = 0; i < SV_SO_MON; ++i) tong += sv->diem_x100[i];
/* Lam tron toi so gan nhat thay vi cat bo */
return (uint16_t)((tong + SV_SO_MON / 2) / SV_SO_MON);
}
const char *sv_xep_loai(const SinhVien *sv) {
uint16_t tb = sv_diem_tb_x100(sv);
if (tb >= 900) return "Xuat sac";
if (tb >= 800) return "Gioi";
if (tb >= 650) return "Kha";
if (tb >= 500) return "Trung binh";
return "Yeu";
}sinhvien.c, phần kiểm tra
#include <ctype.h>
#include <string.h>
MaLoi sv_kiem_mssv(const char *s) {
size_t n = strlen(s);
if (n != 8) return QL_MSSV_SAI; /* dung 8 chu so */
for (size_t i = 0; i < n; ++i)
if (!isdigit((unsigned char)s[i])) return QL_MSSV_SAI;
return QL_OK;
}
MaLoi sv_kiem_ten(const char *s) {
size_t n = strlen(s);
if (n == 0 || n >= SV_TEN_CO) return QL_TEN_RONG;
/* Khong duoc toan khoang trang */
for (size_t i = 0; i < n; ++i)
if (!isspace((unsigned char)s[i])) return QL_OK;
return QL_TEN_RONG;
}
MaLoi sv_kiem_diem(uint16_t d) {
return d <= 1000 ? QL_OK : QL_DIEM_NGOAI_KHOANG; /* 0.00 den 10.00 */
}
MaLoi sv_kiem_nam_sinh(uint16_t nam) {
return (nam >= 1950 && nam <= 2020) ? QL_OK : QL_NAM_SINH_SAI;
}
MaLoi sv_kiem_tat_ca(const SinhVien *sv) {
MaLoi m;
if ((m = sv_kiem_mssv(sv->mssv)) != QL_OK) return m;
if ((m = sv_kiem_ten(sv->ho_ten)) != QL_OK) return m;
if ((m = sv_kiem_nam_sinh(sv->nam_sinh)) != QL_OK) return m;
for (int i = 0; i < SV_SO_MON; ++i)
if ((m = sv_kiem_diem(sv->diem_x100[i])) != QL_OK) return m;
if (sv->gioi_tinh != 'M' && sv->gioi_tinh != 'F') return QL_MSSV_SAI;
return QL_OK;
}#Bộ quản lý mảng động
quanly.h
#ifndef QUANLY_H
#define QUANLY_H
#include <stddef.h>
#include "sinhvien.h"
typedef struct {
SinhVien *ds;
size_t n; /* so phan tu dang dung */
size_t suc_chua; /* so phan tu da cap phat */
int da_thay_doi; /* co bao can luu */
} QuanLy;
void ql_khoi_tao(QuanLy *q);
void ql_giai_phong(QuanLy *q);
MaLoi ql_them(QuanLy *q, const SinhVien *sv);
MaLoi ql_xoa(QuanLy *q, const char *mssv);
MaLoi ql_sua(QuanLy *q, const char *mssv, const SinhVien *moi);
long ql_tim_mssv(const QuanLy *q, const char *mssv); /* -1 neu khong thay */
#endifquanly.c
#include <stdlib.h>
#include <string.h>
#include "quanly.h"
#define SUC_CHUA_DAU 16
void ql_khoi_tao(QuanLy *q) {
q->ds = NULL;
q->n = 0;
q->suc_chua = 0;
q->da_thay_doi = 0;
}
void ql_giai_phong(QuanLy *q) {
free(q->ds);
ql_khoi_tao(q); /* dat lai NULL, tranh con tro treo */
}
static MaLoi bao_dam_cho(QuanLy *q, size_t can) {
if (can <= q->suc_chua) return QL_OK;
size_t moi = q->suc_chua ? q->suc_chua * 2 : SUC_CHUA_DAU;
while (moi < can) moi *= 2;
if (moi > SIZE_MAX / sizeof *q->ds) return QL_HET_BO_NHO; /* chong tran */
SinhVien *tam = realloc(q->ds, moi * sizeof *q->ds);
if (!tam) return QL_HET_BO_NHO; /* q->ds VAN CON NGUYEN */
q->ds = tam;
q->suc_chua = moi;
return QL_OK;
}
MaLoi ql_them(QuanLy *q, const SinhVien *sv) {
MaLoi m = sv_kiem_tat_ca(sv);
if (m != QL_OK) return m;
if (ql_tim_mssv(q, sv->mssv) >= 0) return QL_TRUNG_MSSV;
if ((m = bao_dam_cho(q, q->n + 1)) != QL_OK) return m;
q->ds[q->n] = *sv; /* sao chep ca struct, khong con tro */
q->ds[q->n].con_hoat_dong = 1;
++q->n;
q->da_thay_doi = 1;
return QL_OK;
}
long ql_tim_mssv(const QuanLy *q, const char *mssv) {
for (size_t i = 0; i < q->n; ++i)
if (q->ds[i].con_hoat_dong && strcmp(q->ds[i].mssv, mssv) == 0)
return (long)i;
return -1;
}
MaLoi ql_xoa(QuanLy *q, const char *mssv) {
long i = ql_tim_mssv(q, mssv);
if (i < 0) return QL_KHONG_TIM_THAY;
/* Xoa that: don cac phan tu sau len mot o */
memmove(&q->ds[i], &q->ds[i + 1], (q->n - (size_t)i - 1) * sizeof *q->ds);
--q->n;
q->da_thay_doi = 1;
return QL_OK;
}#Tầng lưu trữ hai định dạng
luutru.h
#ifndef LUUTRU_H
#define LUUTRU_H
#include "sinhvien.h"
/* Ca hai tang deu lam viec voi MANG THUONG, khong biet gi ve QuanLy.
Nho vay chung kiem thu duoc rieng, va dung lai duoc o cho khac. */
MaLoi lt_ghi_nhi_phan(const char *ten, const SinhVien *ds, size_t n);
MaLoi lt_doc_nhi_phan(const char *ten, SinhVien **ra_ds, size_t *ra_n);
MaLoi csv_ghi(const char *ten, const SinhVien *ds, size_t n);
MaLoi csv_doc(const char *ten, SinhVien **ra_ds, size_t *ra_n,
size_t *ra_so_dong_hong);
#endif| Tệp .svdb | Tệp .csv | |
|---|---|---|
| Vai trò | Định dạng chính, dùng khi lưu và tải | Trao đổi với người và với Excel |
| Kích thước 1000 sinh viên | khoảng 110 KB | khoảng 75 KB |
| Giữ đúng điểm | Có, số nguyên | Có, nếu ghi dạng 8.25 |
| Người sửa tay được | Không | Có |
| Phát hiện hỏng | Có, nhờ mã kiểm tra | Không |
| Đọc thiếu cột | Không xảy ra | Phải kiểm số cột từng dòng |
Bố cục tệp nhị phân
Phan dau, 16 byte:
0 magic[4] "SVDB"
4 phien_ban u32, hien tai la 1
8 so_ban_ghi u32
12 ma_kiem u32, FNV-1a tren toan bo vung du lieu
Moi ban ghi, 112 byte:
0 mssv[16]
16 ho_ten[64]
80 lop[16]
96 nam_sinh u16
98 gioi_tinh 1 byte
99 con_hoat_dong 1 byte
100 diem_x100[5] 5 x u16 = 10 byte
110 du_tru[2] 2 byte, luon bang 0
Tong: 16 + n * 112 byte.
Truong du_tru khong dung toi hom nay, nhung no cho phep
them mot truong u16 o phien ban sau MA KHONG doi kich thuoc
ban ghi. Hai byte re hon mot lan doi dinh dang rat nhieu.
Chu y thu tu: trong TEP thi con_hoat_dong nam ngay sau
gioi_tinh, con trong BO NHO thi no nam o cuoi struct.
Hai bo cuc nay khac nhau, xem phan duoi.luutru.c, hàm ghi
#include <stdio.h>
#include <string.h>
#include "luutru.h"
#define MAGIC "SVDB"
#define PHIEN_BAN 1u
#define CO_PHAN_DAU 16u
#define CO_BAN_GHI 112u
static int ghi_u16(FILE *f, uint16_t v) {
unsigned char b[2] = { (unsigned char)(v >> 8), (unsigned char)v };
return fwrite(b, 1, 2, f) == 2 ? 0 : -1;
}
static int ghi_u32(FILE *f, uint32_t v) {
unsigned char b[4] = {
(unsigned char)(v >> 24), (unsigned char)(v >> 16),
(unsigned char)(v >> 8), (unsigned char)(v ),
};
return fwrite(b, 1, 4, f) == 4 ? 0 : -1;
}
static int ghi_mot(FILE *f, const SinhVien *sv) {
unsigned char du_tru[2] = { 0, 0 };
if (fwrite(sv->mssv, 1, SV_MSSV_CO, f) != SV_MSSV_CO) return -1;
if (fwrite(sv->ho_ten, 1, SV_TEN_CO, f) != SV_TEN_CO) return -1;
if (fwrite(sv->lop, 1, SV_LOP_CO, f) != SV_LOP_CO) return -1;
if (ghi_u16(f, sv->nam_sinh) != 0) return -1;
if (fputc(sv->gioi_tinh, f) == EOF) return -1;
if (fputc(sv->con_hoat_dong, f) == EOF) return -1;
for (int i = 0; i < SV_SO_MON; ++i)
if (ghi_u16(f, sv->diem_x100[i]) != 0) return -1;
return fwrite(du_tru, 1, 2, f) == 2 ? 0 : -1;
}
MaLoi lt_ghi_nhi_phan(const char *ten, const SinhVien *ds, size_t n) {
char tam[512];
int k = snprintf(tam, sizeof tam, "%s.tmp", ten);
if (k < 0 || (size_t)k >= sizeof tam) return QL_LOI_GHI;
FILE *f = fopen(tam, "wb");
if (!f) return QL_LOI_MO_TEP;
MaLoi kq = QL_LOI_GHI;
if (fwrite(MAGIC, 1, 4, f) != 4) goto xong;
if (ghi_u32(f, PHIEN_BAN) != 0) goto xong;
if (ghi_u32(f, (uint32_t)n) != 0) goto xong;
if (ghi_u32(f, 0u) != 0) goto xong; /* ma kiem, sua sau */
uint32_t h = 2166136261u;
for (size_t i = 0; i < n; ++i) {
if (ghi_mot(f, &ds[i]) != 0) goto xong;
h = ma_kiem_ban_ghi(&ds[i], h);
}
if (fseek(f, 12L, SEEK_SET) != 0) goto xong;
if (ghi_u32(f, h) != 0) goto xong;
kq = QL_OK;
xong:
if (fclose(f) != 0) kq = QL_LOI_GHI;
if (kq != QL_OK) { remove(tam); return kq; }
remove(ten);
return rename(tam, ten) == 0 ? QL_OK : QL_LOI_GHI;
}Tự làm thử
- Viết
sinhvien.hvàsinhvien.cđầy đủ, rồi viết mộtmainnhỏ kiểm thử riêng các hàm kiểm tra hợp lệ. - Cài
bao_dam_chovà thêm mười nghìn sinh viên, in rasuc_chuasau mỗi lần nó đổi để thấy dãy nhân đôi. - Cố ý trả về NULL từ
reallocbằng cách đặt giới hạn bộ nhớ, và xác nhận danh sách cũ không mất. - In
sizeof(SinhVien)trên máy bạn và so với 112, giải thích chênh lệch. - Viết
lt_ghi_nhi_phanrồi mở tệp kết quả bằngxxdvà tìm ra từng trường của bản ghi đầu tiên.
Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.
Tóm tắt
- Năm tầng với ranh giới rõ ràng: tầng dữ liệu trả mã lỗi, chỉ tầng giao diện in ra màn hình.
- Mảng ký tự cố định thay cho con trỏ, và điểm lưu dạng số nguyên nhân một trăm.
- Điểm trung bình được tính chứ không lưu, nên nó không bao giờ lệch với dữ liệu gốc.
reallocluôn qua biến trung gian, và luôn kiểm tràn trước khi nhân sức chứa với kích thước phần tử.- Kích thước bản ghi trong tệp là hằng số tường minh 112, khác với
sizeof(SinhVien)trong bộ nhớ.