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

Preprocessor làm gì

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

  • Kể tên bốn giai đoạn dịch và sản phẩm của từng giai đoạn
  • Giải thích include là dán nguyên văn nội dung tệp
  • Phân biệt dấu ngoặc nhọn với dấu nháy kép trong include
  • Dùng các cờ -E, -P, -H và -I để xem preprocessor đã làm gì

Suốt tám phần vừa qua bạn gõ #include <stdio.h> ở đầu mỗi chương trình mà chưa bao giờ hỏi nó làm gì. Câu trả lời đơn giản đến mức gây bất ngờ, và hiểu nó sẽ giải thích luôn nửa số lỗi biên dịch khó hiểu mà bạn từng gặp.

#Bốn giai đoạn dịch

Lệnh gcc quen thuộc thật ra chạy bốn chương trình khác nhau nối tiếp nhau.
Giai đoạnCờ dừng lạiSản phẩmNó làm gì
Tiền xử lý-Emain.iDán header vào, thay macro, bỏ nhánh không dùng
Biên dịch-Smain.sPhân tích cú pháp C, kiểm kiểu, sinh hợp ngữ
Hợp dịch-cmain.oĐổi hợp ngữ sang mã máy, tạo bảng ký hiệu
Liên kết(không có)a.outGhép các .o và thư viện thành chương trình chạy được
terminal
# Dừng sau từng giai đoạn để xem sản phẩm
gcc -E main.c -o main.i && gcc -S main.c && gcc -c main.c && gcc main.c -o app
ls -l main.i main.s main.o app
ls -l main.i main.s main.o app
-rw-r--r-- 1 anh anh  24 618 main.i
-rw-r--r-- 1 anh anh     712 main.s
-rw-r--r-- 1 anh anh   1 504 main.o
-rwxr-xr-x 1 anh anh  16 696 app

#Preprocessor không hiểu C

Preprocessor
Một chương trình xử lý văn bản chạy trước trình biên dịch. Nó chỉ biết các dòng bắt đầu bằng dấu thăng và các tên macro. Nó không biết hàm là gì, biến là gì, kiểu là gì.
#define BINH_PHUONG(x) x * x

int a = BINH_PHUONG(2 + 3);

/* Preprocessor KHONG tinh toan. No chi thay the van ban:

   Truoc:  int a = BINH_PHUONG(2 + 3);
   Sau:    int a = 2 + 3 * 2 + 3;

   No khong biet "2 + 3" la mot bieu thuc.
   No khong biet phep nhan uu tien hon phep cong.
   No chi thay ky tu x va thay bang nhung ky tu ban dua vao. */
Preprocessor có biếtCóGhi chú
Dòng bắt đầu bằng dấu thăngCóĐây là toàn bộ ngôn ngữ của nó
Tên macro và tham số của macroCóThay thế thuần túy theo token
Chuỗi ký tự và chú thíchCóĐể không thay macro nằm trong chuỗi
Kiểu dữ liệuKhôngNó không phân biệt int với char
Phạm vi biếnKhôngMacro không theo khối lệnh nào cả
Thứ tự ưu tiên toán tửKhôngNguồn gốc của cạm bẫy thiếu ngoặc
Hàm, vòng lặp, câu lệnhKhôngVới nó tất cả chỉ là token

#include là dán nguyên văn

#include <stdio.h>

Chỉ thị này có nghĩa đúng theo nghĩa đen: tìm tệp stdio.h, đọc toàn bộ nội dung, dán vào đúng chỗ này, rồi xử lý tiếp phần vừa dán. Không có gì thông minh hơn thế.

terminal
# Một chương trình hai dòng
cat p1.c
#include <stdio.h>
int main(void) { printf("Xin chao\n"); return 0; }
wc -l < p1.c
2
# Sau khi tiền xử lý
gcc -E p1.c | wc -l
884
# Bỏ các dòng đánh dấu vị trí cho dễ đọc
gcc -E -P p1.c | wc -l
580
gcc -E -P p1.c | tail -3
  __attribute__ ((__dllimport__)) size_t __attribute__((__cdecl__)) _fread_nolock_s(void *_DstBuf,size_t _DstSize,size_t _ElementSize,size_t _Count,FILE *_File);

int main(void) { printf("Xin chao\n"); return 0; }

#Ngoặc nhọn hay nháy kép

#include <stdio.h>        /* tim trong cac thu muc HE THONG */
#include "sinhvien.h"    /* tim canh tep hien tai TRUOC, roi moi toi he thong */
DạngTìm ở đâuDùng cho
Ngoặc nhọnThư mục hệ thống, rồi tới các thư mục thêm bằng -IHeader của thư viện chuẩn và thư viện bên ngoài
Nháy képThư mục của tệp đang xử lý, rồi mới như trênHeader của chính dự án bạn
terminal
# Thêm một thư mục vào đường tìm kiếm
gcc -Iinclude -c src/main.c -o build/main.o
(khong co dong nao)
# Xem danh sách thư mục mà gcc tìm
gcc -E -v - < /dev/null 2>&1 | sed -n '/search starts here/,/End of search/p'
#include <...> search starts here:
 /usr/lib/gcc/x86_64-linux-gnu/13/include
 /usr/local/include
 /usr/include/x86_64-linux-gnu
 /usr/include
End of search list.

#Nhìn tận mắt kết quả

CờCông dụngKhi nào dùng
-EDừng sau tiền xử lý, in ra màn hìnhXem macro bung ra thành gì
-PBỏ các dòng đánh dấu vị trí, đi kèm -EĐọc kết quả cho dễ
-HIn cây includeTìm xem header lạ từ đâu tới
-I duong_danThêm thư mục vào đường tìm kiếmDự án có thư mục include riêng
-D TEN=gia_triĐịnh nghĩa macro từ dòng lệnhBật tắt chức năng lúc build
-U TENHủy định nghĩa macroTắt một macro do hệ thống đặt sẵn
-dM -EIn mọi macro đang có hiệu lựcXem có sẵn những macro nào
terminal
# Trình biên dịch định nghĩa sẵn hàng trăm macro
gcc -dM -E - < /dev/null | wc -l
359
gcc -dM -E - < /dev/null | grep -E '__STDC_VERSION__|__GNUC__ |_WIN32|__linux__'
#define __GNUC__ 13
#define __STDC_VERSION__ 201710L
#define __linux__ 1

Thói quen nên có khi gặp lỗi macro

Ba bước tìm lỗi
/* 1. Bung ra rieng tep cua ban, khong lan header he thong */
gcc -E -P main.c | tail -40

/* 2. Neu tep dai qua, tam bo cac dong include roi bung lai */

/* 3. Voi mot macro cu the, viet mot tep nho chi co no */
cat > thu.c <<'EOF'
#define SQ(x) x * x
int a = SQ(2 + 3);
int b = 100 / SQ(5);
EOF
gcc -E -P thu.c
terminal
gcc -E -P thu.c
int a = 2 + 3 * 2 + 3;
int b = 100 / 5 * 5;

Tự làm thử

  1. Viết chương trình hai dòng in ra một câu, rồi chạy gcc -E và đếm số dòng thu được.
  2. Chạy gcc -H trên chương trình đó và vẽ lại ba tầng đầu của cây include.
  3. Định nghĩa #define TEN "abc" rồi in cả printf("TEN\n") và printf("%s\n", TEN), giải thích vì sao chỉ một cái bị thay.
  4. Đếm số macro tiền định nghĩa trên máy bạn bằng gcc -dM -E - < /dev/null.
  5. Tạo hai tệp .c và một tệp .h trong thư mục include, rồi biên dịch bằng cờ -I.

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

  • Lệnh gcc chạy bốn chương trình nối tiếp: tiền xử lý, biên dịch, hợp dịch, liên kết.
  • Preprocessor chỉ thao tác trên token, không hiểu kiểu, không hiểu phạm vi, không hiểu thứ tự ưu tiên toán tử.
  • #include dán nguyên văn nội dung tệp vào đúng chỗ đó, và hai dòng mã có thể nở ra thành gần chín trăm dòng.
  • Ngoặc nhọn tìm ở thư mục hệ thống, nháy kép tìm cạnh tệp hiện tại trước. Đừng đặt tên header trùng thư viện chuẩn.
  • Khi macro cho kết quả lạ, chạy gcc -E -P và nhìn tận mắt thay vì suy luận trong đầu.