Bỏ qua để đến nội dung

Tiền xử lý & Macro (#define, #ifdef)

Trước khi trình biên dịch thực sự “hiểu” code C++, một chương trình gọi là bộ tiền xử lý (preprocessor) sẽ xử lý mọi dòng bắt đầu bằng dấu # - đây là bước “Tiền xử lý” đã được nhắc đến ở bài “Tạo dự án C++ đầu tiên”.

#include <iostream> // Bộ tiền xử lý "dán" toàn bộ nội dung file iostream vào đây

#include thực chất chỉ là một lệnh tiền xử lý: nó sao chép-dán nội dung của file được include vào đúng vị trí đó, trước khi trình biên dịch bắt đầu làm việc.

#define tạo ra một macro - về cơ bản là một phép “tìm và thay thế” văn bản, diễn ra trước khi biên dịch:

#include <iostream>
#define MAX_USERS 100
#define PI 3.14159
int main() {
std::cout << MAX_USERS << std::endl; // Bộ tiền xử lý thay MAX_USERS bằng 100 trước khi biên dịch
std::cout << PI << std::endl;
return 0;
}

Về bản chất, dòng std::cout << MAX_USERS được bộ tiền xử lý biến thành std::cout << 100 trước khi trình biên dịch nhìn thấy code.

#define SQUARE(x) ((x) * (x))
int main() {
std::cout << SQUARE(5) << std::endl; // Thay thành ((5) * (5)) = 25
return 0;
}

⚠️ Macro chỉ là thay thế văn bản đơn thuần, không hiểu ngữ nghĩa - dễ gây lỗi khó lường nếu không đặt đủ ngoặc:

#define SQUARE(x) x * x // THIẾU ngoặc - rất nguy hiểm!
int result = SQUARE(2 + 3); // Bị thay thành: 2 + 3 * 2 + 3 = 11 (KHÔNG PHẢI 25!)

Vì lý do này, C++ hiện đại khuyến khích dùng hàm inline hoặc constexpr thay vì macro có tham số bất cứ khi nào có thể - vì chúng an toàn về kiểu dữ liệu và không có cạm bẫy về ngoặc.

Vì sao nên ưu tiên const/constexpr hơn #define cho hằng số?

Phần tiêu đề “Vì sao nên ưu tiên const/constexpr hơn #define cho hằng số?”
#define MAX_SIZE 100 // Cách cũ - không có kiểu dữ liệu, khó debug
constexpr int MAX_SIZE_V2 = 100; // Cách hiện đại - có kiểu rõ ràng, trình biên dịch kiểm tra được

#define không tạo ra một biến thực sự - nó chỉ là thay thế văn bản, nên trình gỡ lỗi (debugger) không thể hiển thị “biến” MAX_SIZE, và không có kiểm tra kiểu dữ liệu nào cả.

Biên dịch có điều kiện: #ifdef, #ifndef, #endif

Phần tiêu đề “Biên dịch có điều kiện: #ifdef, #ifndef, #endif”

Cho phép đưa vào hoặc loại bỏ một đoạn code tùy theo điều kiện tại thời điểm biên dịch - thường dùng để viết code chạy khác nhau trên các hệ điều hành, hoặc bật/tắt code debug:

#include <iostream>
#define DEBUG_MODE
int main() {
#ifdef DEBUG_MODE
std::cout << "Che do debug: bat" << std::endl;
#endif
std::cout << "Chuong trinh chay binh thuong" << std::endl;
return 0;
}

Nếu comment dòng #define DEBUG_MODE lại, dòng in “Che do debug: bat” sẽ hoàn toàn biến mất khỏi file thực thi, không chỉ đơn giản là không chạy.

#include <iostream>
int main() {
#ifdef _WIN32
std::cout << "Dang chay tren Windows" << std::endl;
#elif __APPLE__
std::cout << "Dang chay tren macOS" << std::endl;
#elif __linux__
std::cout << "Dang chay tren Linux" << std::endl;
#endif
return 0;
}

_WIN32, __APPLE__, __linux__ là các macro được trình biên dịch tự động định nghĩa sẵn tùy theo nền tảng đích.

Header guard: ứng dụng quan trọng nhất của #ifndef

Phần tiêu đề “Header guard: ứng dụng quan trọng nhất của #ifndef”

Đã được giới thiệu ở bài trước - đây chính là lý do #ifdef/#ifndef quan trọng trong thực tế:

#ifndef MY_HEADER_H
#define MY_HEADER_H
// nội dung header
#endif
  • Bộ tiền xử lý xử lý mọi dòng bắt đầu bằng # trước khi trình biên dịch thực sự làm việc
  • #define tạo macro - phép thay thế văn bản thuần túy, không hiểu ngữ nghĩa, dễ gây lỗi nếu dùng cho biểu thức có tham số
  • Nên ưu tiên const/constexpr và hàm inline thay vì macro trong C++ hiện đại
  • #ifdef/#ifndef/#endif cho phép biên dịch có điều kiện - ứng dụng quan trọng nhất là header guard