Bỏ qua điều hướng, tới nội dung chính
Học C
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

Phụ thuộc phải chảy theo một chiều. Vòng tròn nghĩa là ranh giới module đặt sai.
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 khaiKiểu mờ
Người dùng tạo biến trên ngăn xếpĐượcKhông, phải qua hàm tạo
Người dùng đọc trường trực tiếpĐượcKhông, phải qua hàm
Đổi bố cục structMọi tệp phải dịch lạiChỉ tệp .c dịch lại
Số dòng phải viếtÍtThêm một hàm cho mỗi trường cần đọc
Tốc độTruy cập trực tiếpMộ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ệuKhôngCó, 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 FILEtypedef struct _IO_FILE FILE; là không khả chuyển, nên đành include <stdio.h>
Con trỏ tới struct của bạnKhai báo trước, không include gì
Struct theo giá trị, hoặc sizeofBắt buộc include đầy đủ

Danh sách kiểm cho sơ đồ phụ thuộc

Câu hỏiCách kiểm
Có cặp header nào include lẫn nhau khôngVò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ôngBỏ từng dòng rồi build lại
Kiểm thử một module riêng có được khônggcc tests/kiem-x.c src/x.c -o kiem
Sửa một header thì mấy tệp phải dịch lạitouch header rồi đếm dòng make in ra

Tự làm thử

  1. 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.
  2. Cố ý tạo vòng phụ thuộc giữa hai header rồi đọc thông báo lỗi.
  3. Phá vòng đó bằng khai báo trước và xác nhận nó biên dịch lại được.
  4. Đổi QuanLy thành kiểu mờ và đếm số hàm phải thêm.
  5. Bỏ từng dòng #include trong một header của bạn rồi build lại, tìm ra dòng nào thừa.
  6. Chạy touch lê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 #include trong header nhân lên theo số tệp .c include nó, nên header chỉ nên include thứ chính nó cần.