Lesson ১৪পড়তে ৭ মিনিট লাগবে

প্রিপ্রসেসর এবং ম্যাক্রো (Preprocessor & Macros)

এখানকার এই প্রিপ্রসেসর (preprocessor) মূলত এমন একটি কপি-পেস্ট রোবট (copy-paste robot) যা আপনার কোডটিকে (code) কম্পাইলারের (compiler) দেখারও আগে রান (runs BEFORE) হয়ে যায়

অদৃশ্য প্রথম ধাপ (The Invisible First Step)

সি কম্পাইলার (C compiler) মূলত আপনার কোডটিকে (code) যেকোনো মেসিনের ইনসট্রাকশনে (machine instructions) অনুবাদ বা ট্রান্সলেট (translates) করার আগে, তার ভেতর দিয়ে সবার প্রথমে একটি প্রিপ্রসেসর (preprocessor) রান (runs) করে থাকে। আপনি চাইলে এটিকে একটি সাধারণ কাজ বা জব (job) করা একটি কপি-পেস্ট রোবটও (copy-paste robot) ভাবতে পারেন: যার কাজ হলো কিছু বিশেষ নির্দেশ বা ডিরেক্টিভস (directives) (যে লাইনগুলোর শুরুতে সাধারণত # থাকে) খুঁজে বের করা, সেগুলোর ওই নির্দেশ বা ইনসট্রাকশনগুলোকে (instructions) ফলো বা অনুসরণ (follow) করা, এবং আপনার সোর্স ফাইলের (source file) একটি সম্পূর্ণ নতুন ভার্সন বা সংস্করণ (new version) তৈরি করা। এর ফলে এখানকার কম্পাইলারটি (compiler) কখনোই ওই # দেওয়া লাইনগুলোকে দেখতে পায় না — সে শুধু এর রিদাল্ট বা ফলাফলটিকেই (result) দেখতে পায়।

এই প্রিপ্রসেসর (preprocessor) মূলত প্রধানত তিনটি (three main) কাজ করে থাকে:

  • ফাইল ইনক্লুশন (File inclusion) — এই #include মূলত যেকোনো অন্য ফাইলের (another file) সম্পূর্ণ কন্টেন্টগুলোকে (contents) পেস্ট (pastes) করে দেয়
  • ম্যাক্রো সাবস্টিটিউশন (Macro substitution) — এই #define মূলত যেকোনো টেক্সটের প্যাটার্নগুলোকে (text patterns) রিপ্লেস বা পরিবর্তন (replaces) করে দেয়
  • কন্ডিশনাল কম্পাইলেশন (Conditional compilation) — এই #ifdef/#ifndef মূলত যেকোনো কোডের ব্লককে (blocks of code) একসাথে ইনক্লুড বা অন্তর্ভুক্ত (includes) করে বা সেগুলোকে স্কিপ (skips) করে যায়

কনস্ট্যান্ট ডিফাইনস (Constant Defines)

#include <stdio.h>
#define PI 3.14159
#define MAX_STUDENTS 100
#define GREETING "Hello, World!"
int main() {
double area = PI * 5.0 * 5.0;
printf("%s\n", GREETING);
printf("Area of circle (r=5): %.2f\n", area);
printf("Max students allowed: %d\n", MAX_STUDENTS);
return 0;
}
Output
Hello, World!
Area of circle (r=5): 78.54
Max students allowed: 100

প্যারামিটারাইজড ম্যাক্রো (Parameterized Macros)

ম্যাক্রোগুলো (Macros) মূলত প্যারামিটার (parameters) নিতে পারে — এগুলো দেখতে ফাংশনের (functions) মতো হলেও, তাদের কাজ করার ধরন সম্পূর্ণ আলাদা (differently)। একটি ফাংশন কল (function call) মূলত কোডের (code) অন্য আরেকটি অংশে লাফ (jumps) দিয়ে চলে যায়। কিন্তু একটি ম্যাক্রো (macro) হলো আক্ষরিক অর্থে টেক্সট রিপ্লেসমেন্ট (literally text replacement) — তাই প্রিপ্রসেসর (preprocessor) মূলত এই ম্যাক্রোটিকে (macro) যেখানেই ব্যবহার করা হয়, ঠিক সেখানেই সরাসরি এর এক্সপ্যান্ড হওয়া কোডটিকে পেস্ট (pastes) করে দেয়।

আর ঠিক এটিই মূলত এখানকার এই ম্যাক্রোগুলোকে (macros) অনেক বেশি ফাস্ট বা দ্রুত (fast) করে তোলে (যাতে কোনো ফাংশন কলের ওভারহেড বা overhead থাকে না), তবে এটি আবার একই সাথে বেশ বিপজ্জনকও (dangerous) (কারণ এখানে কোনো টাইপ চেকিং বা type checking করা যায় না, এবং এর মধ্যে অনেক অপ্রত্যাশিত পার্শ্বপ্রতিক্রিয়া বা side effects থাকার সম্ভাবনা থাকে)।

প্যারামিটারাইজড ম্যাক্রো (Parameterized Macros)

#include <stdio.h>
// প্যারামিটারগুলো (parameters) এবং এর সম্পূর্ণ এক্সপ্রেশনটিতে (entire expression) সবসময় প্যারেনথেসিস বা ব্র্যাকেটগুলো (parenthesize) ব্যবহার করুন!
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define SQUARE(x) ((x) * (x))
#define ABS(x) ((x) < 0 ? -(x) : (x))
int main() {
printf("MAX(3, 7) = %d\n", MAX(3, 7));
printf("MIN(10, 4) = %d\n", MIN(10, 4));
printf("SQUARE(5) = %d\n", SQUARE(5));
printf("ABS(-42) = %d\n", ABS(-42));
// সাবধান (Careful)! SQUARE(1+2) মূলত ((1+2)*(1+2)) = 9 হয়ে যায়
// কোনো প্যারেনথে্সিস (parens) না দিলে এটি 1+2*1+2 = 5 হয়ে যেত (ভুল বা WRONG!)
printf("SQUARE(1+2) = %d\n", SQUARE(1 + 2));
return 0;
}
Output
MAX(3, 7)    = 7
MIN(10, 4)   = 4
SQUARE(5)    = 25
ABS(-42)     = 42
SQUARE(1+2)  = 9
Note: ম্যাক্রোগুলো (Macros) মূলত কোনো প্রকার স্কোপ বা scope অথবা টাইপগুলোকে (types) সম্মান (respect) করে না — এগুলো হলো আক্ষরিক অর্থেই (literally) টেক্সটের রিপ্লেসমেন্ট (text replacement)। তাই ম্যাক্রোগুলোর ডেফিনিশনে বা সংজ্ঞায় (macro definitions) থাকা প্রতিটি জিনিসের চারপাশেই ব্র্যাকেট বা প্যারেনথে্সিস (parentheses) ব্যবহার করবেন: অর্থাৎ প্রতিটি প্যারামিটার (parameter), এর সাথে এর সম্পূর্ণ এক্সপ্রেশনটি (entire expression)। এগুলো ছাড়া, এর অপারেটরের প্রেসিডেন্স (operator precedence) মূলত আপনাকে যেকোনো সময় সাইলেন্টলি বা নীরবে সম্পূর্ণ ভুল ফলাফল (wrong results) দিতে পারে। #define SQUARE(x) x*x দেখতে বেশ ভালো (fine) লাগলেও, কেউ যখন এই SQUARE(1+2) লিখবে, তখন সে 9-এর বদলে 1+2*1+2 = 5 পেয়ে যাবে।

হেডার গার্ড (Header Guards)

যখন আপনি কোনো হেডার ফাইলকে (header file) #include করেন, তখন প্রিপ্রসেসর (preprocessor) মূলত ওই ফাইলের ভেতরের সমস্ত কন্টেন্টগুলোকে (contents) আক্ষরিক অর্থেই (literally) আপনার ওই সোর্সের (source) ভেতরে পেস্ট (pastes) করে দেয়। তাই যদি দুটি ফাইল আলাদাভাবে একই হেডারটিকে (header) ইনক্লুড বা অন্তর্ভুক্ত (include) করে থাকে, তবে আপনি এতে ডুপ্লিকেট ডেফিনিশনস বা দ্বৈত সংজ্ঞা (duplicate definitions) পাবেন — এবং এই নিয়ে কম্পাইলারটিও (compiler) কমপ্লেন বা অভিযোগ (complains) করতে শুরু করে দেবে।

তাই এখানকার এই হেডার গার্ডগুলো (Header guards) মূলত এটিকে আটকাতেই (prevent) কাজ করে। এগুলো মূলত #ifndef / #define / #endif ব্যবহার করে কম্পাইলারকে এটি বোঝায় যে: "শুধুমাত্র তখনই এই কন্টেন্টটিকে (content) পেস্ট (paste) করো, যখন এটি এর আগে কখনো পেস্ট করা বা pasted হয়ে যায়নি।"

হেডার গার্ডের প্যাটার্ন (Header Guard Pattern)

// ========== math_utils.h ==========
#ifndef MATH_UTILS_H // যদি এটি আগে থেকেই ডিফাইন (defined) করা না থাকে...
#define MATH_UTILS_H // ...তবে এটিকে ডিফাইন (define) করো (অর্থাৎ এটিকে ইনক্লুড বা included হিসেবে মার্ক বা mark করো)
#define PI 3.14159
double circle_area(double radius);
double circle_circumference(double radius);
#endif // MATH_UTILS_H
// ========== math_utils.c ==========
#include "math_utils.h"
double circle_area(double radius) {
return PI * radius * radius;
}
double circle_circumference(double radius) {
return 2.0 * PI * radius;
}
// ========== main.c ==========
#include <stdio.h>
#include "math_utils.h" // একাধিকবার ইনক্লুড বা অন্তর্ভুক্ত (include multiple times) করা সম্পূর্ণ নিরাপদ বা Safe
#include "math_utils.h" // এর দ্বিতীয় ইনক্লুডটি (include) মূলত অত্যন্ত নির্বিঘ্নে বা হ্যারমলেসলি (harmlessly) স্কিপ (skipped) বা এড়িয়ে যাওয়া হবে
int main() {
printf("Area: %.2f\n", circle_area(5.0));
printf("Circumference: %.2f\n", circle_circumference(5.0));
return 0;
}
Output
Area: 78.54
Circumference: 31.42

কন্ডিশনাল কম্পাইলেশন (Conditional Compilation)

কখনও কখনও আপনি হয়তো চাইবেন যে আপনার কোডটি (code) শুধুমাত্র কিছু নির্দিষ্ট পরিস্থিতিতেই (certain conditions) এক্সিস্ট (exists) বা কাজ করুক — ডিবাগ লগিংগুলো (debug logging) শুধুমাত্র রিলিজ বিল্ডগুলোতে (release builds) বা প্ল্যাটফর্ম-নির্দিষ্ট কোডগুলোতে (platform-specific code) (যেমন Windows বনাম Linux-এর ক্ষেত্রে) উধাও বা disappears হয়ে যায়। প্রিপ্রসেসরগুলো (preprocessor) মূলত কম্পাইলের সময় (compile time) পুরো কোডের ব্লককে (entire blocks of code) ইনক্লুড বা অন্তর্ভুক্ত (include) করতে পারে বা এক্সক্লুড বা বাদ (exclude) দিয়ে দিতে পারে।

কন্ডিশনাল ডিবাগ লগিং (Conditional Debug Logging)

#include <stdio.h>
// ডিবাগ আউটপুটটিকে (debug output) অ্যানাবল বা সক্ষম (enable) করার জন্য DEBUG কে ডিফাইন (Define) করে নিন
// প্রোডাকশনে (production), এই লাইনটিকে কমেন্ট আউট (comment this line out) করে দিন অথবা এটি কোনো -DDEBUG ছাড়াই কম্পাইল বা compile করুন
#define DEBUG
#ifdef DEBUG
#define LOG(msg, ...) printf("[DEBUG] " msg "\n", ##__VA_ARGS__)
#else
#define LOG(msg, ...) // এটি এক্সপান্ড হয়ে বা বেড়ে গিয়ে মূলত কিছুই তৈরি করে না (Expands to nothing)!
#endif
int factorial(int n) {
LOG("factorial(%d) called", n);
if (n <= 1) return 1;
int result = n * factorial(n - 1);
LOG("factorial(%d) = %d", n, result);
return result;
}
int main() {
int result = factorial(5);
printf("5! = %d\n", result);
return 0;
}
Output
[DEBUG] factorial(5) called
[DEBUG] factorial(4) called
[DEBUG] factorial(3) called
[DEBUG] factorial(2) called
[DEBUG] factorial(1) called
[DEBUG] factorial(2) = 2
[DEBUG] factorial(3) = 6
[DEBUG] factorial(4) = 24
[DEBUG] factorial(5) = 120
5! = 120

অ্যাডভান্সড বা উন্নত (Advanced): স্ট্রিংজিফিকেশন বা Stringification (#) এবং টোকেন পেস্টিং বা Token Pasting (##)

ম্যাক্রোগুলোর (macros) ভেতরে মূলত এই দুটি (Two) বিশেষ অপারেটর (operators) কাজ করে থাকে:

  • # (স্ট্রিংজিফিকেশন বা stringification) — এটি মূলত ম্যাক্রোর যেকোনো আর্গুমেন্টকে বা parameter-কে একটি স্ট্রিং লিটারেলে (string literal) পরিণত করে
  • ## (টোকেন পেস্টিং বা token pasting) — এটি মূলত দুটি (two) ভিন্ন টোকেনকে (tokens) একসাথে আঠা বা গ্লু (glues) লাগিয়ে একটি আইডেন্টিফায়ার বা identifier তৈরি করে

এগুলো মূলত খুবই নীশ (niche) কিন্তু অনেক বেশি শক্তিশালী (powerful)। টেস্টিং ফ্রেমওয়ার্ক (testing frameworks) এবং কোড জেনারেটরগুলোতে (code generators) আপনি এগুলোকে প্রচুর দেখতে পাবেন।

স্ট্রিংজিফিকেশন এবং টোকেন পেস্টিং (Stringification and Token Pasting)

#include <stdio.h>
// এই # মূলত যেকোনো সাধারণ আর্গুমেন্টকে (argument) একটি স্ট্রিংয়ে (string) পরিণত বা turns করে দেয়
#define PRINT_VAR(var) printf(#var " = %d\n", var)
// এই ## মূলত টোকেনগুলোকে (tokens) একসাথে গ্লু বা যুক্ত (glues) করে দেয়
#define MAKE_FUNC(name) \
int func_##name() { return __LINE__; }
MAKE_FUNC(hello) // এগুলো তৈরি করে (Creates): int func_hello() { ... }
MAKE_FUNC(world) // এগুলো তৈরি করে (Creates): int func_world() { ... }
int main() {
int score = 42;
int lives = 3;
PRINT_VAR(score); // এটি এক্সপান্ড (Expands) হয়ে মূলত এটিতে পরিণত হয়: printf("score" " = %d\n", score)
PRINT_VAR(lives);
printf("func_hello returns: %d\n", func_hello());
printf("func_world returns: %d\n", func_world());
return 0;
}
Output
score = 42
lives = 3
func_hello returns: 10
func_world returns: 11
Note: অনেক আধুনিক সি (modern C) প্রোজেক্টগুলো মূলত ঐতিহ্যবাহী হেডার গার্ডের (traditional header guards) বদলে #pragma once-কে ব্যবহার করে থাকে। এটি মূলত ঠিক একই কাজ করে — অর্থাৎ এটি যেকোনো ডাবল-ইনক্লুশনকে (double-inclusion) আটকে (prevents) দেয় — তবে শুধুমাত্র একটিমাত্র লাইনেই (single line)। যদিও টেকনিক্যালি (technically) এটি কোনো স্টান্ডার্ড সি-এর (standard C) অংশ নয়, তবে প্রতিটি মেজর কম্পাইলারই (major compiler) (যেমন GCC, Clang, MSVC) মূলত এটিকে সাপোর্ট (supported) করে থাকে।
চ্যালেঞ্জ

ছোট কুইজ

প্রিপ্রসেসর (preprocessor) মূলত ঠিক কখন রান (run) করে?
File I/OFunction Pointers & Callbacks