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:
#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_grow có static đứ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ệ
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
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).
Đị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.
Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.
Trả lời vài câu để chắc rằng em đã nắm bài.
Trong một module C, file .h và file .c giữ vai trò gì?
- 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
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
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
statictrước hàm:static int vec_grow(Vec *v) { … }→ internal linkage. - 4
Include hay forward-declare
draw.hchỉ cần một CON TRỎ tớiCanvas. 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ỉ#includekhi cần truy cập TRƯỜNG hoặcsizeof. - 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
Đổi implementation
Module stack đổi cách lưu (mảng → danh sách liên kết) nhưng interface
stack.hgiữ 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.