Bài 20.526 phút đọc
Sơ đồ phụ thuộc và kiểu mờ
Sau bài này bạn sẽ làm được
- Vẽ sơ đồ phụ thuộc của một dự án và tìm vòng tròn
- Phá vòng phụ thuộc bằng khai báo trước kiểu
- Cài kiểu mờ để giấu hẳn bố cục struct khỏi người dùng
- Giảm thời gian build bằng cách bớt include trong header
Khi hai module gọi lẫn nhau, không tệp nào dịch trước được, và mọi thay đổi ở một bên đều kéo bên kia dịch lại. Bài này chỉ ra cách phát hiện vòng phụ thuộc và ba cách phá nó.
#Vẽ sơ đồ phụ thuộc
terminal
# Liệt kê mọi quan hệ include trong dự án
grep -Hn '#include "' src/*.c include/*.h
src/main.c:4:#include "ui.h" src/quanly.c:5:#include "quanly.h" src/quanly.c:6:#include "sinhvien.h" src/ui.c:6:#include "ui.h" src/ui.c:7:#include "quanly.h" src/ui.c:8:#include "luutru.h" include/quanly.h:6:#include "sinhvien.h" include/quanly.h:7:#include "loi.h" include/luutru.h:5:#include "sinhvien.h" include/ui.h:5:#include "quanly.h"
/* Ve lai thanh so do, chi xet header: */
main.c
|
ui.h
|
quanly.h
/ \
sinhvien.h loi.h
^
|
luutru.h
Nguyen tac: MOI mui ten di xuong. Khong co mui ten nao di len.#Phá vòng phụ thuộc
Cách một: khai báo trước
Hai header include lẫn nhau
/* luutru.h */
#include "quanly.h" /* keo theo ca QuanLy */
MaLoi lt_ghi(const char *ten, const QuanLy *q);
/* quanly.h */
#include "luutru.h" /* de goi lt_ghi trong ql_luu */
MaLoi ql_luu(QuanLy *q, const char *ten);Khai báo trước trong .h, include trong .c
/* luutru.h: KHONG include quanly.h */
typedef struct QuanLy QuanLy; /* khai bao truoc */
MaLoi lt_ghi(const char *ten, const QuanLy *q);
/* luutru.c: cai dat THI include */
#include "luutru.h"
#include "quanly.h" /* de truy cap q->ds va q->n */
/* quanly.h: khong can biet gi ve luutru */
MaLoi ql_luu(QuanLy *q, const char *ten);
/* quanly.c */
#include "quanly.h"
#include "luutru.h" /* de goi lt_ghi */Cách hai: tách ra module thứ ba
/* Truoc: hai module can du lieu chung, nen chung include nhau */
quanly.h <-> luutru.h
/* Sau: dua phan chung xuong tang duoi */
quanly.h luutru.h
\ /
sinhvien.h
/* Neu hai module can biet ve nhau, thuong la vi co mot khai niem
thu ba dang bi chia doi giua chung. Tim ra khai niem do
va cho no mot module rieng. */Cách ba: đảo chiều bằng con trỏ hàm
/* Van de: quanly.c muon bao cho ui.c biet khi co thay doi,
nhung quanly la tang duoi, khong duoc biet ve ui. */
/* quanly.h: khai bao mot cho cam vao, khong nhac toi ui */
typedef void (*HamBaoThayDoi)(size_t so_luong, void *ngu_canh);
void ql_dat_bao_thay_doi(QuanLy *q, HamBaoThayDoi f, void *ngu_canh);
/* ui.c: cam ham cua minh vao */
static void khi_thay_doi(size_t n, void *ngu_canh) {
(void)ngu_canh;
printf("Danh sach gio co %zu sinh vien\n", n);
}
void ui_khoi_tao(QuanLy *q) {
ql_dat_bao_thay_doi(q, khi_thay_doi, NULL);
}
/* Bay gio quanly goi nguoc len duoc MA KHONG biet ui ton tai.
Bai 13.6 goi day la ham goi lai. */#Kiểu mờ
Kiểu mờ
Kiểu mà header chỉ khai báo trước, không định nghĩa. Người dùng giữ được con trỏ tới nó và truyền vào hàm, nhưng không tạo biến được và không đọc trường được. Bố cục chỉ tồn tại trong tệp
.c cài đặt.quanly.h, phiên bản kiểu mờ
#ifndef QUANLY_H
#define QUANLY_H
#include <stddef.h>
#include "loi.h"
#include "sinhvien.h"
typedef struct QuanLy QuanLy; /* CHI khai bao, khong dinh nghia */
QuanLy *ql_tao(void); /* tra ve con tro, nguoi dung khong tu tao */
void ql_huy(QuanLy *q);
MaLoi ql_them(QuanLy *q, const SinhVien *sv);
size_t ql_so_luong(const QuanLy *q); /* thay cho q->n */
const SinhVien *ql_lay(const QuanLy *q, size_t i);
#endif /* QUANLY_H */quanly.c
#include <stdlib.h>
#include <string.h>
#include "quanly.h"
struct QuanLy { /* DINH NGHIA chi o day */
SinhVien *ds;
size_t n, suc_chua;
int da_thay_doi;
};
QuanLy *ql_tao(void) {
QuanLy *q = calloc(1, sizeof *q); /* calloc: moi truong bang 0 */
return q; /* NULL neu that bai, nguoi goi kiem */
}
void ql_huy(QuanLy *q) {
if (!q) return; /* nhan NULL vo hai, giong free */
free(q->ds);
free(q);
}
size_t ql_so_luong(const QuanLy *q) { return q->n; }
const SinhVien *ql_lay(const QuanLy *q, size_t i) {
return i < q->n ? &q->ds[i] : NULL;
}| Struct công khai | Kiểu mờ | |
|---|---|---|
| Người dùng tạo biến trên ngăn xếp | Được | Không, phải qua hàm tạo |
| Người dùng đọc trường trực tiếp | Được | Không, phải qua hàm |
| Đổi bố cục struct | Mọi tệp phải dịch lại | Chỉ tệp .c dịch lại |
| Số dòng phải viết | Ít | Thêm một hàm cho mỗi trường cần đọc |
| Tốc độ | Truy cập trực tiếp | Một lời gọi hàm, thường được nội tuyến hết |
| Kiểm được bất biến của dữ liệu | Không | Có, mọi thay đổi đi qua hàm của bạn |
#Bớt include trong header
| Header cần gì | Cách rẻ nhất |
|---|---|
| Kiểu size_t | #include <stddef.h>, rất nhẹ |
| Kiểu uint16_t | #include <stdint.h>, cũng nhẹ |
| Kiểu bool | #include <stdbool.h>, rất nhẹ |
| Con trỏ tới FILE | typedef struct _IO_FILE FILE; là không khả chuyển, nên đành include <stdio.h> |
| Con trỏ tới struct của bạn | Khai báo trước, không include gì |
| Struct theo giá trị, hoặc sizeof | Bắt buộc include đầy đủ |
Danh sách kiểm cho sơ đồ phụ thuộc
| Câu hỏi | Cách kiểm |
|---|---|
| Có cặp header nào include lẫn nhau không | Vòng lặp shell ở đầu bài |
| Mỗi header include những gì | grep '#include' include/*.h |
| Có include nào thừa không | Bỏ từng dòng rồi build lại |
| Kiểm thử một module riêng có được không | gcc tests/kiem-x.c src/x.c -o kiem |
| Sửa một header thì mấy tệp phải dịch lại | touch header rồi đếm dòng make in ra |
Tự làm thử
- Vẽ sơ đồ phụ thuộc của dự án Phần 8 sau khi tách, và chỉ ra tầng của từng module.
- Cố ý tạo vòng phụ thuộc giữa hai header rồi đọc thông báo lỗi.
- Phá vòng đó bằng khai báo trước và xác nhận nó biên dịch lại được.
- Đổi
QuanLythành kiểu mờ và đếm số hàm phải thêm. - Bỏ từng dòng
#includetrong một header của bạn rồi build lại, tìm ra dòng nào thừa. - Chạy
touchlên một header và đếm số tệp phải dịch lại.
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
- Phụ thuộc phải đi một chiều. Module tầng thấp không được biết gì về module tầng cao.
- Header guard chặn được lặp vô tận nhưng không tạo ra được thứ tự mà vòng phụ thuộc đã phá vỡ.
- Phá vòng bằng khai báo trước là cách rẻ nhất, nên thử trước khi tính tới hai cách còn lại.
- Kiểu mờ giấu hẳn bố cục struct, cho phép đổi cài đặt mà không ai phải dịch lại. Giá của nó là bắt buộc cấp phát động.
- Mỗi
#includetrong header nhân lên theo số tệp.cinclude nó, nên header chỉ nên include thứ chính nó cần.