Bỏ qua điều hướng, tới nội dung chính
Học C
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àmMacro
Kiểm kiểu đối sốCóKhông
Đối số được tính đúng một lầnCóKhông
Có địa chỉ, lấy con trỏ đượcCóKhông
Xuất hiện trong trình gỡ lỗiCóKhông
Làm việc với mọi kiểuKhôngCó
Truy cập được FILE và LINE của nơi gọiKhôngCó
Chắc chắn không có chi phí gọi hàmKhông đảm bảoCó

#Cạm bẫy một: thiếu ngoặc

Preprocessor thay token, không thay giá trị. Dấu ngoặc là thứ duy nhất giữ lại ý định của bạn.
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ứcKết quảMong đợi
SQ_XAU(2 + 3)1125
SQ(2 + 3)2525
100 / SQ_XAU(5)1004
100 / SQ(5)44

#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ọcDùng sau if không ngoặcNuốt được dấu chấm phẩy
Không bọc gìKhôngKhông
Bọc bằng ngoặc nhọnĐượcKhông
do while khôngĐượcĐược

#Danh sách kiểm khi viết macro

Câu hỏiNế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ưaBiể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ưaHỏng khi dùng sau if hoặc trước else
Tên macro có viết hoa hết khôngCó 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ưaCó thể che biến cùng tên của người gọi
Có thể viết bằng static inline khôngNế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ử

  1. Viết SQ_XAU và SQ rồi chạy gcc -E -P, so kết quả với bảng ở trên.
  2. 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.
  3. Chạy SQ(i++) với i = 5 trên máy bạn và so sánh với kết quả trong bài.
  4. Viết HOAN_DOI_XAU rồi dùng nó sau if với điều kiện sai, xác nhận hai biến vẫn bị đổi.
  5. 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.
  6. Gọi HOAN_DOI(t, u) với một biến tên đúng là t và 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 sau if vừ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 để.
  • -Wall bắ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.