Bài 19.524 phút đọc
Toán tử biến thành chuỗi và nối token
Sau bài này bạn sẽ làm được
- Dùng dấu thăng để biến tham số thành chuỗi ký tự
- Dùng dấu thăng đôi để ghép hai token thành một tên
- Giải thích vì sao cần hai tầng macro và khi nào cần
- Viết macro biến đối số bằng VA_ARGS và VA_OPT
Hai toán tử trong bài này chỉ preprocessor mới có, và không hàm nào thay thế được. Chúng cho phép macro nhìn thấy văn bản gốc mà người gọi đã gõ, và tạo ra tên mới từ các mảnh ghép.
#Biến tham số thành chuỗi
#define THANH_CHUOI(x) #x
THANH_CHUOI(a + b) /* -> "a + b" */
THANH_CHUOI(hello) /* -> "hello" */
THANH_CHUOI(1 == 2) /* -> "1 == 2" */Toán tử biến thành chuỗi
Dấu thăng đặt trước tên một tham số trong thân macro. Nó biến những token của đối số thành một chuỗi ký tự, giữ nguyên văn bản người gọi đã viết.
Ứng dụng đầu tiên: in tên biến cùng giá trị
#define IN_INT(x) printf(#x " = %d\n", (x))
#define IN_STR(x) printf(#x " = \"%s\"\n", (x))
int tuoi = 20;
int *p = &tuoi;
IN_INT(tuoi); /* tuoi = 20 */
IN_INT(tuoi * 2); /* tuoi * 2 = 40 */
IN_INT(*p); /* *p = 20 */
/* Khong ham nao lam duoc: ham chi nhan GIA TRI 20,
no khong biet nguoi goi da go chu gi. */#Mẹo hai tầng
terminal
./thu-hai-tang
STR_(PHIEN_BAN) = PHIEN_BAN STR (PHIEN_BAN) = 3 STR_(1 + 2) = 1 + 2 STR (__LINE__) = 18 STR_(__LINE__) = __LINE__
#define STR_(x) #x
#define STR(x) STR_(x)
#define PHIEN_BAN 3
STR_(PHIEN_BAN) /* -> "PHIEN_BAN" <- ten macro, khong phai gia tri */
STR(PHIEN_BAN) /* -> "3" <- dung y dinh */#Nối hai token
#define NOI(a, b) a##b
NOI(bien_, 1) /* -> bien_1 */
NOI(x, y) /* -> xy */Toán tử nối token
Dấu thăng đôi giữa hai thứ trong thân macro. Nó dán token bên trái với token bên phải thành một token duy nhất. Kết quả phải là một token hợp lệ, nếu không thì hành vi không xác định.
terminal
./thu-noi
NOI(bien_, 1) = 111 NOI2(TIEN_TO,PHIEN_BAN) = 333
Ứng dụng: sinh nhiều hàm cùng khuôn
danh-sach.h
#ifndef DANH_SACH_H
#define DANH_SACH_H
#include <stdlib.h>
#define KHAI_BAO_DANH_SACH(T) \
typedef struct { \
T *ds; \
size_t n, suc_chua; \
} DanhSach_##T; \
\
static inline void ds_khoi_tao_##T(DanhSach_##T *d) { \
d->ds = NULL; \
d->n = 0; \
d->suc_chua = 0; \
} \
\
static inline int ds_them_##T(DanhSach_##T *d, T gia_tri) { \
if (d->n == d->suc_chua) { \
size_t moi = d->suc_chua ? d->suc_chua * 2 : 8; \
T *tam = realloc(d->ds, moi * sizeof *d->ds); \
\
if (!tam) return -1; \
\
d->ds = tam; \
d->suc_chua = moi; \
} \
\
d->ds[d->n++] = gia_tri; \
\
return 0; \
} \
\
static inline void ds_giai_phong_##T(DanhSach_##T *d) { \
free(d->ds); \
ds_khoi_tao_##T(d); \
}
#endif /* DANH_SACH_H */Dùng nó
#include "danh-sach.h"
KHAI_BAO_DANH_SACH(int)
KHAI_BAO_DANH_SACH(double)
int main(void) {
DanhSach_int a;
ds_khoi_tao_int(&a);
for (int i = 0; i < 100; ++i)
if (ds_them_int(&a, i * i) != 0) return 1;
printf("%zu phan tu, phan tu cuoi = %d\n", a.n, a.ds[a.n - 1]);
ds_giai_phong_int(&a);
return 0;
}#Macro biến đối số
#define GHI(fmt, ...) fprintf(stderr, fmt "\n", __VA_ARGS__)
GHI("Loi tai %s dong %d", ten, dong);
/* -> fprintf(stderr, "Loi tai %s dong %d" "\n", ten, dong); */Ba cách viết cùng một macro nhật ký
/* Cach 1: mo rong GNU, chay tren gcc va clang */
#define GHI(fmt, ...) \
fprintf(stderr, "[%s:%d] " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__)
/* Cach 2: C23 */
#define GHI(fmt, ...) \
fprintf(stderr, "[%s:%d] " fmt "\n", __FILE__, __LINE__ __VA_OPT__(,) __VA_ARGS__)
/* Cach 3: chuan tu C99, khong can mo rong nao
Meo: dat __VA_ARGS__ vao GIUA thay vi o cuoi */
#define GHI(...) \
fprintf(stderr, "[%s:%d] ", __FILE__, __LINE__), \
fprintf(stderr, __VA_ARGS__), \
fprintf(stderr, "\n")
/* Cach 3 luon co it nhat mot doi so cho __VA_ARGS__,
nen khong bao gio co dau phay thua.
Danh doi: ba loi goi thay vi mot, va khong noi chuoi
dinh dang lai duoc. */#Ba ứng dụng thật
Một: bảng tên cho enum
X macro
/* Dinh nghia danh sach MOT lan */
#define DANH_SACH_LOI \
X(QL_OK, "thanh cong") \
X(QL_MSSV_SAI, "MSSV khong hop le") \
X(QL_TEN_RONG, "ho ten khong duoc rong") \
X(QL_TRUNG_MSSV, "MSSV da ton tai") \
X(QL_HET_BO_NHO, "khong du bo nho")
/* Dung lan mot: sinh enum */
#define X(ten, mo_ta) ten,
typedef enum { DANH_SACH_LOI } MaLoi;
#undef X
/* Dung lan hai: sinh bang mo ta */
#define X(ten, mo_ta) [ten] = mo_ta,
static const char *const MO_TA[] = { DANH_SACH_LOI };
#undef X
const char *ql_mo_ta_loi(MaLoi m) {
if ((size_t)m >= sizeof MO_TA / sizeof MO_TA[0]) return "loi khong ro";
return MO_TA[m];
}Hai: kiểm tra giả định có thông báo tự sinh
#define KIEM_TINH(dk) _Static_assert(dk, #dk)
KIEM_TINH(sizeof(int) == 4);
KIEM_TINH(SV_MSSV_CO >= 8);
/* Khi sai, thong bao chinh la dieu kien ban viet:
error: static assertion failed: "sizeof(int) == 4"
Neu tu go chuoi thong bao thi som muon no lech voi dieu kien. */Ba: sinh hàm kiểm thử
#define KIEM(bieu_thuc) \
do { \
++s_tong; \
\
if (bieu_thuc) { \
++s_dat; \
} else { \
printf(" THAT BAI %s:%d: %s\n", \
__FILE__, __LINE__, #bieu_thuc); \
} \
} while (0)
/* Dung: */
KIEM(sv_kiem_mssv("20210001") == QL_OK);
KIEM(sv_kiem_mssv("123") == QL_MSSV_SAI);
KIEM(sv_kiem_ten("") == QL_TEN_RONG);terminal
./kiem-thu
THAT BAI kiem-sinhvien.c:24: sv_kiem_ten(" ") == QL_TEN_RONG
17/18 phep kiem datTự làm thử
- Viết
STR_vàSTRrồi thử với một macro, xác nhận chỉ bản hai tầng cho ra giá trị. - Viết
IN_INT(x)in cả tên biểu thức lẫn giá trị, thử vớituoi * 2và*p. - Thử
NOI(1, x)và đọc thông báo lỗi của trình biên dịch. - Cài
KHAI_BAO_DANH_SACHchointvàdouble, thêm một trăm phần tử vào mỗi cái. - Viết bảng mã lỗi bằng X macro, thêm một mã mới và đếm số dòng phải sửa.
- Viết macro
KIEMrồi cố ý cho một phép kiểm thất bại, xác nhận nó in ra đúng số dòng và đúng biểu thức.
Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.
Tóm tắt
- Dấu thăng biến đối số thành chuỗi giữ nguyên văn bản gốc, và tự thêm dấu thoát cho dấu nháy.
- Đối số đi kèm dấu thăng hoặc dấu thăng đôi thì không được bung trước, nên cần một tầng macro nữa khi bạn muốn giá trị.
- Dấu thăng đôi dán hai token thành một, và kết quả phải là một token hợp lệ.
- Macro biến đối số cần
##__VA_ARGS__của GNU hoặc__VA_OPT__của C23 để nuốt dấu phẩy khi không có đối số. - X macro cho phép sinh nhiều bảng song song từ một danh sách duy nhất, nên chúng không bao giờ lệch nhau.