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

Thủ thuật và cách gỡ lỗi macro

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

  • Viết ARRAY_LEN báo lỗi khi truyền con trỏ
  • Sinh tên biến duy nhất bằng __LINE__
  • Xem macro bung ra bằng gcc -E
  • Biết khi nào nên dừng lại và không dùng macro nữa

Bốn thủ thuật cuối cùng, rồi một mục nói về chuyện quan trọng hơn tất cả: khi nào không nên dùng macro nữa. Bộ tiền xử lý mạnh tới mức rất dễ đi quá xa.

#ARRAY_LEN an toàn

Bản thường
#define MANG_LEN(a) (sizeof(a) / sizeof((a)[0]))

int a[10];
MANG_LEN(a)                  /* 10, dung */

void f(int a[10]) {
    MANG_LEN(a)              /* 2 tren may 64 bit!
                                Vi tham so mang phan ra thanh con tro,
                                nen sizeof(a) la 8 va sizeof(a[0]) la 4 */
}

int *p = malloc(10 * sizeof *p);
MANG_LEN(p)                  /* 2, va khong canh bao nao */
Bản kiểm tra
/* Ban bao loi khi truyen con tro, tren GCC va Clang */

#define LA_MANG(a) \
    __builtin_types_compatible_p(__typeof__(a), __typeof__(&(a)[0]))

#define MANG_LEN(a)                              \
    (sizeof(a) / sizeof((a)[0])                  \
     + sizeof(char[1 - 2 * LA_MANG(a)]) - 1)

int a[10];
MANG_LEN(a)                  /* 10 */

int *p;
MANG_LEN(p)                  /* error: size of unnamed array is negative */
terminal
# GCC 8.1 báo cả hai: một cảnh báo và một lỗi
gcc -std=c11 -Wall -c manglen.c
manglen.c: In function 'f':
manglen.c:7:16: warning: division 'sizeof (int *) / sizeof (int)' does not compute the number of array elements [-Wsizeof-pointer-div]
     (sizeof(a) / sizeof((a)[0]) + sizeof(char[1 - 2 * LA_MANG(a)]) - 1)
                ^
manglen.c:14:12: note: in expansion of macro 'MANG_LEN'
     return MANG_LEN(p);
manglen.c:7:46: error: size of unnamed array is negative
     (sizeof(a) / sizeof((a)[0]) + sizeof(char[1 - 2 * LA_MANG(a)]) - 1)
                                              ^

#Tên biến duy nhất

Bài 54.1 để lại một vấn đề chưa giải
#define DOI_CHO(a, b) do { int _t = (a); (a) = (b); (b) = _t; } while (0)

int _t = 5, y = 10;
DOI_CHO(_t, y);              /* im lang khong lam gi, Bai 54.1 */

/* Cach chua: sinh ten bien theo SO DONG */

#define NOI2(a, b) a##b
#define NOI(a, b)  NOI2(a, b)          /* hai tang, Bai 54.2 */
#define DUY_NHAT(x) NOI(x, __LINE__)

#define DOI_CHO(a, b) do {                              \
    int DUY_NHAT(_dc) = (a);                            \
    (a) = (b);                                          \
    (b) = DUY_NHAT(_dc);                                \
} while (0)

int _t = 5, y = 10;
DOI_CHO(_t, y);              /* sinh bien "_dc42" neu o dong 42 */
Ứng dụng: khối dọn dẹp tự động
/* Mau "mo, lam viec, tu dong dong" bang mot vong lap chay mot lan */

#define VOI_TEP(bien, ten, che_do)                                  \
    for (FILE *bien = fopen(ten, che_do), *DUY_NHAT(_c) = bien;     \
         DUY_NHAT(_c);                                              \
         fclose(bien), DUY_NHAT(_c) = NULL)

VOI_TEP(f, "a.txt", "r") {
    char dong[256];
    while (fgets(dong, sizeof dong, f)) fputs(dong, stdout);
}
/* fclose tu chay khi ra khoi khoi */

/* CANH BAO ba dieu:
     1. "break" trong khoi se thoat vong lap MA KHONG chay fclose
     2. "return" trong khoi cung vay
     3. fopen that bai thi bien la NULL va khoi khong chay,
        nen ban khong biet la co loi

   Nen mau nay tien nhung NGUY HIEM. C khong co destructor,
   va moi no bat chuoc deu co lo hong.

   Cach an toan hon: goto va mot nhan don dep, Bai 20.6. */

#Kiểm tra kiểu trong macro

/* 1. _Generic, C11 */

#define KIEM_KIEU(x, T) \
    _Static_assert(_Generic((x), T: 1, default: 0), "sai kieu")

void f(int x) {
    KIEM_KIEU(x, int);       /* dat */
    KIEM_KIEU(x, double);    /* error: static assertion failed: "sai kieu" */
}

/* 2. __builtin_types_compatible_p, GNU */

#define CUNG_KIEU(a, b) \
    __builtin_types_compatible_p(__typeof__(a), __typeof__(b))

_Static_assert(CUNG_KIEU(x, y), "hai bien phai cung kieu");

/* 3. Thu thuat "1 ? a : b", chuan hoan toan */

#define EP_AN_TOAN(T, x) ((T)(1 ? (x) : (T)0))

/* Neu x khong chuyen duoc sang T thi hai nhanh cua ?: khong co
   kieu chung, va trinh bien dich bao loi.

   Bai 53.3 da dung ky thuat nay cho container_of. */

/* 4. Kiem tra con tro khong phai void * */

#define KHONG_VOID(p) \
    _Static_assert(!__builtin_types_compatible_p(__typeof__(p), void *), \
                   "can con tro co kieu")

#Bốn lệnh gỡ lỗi macro

# 1. Xem ma sau khi tien xu ly
gcc -E file.c                  # ke ca #line va noi dung header
gcc -E -P file.c               # BO dong #line, de doc hon
gcc -E -P file.c | grep -A5 MACRO_CUA_BAN

# Meo: dua qua mot bo dinh dang cho de doc
gcc -E -P file.c | clang-format

# 2. Liet ke MOI macro da dinh nghia
gcc -dM -E file.c              # ke ca macro cua he thong
gcc -dM -E - < /dev/null       # chi macro dinh nghia san
gcc -dM -E file.c | grep MY_   # loc theo tien to

# 3. Canh bao khi dung macro chua dinh nghia trong #if
gcc -Wundef file.c
# warning: "CO_TINH_NANG" is not defined, evaluates to 0

# 4. Trong GDB, voi -g3
(gdb) info macro TOI_DA
(gdb) macro expand BINH(3)
(gdb) print TOI_DA
terminal
gcc -E -P macro.c
int main(void) {
    int a = 3 + 1 * 3 + 1;
    int b = ((3 + 1) * (3 + 1));
    return a + b;
}
GDB với -g3
$ gcc -g3 -O0 -o prog prog.c
$ gdb -q ./prog

(gdb) info macro TOI_DA
Defined at /home/a/prog.c:3
#define TOI_DA 100

(gdb) macro expand BINH(3)
expands to: ((3) * (3))

(gdb) print TOI_DA
$1 = 100

/* Voi -g thuong thi ca ba lenh nay deu bao "no symbol".
   Chi -g3 moi luu thong tin macro. Bai 35.1. */

#Khi nào nên dừng lại

Nếu bạn cầnDùng cái này thay vì macro
Một hàm nhỏ, nhanhstatic inline. Bài 54.1
Cùng một hàm cho vài kiểu_Generic bọc vài hàm. Bài 51.3
Cùng một hàm cho mọi kiểuvoid * và con trỏ hàm. Bài 30.1
Một hằng sốenum, hoặc constexpr từ C23. Bài 32.2
Sinh mã cho nhiều kiểuX Macro, hoặc chương trình sinh mã. Bài 54.3
Đa hìnhBảng con trỏ hàm. Bài 29.5
Cấu hình lúc dịchMacro, đây đúng là việc của nó
Thông tin về nơi gọiMacro, không có cách khác

Tự làm thử

  1. Viết MANG_LEN bản kiểm tra và truyền cho nó một con trỏ.
  2. Viết DUY_NHAT bằng __LINE__ rồi thử đặt hai lời gọi trên cùng một dòng.
  3. Sửa bằng __COUNTER__ và xác nhận nó hoạt động.
  4. Viết KIEM_KIEU bằng _Generic và cố tình truyền sai kiểu.
  5. Chạy gcc -dM -E và đếm số macro trình biên dịch định nghĩa sẵn.
  6. Tìm macro phức tạp nhất trong một dự án và thử viết lại bằng hàm.

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

  • MANG_LEN bản thường cho ra 2 khi nhận con trỏ; __builtin_types_compatible_p biến việc đó thành lỗi dịch.
  • __LINE__ sinh tên duy nhất trừ khi hai lời gọi cùng một dòng; __COUNTER__ giải quyết triệt để.
  • Macro được phép làm một thao tác ngắn hơn, nhưng không được làm nó vô hình.
  • gcc -E -P để xem macro bung ra, gcc -dM -E để liệt kê macro, -Wundef để bắt tên gõ nhầm trong #if.
  • Bốn việc chỉ macro làm được: lấy __LINE__ nơi gọi, chuỗi hóa, sinh tên định danh, và cấu hình lúc dịch.