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

Biên dịch có điều kiện

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

  • Dùng ifdef, if defined, elif và error để chọn nhánh mã theo nền tảng
  • Xây hệ thống nhật ký biến mất hoàn toàn ở bản phát hành
  • Nêu vì sao không được đặt mã có tác dụng phụ trong assert
  • Dùng Static_assert để kiểm tra giả định ngay lúc biên dịch

Biên dịch có điều kiện cho phép một mã nguồn chạy trên Windows lẫn Linux, và cho phép hệ thống nhật ký của bạn biến mất không còn một lệnh nào ở bản phát hành. Nó cũng là công cụ dễ bị lạm dụng nhất trong C.

#Cú pháp và bốn dạng điều kiện

#if     bieu_thuc_hang_nguyen
#ifdef  TEN            /* tuong duong #if defined(TEN)  */
#ifndef TEN            /* tuong duong #if !defined(TEN) */
#elif   bieu_thuc
#else
#endif
DạngĐúng khiDùng cho
#ifdef TENTEN đã được define, bất kể giá trịBật tắt chức năng bằng cờ -D
#ifndef TENTEN chưa được defineHeader guard, giá trị mặc định
#if TEN > 3Biểu thức hằng nguyên khác khôngSo sánh số phiên bản
#if defined(A) && !defined(B)Kết hợp nhiều điều kiệnPhát hiện nền tảng

#Phát hiện nền tảng

nen-tang.h
#ifndef NEN_TANG_H
#define NEN_TANG_H

#if defined(_WIN32)
    #include <windows.h>
    #define PHAN_CACH_DUONG_DAN '\\'
    #define NGU_MS(ms)          Sleep(ms)
#elif defined(__linux__) || defined(__APPLE__)
    #include <unistd.h>
    #define PHAN_CACH_DUONG_DAN '/'
    #define NGU_MS(ms)          usleep((ms) * 1000u)
#else
    #error "He dieu hanh khong duoc ho tro"
#endif

#endif /* NEN_TANG_H */
MacroĐúng khiGhi chú
_WIN32Mọi bản WindowsĐúng cả trên Windows 64 bit
_WIN64Windows 64 bitKhi đúng thì _WIN32 cũng đúng
__linux__LinuxHai gạch dưới ở cả hai đầu
__APPLE__macOS và iOSKèm TARGET_OS_MAC để phân biệt
__unix__Hầu hết hệ UnixKể cả Linux
__GNUC__GCC hoặc ClangClang cũng đặt macro này
__clang__Chỉ ClangKiểm cái này trước __GNUC__
_MSC_VERTrình biên dịch của MicrosoftGiá trị là số phiên bản
__STDC_VERSION__Luôn có từ C95201112L là C11

#Hệ thống nhật ký tắt được

ghi.h
#ifndef GHI_H
#define GHI_H

#include <stdio.h>

#ifdef GO_LOI
  #define GHI(fmt, ...) \
      fprintf(stderr, "[%s:%d] " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__)
#else
  #define GHI(fmt, ...) ((void)0)
#endif

#endif /* GHI_H */
terminal
# Không có -DGO_LOI
gcc -std=c11 -Wall -Wextra -O2 -o a a.c && ./a
chuong trinh chay xong, x = 42
# Có -DGO_LOI
gcc -std=c11 -Wall -Wextra -O2 -DGO_LOI -o b a.c && ./b
chuong trinh chay xong, x = 42
[a.c:12] Bat dau
[a.c:13] Gia tri x = 42
# Nó tốn bao nhiêu khi tắt
gcc -O2 -c -o a.o a.c && gcc -O2 -DGO_LOI -c -o b.o a.c && size a.o b.o
   text    data     bss     dec     hex filename
    164       0       0     164      a4 a.o
    292       0       0     292     124 b.o

#assert và cái bẫy NDEBUG

#include <assert.h>

void xu_ly(int *p, size_t n) {
    assert(p != NULL);
    assert(n > 0);

    /* ... */
}
assert
Macro kiểm tra một điều kiện lúc chạy. Nếu điều kiện sai, nó in tên tệp, số dòng, biểu thức, rồi gọi abort. Nếu macro NDEBUG được định nghĩa, assert biến mất hoàn toàn.
terminal
gcc -std=c11 -o a a.c && ./a
doc_file duoc goi 1 lan
Assertion failed!

Program: a.exe
File: a.c, Line 9

Expression: p != NULL
echo $?
3

#Static_assert kiểm lúc biên dịch

_Static_assert(bieu_thuc_hang, "thong bao");     /* C11 */

/* Tu C23 va trong <assert.h> tu C11: */
static_assert(bieu_thuc_hang, "thong bao");
terminal
cat sa.c
#include <stdint.h>
typedef struct { char ma[16]; int32_t diem; } BG;
_Static_assert(sizeof(int) == 4, "Ma nay yeu cau int 4 byte");
_Static_assert(sizeof(BG) == 24, "Bo cuc BG thay doi, tep cu se hong");
int main(void) { return 0; }
gcc -std=c11 -fsyntax-only sa.c
sa.c:4:1: error: static assertion failed: "Bo cuc BG thay doi, tep cu se hong"
 _Static_assert(sizeof(BG) == 24, "Bo cuc BG thay doi, tep cu se hong");
 ^~~~~~~~~~~~~~
assert_Static_assert
Kiểm khi nàoLúc chạyLúc biên dịch
Điều kiện có thể là gìBiểu thức bất kỳChỉ hằng số biên dịch
Thất bại thì saoChương trình abortKhông dịch được
Chi phí lúc chạyMột phép so sánhKhông có gì
Bị NDEBUG tắtCóKhông
Dùng được sizeofĐượcĐược

Tự làm thử

  1. Viết #if PHIEN_BAM >= 3 gõ sai tên rồi biên dịch có và không có -Wundef, so sánh kết quả.
  2. Xây header nền tảng tách _WIN32 với __linux__ và xác nhận mã nghiệp vụ không còn ifdef nào.
  3. Viết macro GHI có hai nhánh, biên dịch hai bản, và so số byte text bằng lệnh size.
  4. Đặt assert(doc_file(f) == 0) rồi biên dịch với -DNDEBUG và đếm số lần doc_file chạy.
  5. Thêm _Static_assert(sizeof(SinhVien) == 112, ...) vào dự án Phần 8, rồi thêm một trường vào struct và xem điều gì xảy ra.

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

  • Tên chưa định nghĩa trong #if bằng 0 và không có cảnh báo, nên hãy bật -Wundef.
  • #if chỉ hiểu số nguyên. Không có số thực, không có chuỗi, không có sizeof.
  • Gom mọi khác biệt nền tảng vào một header, đừng rải ifdef giữa mã nghiệp vụ.
  • Macro nhật ký định nghĩa thành ((void)0) biến mất hoàn toàn, còn hàm rỗng thì vẫn phải tính đối số.
  • Không đặt mã có tác dụng phụ trong assert, vì -DNDEBUG xóa cả biểu thức bên trong.
  • _Static_assert kiểm lúc biên dịch và dùng được sizeof, thứ mà #if không có.