Bài 19.424 phút đọc
Khi nào macro, khi nào hàm
Sau bài này bạn sẽ làm được
- Nêu bốn nhược điểm của macro so với hàm nội tuyến
- Viết static inline thay cho macro trong trường hợp thông thường
- Kể bốn tình huống bắt buộc phải dùng macro
- Viết bộ macro tiện ích dùng chung cho mọi dự án
Sau ba cạm bẫy ở bài trước, câu hỏi tự nhiên là: vậy có cách nào không dùng macro không? Câu trả lời cho hầu hết trường hợp là có, và nó không chậm hơn một chu kỳ nào.
#static inline là gì
Macro
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int m = MAX(f(), g()); /* f va g moi cai chay hai lan */Hàm nội tuyến
static inline int imax(int a, int b) {
return a > b ? a : b;
}
int m = imax(f(), g()); /* moi cai chay dung mot lan */inline
Gợi ý cho trình biên dịch rằng nên chép thân hàm vào chỗ gọi thay vì sinh lệnh gọi. Đó là gợi ý chứ không phải mệnh lệnh, và trình biên dịch được quyền bỏ qua theo cả hai chiều.
#Bốn điểm hàm thắng macro
| Điểm | Với macro | Với hàm |
|---|---|---|
| Đối số được tính một lần | Không, mỗi lần xuất hiện là một lần tính | Có, luôn luôn |
| Kiểm kiểu | Chỉ kiểm được sau khi bung, nên lỗi mơ hồ | Kiểm theo nguyên mẫu, thông báo gọi tên tham số |
| Trình gỡ lỗi | Không thấy tên, không đặt điểm dừng được | Thấy tên, vào được, xem biến được |
| Thông báo lỗi | Chỉ vào dòng define, chỗ gọi chỉ là một dòng note | Chỉ đúng dòng bạn viết |
Đổi macro sang hàm, ví dụ đầy đủ
/* Truoc */
#define DIEM_TB(sv) (((sv)->d1 + (sv)->d2 + (sv)->d3) / 3)
#define DAT(sv) (DIEM_TB(sv) >= 500)
#define XEP_LOAI(sv) (DIEM_TB(sv) >= 900 ? "Xuat sac" : \
DIEM_TB(sv) >= 800 ? "Gioi" : \
DIEM_TB(sv) >= 650 ? "Kha" : "Trung binh")
/* XEP_LOAI goi DIEM_TB toi ba lan, moi lan cong ba so va chia. */
/* Sau */
static inline uint16_t sv_diem_tb(const SinhVien *sv) {
return (uint16_t)((sv->d1 + sv->d2 + sv->d3) / 3);
}
static inline int sv_dat(const SinhVien *sv) {
return sv_diem_tb(sv) >= 500;
}
static inline const char *sv_xep_loai(const SinhVien *sv) {
uint16_t tb = sv_diem_tb(sv); /* tinh MOT lan */
if (tb >= 900) return "Xuat sac";
if (tb >= 800) return "Gioi";
if (tb >= 650) return "Kha";
return "Trung binh";
}#Bốn lúc macro vẫn thắng
Một: cần biết vị trí trong mã nguồn
#define GHI(fmt, ...) \
fprintf(stderr, "[%s:%d] " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__)
/* Mot HAM khong lam duoc dieu nay: __FILE__ va __LINE__ trong
than ham se luon la vi tri cua CHINH HAM DO, chu khong phai
vi tri noi goi.
Day la ly do assert la macro chu khong phai ham. */Hai: cần làm việc với nhiều kiểu
#define SO_PHAN_TU(a) (sizeof (a) / sizeof (a)[0])
/* Chay voi int[10], double[5], SinhVien[100], char[64].
Mot ham thi phai viet lai cho tung kieu. */
/* Tu C11 co _Generic, lam duoc mot phan: */
#define ABS(x) _Generic((x), \
int: abs, \
long: labs, \
float: fabsf, \
double: fabs \
)(x)
/* Nhung _Generic van phai liet ke tung kieu, va no van
nam trong mot macro. */Ba: cần biến mất hoàn toàn ở bản phát hành
#ifdef GO_LOI
#define GHI(fmt, ...) fprintf(stderr, fmt "\n", ##__VA_ARGS__)
#else
#define GHI(fmt, ...) ((void)0)
#endif
/* Mot ham rong van co the sinh ra lenh goi, va doi so
van duoc tinh. Macro thi bien mat truoc khi trinh bien dich
nhin thay. Bai 19.6 se do bang so lieu. */Bốn: cần sinh mã hoặc sinh tên
#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; \
}
KHAI_BAO_DANH_SACH(int) /* sinh ra DanhSach_int, ds_khoi_tao_int */
KHAI_BAO_DANH_SACH(double) /* sinh ra DanhSach_double, ... */
/* Day la cach C gia lap kieu tong quat. Khong dep, nhung no
la cach duy nhat khi ban can container cho nhieu kieu.
Bai 19.5 se noi ky ve toan tu ## dung o day. */#Bộ macro tiện ích cho mọi dự án
tien-ich.h
#ifndef TIEN_ICH_H
#define TIEN_ICH_H
#include <stddef.h>
#include <stdlib.h>
/* --- Macro: co ly do chinh dang de la macro --- */
/* Nhieu kieu: chay voi mang cua bat cu kieu nao */
#define SO_PHAN_TU(a) (sizeof (a) / sizeof (a)[0])
/* Nhieu cau lenh, va can sua chinh bien duoc truyen vao */
#define GIAI_PHONG(p) do { free(p); (p) = NULL; } while (0)
/* Can nhan mot bieu thuc bat ky va vut no di */
#define KHONG_DUNG(x) ((void)(x))
/* Can bien thanh chuoi: chi macro lam duoc */
#define KIEM_TINH(dk) _Static_assert(dk, #dk)
/* --- Ham: khong co ly do gi de la macro --- */
static inline int imin(int a, int b) { return a < b ? a : b; }
static inline int imax(int a, int b) { return a > b ? a : b; }
static inline int ikep(int x, int lo, int hi) { return imin(imax(x, lo), hi); }
static inline size_t zmin(size_t a, size_t b) { return a < b ? a : b; }
static inline size_t zmax(size_t a, size_t b) { return a > b ? a : b; }
static inline double dmin(double a, double b) { return a < b ? a : b; }
static inline double dmax(double a, double b) { return a > b ? a : b; }
#endif /* TIEN_ICH_H */Tự làm thử
- Viết
MAXdạng macro vàimaxdạng hàm, gọi cả hai với một hàm có tác dụng phụ, đếm số lần hàm đó chạy. - Truyền một con trỏ vào
MAXcùng với mộtint, so sánh thông báo của trình biên dịch với khi dùngimax. - Biên dịch một chương trình gọi
imaxở mức-O0và-O2, đếm số lệnhcalltrong hợp ngữ. - Viết hàm
giai_phong(void *p)đặtp = NULLrồi kiểm tra con trỏ của người gọi có thành NULL không. - Đổi ba macro
DIEM_TB,DAT,XEP_LOAIở trên thành hàm và đếm số phép chia trong cả hai bản.
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
- Hàm trong header phải viết
static inline, vì quy tắcinlineđơn lẻ của C99 dễ gây lỗi undefined reference. - Hàm thắng macro ở bốn điểm: đối số tính một lần, có kiểm kiểu, gỡ lỗi được, và thông báo lỗi đúng chỗ.
- Macro chỉ cần thiết khi bạn cần vị trí trong mã nguồn, cần nhiều kiểu, cần biến mất hoàn toàn, hoặc cần sinh tên.
GIAI_PHONGphải là macro vì nó cần gán NULL vào chính biến của người gọi, việc mà hàm nhận con trỏ không làm được.- Ở mức
-O2, trình biên dịch tự nội tuyến hàm nhỏ, nên viết hàm không mất gì về tốc độ.