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

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

Macro hàm, inline & _Generic

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

Macro dạng hàm trong C và cạm bẫy của nó (thiếu ngoặc, lượng giá hai lần), hàm static inline, và _Generic (C11) - đa hình lúc biên dịch của C hiện đại.

Bạn đã gặp #define cho hằng số (khoá cơ bản). Tiền xử lý còn cho nó nhận tham số - và trông y như một lời gọi hàm:

binh-phuong.c - hai phiên bản, một cái gài bẫy

#include <stdio.h>

#define BINH_PHUONG(x)      x * x          /* SAI: thieu ngoac      */
#define BINH_PHUONG_OK(x)   ((x) * (x))    /* DUNG: ngoac moi noi   */

int main(void) {
    int a = 4;
    printf("%d\n", BINH_PHUONG(a + 1));     /* mong 25... */
    printf("%d\n", BINH_PHUONG_OK(a + 1));
    return 0;
}

Kết quả khi chạy

9
25
  • Macro KHÔNG phải hàm: tiền xử lý DÁN văn bản tham số vào thân macro trước khi biên dịch (nhớ Bài 4 khoá cơ bản: chỉ thị chạy trước tất cả).
  • 9 thay vì 25: bản dán a + 1 * a + 1 bị ưu tiên toán tử xé - nhân trước, cộng sau.
  • Tên macro viết CHỮ_HOA - lời cảnh báo cho người gọi rằng đây là dán văn bản.

Xem tiền xử lý làm việc - dự đoán kết quả trước rồi bấm qua từng bước dán và lượng giá. Kịch bản thứ ba là cạm bẫy số hai, kín đáo hơn nhiều:

#define BINH_PHUONG(x)  x * x
int a = 4;
printf("%d", BINH_PHUONG(a + 1));   /* mong 25 */
bước 0/4
  • Bẫy 1 - thiếu ngoặc: chữa bằng quy ước hai lớp ngoặc ((x) * (x)).
  • Bẫy 2 - lượng giá nhiều lần: tham số xuất hiện mấy lần trong thân là chạy mấy lần. Ngoặc KHÔNG chữa được bẫy này.
  • Quy tắc an toàn khi GỌI macro của người khác: đừng bao giờ truyền biểu thức có tác dụng phụ (x++, gọi hàm…).

Hai cạm bẫy trên có chung gốc: macro không phải hàm. Từ C99, C cho bạn giữ tốc độ mà vẫn là hàm thật - inline:

inline.c - i++ chỉ chạy MỘT lần, vì đây là hàm thật

#include <stdio.h>

static inline long binh_phuong(long x) { return x * x; }

int main(void) {
    long i = 4;
    printf("%ld\n", binh_phuong(i++));   /* i++ luong gia DUNG MOT lan */
    printf("i = %ld\n", i);
    return 0;
}

Kết quả khi chạy

16
i = 5
  • Hàm thật: kiểm tra kiểu, tham số lượng giá đúng một lần, debug đặt breakpoint được.
  • static inline đặt trong header là khuôn chuẩn: mỗi .c nhận bản riêng, không xung đột ký hiệu (nhớ static ở bài Header & module).
  • inline là gợi ý - trình tối ưu hoá quyết định; với -O2 hiệu năng thường ngang macro.

Trung thực

Còn một hạn chế hàm chưa chữa được cho macro: hàm bị khoá vào một kiểu - binh_phuong(long) không nhận double mà không chuyển đổi ngầm định (Bài 8 khoá cơ bản). Mảnh ghép cuối nằm ở bước kế.

C11 thêm _Generic: một biểu thức chọn nhánh theo kiểu của đối số - ngay lúc biên dịch, không tốn gì lúc chạy. Ghép với macro, bạn có "một tên gọi cho cả họ hàm":

generic.c - một TRI_TUYET_DOI cho int, long lẫn double

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

#define TRI_TUYET_DOI(x) _Generic((x), \
    int:    abs,                       \
    long:   labs,                      \
    double: fabs                       \
)(x)

#define TEN_KIEU(x) _Generic((x),      \
    int: "int", double: "double",      \
    char *: "char *", default: "kieu khac")

int main(void) {
    printf("%d\n", TRI_TUYET_DOI(-5));
    printf("%.1f\n", TRI_TUYET_DOI(-2.5));
    printf("%s\n", TEN_KIEU(3.14));
    printf("%s\n", TEN_KIEU("meo"));
    return 0;
}

Kết quả khi chạy

5
2.5
double
char *
  • _Generic((x), kiểu1: giá_trị1, kiểu2: giá_trị2, default: …) - như switch nhưng rẽ theo KIỂU, quyết xong lúc biên dịch.
  • Khuôn phổ biến nhất: chọn đúng hàm trong một họ (abs/labs/fabs) rồi gọi - thư viện chuẩn làm y vậy trong <tgmath.h>.
  • Truyền kiểu không có trong danh sách (và không có default) → lỗi BIÊN DỊCH - an toàn hơn chuyển đổi ngầm định im lặng.
Cần gìDùng
Hằng số có kiểuconst (hoặc enum cho số nguyên)
Hàm nhỏ, nóng, một kiểustatic inline
Một tên cho cả họ hàm theo kiểumacro + _Generic
Việc hàm không làm được (ghép token, #x, dùng __LINE__…)macro - cẩn thận hai lớp ngoặc
  • Thứ tự ưu tiên hiện đại: const → static inline → _Generic → macro trần là lựa chọn CUỐI.
  • Đã viết macro: CHỮ_HOA + ngoặc mọi nơi + tài liệu cảnh báo tác dụng phụ.
  • Đã gọi macro: không truyền x++ hay lời gọi hàm có tác dụng phụ.

Bài tiếp theo

Bộ đồ nghề C hiện đại đã đủ. Bài kế tổng duyệt trước khi sang kiểm thử: Bẫy kinh điển & undefined behavior - nơi cái bẫy "lượng giá hai lần" hôm nay tái xuất ở dạng nguy hiểm nhất của nó.

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

Ba lý do lịch sử: tốc độ (thời chưa có inline, gọi hàm tốn chi phí), làm việc với MỌI kiểu (macro không có chữ ký kiểu), và những việc hàm không làm được (dùng tên biến, ghép token, lấy #x thành chuỗi). Hai lý do đầu nay đã có inline_Generic thay thế tử tế - macro chỉ còn đáng dùng cho lý do thứ ba.

Không - inline chỉ là GỢI Ý kiêm điều chỉnh luật liên kết; trình tối ưu hoá mới là người quyết (và với -O2, nó tự inline cả hàm không đánh dấu). Điều bạn nhận chắc chắn là ngữ nghĩa HÀM: kiểm tra kiểu, tham số lượng giá đúng một lần - những thứ macro không có.

inline trần trong C có luật liên kết rắc rối nổi tiếng (cần một định nghĩa extern ở đâu đó, khác hẳn C++). static inline thì đơn giản: mỗi file .c nhận một bản riêng, không xung đột ký hiệu khi nhiều file cùng include - đúng kiểu "bỏ vào header là chạy" (nhớ static của bài Header & module).

Bài Header & module →

Không - nó khiêm tốn hơn nhiều: một biểu thức RẼ NHÁNH THEO KIỂU lúc biên dịch, thường dùng để chọn đúng hàm trong một họ (abs/labs/fabs). Nó không sinh code mới cho từng kiểu như template. Chính thư viện chuẩn dùng nó trong <tgmath.h> để sin/cos "ăn" được mọi kiểu số thực.

Ví dụ MAX trong bài may mắn: toán tử ?: có sequence point nên hai lần x++ được xếp thứ tự - kết quả xác định (m = 6, x = 7) dù sai ý. Nhưng đổi thân macro thành thứ không có sequence point (vd (a)*(a) với ax++) là rơi thẳng vào undefined behavior - bài kế tiếp nói chuyện đó tới nơi tới chốn.

Không bắt buộc về cú pháp, nhưng là quy ước toàn ngành: tên CHỮ_HOA cảnh báo người đọc "đây là dán văn bản, đừng truyền x++ vào". Phá quy ước này - đặt macro tên thường - là gài bẫy chính đồng đội của mình.

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

Với #define BINH_PHUONG(x) x * x, lời gọi BINH_PHUONG(a + 1) khi a = 4 cho ra mấy?

  1. 1

    Chạy đủ 3 kịch bản

    Trong công cụ Bước 2: bấm từng bước cả 3 kịch bản, sau đó tự viết bản dán của BINH_PHUONG(a + b + 1) ra giấy trước khi kiểm lại.

    Hoàn thành khi: Viết đúng a + b + 1 * a + b + 1 (bản thiếu ngoặc) và tính đúng giá trị với a = 2, b = 3.

  2. 2

    Tự ngã rồi tự đứng dậy

    Viết macro LAP_PHUONG(x) thiếu ngoặc, gọi với x1 + 1, dự đoán rồi chạy. Sửa lại đúng quy ước hai lớp ngoặc.

    Hoàn thành khi: Bản lỗi ra kết quả khác 8; bản sửa ((x)*(x)*(x)) ra đúng 8.

  3. 3

    Soi bản dán bằng trình biên dịch

    Chạy cc -E (chỉ tiền xử lý - nhớ bài Hành trình biên dịch khoá Toolchain) lên file chứa MAX(x++, y) và đọc bản dán thật.

    Hoàn thành khi: Thấy ((x++) > (y) ? (x++) : (y)) bằng chính mắt - đúng như công cụ Bước 2 mô tả.

  4. 4

    Đổi macro thành inline

    Viết static inline int max_int(int a, int b) thay MAX, gọi max_int(x++, y) với x = 5, y = 3 và so kết quả với phiên bản macro.

    Hoàn thành khi: inline: m = 5, x = 6 (lượng giá một lần); macro: m = 6, x = 7 - giải thích được khác biệt.

  5. 5

    _Generic của mèo con

    Viết macro IN(x) dùng _Generic chọn đúng printf format: int"%d\n", double"%f\n", char *"%s\n".

    Hoàn thành khi: IN(5), IN(3.14), IN("meo") đều in đúng - không cần nhớ format specifier nữa.

  6. 6

    Khảo cổ tgmath

    Mở <tgmath.h> trên máy (echo "#include <tgmath.h>" | cc -E - | grep -m3 sqrt) hoặc đọc tài liệu: sqrt ở đó được định nghĩa thế nào?

    Hoàn thành khi: Chỉ ra được _Generic (hoặc cơ chế tương đương) chọn sqrtf/sqrt/sqrtl theo kiểu đối số - thư viện chuẩn dùng đúng kỹ thuật của bài.