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
| Giai đoạn | Cờ dừng lại | Sản phẩm | Nó làm gì |
|---|---|---|---|
| Tiền xử lý | -E | main.i | Dán header vào, thay macro, bỏ nhánh không dùng |
| Biên dịch | -S | main.s | Phân tích cú pháp C, kiểm kiểu, sinh hợp ngữ |
| Hợp dịch | -c | main.o | Đổi hợp ngữ sang mã máy, tạo bảng ký hiệu |
| Liên kết | (không có) | a.out | Ghé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ết | Có | Ghi chú |
|---|---|---|
| Dòng bắt đầu bằng dấu thăng | Có | Đây là toàn bộ ngôn ngữ của nó |
| Tên macro và tham số của macro | Có | Thay thế thuần túy theo token |
| Chuỗi ký tự và chú thích | Có | Để không thay macro nằm trong chuỗi |
| Kiểu dữ liệu | Không | Nó không phân biệt int với char |
| Phạm vi biến | Không | Macro không theo khối lệnh nào cả |
| Thứ tự ưu tiên toán tử | Không | Nguồn gốc của cạm bẫy thiếu ngoặc |
| Hàm, vòng lặp, câu lệnh | Không | Vớ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ạng | Tìm ở đâu | Dùng cho |
|---|---|---|
| Ngoặc nhọn | Thư mục hệ thống, rồi tới các thư mục thêm bằng -I | Header của thư viện chuẩn và thư viện bên ngoài |
| Nháy kép | Thư mục của tệp đang xử lý, rồi mới như trên | Header 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ụng | Khi nào dùng |
|---|---|---|
| -E | Dừng sau tiền xử lý, in ra màn hình | Xem macro bung ra thành gì |
| -P | Bỏ các dòng đánh dấu vị trí, đi kèm -E | Đọc kết quả cho dễ |
| -H | In cây include | Tìm xem header lạ từ đâu tới |
| -I duong_dan | Thêm thư mục vào đường tìm kiếm | Dự án có thư mục include riêng |
| -D TEN=gia_tri | Định nghĩa macro từ dòng lệnh | Bật tắt chức năng lúc build |
| -U TEN | Hủy định nghĩa macro | Tắt một macro do hệ thống đặt sẵn |
| -dM -E | In mọi macro đang có hiệu lực | Xem 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.cterminal
gcc -E -P thu.c
int a = 2 + 3 * 2 + 3; int b = 100 / 5 * 5;
Tự làm thử
- Viết chương trình hai dòng in ra một câu, rồi chạy
gcc -Evà đếm số dòng thu được. - Chạy
gcc -Htrên chương trình đó và vẽ lại ba tầng đầu của cây include. - Đị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. - Đếm số macro tiền định nghĩa trên máy bạn bằng
gcc -dM -E - < /dev/null. - Tạo hai tệp
.cvà một tệp.htrong thư mụcinclude, 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
gccchạ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ử.
#includedá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 -Pvà nhìn tận mắt thay vì suy luận trong đầu.