Bài 12 · Nâng cao · 24 phút· Cập nhật 11/06/2026
Đóng gói & thư viện
Biên soạn bởi Nguyễn Anh Tuấn
Đóng gói trong C với opaque type: giấu struct trong .c, thiết kế module tái dùng như thư viện (library), bố cục dự án include/ src/ tests/ để dễ kiểm thử.
Module vec ở bài trước vẫn để lộ cấu trúc trong header - người dùng có thể nghịch v.data, v.len trực tiếp. Hệ quả: khi bạn muốn đổi cách lưu bên trong, mọi code đang “thò tay” vào ruột sẽ vỡ. Senior giải quyết bằng opaque type - giấu hẳn cấu trúc.
- ▸Để lộ struct = người dùng phụ thuộc vào bố cục bên trong của bạn.
- ▸Phụ thuộc đó khoá bạn lại: đổi struct là làm vỡ code họ.
- ▸Giải pháp: lộ tên kiểu, GIẤU nội dung → opaque type.
Header chỉ khai báo tên kiểu (chưa có nội dung). Cấu trúc đầy đủ nằm trong .c - chỉ file đó biết. Người dùng chỉ có con trỏ và các hàm:
stack.h - opaque (chỉ lộ interface)
#ifndef STACK_H
#define STACK_H
typedef struct Stack Stack; /* kieu CHUA HOAN CHINH (incomplete) */
Stack *stack_create(void); /* tao moi; tra NULL neu het bo nho */
void stack_push(Stack *s, int x);
int stack_pop(Stack *s); /* yeu cau stack khong rong */
int stack_empty(const Stack *s);
void stack_destroy(Stack *s); /* giai phong (nhan NULL an toan) */
#endif stack.c - định nghĩa struct được giấu ở đây
#include <stdlib.h>
#include "stack.h"
struct Stack { /* CHI file nay biet bo cuc */
int *data;
int len, cap;
};
Stack *stack_create(void) {
Stack *s = malloc(sizeof *s);
if (!s) return NULL;
s->data = NULL; s->len = s->cap = 0;
return s;
}
void stack_destroy(Stack *s) {
if (!s) return; /* free(NULL) an toan, nhung kiem tra cho ro */
free(s->data);
free(s);
}
/* push / pop / empty: thao tac s->... ben trong .c */ Cái hay của opaque: trình biên dịch tự ép đóng gói. Đổi giữa header opaque và transparent để xem người dùng làm được gì:
typedef struct Stack Stack; // .h chỉ có dòng này
Stack *s = stack_create(); biên dịch OKstack_push(s, 5); biên dịch OKint x = stack_pop(s); biên dịch OKstack_destroy(s); biên dịch OKint n = s->len; lỗiStack s2; lỗisize_t z = sizeof(Stack); lỗi- ▸Opaque: người dùng CHỈ gọi được API (qua Stack*); chạm ruột → lỗi biên dịch.
- ▸Vì kiểu “chưa hoàn chỉnh”: không sizeof, không khai báo giá trị, không truy cập trường.
- ▸Nhờ vậy bạn được TỰ DO đổi struct trong .c mà không làm vỡ code ai.
Gom lại thành một thư viện nhỏ gọn: vòng đời rõ (create/destroy), quy ước lỗi nhất quán, tài liệu trong header. Người dùng chỉ cần #include "stack.h":
main.c - dùng thư viện stack
#include "stack.h" /* chi can interface */
int main(void) {
Stack *s = stack_create();
if (!s) return 1; /* luon kiem tra */
stack_push(s, 1);
stack_push(s, 2);
int top = stack_pop(s); /* 2 */
stack_destroy(s); /* nguoi tao chiu trach nhiem giai phong */
/* s->len; sizeof(Stack); Stack x; -> LOI: kieu chua hoan chinh */
return 0;
} - ▸Vòng đời: create cấp phát & trả con trỏ (NULL nếu lỗi); destroy giải phóng.
- ▸Quy ước lỗi nhất quán (trả NULL/mã lỗi) và ghi rõ ai sở hữu, ai free.
- ▸Tài liệu hoá ngay trong .h: mỗi hàm một dòng - đó là “hợp đồng”.
Cuối cùng, sắp xếp file như một dự án thật - tách header công khai, implementation, và test:
cấu trúc thư mục
myproject/
├── include/ # header CONG KHAI (API) -> nguoi ngoai #include
│ └── stack.h
├── src/ # implementation (.c) + header noi bo neu can
│ └── stack.c
├── tests/ # unit test
│ └── test_stack.c
├── CMakeLists.txt # build (xem khoa Toolchain)
└── README.md # gioi thieu, cach build & dung Liên hệ build
Tiếp theo: hoàn thiện kỹ năng rồi ghép thành sản phẩm
Câu hỏi thường gặp
Là kiểu được KHAI BÁO (typedef struct Stack Stack;) nhưng CHƯA định nghĩa nội dung - gọi là “incomplete type”. Người dùng chỉ giữ được con trỏ Stack* và gọi hàm; trình biên dịch CẤM truy cập trường, sizeof, hay khai báo Stack giá trị. Định nghĩa đầy đủ của struct nằm trong .c.
Phải cấp phát động (create trả về con trỏ, vì người dùng không tạo được giá trị trên stack khi không biết kích thước), và mọi truy cập đi qua hàm (chi phí gọi hàm nhỏ). Đổi lại: đóng gói thật sự, ABI/interface ổn định - đáng giá cho thư viện dùng chung.
Vì với opaque type, người dùng không biết sizeof(Stack) → không khai báo được Stack s; trên stack. Nên module tự malloc và trả con trỏ; người dùng giữ con trỏ đó và gọi stack_destroy khi xong.
Bên GỌI stack_create là chủ sở hữu, phải gọi stack_destroy đúng một lần. Ghi rõ quy ước này trong tài liệu của stack.h. (Xem lại kỷ luật sở hữu ở bài Bẫy & quản lý bộ nhớ.)
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.
Một opaque type được khai báo thế nào và người dùng làm được gì với nó?
- 1
Truy cập ruột
Ở chế độ Opaque trong công cụ Bước 3,
int n = s->len;có biên dịch không? Vì sao?Hoàn thành khi: Không -
Stacklà kiểu chưa hoàn chỉnh (incomplete), trình biên dịch không biết bố cục nên cấm truy cập trường. - 2
Vì sao trả con trỏ
Vì sao
stack_createtrả vềStack*chứ không phảiStack(giá trị)?Hoàn thành khi: Người dùng không biết
sizeof(Stack)(opaque) → không khai báo giá trị được; module tự cấp phát và trả con trỏ. - 3
Đổi implementation
Stack đang lưu bằng mảng; bạn đổi sang danh sách liên kết. Với opaque, người dùng có phải sửa code không?
Hoàn thành khi: Không sửa code - interface
stack.hkhông đổi. Chỉ cần liên kết lại bảnstack.cmới. - 4
Ai free
Ai phải gọi
stack_destroy, và ghi quy ước đó ở đâu?Hoàn thành khi: Bên gọi
stack_create(chủ sở hữu). Ghi rõ trong tài liệu/bình luận củastack.h. - 5
Header công khai
Trong bố cục dự án, file header CÔNG KHAI
stack.hnên nằm thư mục nào?Hoàn thành khi:
include/- tách header công khai (API) khỏisrc/(implementation). - 6
Opaque hay không
Một kiểu
Point { int x, y; }nhỏ và bất biến - nên opaque hay transparent? Vì sao?Hoàn thành khi: Transparent (để lộ): nhỏ, ổn định, truy cập trực tiếp tiện & nhanh; opaque ở đây là thừa.