Bài 20.124 phút đọc
Chia dự án thành nhiều tệp
Sau bài này bạn sẽ làm được
- Nêu ba dấu hiệu cho biết đã tới lúc tách tệp
- Bố trí thư mục cho dự án nhỏ và dự án vừa
- Quyết định thứ nào vào .h và thứ nào vào .c
- Đặt tên tệp và tiền tố hàm theo module
Dự án Phần 8 của bạn có lẽ đã hơn tám trăm dòng trong một tệp. Nó vẫn chạy, nhưng mỗi lần sửa một chỗ bạn phải cuộn qua bốn trăm dòng không liên quan. Bài này chỉ ra cách cắt nó ra, và quan trọng hơn là cắt ở đâu.
#Khi nào nên tách
| Dấu hiệu | Vì sao nó là dấu hiệu |
|---|---|
| Tệp vượt khoảng năm trăm dòng | Không nhìn hết được bằng vài lần cuộn, phải dùng tìm kiếm |
| Bạn phải cuộn qua nhóm hàm không liên quan để tới nơi cần sửa | Trong tệp đang có ít nhất hai nhóm chức năng tách được |
| Sửa một dòng thì phải dịch lại cả chương trình | Với dự án lớn thì đây là chi phí thật, xem Bài 20.4 |
| Muốn kiểm thử một phần mà phải kéo theo cả main | Chưa có ranh giới nào để cô lập phần đó |
| Hai người cùng sửa một tệp | Xung đột hợp nhất liên tục |
#Bố trí thư mục
Dự án nhỏ: một thư mục
du-an/
├── main.c
├── sinhvien.c
├── sinhvien.h
├── quanly.c
├── quanly.h
├── luutru.c
├── luutru.h
├── Makefile
└── README.mdDự án vừa: tách nguồn khỏi giao diện
du-an/
├── src/
│ ├── main.c
│ ├── sinhvien.c
│ ├── quanly.c
│ ├── luutru.c
│ └── ui.c
├── include/
│ ├── sinhvien.h
│ ├── quanly.h
│ ├── luutru.h
│ ├── ui.h
│ └── loi.h
├── tests/
│ ├── kiem-sinhvien.c
│ └── kiem-quanly.c
├── build/ (sinh ra, dua vao .gitignore)
├── Makefile
├── README.md
└── .gitignore| Cách | Số tệp hợp lý | Ưu | Nhược |
|---|---|---|---|
| Cùng thư mục | Dưới khoảng 12 | Đơn giản, không cần cờ -I | Rối khi nhiều tệp, khó biết header nào công khai |
| src và include tách riêng | Trên 10 | Rõ header nào là giao diện, dễ đóng gói thành thư viện | Phải thêm -Iinclude, và sửa một module phải mở hai thư mục |
| Mỗi module một thư mục con | Trên khoảng 40 | Chịu được dự án lớn | Quá nặng cho dự án học tập |
#Cái gì vào .h, cái gì vào .c
Giao diện và cài đặt
Tệp
.h là giao diện: nó nói người khác dùng module này như thế nào. Tệp .c là cài đặt: nó nói module làm việc đó ra sao. Người dùng module chỉ cần đọc .h.| Thứ | Vào .h | Vào .c |
|---|---|---|
| Kiểu dữ liệu công khai | Có | Không |
| Kiểu dữ liệu chỉ dùng nội bộ | Không | Có |
| Hằng số công khai, enum | Có | Không |
| Nguyên mẫu hàm công khai | Có | Không |
| Thân hàm công khai | Không | Có |
| Hàm nội bộ, khai báo static | Không | Có, cả nguyên mẫu lẫn thân |
| Khai báo biến toàn cục, có extern | Có | Không |
| Định nghĩa biến toàn cục | Không | Có, đúng một lần |
| Biến static cấp tệp | Không | Có |
| Hàm nhỏ static inline | Có | Không |
| #include mà chính header cần | Có | Không |
| #include mà chỉ cài đặt cần | Không | Có |
quanly.h, chỉ có giao diện
#ifndef QUANLY_H
#define QUANLY_H
#include <stddef.h>
#include "loi.h"
#include "sinhvien.h"
typedef struct {
SinhVien *ds;
size_t n, suc_chua;
int da_thay_doi;
} 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);
long ql_tim_mssv(const QuanLy *q, const char *mssv);
#endif /* QUANLY_H */quanly.c, cài đặt
#include <stdlib.h> /* chi CAI DAT can realloc va free */
#include <string.h> /* chi CAI DAT can strcmp va memmove */
#include "quanly.h"
#define SUC_CHUA_DAU 16 /* hang so noi bo, khong dua vao .h */
/* Ham noi bo: static, va KHONG co trong .h */
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;
SinhVien *tam = realloc(q->ds, moi * sizeof *q->ds);
if (!tam) return QL_HET_BO_NHO;
q->ds = tam;
q->suc_chua = moi;
return QL_OK;
}
void ql_khoi_tao(QuanLy *q) {
q->ds = NULL;
q->n = q->suc_chua = 0;
q->da_thay_doi = 0;
}
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;
q->da_thay_doi = 1;
return QL_OK;
}
/* ... cac ham con lai ... */#Đặt tên tệp và tiền tố hàm
| Module | Tệp | Tiền tố hàm | Ví dụ |
|---|---|---|---|
| Một sinh viên | sinhvien.c, sinhvien.h | sv_ | sv_kiem_mssv |
| Danh sách | quanly.c, quanly.h | ql_ | ql_them |
| Lưu trữ nhị phân | luutru.c, luutru.h | lt_ | lt_ghi_nhi_phan |
| CSV | csv.c, csv.h | csv_ | csv_tach |
| Giao diện | ui.c, ui.h | ui_ | ui_hien_thi |
#Tách ở đâu
Liệt kê mọi hàm trong tệp lớn
terminalgrep -n '^[a-zA-Z].*(' main.c | head -2023:int kiem_mssv(const char *s) 34:int kiem_ten(const char *s) 45:float diem_tb(const SinhVien *sv) 56:const char *xep_loai(const SinhVien *sv) 70:int them(SinhVien **ds, size_t *n, ...) 95:int xoa(SinhVien *ds, size_t *n, const char *mssv) 112:long tim_mssv(const SinhVien *ds, size_t n, ...) 130:int luu_file(const char *ten, ...) 180:int tai_file(const char *ten, ...) 230:void in_menu(void) 245:void in_bang(const SinhVien *ds, size_t n)
Nhóm chúng theo dữ liệu mà chúng chạm vào
Chi cham vao MOT SinhVien: kiem_mssv, kiem_ten, diem_tb, xep_loai -> sinhvien.c Cham vao MANG sinh vien: them, xoa, tim_mssv, sap_xep, thong_ke -> quanly.c Cham vao TEP: luu_file, tai_file, xuat_csv, nhap_csv -> luutru.c va csv.c Cham vao MAN HINH va BAN PHIM: in_menu, in_bang, doc_so, doc_dong -> ui.cKiểm tra hướng phụ thuộc
quanly.cgọi hàm củasinhvien.c, không có chiều ngược lại.ui.cgọi mọi thứ, không ai gọi nó. Nếu tìm thấy hai module gọi lẫn nhau thì nhóm chưa đúng, xem Bài 20.5.Tách từng module một, build lại sau mỗi bước
Đừng tách cả năm module rồi mới thử biên dịch. Tách
sinhvientrước, build, chạy. Rồi mới tớiquanly. Bài 20.6 sẽ đi qua toàn bộ quy trình.
Tự làm thử
- Chạy
grep -n "^[a-zA-Z].*(" main.ctrên dự án Phần 8 và liệt kê mọi hàm. - Nhóm chúng theo dữ liệu mà chúng chạm vào, và đếm xem có mấy nhóm.
- Với mỗi nhóm, viết ra tệp
.hchỉ có nguyên mẫu, chưa cần viết.c. - Đếm số dòng của mỗi header và so với số dòng tệp
.ctương ứng. - Viết tệp
.gitignorevàREADME.mdcho dự án của bạ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
- Tách khi có lý do cụ thể, không tách vì nguyên tắc. Một tệp hai trăm dòng làm một việc thì cứ để nguyên.
- Tệp
.hlà giao diện, tệp.clà cài đặt. Người dùng module chỉ cần đọc.h. - Mỗi tệp
.cphải include header của chính nó, để trình biên dịch bắt được nguyên mẫu lệch với định nghĩa. - C không có không gian tên, nên mọi hàm công khai phải có tiền tố module.
- Nhóm hàm theo dữ liệu mà chúng chạm vào, không theo loại việc chúng làm.