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ẫ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
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ểu | const (hoặc enum cho số nguyên) |
| Hàm nhỏ, nóng, một kiểu | static inline |
| Một tên cho cả họ hàm theo kiểu | macro + _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
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 và _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).
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.
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.
Với #define BINH_PHUONG(x) x * x, lời gọi BINH_PHUONG(a + 1) khi a = 4 cho ra mấy?
- 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ớia = 2,b = 3. - 2
Tự ngã rồi tự đứng dậy
Viết macro
LAP_PHUONG(x)thiếu ngoặc, gọi vớixlà1 + 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
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ứaMAX(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
Đổi macro thành inline
Viết
static inline int max_int(int a, int b)thayMAX, gọimax_int(x++, y)vớix = 5,y = 3và 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
_Generic của mèo con
Viết macro
IN(x)dùng_Genericchọn đúngprintfformat: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
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ọnsqrtf/sqrt/sqrtltheo kiểu đối số - thư viện chuẩn dùng đúng kỹ thuật của bài.