1. Bản dịch và bộ tiền xử lý

Khi bạn biên dịch code của mình, bạn có thể mong đợi rằng trình biên dịch sẽ biên dịch code chính xác như bạn đã viết. Điều này thực sự không phải như vậy.

Trước khi biên dịch, file code trải qua một giai đoạn được gọi là dịch. Nhiều điều xảy ra trong giai đoạn dịch để code của bạn sẵn sàng được biên dịch (nếu bạn tò mò, bạn có thể tìm thấy danh sách các giai đoạn dịch tại đây). file code có các bản dịch được áp dụng cho nó được gọi là đơn vị dịch.

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

Khi bộ tiền xử lý chạy, nó sẽ quét qua file code (từ trên xuống dưới), tìm kiếm các chỉ thị của bộ tiền xử lý. Các chỉ thị tiền xử lý (thường chỉ được gọi là chỉ thị) là các lệnh 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 dấu chấm phẩy). Các lệnh này yêu cầu bộ tiền xử lý thực hiện các tác vụ thao tác văn bản cụ thể.

 Lưu ý rằng bộ tiền xử lý không hiểu cú pháp C ++ – thay vào đó, các chỉ thị có cú pháp riêng (trong một số trường hợp giống với cú pháp C ++, và trong các trường hợp khác).

Đầu ra của bộ tiền xử lý trải qua một số giai đoạn dịch khác, 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 văn bản do bộ tiền xử lý thực hiện tạm thời trong bộ nhớ mỗi khi file code được biên dịch.

Trong bài học này, cafedev sẽ thảo luận về tác dụng của một số chỉ thị tiền xử lý phổ biến nhất.

2. Include

Bạn đã thấy lệnh #include đang hoạt động (thường đối với #include <iostream>). Khi bạn #include file, bộ xử lý trước sẽ thay thế lệnh #include bằng nội dung của file được bao gồm. Nội dung include sau đó được xử lý trước (cùng với phần còn lại của file), và sau đó được biên dịch.

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

#include <iostream>
 
int main()
{
    std::cout << "Hello, Cafedev!";
    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 <iostream> bằng nội dung được xử lý trước của file có tên “iostream”.

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

3. Định nghĩa Macro

Chỉ thị #define có thể được sử dụng để tạo Macro. Trong C ++, Macro là một quy tắc xác định cách văn bản đầu vào được chuyển đổi thành một văn bản đầu ra thay thế theo ý mình muốn.

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

Các Macro giống hàm hoạt động giố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, bởi 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 đối tượng có thể được xác định theo một trong hai cách:

1 – #define ký hiệu nhận dạng

2 – #define đoạn văn bản thay thế

Định nghĩa 1 không có văn bản thay thế, trong khi định nghĩa 2 thì có. Vì đây là các chỉ thị tiền xử lý (không phải câu lệnh), lưu ý rằng không phải Macro nào cũng kết thúc bằng dấu chấm phẩy.

4. Macro giống đối tượng với văn bản thay thế

Khi bộ tiền xử lý gặp chỉ thị này, bất kỳ sự xuất hiện nào khác của code với tên định danh của Macro chúng ta đã khai báo sẽ được thay thế bằng văn bản mà chúng ta đã tạo ở phần khai báo macro. code định danh theo truyền thống được nhập bằng tất cả các chữ cái in hoa, sử dụng dấu gạch dưới để biểu thị dấu cách.

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

/*
Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
@author cafedevn
Contact: cafedevn@gmail.com
Fanpage: https://www.facebook.com/cafedevn
Group: https://www.facebook.com/groups/cafedev.vn/
Instagram: https://instagram.com/cafedevn
Twitter: https://twitter.com/CafedeVn
Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
Pinterest: https://www.pinterest.com/cafedevvn/
YouTube: https://www.youtube.com/channel/UCE7zpY_SlHGEgo67pHxqIoA/
*/

#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 những điều trên thành như sau:

// The contents of iostream are inserted here
 
int main()
{
    std::cout << "My name is: " << "Alex";
 
    return 0;
}

code khi chạy sẽ in ra kết quả Tên là: Alex.

Macro giống đối tượng được sử dụng như một sự thay thế tốt hơn cho các biến không đổi. Thời đó đã qua lâu khi các trình biên dịch ngày càng thông minh hơn và ngôn ngữ ngày càng phát triển. Các Macro giống đối tượng sẽ chỉ được nhìn thấy trong code cũ.

Chúng ta khuyên bạn nên tránh hoàn toàn những loại Macro này, vì có nhiều cách tốt hơn để thực hiện điều này. Chúng ta sẽ thảo luận thêm về vấn đề này trong bài học – Hằng số và các hằng biểu tượng.

5. Macro giống đối tượng không có văn bản thay thế

Macro giống đối tượng cũng có thể được xác định code không cần văn bản thay thế.

Ví dụ:

#define USE_YEN

Macro của câu này hoạt động như bạn có thể mong đợi: bất kỳ sự xuất hiện nào khác của ký tự nhận dạng ở đây là USE_YEN sẽ bị xóa và không thay thế bằng gì!

Điều này có vẻ khá vô ích và nó cũng vô ích khi thực hiện thay thế văn bản. Tuy nhiên, đó không phải là những gì Macro của chỉ thị này thường được sử dụng. Chúng ta sẽ thảo luận về việc sử dụng Macro này trong giây lát.

Không giống như Macro đối tượng với văn bản thay thế, Macro dạng này thường được sử dụng nhiều.

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

Các chỉ thị tiền xử lý biên dịch có điều kiện cho phép bạn chỉ định một thứ gì đó sẽ hoặc sẽ không biên dịch theo những điều kiện nào đó. Có khá nhiều chỉ thị biên dịch có điều kiện khác nhau, nhưng chúng ta sẽ chỉ đề cập đến 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 ký hiệu nhận dạng này đã được #define trước đó chưa. Nếu vậy, code giữa #ifdef và #endif phù hợp sẽ được biên dịch. Nếu không, code sẽ bị bỏ qua.

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

#include <iostream>
 
#define PRINT_JOE
 
int main()
{
#ifdef PRINT_JOE
    std::cout << "Joe\n"; // if PRINT_JOE is defined, compile this code
#endif
 
#ifdef PRINT_BOB
    std::cout << "Bob\n"; // 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 ký hiệu nhận dạng CHƯA được #define.

#include <iostream>
 
int main()
{
#ifndef PRINT_BOB
    std::cout << "Bob\n";
#endif
 
    return 0;
}

Chương trình này in “Bob”, vì PRINT_BOB chưa bao giờ được #define.

Thay cho #ifdef PRINT_BOB và #ifndef PRINT_BOB, bạn cũng sẽ thấy #if define (PRINT_BOB) và #if! define (PRINT_BOB). Chúng làm tương tự như sử dụng cú pháp kiểu C ++ hơn một chút.

7. #If 0

Một cách sử dụng phổ biến hơ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 comment):

/*
Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
@author cafedevn
Contact: cafedevn@gmail.com
Fanpage: https://www.facebook.com/cafedevn
Group: https://www.facebook.com/groups/cafedev.vn/
Instagram: https://instagram.com/cafedevn
Twitter: https://twitter.com/CafedeVn
Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
Pinterest: https://www.pinterest.com/cafedevvn/
YouTube: https://www.youtube.com/channel/UCE7zpY_SlHGEgo67pHxqIoA/
*/

#include <iostream>
 
int main()
{
    std::cout << "Joe\n";
 
#if 0 // Don't compile anything starting here
    std::cout << "Bob\n";
    std::cout << "Steve\n";
#endif // until this point
 
    return 0;
}

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

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

Macro giống đố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:

#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 bằng không?

Macro chỉ gây ra sự thay thế văn bản cho code bình thường. Các lệnh tiền xử lý khác bị bỏ qua. Do đó, PRINT_JOE trong #ifdef PRINT_JOE bị bỏ lại.

#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

Trên thực tế, đầu ra của bộ tiền xử lý không chứa chỉ thị nào – tất cả chúng đều được giải quyết / loại bỏ trước khi biên dịch, bởi vì trình biên dịch sẽ không biết phải làm gì với chúng.

8. Xác định Phạm vi

Các chỉ thị được giải quyết trước khi biên dịch, từ trên xuống dưới trên cơ sở từng file.

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

/*
Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
@author cafedevn
Contact: cafedevn@gmail.com
Fanpage: https://www.facebook.com/cafedevn
Group: https://www.facebook.com/groups/cafedev.vn/
Instagram: https://instagram.com/cafedevn
Twitter: https://twitter.com/CafedeVn
Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
Pinterest: https://www.pinterest.com/cafedevvn/
YouTube: https://www.youtube.com/channel/UCE7zpY_SlHGEgo67pHxqIoA/
*/

#include <iostream>
 
void foo()
{
#define MY_NAME "Alex"
}
 
int main()
{
	std::cout << "My name is: " << MY_NAME;
 
	return 0;
}

macro dù có vẻ như #define MY_NAME “Alex” được định nghĩa bên trong function foo, bộ xử lý tiền xử lý sẽ không nhận thấy, vì nó không hiểu các khái niệm C ++ như hàm. Do đó, chương trình này hoạt động giống hệt với một chương trình code #define MY_NAME “Alex” được xác định trước hoặc ngay sau hàm foo. Để có thể đọc chúng, bạn thường muốn #define ký hiệu nhận dạng bên ngoài các hàm.

Khi bộ tiền xử lý hoàn tất, tất cả các ký hiệu nhận dạng đã xác định từ file đó sẽ bị loại bỏ. Điều này có nghĩa là các chỉ thị chỉ có giá trị từ thời điểm định nghĩa đến cuối file code của chúng được định nghĩa. Các chỉ thị được xác định trong một file code không ảnh hưở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:

#include <iostream>
 
void doSomething()
{
#ifdef PRINT
    std::cout << "Printing!";
#endif
#ifndef PRINT
    std::cout << "Not printing!";
#endif
}

codein.cpp:

void doSomething(); // forward declaration for function doSomething()
 
#define PRINT
 
int main()
{
    doSomething();
 
    return 0;
}

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

Not printing!

Macro PRINT dù đã được định nghĩa trong code của main.cpp, điều đó không có bất kỳ tác động nào đến bất kỳ code nào trong function.cpp (PRINT chỉ được xác định bằng #defined từ lúc nó được định nghĩa đến cuối code của main.cpp). Điều này sẽ có hậu quả khi chúng ta thảo luận về bộ bảo vệ header trong một bài học trong tương lai.

Cài ứng dụng cafedev để dễ dàng cập nhật tin và học lập trình mọi lúc mọi nơi tại đây.

Nguồn và Tài liệu tiếng anh tham khảo:

Tài liệu từ cafedev:

Nếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của cafedev để nhận được nhiều hơn nữa:

Chào thân ái và quyết thắng!

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!