Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 54.126 phút đọc

Macro hàm và các bẫy

Sau bài này bạn sẽ làm được

  • Bọc ngoặc đúng chỗ cho mọi tham số và cho cả biểu thức
  • Nhận ra bẫy đánh giá nhiều lần
  • Dùng do while (0) cho macro dạng câu lệnh
  • Biết khi nào static inline tốt hơn macro

Macro không phải hàm. Nó là phép thay thế văn bản, và bốn cái bẫy trong bài này đều đến từ chính điều đó. Ba cái đầu có cách chữa cơ học, cái thứ tư thì không.

#Bẫy một: thiếu ngoặc

Thiếu ngoặc
#define BINH(x) x * x

BINH(3 + 1)        /* bung ra: 3 + 1 * 3 + 1  =  7, khong phai 16 */

#define NUA(x) (x) / 2

10 / NUA(4)        /* bung ra: 10 / (4) / 2  =  1, khong phai 5 */

#define TOI_DA 10 + 5

int a[TOI_DA];     /* int a[10 + 5], tinh co dung */
int b = TOI_DA * 2;  /* 10 + 5 * 2 = 20, khong phai 30 */
Ngoặc đầy đủ
#define BINH(x) ((x) * (x))

BINH(3 + 1)        /* ((3 + 1) * (3 + 1)) = 16 */

#define NUA(x) ((x) / 2)

10 / NUA(4)        /* 10 / ((4) / 2) = 5 */

#define TOI_DA (10 + 5)

int b = TOI_DA * 2;  /* (10 + 5) * 2 = 30 */
terminal
# Xem macro bung ra thành gì
gcc -E -P macro.c
int main(void) {
    int a = 3 + 1 * 3 + 1;
    int b = ((3 + 1) * (3 + 1));
    return a + b;
}

#Bẫy hai: đánh giá nhiều lần

Ngoặc đủ vẫn sai
#define TOI_DA(a, b) ((a) > (b) ? (a) : (b))

int i = 0;
int x = TOI_DA(i++, 5);

/* Bung ra:
     int x = ((i++) > (5) ? (i++) : (5));

   i++ chay HAI lan neu nhanh dau duoc chon.
   Va day cung la sua i nhieu lan trong mot cau lenh, tuc UB
   theo Bai 33.5. */

int y = TOI_DA(f(), g());
/* f() co the chay hai lan. Neu f doc mot dong tu tep
   thi ban vua mat mot dong du lieu. */
Đối sốNguy hiểmVì sao
Biến thường, xKhôngĐọc nhiều lần vô hại
Hằng số, 5KhôngVô hại
i++ hoặc ++iCóTác dụng phụ chạy nhiều lần
f()CóHàm chạy nhiều lần
*p++CóCon trỏ tiến nhiều lần
a[i++]CóCả hai vấn đề
x + 1Không, nhưng chậmPhép cộng tính nhiều lần

#Bẫy ba: macro dạng câu lệnh

Nhiều câu lệnh không bọc
#define GHI_LOG(tin) \
    dem_log++;       \
    printf("%s\n", tin)

if (co_loi)
    GHI_LOG("co loi");

/* Bung ra:
     if (co_loi)
         dem_log++;
     printf("%s\n", "co loi");     <- LUON chay!

   Va te hon: */

if (co_loi)
    GHI_LOG("co loi");
else
    xu_ly();
/* error: 'else' without a previous 'if' */
do while (0)
#define GHI_LOG(tin) do {   \
    dem_log++;              \
    printf("%s\n", tin);    \
} while (0)

if (co_loi)
    GHI_LOG("co loi");
else
    xu_ly();

/* Bung ra:
     if (co_loi)
         do { dem_log++; printf(...); } while (0);
     else
         xu_ly();

   Dung. Va dau cham phay o cuoi loi goi macro tro thanh
   dau cham phay ket thuc "do while", tuc dung cu phap. */
Ba dạng macro và cách bọc từng dạng
/* 1. Macro BIEU THUC: co gia tri, dung ngoac tron */
#define BINH(x) ((x) * (x))
int y = BINH(3) + 1;

/* 2. Macro CAU LENH: khong co gia tri, dung do while (0) */
#define DOI_CHO(a, b) do {          \
    int _t = (a); (a) = (b); (b) = _t; \
} while (0)
DOI_CHO(x, y);

/* 3. Macro KHAI BAO: khong bo gi ca, va nho KHONG co dau cham phay */
#define KHAI_BAO_VEC(T, ten)                             \
    typedef struct { T *d; size_t n; } ten;              \
    int ten##_them(ten *v, T x);
KHAI_BAO_VEC(int, VecInt)      /* khong dau cham phay */

/* Neu ban muon nguoi dung viet dau cham phay cho dep thi them
   mot khai bao gia o cuoi: */
#define KHAI_BAO_VEC(T, ten)                             \
    typedef struct { T *d; size_t n; } ten;              \
    int ten##_them(ten *v, T x);                         \
    _Static_assert(1, "")            /* nuot dau cham phay */

KHAI_BAO_VEC(int, VecInt);     /* gio co dau cham phay, va dung */

#Bẫy bốn: biến che khuất

Bẫy không có cách chữa cơ học
#define DOI_CHO(a, b) do {          \
    int _t = (a); (a) = (b); (b) = _t; \
} while (0)

int _t = 5, y = 10;
DOI_CHO(_t, y);

/* Bung ra:
     do { int _t = (_t); (_t) = (y); (y) = _t; } while (0);
              ^^^^^^^^^^^
              bien _t MOI che khuat bien _t cua ban,
              va khoi tao no bang CHINH NO, tuc gia tri rac

   Ket qua: hoan toan sai, va khong canh bao nao. */
terminal
# Hoán đổi im lặng không làm gì cả
gcc -std=c11 -Wall -Wextra -Wshadow -o che.exe che.c && ./che.exe
che.c: In function 'main':
che.c:3:32: warning: declaration of '_t' shadows a previous local [-Wshadow]
 #define DOI_CHO(a, b) do { int _t = (a); (a) = (b); (b) = _t; } while (0)
                                ^~
che.c:8:5: note: in expansion of macro 'DOI_CHO'
     DOI_CHO(_t, y);
     ^~~~~~~
che.c:6:9: note: shadowed declaration is here
5 10

#Khi nào dùng static inline thay thế

Macrostatic inline
Kiểm tra kiểuKhôngCó
Đánh giá đối số một lầnKhông, trừ khi dùng thủ thuậtCó
Gỡ lỗi bằng GDBKhó, cần -g3Dễ, bước vào được
Thông báo lỗiTrỏ vào dòng #defineTrỏ đúng chỗ
Làm việc với nhiều kiểuCóKhông, trừ khi dùng _Generic
Dùng được trong #ifCóKhông
Dùng làm kích thước mảngCó, nếu là hằngKhông
Truy cập __FILE__ và __LINE__ của nơi gọiCóKhông
Tốc độBằng nhau ở -O2Bằng nhau ở -O2
Bốn quy tắc gói lại
/* 1. NGOAC quanh moi tham so VA quanh ca bieu thuc */
#define BINH(x) ((x) * (x))

/* 2. do while (0) cho macro dang cau lenh */
#define DOI_CHO(a, b) do { ... } while (0)

/* 3. TEN VIET HOA, va co tien to neu la thu vien */
#define ML_TOI_DA(a, b) ...

/* 4. Ghi vao chu thich neu macro danh gia doi so nhieu lan */
/* CANH BAO: danh gia a va b hai lan. Khong truyen bieu thuc
   co tac dung phu. */
#define ML_TOI_DA(a, b) ((a) > (b) ? (a) : (b))

Tự làm thử

  1. Viết BINH(3 + 1) không ngoặc và xem nó bung ra bằng gcc -E -P.
  2. Truyền i++ cho một macro đánh giá hai lần và in giá trị i sau đó.
  3. Viết macro nhiều câu lệnh không bọc và dùng nó trong if else.
  4. Đặt tên biến trùng với biến tạm của macro và chạy với -Wshadow.
  5. Viết cùng một thứ bằng macro và bằng static inline, so hợp ngữ ở -O2.
  6. Tìm trong một dự án mọi macro dài hơn năm dòng và xem có thay được bằng hàm không.

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

  • Ngoặc quanh mọi tham số và quanh cả biểu thức. Hai lớp chống hai loại lỗi ưu tiên khác nhau.
  • Ngoặc đủ vẫn không cứu được bẫy đánh giá nhiều lần; chỉ biểu thức khối hoặc hàm mới cứu được.
  • do while (0) biến nhiều câu lệnh thành một câu lệnh đơn cần dấu chấm phẩy, giống hệt lời gọi hàm.
  • Biến tạm trong macro có thể che khuất biến của người dùng, và không có cách chữa cơ học nào ngoài đặt tên khó trùng.
  • Bốn trường hợp macro là đúng: cần __LINE__ của nơi gọi, cần chuỗi hóa, cần nhiều kiểu, cần hằng số dùng trong #if.