← Lập trình C nâng cao

Bài 11 · Nâng cao · 22 phút· Cập nhật 11/06/2026

Header & module

Biên soạn bởi Nguyễn Anh Tuấn

Header và module trong C: tách interface (.h) khỏi implementation (.c), hàm static, include guard, forward declaration và đặt tên module có tiền tố.

Chương trình thật không nằm trong một file khổng lồ. Dân chuyên chia nó thành các module, mỗi module là một cặp: file .h mô tả CÁI GÌ (interface công khai) và file .c chứa LÀM SAO (implementation). File khác chỉ cần #include "vec.h" là dùng được:

vec.h - interface (cái gì)

#ifndef VEC_H
#define VEC_H

typedef struct { int *data; int len, cap; } Vec;

void vec_init(Vec *v);
int  vec_push(Vec *v, int x);   /* tra 0 neu OK */
void vec_free(Vec *v);

#endif

vec.c - implementation (làm sao)

#include <stdlib.h>
#include "vec.h"

static int vec_grow(Vec *v) {       /* RIENG TU: khong lo ra .h */
    int cap = v->cap ? v->cap * 2 : 8;
    int *p = realloc(v->data, cap * sizeof(int));
    if (!p) return -1;
    v->data = p; v->cap = cap;
    return 0;
}

void vec_init(Vec *v) { v->data = NULL; v->len = v->cap = 0; }

int vec_push(Vec *v, int x) {
    if (v->len == v->cap && vec_grow(v) != 0) return -1;
    v->data[v->len++] = x;
    return 0;
}

void vec_free(Vec *v) { free(v->data); v->data = NULL; }

main.c - người dùng: chỉ cần interface

#include "vec.h"      /* khong can biet vec.c lam gi ben trong */

int main(void) {
    Vec v;
    vec_init(&v);
    vec_push(&v, 10);
    vec_push(&v, 20);
    vec_free(&v);
    return 0;
}

Đây là kỹ năng cốt lõi. Phân loại từng mảnh của module vec dưới đây vào vec.h hay vec.c - bấm để xem đúng/sai và lý do:

Mỗi mảnh dưới đây thuộc vec.h (interface) hay vec.c (implementation)?
0/7 đúng
#ifndef VEC_H #define VEC_H ... #endif
typedef struct { int *data; int len, cap; } Vec;
enum { VEC_OK, VEC_NOMEM };
int vec_push(Vec *v, int x);
int vec_push(Vec *v, int x) { ... }
static int vec_grow(Vec *v) { ... }
#include <stdlib.h> /* cho malloc */

Quy tắc vàng: .h nói “CÁI GÌ” (interface công khai - prototype, kiểu, hằng); .c nói “LÀM SAO” (implementation, hàm static riêng, include nội bộ).

  • .h (công khai): prototype hàm, kiểu (struct/typedef/enum), hằng/macro công khai, include guard.
  • .c (riêng tư): định nghĩa hàm, hàm static phụ trợ, include mà chỉ implementation cần.
  • Nguyên tắc: lộ ra interface tối thiểu, giấu mọi chi tiết còn lại.

Trong vec.c, hàm vec_growstatic đứng trước. Nó là chi tiết nội bộ - người dùng không cần và không nên thấy. static ở phạm vi file biến nó thành riêng tư (chỉ file đó dùng được):

  • static <hàm> → internal linkage: chỉ file .c đó gọi được; không lộ ra header.
  • Đóng gói: đổi/xoá hàm static thoải mái, không ai phụ thuộc nó.
  • Tránh đụng tên: hai file có thể cùng có hàm static tên grow() mà không xung đột.

Liên hệ

Cơ chế của static/extern, include guard, và cách biên dịch nhiều file thành thư viện .a/.so được mổ xẻ kỹ ở khoá C Thực Chiến · Toolchain, bài Thư viện & liên kết. Ở đây ta tập trung vào thiết kế: chia module sao cho gọn.

Header tốt thì nhẹ và lộ ít. Hai thói quen quan trọng: chỉ #include thứ header THỰC SỰ cần, và dùng forward declaration khi chỉ cần một con trỏ:

forward-declaration

/* draw.h - chi can CON TRO toi Canvas, khong can biet ben trong no */
struct Canvas;                       /* forward declaration */
void draw_line(struct Canvas *c, int x0, int y0, int x1, int y1);

/* So voi: #include "canvas.h"  -> keo theo ca the gioi cua canvas,
   tang phu thuoc & thoi gian bien dich. Chi #include khi can truy
   cap TRUONG cua Canvas hoac sizeof(Canvas). */
  • Include guard (#ifndef hoặc #pragma once): mỗi header chỉ dán một lần.
  • Include what you use: header chỉ include thứ chính nó cần cho khai báo.
  • Forward-declare struct khi chỉ cần con trỏ → giảm phụ thuộc, build nhanh.

Vài quy ước nhỏ tạo nên codebase chuyên nghiệp:

  • Tiền tố module cho mọi tên công khai: vec_init, vec_push, vec_free (C không có namespace).
  • Một module = MỘT trách nhiệm rõ ràng; tên file = tên module (vec.h/vec.c).
  • Interface ổn định: đổi implementation trong .c không làm vỡ code người dùng.
  • Tài liệu hoá ngay trong .h: mỗi hàm công khai một dòng nói nó làm gì, trả gì.

Tiếp theo: giấu cả cấu trúc dữ liệu

Module vec ở đây vẫn để lộ nội dung struct trong header - người dùng có thể nghịch v.data trực tiếp. Bài sau (Đóng gói & thư viện) sẽ giấu luôn cấu trúc bằng opaque type, để người dùng CHỈ gọi được qua hàm - đóng gói ở mức cao nhất, như một thư viện thật.

Câu hỏi thường gặp

.h là HỢP ĐỒNG công khai - người khác đọc để biết cách dùng module. .c giấu chi tiết implementation. Đổi cách làm bên trong .c (miễn giữ nguyên interface .h) thì người dùng KHÔNG bị ảnh hưởng. Tách ra còn cho phép biên dịch riêng từng file (build tăng dần - xem khoá Toolchain).

Bài Make & đồ thị build - khoá Toolchain →

Định nghĩa hàm (nhiều .c include sẽ gây “multiple definition”), biến toàn cục có định nghĩa (dùng extern trong .h, định nghĩa một lần trong .c), hàm phụ trợ riêng (static trong .c), và những #include mà chỉ phần implementation mới cần.

Internal linkage: hàm chỉ nhìn thấy trong file .c đó, KHÔNG lộ ra cho trình liên kết hay header. Dùng cho hàm phụ trợ riêng tư → đóng gói, và tránh đụng tên với module khác. (Chi tiết về linkage ở khoá Toolchain, bài Thư viện & liên kết.)

Là khai báo struct Foo; mà chưa định nghĩa nội dung. Dùng khi header chỉ cần một CON TRỎ tới Foo (không truy cập trường) → khỏi #include "foo.h", giảm phụ thuộc và thời gian biên dịch. Đây cũng là nền của “opaque type” (đóng gói) ở bài sau.

C KHÔNG có namespace - mọi tên hàm/biến toàn cục dùng chung một không gian. Nếu hai thư viện cùng có hàm init() sẽ đụng nhau. Quy ước của dân chuyên: đặt tiền tố theo module (vec_, str_, list_…) cho mọi tên công khai.

Một TRÁCH NHIỆM rõ ràng: thường là một kiểu dữ liệu + các thao tác của nó (vd Vecvec_*), hoặc một nhóm chức năng liên quan. Tên file = tên module. Quá to → tách; nhiều mảnh vụn rời rạc → gộp lại.

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/3 Điểm: 0

Trong một module C, file .h và file .c giữ vai trò gì?

  1. 1

    Prototype hay thân hàm

    int vec_push(Vec *v, int x); nên vào file nào? Còn phần thân { … } của nó?

    Hoàn thành khi: Prototype (có dấu ;) → vec.h. Định nghĩa (thân hàm) → vec.c (chỉ một nơi).

  2. 2

    Viết include guard

    Viết include guard cho file list.h.

    Hoàn thành khi: #ifndef LIST_H\n#define LIST_H\n…\n#endif

  3. 3

    Giữ riêng tư

    Một hàm phụ trợ chỉ dùng nội bộ trong vec.c. Làm sao giữ nó riêng, không lộ ra interface?

    Hoàn thành khi: Thêm static trước hàm: static int vec_grow(Vec *v) { … } → internal linkage.

  4. 4

    Include hay forward-declare

    draw.h chỉ cần một CON TRỎ tới Canvas. Nên #include "canvas.h" hay forward-declare? Vì sao?

    Hoàn thành khi: Forward-declare: struct Canvas; → giảm phụ thuộc & build nhanh hơn. Chỉ #include khi cần truy cập TRƯỜNG hoặc sizeof.

  5. 5

    Đụng tên

    Hai thư viện đều có hàm tên init(). Vì sao đụng nhau, và cách tránh?

    Hoàn thành khi: C không có namespace → trùng tên toàn cục → lỗi “multiple definition”/nhập nhằng. Tránh bằng tiền tố module: vec_init, net_init.

  6. 6

    Đổi implementation

    Module stack đổi cách lưu (mảng → danh sách liên kết) nhưng interface stack.h giữ nguyên. Người dùng có phải sửa code không?

    Hoàn thành khi: Không - chỉ sửa stack.c. Interface không đổi nên code người dùng vẫn chạy. Đó chính là lợi ích của việc tách interface/implementation.