Bỏ qua điều hướng, tới nội dung chính
Học C
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ểmVới macroVới hàm
Đối số được tính một lầnKhông, mỗi lần xuất hiện là một lần tínhCó, luôn luôn
Kiểm kiểuChỉ 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ỗiKhông thấy tên, không đặt điểm dừng đượcThấy tên, vào được, xem biến được
Thông báo lỗiChỉ vào dòng define, chỗ gọi chỉ là một dòng noteChỉ đú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ử

  1. Viết MAX dạng macro và imax dạ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.
  2. Truyền một con trỏ vào MAX cùng với một int, so sánh thông báo của trình biên dịch với khi dùng imax.
  3. Biên dịch một chương trình gọi imax ở mức -O0 và -O2, đếm số lệnh call trong hợp ngữ.
  4. Viết hàm giai_phong(void *p) đặt p = NULL rồi kiểm tra con trỏ của người gọi có thành NULL không.
  5. Đổ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ắc inline đơ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_PHONG phả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 độ.