Bỏ qua điều hướng, tới nội dung chính
Học C
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ệuVì sao nó là dấu hiệu
Tệp vượt khoảng năm trăm dòngKhô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ửaTrong 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ìnhVớ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ả mainChưa có ranh giới nào để cô lập phần đó
Hai người cùng sửa một tệpXung độ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.md

Dự á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áchSố tệp hợp lýƯuNhược
Cùng thư mụcDưới khoảng 12Đơn giản, không cần cờ -IRối khi nhiều tệp, khó biết header nào công khai
src và include tách riêngTrên 10Rõ header nào là giao diện, dễ đóng gói thành thư việnPhả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 conTrên khoảng 40Chịu được dự án lớnQuá 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 .hVào .c
Kiểu dữ liệu công khaiCóKhông
Kiểu dữ liệu chỉ dùng nội bộKhôngCó
Hằng số công khai, enumCóKhông
Nguyên mẫu hàm công khaiCóKhông
Thân hàm công khaiKhôngCó
Hàm nội bộ, khai báo staticKhôngCó, cả nguyên mẫu lẫn thân
Khai báo biến toàn cục, có externCóKhông
Định nghĩa biến toàn cụcKhôngCó, đúng một lần
Biến static cấp tệpKhôngCó
Hàm nhỏ static inlineCóKhông
#include mà chính header cầnCóKhông
#include mà chỉ cài đặt cầnKhôngCó
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

ModuleTệpTiền tố hàmVí dụ
Một sinh viênsinhvien.c, sinhvien.hsv_sv_kiem_mssv
Danh sáchquanly.c, quanly.hql_ql_them
Lưu trữ nhị phânluutru.c, luutru.hlt_lt_ghi_nhi_phan
CSVcsv.c, csv.hcsv_csv_tach
Giao diệnui.c, ui.hui_ui_hien_thi

#Tách ở đâu

  1. Liệt kê mọi hàm trong tệp lớn

    terminal
    grep -n '^[a-zA-Z].*(' main.c | head -20
    23: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)
  2. 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.c
  3. Kiểm tra hướng phụ thuộc

    quanly.c gọi hàm của sinhvien.c, không có chiều ngược lại. ui.c gọ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.

  4. 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 sinhvien trước, build, chạy. Rồi mới tới quanly. Bài 20.6 sẽ đi qua toàn bộ quy trình.

Tự làm thử

  1. Chạy grep -n "^[a-zA-Z].*(" main.c trên dự án Phần 8 và liệt kê mọi hàm.
  2. Nhóm chúng theo dữ liệu mà chúng chạm vào, và đếm xem có mấy nhóm.
  3. Với mỗi nhóm, viết ra tệp .h chỉ có nguyên mẫu, chưa cần viết .c.
  4. Đếm số dòng của mỗi header và so với số dòng tệp .c tương ứng.
  5. Viết tệp .gitignore và README.md cho 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 .h là giao diện, tệp .c là cài đặt. Người dùng module chỉ cần đọc .h.
  • Mỗi tệp .c phả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.