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: bạn đã dùng từ bài đầu tiên
Phần tiêu đề “#include: bạn đã dùng từ bài đầ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: định nghĩa macro
Phần tiêu đề “#define: định nghĩa macro”#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.
Macro có tham số (function-like macro)
Phần tiêu đề “Macro có tham số (function-like macro)”#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ó debugconstexpr 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.
Phân biệt hệ điều hành lúc biên dịch
Phần tiêu đề “Phân biệt hệ điều hành lúc biên dịch”#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#endifTổng kết
Phần tiêu đề “Tổng kết”- 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 #definetạ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/constexprvà hàminlinethay vì macro trong C++ hiện đại #ifdef/#ifndef/#endifcho phép biên dịch có điều kiện - ứng dụng quan trọng nhất là header guard