Giới thiệu về bộ tiền xử lý (preprocessor)

13870

Sự thông dịch và tiền xử lý

Khi bạn biên dịch code của mình, bạn mong đợi rằng trình biên dịch sẽ biên dịch code một cách chính xác như bạn đã viết nó.

Trước khi biên dịch, file code sẽ trải qua giai đoạn được gọi là thông dịch. Nhiều điều xảy ra trong giai đoạn thông dịch này để code của bạn sẵn sàng được biên dịch. Một file code được thông dịch thì được gọi là một đơn vị thông dịch.

Đáng chú ý nhất trong các giai đoạn thông dịch là liên quan đến bộ tiền xử lý. Bộ tiền xử lý được coi là một chương trình riêng biệt để thao tác với các đoạn code(văn bản code) trong mỗi file code.

Khi bộ tiền xử lý chạy, nó quét qua file code (từ trên xuống dưới), tìm kiếm các chỉ thị tiền xử lý. Các chỉ thị tiền xử lý (thường được gọi là các chỉ thị) là các hướng dẫn bắt đầu bằng ký hiệu # và kết thúc bằng một dòng mới (KHÔNG phải là dấu chấm phẩy). Các chỉ thị này báo cho bộ tiền xử lý thực hiện các tác vụ với văn bản code cụ thể. Lưu ý rằng bộ tiền xử lý không hiểu cú pháp C ++.

Đầu ra của bộ tiền xử lý trải qua nhiều giai đoạn thông dịch, và sau đó được biên dịch. Lưu ý rằng bộ tiền xử lý không sửa đổi các file code gốc theo bất kỳ cách nào – thay vào đó, tất cả các thay đổi của đoạn code được thực hiện bởi bộ tiền xử lý xảy ra tạm thời trong bộ nhớ mỗi khi file code được biên dịch.

Trong bài học này, chúng tôi sẽ thảo luận về những gì một số chỉ thị tiền xử lý thường làm nhất.

#includes

Bạn đã thấy lệnh #include hoạt động. Khi bạn #include một file nào đó, bộ xử lý trước tiên sẽ thay thế lệnh #include bằng nội dung của file mà bạn include nó. Các nội dung đã include sẽ được xử lý trước (cùng với phần còn lại của file code của bạn), và sau đó nó sẽ được biên dịch.

Hãy xem xét chương trình sau:

1
2
3
4
5
6
7
#include <iostream>
 
int main()
{
    std::cout << "Hello, world!";
    return 0;
}

Khi bộ tiền xử lý chạy trên chương trình này, bộ tiền xử lý sẽ thay thế #include bằng nội dung được xử lý trước của file có tên là iostream.

Vì #include hầu như chỉ được sử dụng để include các file trong header(file .h), chúng ta sẽ thảo luận về #include chi tiết hơn trong bài học tiếp theo (khi chúng tôi thảo luận chi tiết hơn về các file header).

Định nghĩa Macro

Lệnh #define có thể được sử dụng để tạo macro. Trong C ++, macro là một quy tắc định nghĩa cách chuyển văn bản code đầu vào nào đó thành văn bản code đầu ra theo ý mình muốn.

Có hai loại macro cơ bản: macro giống như đối tượng và macro giống như hàm.

Các macro giống như hàm hoạt động như các hàm và phục vụ một mục đích tương tự. Chúng ta sẽ không thảo luận về chúng ở đây, vì việc sử dụng chúng thường được coi là nguy hiểm và hầu hết mọi thứ chúng có thể làm đều có thể được thực hiện bởi một hàm bình thường.

Các macro giống như đối tượng có thể được định nghĩa theo một trong hai cách:

1
2
#define identifier
#define identifier substitution_text

Định nghĩa trên không có định danh khác để thay thế, trong khi định nghĩa dưới cùng thì có. Bởi vì đây là các chỉ thị tiền xử lý (không phải câu lệnh) nên lưu ý rằng không có dòng nào kết thúc bằng dấu chấm phẩy.

Các macro giống như một đối tượng với một văn bản thay thế

Khi bộ tiền xử lý gặp lệnh này, bất kỳ sự xuất hiện nào nữa của định danh macro sẽ được thay thế bằng một đoạn văn bản mà chúng ta đã định nghĩa trước đó. Thông thường thì tên định danh của macro sẽ được ghi bằng các chữ in hoa, sử dụng dấu gạch dưới để thể hiện khoảng trắng.

Hãy xem xét chương trình sau:

1
2
3
4
5
6
7
8
9
10
#include <iostream>
 
#define MY_NAME "Alex"
 
int main()
{
    std::cout << "My name is: " << MY_NAME;
 
    return 0;
}

Bộ tiền xử lý chuyển đổi phần trên thành như sau:

1
2
3
4
5
6
7
8
// The contents of iostream are inserted here
 
int main()
{
    std::cout << "My name is: " << "Alex";
 
    return 0;
}

Mà khi chạy, in đầu ra My name is: Alex

Các macro giống như đối tượng mà không có văn bản thay thế

Các macro giống như đối tượng cũng có thể được định nghĩa mà không cần văn bản thay thế.

Ví dụ:

define USE_YEN

Các macro của biểu mẫu này hoạt động như sau: mọi sự xuất hiện tiếp theo của định danh macro này(USE_YEN) sẽ bị xóa và được thay thế bởi không có gì!

Điều này có vẻ khá vô dụng. Tuy nhiên, đó không phải là những gì lệnh này thường được sử dụng. Chúng ta sẽ thảo luận về việc sử dụng hình thức này ngay sau đây.

Biên soạn code có điều kiện

Các chỉ thị tiền xử lý biên dịch có điều kiện sẽ cho phép bạn chỉ định những điều kiện nào đó thì sẽ thực hiện một cái gì đó hoặc giành được biên dịch hoặc không được biên dịch. Có khá nhiều chỉ thị biên dịch có điều kiện khác nhau, nhưng chúng tôi sẽ giới thiệu ba chỉ thị được sử dụng nhiều nhất ở đây: #ifdef, #ifndef và #endif.

Chỉ thị tiền xử lý #ifdef cho phép bộ tiền xử lý kiểm tra xem một định danh đã được #define trước đó chưa. Nếu đã định nghĩa rồi thì code ở giữa #ifdef và #endif sẽ được biên dịch. Nếu không, code được bỏ qua.

Hãy xem xét chương trình sau:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
 
#define PRINT_JOE
 
int main()
{
#ifdef PRINT_JOE
    std::cout << "Joen"; // if PRINT_JOE is defined, compile this code
#endif
 
#ifdef PRINT_BOB
    std::cout << "Bobn"; // if PRINT_BOB is defined, compile this code
#endif
 
    return 0;
}

Vì PRINT_JOE đã được #define, dòng cout << “Joe n” sẽ được biên dịch. Vì PRINT_BOB chưa được #define, dòng cout << “Bob n” sẽ bị bỏ qua.

#ifndef ngược lại với #ifdef, ở chỗ nó cho phép bạn kiểm tra xem một định danh chưa được định nghĩa.

1
2
3
4
5
6
7
8
9
10
#include <iostream>
 
int main()
{
#ifndef PRINT_BOB
    std::cout << "Bobn";
#endif
 
    return 0;
}

Chương trình này in ra Bob, vì PRINT_BOB chưa bao giờ được định nghĩa.

#if 0

Một cách sử dụng phổ biến của biên dịch có điều kiện liên quan đến việc sử dụng #if 0 để loại trừ một khối code khỏi được biên dịch (như thể nó nằm trong một khối comments):

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
 
int main()
{
    std::cout << "Joen";
 
#if 0 // Don't compile anything starting here
    std::cout << "Bobn";
    std::cout << "Steven";
#endif // until this point
 
    return 0;
}

Đoạn code trên chỉ in ra Joe, bởi vì Bob và và Steve đã ở trong một khối #if 0 mà bộ tiền xử lý sẽ loại trừ nó khỏi quá trình biên dịch.

Điều này cung cấp một cách thuận tiện để comments code có chứa các nhiều dòng.

Các macro giống như đối tượng không ảnh hưởng đến các chỉ thị tiền xử lý khác

Bây giờ bạn có thể tự hỏi:

1
2
3
4
#define PRINT_JOE
 
#ifdef PRINT_JOE
// ...

Vì chúng ta đã định nghĩa PRINT_JOE là không có gì, tại sao bộ tiền xử lý lại không thay thế PRINT_JOE trong #ifdef PRINT_JOE không có gì?

Macro chỉ thay thế văn bản cho code bình thường. Còn với các lệnh tiền xử lý có điều kiện này sẽ xử lý khác. Do đó, PRINT_JOE trong #ifdef PRINT_JOE chỉ có tác dụng thay thế một văn bản nào đó với code bình thường, bạn có thể xem qua ví dụ sau đây để hiểu hơn

Ví dụ:

1
2
3
4
5
#define FOO 9 // Here's a macro substitution
 
#ifdef FOO // This FOO does not get replaced because it’s part of another preprocessor directive
    std::cout << FOO; // This FOO gets replaced with 9 because it's part of the normal code
#endif

Trong thực tế, đầu ra của bộ tiền xử lý hoàn toàn không chứa bất kỳ chỉ thị nào cho trình biên dịch – tất cả chúng đều được xử lý và loại bỏ trước khi biên dịch.

Phạm vi định nghĩa

Các chỉ thị sẽ được xử lý trước khi biên dịch, từ trên xuống dưới trên của từng file.

Hãy xem xét chương trình sau:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
 
void foo()
{
#define MY_NAME "Alex"
}
 
int main()
{
    std::cout << "My name is: " << MY_NAME;
 
    return 0;
}

Mặc dù có vẻ như #define MY_NAME “Alex” Được định nghĩa bên trong hàm foo, bộ tiền xử lý đã được thông báo, vì nó không hiểu các khái niệm C ++ như các hàm. Do đó, chương trình này hoạt động bình thường và nó không phụ thuộc vào vị trí định nghĩa của #define MY_NAME “Alex”, Khi đó, thì chúng ta có thể định nghĩa nó trước hoặc ngay sau hàm foo cũng được nhưng để dễ đọc, bạn thường muốn #define định danh bên ngoài các hàm.

Khi bộ tiền xử lý kết thúc, tất cả các định danh được định nghĩa từ file đó sẽ bị loại bỏ. Điều này có nghĩa là các chỉ thị chỉ có giá trị từ điểm định nghĩa đến cuối file mà chúng được định nghĩa. Các chỉ thị được định nghĩa trong một file code không có tác động đến các file code khác trong cùng một dự án.

Hãy xem xét ví dụ sau:

function.cpp:

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
 
void doSomething()
{
#ifdef PRINT
    std::cout << "Printing!";
#endif
#ifndef PRINT
    std::cout << "Not printing!";
#endif
}

main.cpp:

1
2
3
4
5
6
7
8
9
10
void doSomething(); // forward declaration for function doSomething()
 
#define PRINT
 
int main()
{
    doSomething();
 
    return 0;
}

Chương trình trên sẽ in:

1
Not printing!

Mặc dù PRINT đã được định nghĩa trong main.cpp, nhưng điều đó không có bất kỳ tác động nào đối với bất kỳ code nào trong file function.cpp (PRINT chỉ được #define từ điểm định nghĩa đến cuối main.cpp).

Nguồn gốc bài viết từ CafeDev