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

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 vecbà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 opaquetransparent để xem người dùng làm được gì:

Kiểu trong stack.h là:
typedef struct Stack Stack;            // .h chỉ có dòng này
Người dùng (chỉ #include "stack.h") thử các thao tác:
Stack *s = stack_create(); biên dịch OK
stack_push(s, 5); biên dịch OK
int x = stack_pop(s); biên dịch OK
stack_destroy(s); biên dịch OK
int n = s->len; lỗi
Truy cập TRƯỜNG: opaque cấm (kiểu chưa hoàn chỉnh, không biết bố cục). Transparent thì được - nhưng đã phá đóng gói.
Stack s2; lỗi
Khai báo một Stack GIÁ TRỊ cần biết kích thước → opaque không có (incomplete type).
size_t z = sizeof(Stack); lỗi
sizeof cần kích thước → opaque báo lỗi “invalid application of sizeof to incomplete type”.
🔒 Opaque: người dùng CHỈ dùng được 4/7 thao tác - đúng những gì API cho phép. Trình biên dịch tự chặn mọi cách chạm vào ruột → bạn được tự do đổi struct trong .c.
  • 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

Cách biến bố cục này thành thư viện .a/.so, đặt include/ là PUBLIC, và chạy test/CI - nằm ở khoá C Thực Chiến · Toolchain (bài Thư viện & liên kết, CMake, Kiểm thử & CI). Ở đây bạn đã có phần thiết kế; khoá kia cho bạn phần thi công.

Tiếp theo: hoàn thiện kỹ năng rồi ghép thành sản phẩm

Bạn vừa học cách thiết kế một module tái dùng và đóng gói nó thành thư viện. Chặng còn lại của khoá: làm việc với tệp (FILE *), xử lý lỗi kiểu C, macro & _Generic, tổng kết bẫy & undefined behavior, rồi kiểm thử (unit + integration) - và khép lại bằng bài “Dự án cuối khoá: 3 phần mềm thật” ghép tất cả (Todo, Quản lý chi tiêu, Bình chọn) thành sản phẩm đưa lên portfolio.

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ớ.)

Bài Bẫy & quản lý bộ nhớ →

include/ chứa header CÔNG KHAI - API mà người ngoài #include. src/ chứa các .c (implementation) và header nội bộ riêng. Hệ thống build đặt include/ vào đường tìm header công khai (PUBLIC include - xem khoá C Thực Chiến · Toolchain, bài CMake).

Opaque khi muốn đóng gói, tự do đổi implementation, hoặc làm thư viện cho người khác (Stack, List, File handle…). Để lộ (transparent) cho kiểu dữ liệu nhỏ, ổn định, cần truy cập trực tiếp cho tiện/nhanh (vd Point {x, y}). Luôn cân nhắc đánh đổi giữa đóng gói và tiện lợi/hiệu năng.

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

Một opaque type được khai báo thế nào và người dùng làm được gì với nó?

  1. 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 - Stack là 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. 2

    Vì sao trả con trỏ

    Vì sao stack_create trả về Stack* chứ không phải Stack (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. 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.h không đổi. Chỉ cần liên kết lại bản stack.c mới.

  4. 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ủa stack.h.

  5. 5

    Header công khai

    Trong bố cục dự án, file header CÔNG KHAI stack.h nên nằm thư mục nào?

    Hoàn thành khi: include/ - tách header công khai (API) khỏi src/ (implementation).

  6. 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.