Bài 19.328 phút đọc
Macro hàm và ba cạm bẫy
Sau bài này bạn sẽ làm được
- Chỉ ra kết quả sai khi macro thiếu ngoặc quanh tham số và quanh cả biểu thức
- Giải thích vì sao đối số có tác dụng phụ làm macro chạy sai
- Bọc macro nhiều câu lệnh bằng do while không và nói được lý do
- Đọc kết quả gcc -E để kiểm tra macro bung ra thành gì
Macro hàm trông giống hàm, được gọi giống hàm, và cư xử hoàn toàn không giống hàm. Ba cạm bẫy trong bài này là ba cách khác nhau mà sự khác biệt đó cắn bạn, và cả ba đều là những lỗi im lặng.
#Macro hàm
#define BINH_PHUONG(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))Macro hàm
Macro có danh sách tham số. Preprocessor thay tên macro cùng danh sách đối số bằng phần thân, sau khi thay mỗi tên tham số bằng đúng những token của đối số tương ứng.
| Hàm | Macro | |
|---|---|---|
| Kiểm kiểu đối số | Có | Không |
| Đối số được tính đúng một lần | Có | Không |
| Có địa chỉ, lấy con trỏ được | Có | Không |
| Xuất hiện trong trình gỡ lỗi | Có | Không |
| Làm việc với mọi kiểu | Không | Có |
| Truy cập được FILE và LINE của nơi gọi | Không | Có |
| Chắc chắn không có chi phí gọi hàm | Không đảm bảo | Có |
#Cạm bẫy một: thiếu ngoặc
terminal
cat p2.c
#define SQ_XAU(x) x * x
#define SQ(x) ((x) * (x))
int main(void) {
int a = SQ_XAU(2 + 3);
int b = SQ(2 + 3);
int c = 100 / SQ_XAU(5);
int d = 100 / SQ(5);
return a + b + c + d;
}gcc -E -P p2.c
int main(void) {
int a = 2 + 3 * 2 + 3;
int b = ((2 + 3) * (2 + 3));
int c = 100 / 5 * 5;
int d = 100 / ((5) * (5));
return a + b + c + d;
}| Biểu thức | Kết quả | Mong đợi |
|---|---|---|
| SQ_XAU(2 + 3) | 11 | 25 |
| SQ(2 + 3) | 25 | 25 |
| 100 / SQ_XAU(5) | 100 | 4 |
| 100 / SQ(5) | 4 | 4 |
#Cạm bẫy hai: đánh giá nhiều lần
p3.c
#include <stdio.h>
#define SQ(x) ((x) * (x))
static int dem_goi = 0;
static int f(void) { ++dem_goi; return 3; }
int main(void) {
int i = 5;
int r = SQ(i++);
printf("i sau SQ(i++) = %d (mong doi 6)\n", i);
printf("r = %d\n", r);
dem_goi = 0;
int m = ((f()) > (2) ? (f()) : (2)); /* chinh la MAX(f(), 2) */
printf("MAX(f(), 2): f() duoc goi %d lan, ket qua %d\n", dem_goi, m);
return 0;
}terminal
gcc -std=c11 -o p3 p3.c && ./p3
i sau SQ(i++) = 7 (mong doi 6) r = 30 MAX(f(), 2): f() duoc goi 2 lan, ket qua 3
Đối số có tác dụng phụ
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int m = MAX(f(), g()); /* f va g moi cai chay hai lan */
int n = MAX(*p++, *q++); /* hai con tro nhay hai buoc */
int k = MAX(doc_adc(), 0); /* doc phan cung hai lan, hai gia tri khac nhau */Hàm nội tuyến, hoặc tính trước
static inline int imax(int a, int b) { return a > b ? a : b; }
int m = imax(f(), g()); /* moi cai chay dung mot lan */
/* Hoac neu buoc phai dung macro, tinh truoc: */
int fa = f(), gb = g();
int m2 = MAX(fa, gb); /* gio doi so la bien thuan tuy */#Cạm bẫy ba: nhiều câu lệnh
p4.c
#include <stdio.h>
#define HOAN_DOI_XAU(a, b) t = a; a = b; b = t;
int main(void) {
int p = 1, q = 2, t = 0;
int x = 0; /* dieu kien SAI */
if (x) HOAN_DOI_XAU(p, q);
printf("p = %d, q = %d, t = %d\n", p, q, t);
return 0;
}terminal
gcc -std=c11 -Wall -o p4 p4.c
p4.c:3:28: warning: macro expands to multiple statements [-Wmultistatement-macros]
3 | #define HOAN_DOI_XAU(a, b) t = a; a = b; b = t;
| ^
p4.c:9:12: note: in expansion of macro 'HOAN_DOI_XAU'./p4
p = 2, q = 0, t = 0
Vì sao dùng do while không
#define HOAN_DOI(a, b) do { \
int t = (a); \
(a) = (b); \
(b) = t; \
} while (0)| Cách bọc | Dùng sau if không ngoặc | Nuốt được dấu chấm phẩy |
|---|---|---|
| Không bọc gì | Không | Không |
| Bọc bằng ngoặc nhọn | Được | Không |
| do while không | Được | Được |
#Danh sách kiểm khi viết macro
| Câu hỏi | Nếu trả lời sai thì |
|---|---|
| Mỗi tham số có ngoặc quanh nó chưa | Đối số là biểu thức sẽ bị xé ra |
| Cả thân macro có ngoặc quanh nó chưa | Biểu thức xung quanh sẽ xé macro ra |
| Mỗi tham số xuất hiện đúng một lần chưa | Đối số có tác dụng phụ sẽ chạy nhiều lần |
| Macro nhiều câu lệnh đã bọc do while chưa | Hỏng khi dùng sau if hoặc trước else |
| Tên macro có viết hoa hết không | Có thể phá tên biến hoặc hàm của người khác |
| Tên biến bên trong có tiền tố gạch dưới chưa | Có thể che biến cùng tên của người gọi |
| Có thể viết bằng static inline không | Nếu có thì nên viết bằng hàm, xem Bài 19.4 |
Bộ macro tiện ích viết đúng
tien-ich.h
#ifndef TIEN_ICH_H
#define TIEN_ICH_H
#include <stddef.h>
#include <stdlib.h>
/* So phan tu cua mot MANG THAT SU. Xem canh bao ben duoi. */
#define SO_PHAN_TU(a) (sizeof (a) / sizeof (a)[0])
/* Giai phong roi dat lai NULL, chong giai phong hai lan */
#define GIAI_PHONG(p) do { free(p); (p) = NULL; } while (0)
/* Danh dau tham so co y khong dung, tat canh bao -Wunused-parameter */
#define KHONG_DUNG(x) ((void)(x))
/* Kep gia tri vao khoang. Chu y: x duoc tinh NHIEU LAN. */
#define KEP(x, lo, hi) ((x) < (lo) ? (lo) : ((x) > (hi) ? (hi) : (x)))
#endif /* TIEN_ICH_H */Tự làm thử
- Viết
SQ_XAUvàSQrồi chạygcc -E -P, so kết quả với bảng ở trên. - Tìm một biểu thức mà
#define SQ_NUA(x) (x) * (x)vẫn cho kết quả sai, và giải thích. - Chạy
SQ(i++)vớii = 5trên máy bạn và so sánh với kết quả trong bài. - Viết
HOAN_DOI_XAUrồi dùng nó sauifvới điều kiện sai, xác nhận hai biến vẫn bị đổi. - Bọc lại bằng ngoặc nhọn thôi rồi thêm nhánh
else, đọc thông báo lỗi của trình biên dịch. - Gọi
HOAN_DOI(t, u)với một biến tên đúng làtvà xem kết quả.
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
- Macro cần ngoặc quanh mỗi tham số và quanh cả thân, thiếu bên nào cũng có biểu thức làm nó sai.
- Tham số xuất hiện nhiều lần trong thân thì đối số được tính nhiều lần, nên
i++tăng hai lần vàf()chạy hai lần. - Macro nhiều câu lệnh phải bọc
do { ... } while (0), vì chỉ cách đó mới vừa dùng được sauifvừa nuốt được dấu chấm phẩy. - Biến khai báo bên trong macro có thể che biến cùng tên của người gọi, và không có cách nào chữa triệt để.
-Wallbắt được cạm bẫy nhiều câu lệnh, còn hai cạm bẫy kia thì không ai báo cho bạn.