Bài 18.1230 phút đọc
Project: chức năng, kiểm thử và hoàn thiện
Sau bài này bạn sẽ làm được
- Cài tìm kiếm tuyến tính theo tên và nhị phân theo mã số
- Dùng qsort với con trỏ hàm cho nhiều tiêu chí sắp xếp
- Kiểm tra mọi dữ liệu vào trước khi nhận
- Chạy qua danh sách nghiệm thu gồm biên dịch sạch, Valgrind và ASan
Thiết kế đã xong ở bài trước. Bài này viết phần người dùng nhìn thấy: nhập liệu chống được mọi kiểu gõ bậy, tìm kiếm, sắp xếp nhiều tiêu chí, và một danh sách nghiệm thu để biết lúc nào thì dự án thật sự xong.
#Đọc dữ liệu từ người dùng
ui.c, các hàm nhập
/* Doc mot so nguyen trong khoang, lap lai cho toi khi hop le.
Tra ve 0 neu het dau vao. */
int doc_so(const char *nhac, long min, long max, long *ra) {
char dem[64];
for (;;) {
printf("%s (%ld den %ld): ", nhac, min, max);
fflush(stdout); /* nhac khong xuong dong */
if (!doc_dong(dem, sizeof dem)) return 0;
char *het;
long v = strtol(dem, &het, 10);
if (het == dem) { /* khong doc duoc chu so nao */
printf(" Phai la mot so.\n");
continue;
}
while (isspace((unsigned char)*het)) ++het;
if (*het != '\0') { /* con rac o cuoi */
printf(" Co ky tu la sau con so.\n");
continue;
}
if (v < min || v > max) {
printf(" Phai trong khoang %ld den %ld.\n", min, max);
continue;
}
*ra = v;
return 1;
}
}
/* Doc diem dang "8.25" thanh so nguyen 825 */
int doc_diem(const char *nhac, uint16_t *ra) {
char dem[64];
for (;;) {
printf("%s (0.00 den 10.00): ", nhac);
fflush(stdout);
if (!doc_dong(dem, sizeof dem)) return 0;
unsigned nguyen = 0, le = 0;
int k = 0;
int so_muc = sscanf(dem, "%u.%2u%n", &nguyen, &le, &k);
if (so_muc < 1 || !chi_con_khoang_trang(dem + k)) {
printf(" Dang khong hop le. Vi du: 8.25\n");
continue;
}
if (so_muc == 1) le = 0;
unsigned tong = nguyen * 100u + le;
if (tong > 1000u) {
printf(" Diem toi da la 10.00\n");
continue;
}
*ra = (uint16_t)tong;
return 1;
}
}terminal
./ql-sinhvien
Chon (0 den 16): abc Phai la mot so. Chon (0 den 16): 99 Phai trong khoang 0 den 16. Chon (0 den 16): 3 xyz Co ky tu la sau con so. Chon (0 den 16): 1 --- Them sinh vien --- MSSV: 123 MSSV phai co dung 8 chu so. MSSV: 20210001 Ho ten: Ho ten khong duoc rong. Ho ten: Nguyen Van An Diem mon 1 (0.00 den 10.00): 12 Diem toi da la 10.00 Diem mon 1 (0.00 den 10.00): 8.25
#Tìm kiếm
quanly.c, tìm theo tên
#include <ctype.h>
#include <string.h>
/* So sanh chuoi con, khong phan biet hoa thuong.
Tra ve 1 neu kim nam trong dong co. */
static int chua_khong_phan_biet(const char *dong_co, const char *kim) {
size_t nk = strlen(kim);
if (nk == 0) return 1;
for (const char *p = dong_co; *p; ++p) {
size_t i = 0;
while (i < nk
&& tolower((unsigned char)p[i]) == tolower((unsigned char)kim[i]))
++i;
if (i == nk) return 1;
}
return 0;
}
/* Tim tuyen tinh, ghi chi so cac ket qua vao mang ra.
Tra ve so ket qua tim duoc. */
size_t ql_tim_ten(const QuanLy *q, const char *kim, size_t ra[], size_t toi_da) {
size_t k = 0;
for (size_t i = 0; i < q->n && k < toi_da; ++i)
if (chua_khong_phan_biet(q->ds[i].ho_ten, kim)) ra[k++] = i;
return k;
}quanly.c, tìm nhị phân theo mã
#include <stdlib.h>
static int ss_mssv(const void *a, const void *b) {
return strcmp(((const SinhVien *)a)->mssv, ((const SinhVien *)b)->mssv);
}
/* Chi goi duoc SAU KHI da sap xep theo mssv */
const SinhVien *ql_tim_nhi_phan(const QuanLy *q, const char *mssv) {
SinhVien khoa;
memset(&khoa, 0, sizeof khoa);
snprintf(khoa.mssv, sizeof khoa.mssv, "%s", mssv);
return bsearch(&khoa, q->ds, q->n, sizeof q->ds[0], ss_mssv);
}terminal
./thu-tim
bsearch 20210004 -> Pham Thi Dung bsearch 99999999 -> khong thay
#Sắp xếp bằng qsort
void qsort(void *co_so, size_t n, size_t co_phan_tu,
int (*so_sanh)(const void *, const void *));
/* Ham so sanh tra ve:
so am neu a dung truoc b
so 0 neu hai cai tuong duong
so duong neu a dung sau b */quanly.c, các hàm so sánh
#include <string.h>
static int ss_mssv(const void *a, const void *b) {
return strcmp(((const SinhVien *)a)->mssv, ((const SinhVien *)b)->mssv);
}
static int ss_ten(const void *a, const void *b) {
int k = strcmp(((const SinhVien *)a)->ho_ten,
((const SinhVien *)b)->ho_ten);
if (k != 0) return k;
return ss_mssv(a, b); /* pha the hoa */
}
static int ss_diem_giam(const void *a, const void *b) {
uint16_t x = sv_diem_tb_x100((const SinhVien *)a);
uint16_t y = sv_diem_tb_x100((const SinhVien *)b);
if (x != y) return (x < y) ? 1 : -1; /* GIAM dan */
return ss_mssv(a, b); /* pha the hoa */
}
void ql_sap_xep(QuanLy *q, TieuChi tc) {
static int (*const BANG[])(const void *, const void *) = {
[TC_MSSV] = ss_mssv,
[TC_TEN] = ss_ten,
[TC_DIEM_GIAM] = ss_diem_giam,
};
qsort(q->ds, q->n, sizeof q->ds[0], BANG[tc]);
q->da_sap_theo_mssv = (tc == TC_MSSV);
}terminal
./thu-sap-xep
theo mssv: 20210001 Nguyen Van An 8.25 20210002 Tran Thi Binh 7.10 20210003 Le Hoang Cuong 9.00 20210004 Pham Thi Dung 8.25 theo diem giam dan: 20210003 Le Hoang Cuong 9.00 20210001 Nguyen Van An 8.25 20210004 Pham Thi Dung 8.25 20210002 Tran Thi Binh 7.10
#Thống kê và hiển thị
quanly.c, thống kê theo xếp loại
typedef struct {
size_t theo_xep_loai[5]; /* Yeu, TB, Kha, Gioi, Xuat sac */
size_t tong;
uint16_t diem_cao_nhat, diem_thap_nhat;
uint32_t tong_diem; /* de tinh trung binh, u32 chong tran */
} ThongKe;
void ql_thong_ke(const QuanLy *q, ThongKe *tk) {
memset(tk, 0, sizeof *tk);
if (q->n == 0) return;
tk->diem_thap_nhat = 1000;
for (size_t i = 0; i < q->n; ++i) {
uint16_t tb = sv_diem_tb_x100(&q->ds[i]);
++tk->theo_xep_loai[muc_xep_loai(tb)];
++tk->tong;
tk->tong_diem += tb;
if (tb > tk->diem_cao_nhat) tk->diem_cao_nhat = tb;
if (tb < tk->diem_thap_nhat) tk->diem_thap_nhat = tb;
}
}ui.c, hiển thị có phân trang
#define MOI_TRANG 20
void ui_hien_thi(const QuanLy *q) {
if (q->n == 0) { printf("Danh sach trong.\n"); return; }
size_t so_trang = (q->n + MOI_TRANG - 1) / MOI_TRANG; /* lam tron len */
size_t trang = 0;
for (;;) {
size_t dau = trang * MOI_TRANG;
size_t cuoi = dau + MOI_TRANG < q->n ? dau + MOI_TRANG : q->n;
printf("\n%-10s %-24s %-10s %6s %-10s\n",
"MSSV", "HO TEN", "LOP", "TB", "XEP LOAI");
for (size_t i = dau; i < cuoi; ++i) {
uint16_t tb = sv_diem_tb_x100(&q->ds[i]);
printf("%-10s %-24s %-10s %3u.%02u %-10s\n",
q->ds[i].mssv, q->ds[i].ho_ten, q->ds[i].lop,
tb / 100u, tb % 100u, sv_xep_loai(&q->ds[i]));
}
printf("\nTrang %zu/%zu. [n] sau [p] truoc [q] thoat: ",
trang + 1, so_trang);
fflush(stdout);
char dem[16];
if (!doc_dong(dem, sizeof dem)) return;
if (dem[0] == 'q') return;
else if (dem[0] == 'n' && trang + 1 < so_trang) ++trang;
else if (dem[0] == 'p' && trang > 0) --trang;
}
}#Danh sách nghiệm thu
Biên dịch sạch
terminal
gcc -std=c11 -Wall -Wextra -Wpedantic -Wshadow -Wconversion -c *.c
(khong co dong nao)
# Nếu có cảnh báo thì sửa, đừng tắt cảnh báo
gcc -std=c11 -Wall -Wextra -c quanly.c
quanly.c:88:23: warning: comparison of integer expressions of different
signedness: 'long int' and 'size_t' [-Wsign-compare]
88 | for (long i = 0; i < q->n; ++i)
| ^Không rò rỉ, không lỗi bộ nhớ
terminal
valgrind --leak-check=full --show-leak-kinds=all ./ql-sinhvien du-lieu.svdb < kich-ban.txt
==12345== HEAP SUMMARY: ==12345== in use at exit: 0 bytes in 0 blocks ==12345== total heap usage: 42 allocs, 42 frees, 128,304 bytes allocated ==12345== ==12345== All heap blocks were freed -- no leaks are possible ==12345== ERROR SUMMARY: 0 errors from 0 contexts
# ASan bắt được những lỗi Valgrind bỏ sót, và ngược lại. Chạy cả hai.
gcc -std=c11 -g -fsanitize=address,undefined -o ql-asan *.c && ./ql-asan du-lieu.svdb < kich-ban.txt
(khong co dong nao)
Kịch bản kiểm thử
kich-ban.txt
1
20210001
Nguyen Van An
CNTT01
2003
M
8.25
7.50
9.00
8.00
6.75
1
20210001
Nguyen Van An
CNTT01
2003
M
8.00
8.00
8.00
8.00
8.00
5
q
13
0terminal
./ql-sinhvien du-lieu.svdb < kich-ban.txt
Da them 20210001 Khong them duoc: MSSV da ton tai MSSV HO TEN LOP TB XEP LOAI 20210001 Nguyen Van An CNTT01 7.90 Kha Trang 1/1. Da luu 1 sinh vien vao du-lieu.svdb
Danh sách đầy đủ
| Hạng mục | Bắt buộc | Cách kiểm |
|---|---|---|
| Không cảnh báo với -Wall -Wextra -Wpedantic | Có | Biên dịch lại từ đầu, đọc hết đầu ra |
| Valgrind không rò rỉ, không lỗi | Có | Chạy với kịch bản dài đi qua mọi chức năng |
| ASan và UBSan không báo gì | Có | Dịch riêng một bản rồi chạy cùng kịch bản |
| Mọi giá trị trả về của fopen, malloc, realloc được kiểm | Có | grep tìm từng lời gọi, đọc dòng ngay sau nó |
| Lưu rồi tải lại cho ra dữ liệu giống hệt | Có | Ghi, tải, ghi lần hai, so hai tệp bằng cmp |
| Xuất CSV rồi nhập lại cho ra dữ liệu giống hệt | Có | Như trên |
| Tệp cắt cụt bị từ chối có thông báo | Có | truncate -s 100 rồi tải |
| Tệp sai số nhận dạng bị từ chối | Có | Thử tải một tệp ảnh |
| Danh sách rỗng không làm sập chức năng nào | Có | Chạy hết mười sáu chức năng khi chưa thêm ai |
| Thoát mà chưa lưu thì được hỏi | Có | Thêm một sinh viên rồi chọn 0 |
| README nói rõ cách build và định dạng tệp | Có | Nhờ người khác làm theo README |
| Chia thành ít nhất năm tệp nguồn | Có | ls *.c |
Tự làm thử
- Cài
doc_dongvàdoc_so, rồi thử gõabc,99,3 xyzvà một dòng dài hai trăm ký tự. - Viết ba hàm so sánh và kiểm tra kết quả sắp xếp ổn định qua nhiều lần chạy.
- Viết hàm so sánh bằng phép trừ với hai giá trị gần biên của
intvà xác nhận thứ tự sai. - Chạy mọi chức năng khi danh sách rỗng và sửa mọi chỗ sập.
- Viết một tệp kịch bản đi qua cả mười sáu chức năng, chạy dưới Valgrind, và đạt không rò rỉ.
- Lưu, tải, lưu lần hai, rồi so hai tệp bằng
cmpvà xác nhận chúng giống hệt nhau.
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
- Mọi lần nhập đều qua
fgetsrồi phân tích, không bao giờ dùng thẳngscanftrong vòng lặp menu. strtolcùng con trỏhetphân biệt được số 0 với chuỗi rác, điều màatoikhông làm được.- Hàm so sánh cho
qsortphải dùng(x > y) - (x < y)chứ không dùng phép trừ, và phải có khóa phụ duy nhất để phá thế hòa. bsearchtrên mảng chưa sắp xếp trả về kết quả sai mà không báo lỗi, nên phải theo dõi trạng thái sắp xếp.- Dự án chỉ xong khi qua hết danh sách nghiệm thu, và ba kịch bản hay bị quên là danh sách rỗng, đúng một phần tử, và người dùng đóng đầu vào.