Bỏ qua điều hướng, tới nội dung chính
Học C
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ầuDù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ênBài 14.8 realloc
Lưu nhị phân có phần đầu và mã kiểm traBài 18.5 và 18.8
Xuất và nhập CSVBài 18.9
Ghi an toàn bằng tệp tạm rồi đổi tênBà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 qsortBài 13.5 con trỏ hàm
Kiểm tra mọi dữ liệu vàoBài 11.3 và Bài 18.4
Không rò rỉ bộ nhớ, chạy sạch ValgrindBài 14.10
Chia nhiều tệp nguồnChuẩ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ùngBị cấm
sinhvien.cMọi tầngstring.h, ctype.hKhông in ra màn hình, không đụng tệp
quanly.cui.csinhvien.h, stdlib.hKhông đụng tệp, không in ra màn hình
luutru.c, csv.cui.csinhvien.h, stdio.hKhông biết QuanLy, chỉ nhận mảng và số lượng
ui.cmain.cTất cảKhông chứa logic nghiệp vụ
main.cHệ điều hànhui.hKhô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);

#endif
sinhvien.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 */

#endif
quanly.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 .svdbTệp .csv
Vai tròĐịnh dạng chính, dùng khi lưu và tảiTrao đổi với người và với Excel
Kích thước 1000 sinh viênkhoảng 110 KBkhoảng 75 KB
Giữ đúng điểmCó, số nguyênCó, nếu ghi dạng 8.25
Người sửa tay đượcKhôngCó
Phát hiện hỏngCó, nhờ mã kiểm traKhông
Đọc thiếu cộtKhông xảy raPhả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ử

  1. Viết sinhvien.h và sinhvien.c đầy đủ, rồi viết một main nhỏ kiểm thử riêng các hàm kiểm tra hợp lệ.
  2. Cài bao_dam_cho và thêm mười nghìn sinh viên, in ra suc_chua sau mỗi lần nó đổi để thấy dãy nhân đôi.
  3. Cố ý trả về NULL từ realloc bằng cách đặt giới hạn bộ nhớ, và xác nhận danh sách cũ không mất.
  4. In sizeof(SinhVien) trên máy bạn và so với 112, giải thích chênh lệch.
  5. Viết lt_ghi_nhi_phan rồi mở tệp kết quả bằng xxd và 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.
  • realloc luô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ớ.