এই পর্বে যা শিখবেন
- Preprocessor কেন “লেখা-বদলের যন্ত্র”, আর এই একটি সত্য থেকেই কীভাবে তার সব শক্তি ও সব ফাঁদ আসে
#include-এর<>আর""-এর পার্থক্য, include guard ও#pragma once#define,constআরenum— কোন ধ্রুবকের জন্য কোনটি- Function-like macro-র ফাঁদ,
do { } while (0)কৌশল, আর কেন আজstatic inlineভালো #ifdefদিয়ে debug/release ও Windows/Linux-এর আলাদা কোড, আর__FILE__,__LINE__,assertদিয়ে ভুল ধরা
১ · Compile-এর আগে একজন লেখা-বদলকারী
পর্ব ০৩-এ আপনি toolchain-এর চার ধাপ দেখেছিলেন। প্রথম ধাপের নাম ছিল preprocessor — সে #include <stdio.h> দেখে পুরো ফাইলটি এনে বসিয়ে দেয়। gcc -E দিয়ে তার কাজের ফলও দেখেছিলেন। এই পর্বে আমরা সেই ধাপটিকেই কাছ থেকে দেখব, কারণ এখানে এমন কিছু ভুল জন্মায় যা পরে compiler বা debugger দিয়ে খুঁজে পাওয়া খুব কঠিন।
একটি তুলনা দিয়ে শুরু করি। Microsoft Word-এ “Find & Replace” আছে — “ঢাকা” খুঁজে সব জায়গায় “Dhaka” বসিয়ে দেয়। Word কিন্তু বোঝে না শব্দটা কোথায় নাম, কোথায় বাক্যের অংশ; সে শুধু অক্ষর মিলিয়ে বদলায়। Preprocessor ঠিক এই কাজটিই করে — আপনার .c ফাইলের ওপর, compiler পড়ার আগে।
ধরুন একটি কোচিং সেন্টারে প্রশ্নপত্র ছাপা হবে। ছাপাখানায় পাঠানোর আগে অফিসের একজন কর্মী খসড়াটি হাতে নেন। খসড়ায় লেখা আছে “[এখানে গত বছরের ৫টি প্রশ্ন বসাও]”, “[পরীক্ষার তারিখ]”, “[শুধু বিজ্ঞান বিভাগের জন্য: …]”। কর্মী প্রশ্নগুলো কপি করে বসান, তারিখ লিখে দেন, আর মানবিক বিভাগের কপি থেকে বিজ্ঞানের অংশ কেটে দেন। তিনি প্রশ্নের বিষয় বোঝেন না, বোঝার দরকারও নেই — তিনি শুধু নির্দেশ মেনে লেখা বদলান। ছাপাখানা (compiler) পায় চূড়ান্ত, পরিষ্কার একটি কপি। Preprocessor হলো সেই কর্মী।
Preprocessor-কে দেওয়া প্রতিটি নির্দেশকে বলে directive. চেনার উপায় সহজ — লাইনের প্রথম অক্ষর (ফাঁকা জায়গা বাদে) #. মূল directive-গুলো হলো:
| Directive | কাজ | প্রশ্নপত্রের তুলনায় |
|---|---|---|
#include | অন্য ফাইলের পুরো লেখা এনে এখানে বসাও | “গত বছরের প্রশ্ন এখানে বসাও” |
#define / #undef | একটি নামকে অন্য লেখা দিয়ে বদলানোর নিয়ম বানাও / মুছে ফেলো | “[তারিখ] যেখানে আছে, ১৫ মার্চ লেখো” |
#if, #ifdef, #ifndef, #elif, #else, #endif | শর্ত মিললে অংশটি রাখো, না মিললে পুরো অংশটি ফেলে দাও | “শুধু বিজ্ঞান বিভাগের কপিতে” |
#error | এখানে পৌঁছালে build থামিয়ে দাও, এই বার্তা দেখাও | “বিভাগ না জানালে ছাপা বন্ধ” |
#pragma | Compiler-কে বিশেষ নির্দেশ (compiler ভেদে আলাদা) | ছাপাখানার জন্য আলাদা চিরকুট |
Preprocessor চলে compiler-এর আগে, আর সে C ভাষা বোঝে না। int কী, কোন ভ্যারিয়েবল কোন ফাংশনের ভেতরে, কোন অপারেটর আগে হিসাব হবে — এসব তার কাছে অর্থহীন। সে দেখে শুধু শব্দের টুকরো (token) আর লেখা। এই পর্বের প্রতিটি ফাঁদ এই একটি বাক্য থেকেই এসেছে। মনে রাখলে বাকিটা প্রায় নিজেই বুঝে যাবেন।
Preprocessor কাজ শেষ করলে যে একটি বড় লেখা তৈরি হয় — আপনার .c ফাইল আর তার ভেতরে ঢোকা সব header মিলিয়ে — compiler সেটিকেই একটি একক হিসেবে compile করে। এই এককের নাম translation unit. পরের অংশগুলোতে শব্দটি কাজে লাগবে।
C-এর শুরুর দিকে preprocessor ছিল না। Dennis Ritchie নিজের লেখা C-এর ইতিহাসে জানিয়েছেন, ১৯৭২-৭৩ সালের দিকে এটি যোগ হয়, আর প্রথম সংস্করণে ছিল মাত্র দুটি জিনিস — ফাইল #include করা আর parameter ছাড়া সাধারণ #define. Argument নেওয়া macro আর conditional compilation যোগ হয় এর কিছু পরে। মজার ব্যাপার হলো, শুরুতে একে ভাষার একটি ঐচ্ছিক সংযোজন হিসেবে দেখা হতো — বেশ কিছুদিন ফাইলের শুরুতে বিশেষ চিহ্ন না থাকলে preprocessor চালানোই হতো না। আজ এটি C স্ট্যান্ডার্ডের অবিচ্ছেদ্য অংশ।
২ · #include — অন্য ফাইল এনে বসানো, আর দুবার বসলে বিপদ
#include-এর কাজ সবচেয়ে সরল: নির্দিষ্ট ফাইলটি খুঁজে তার পুরো লেখা ঠিক এই লাইনের জায়গায় বসিয়ে দেওয়া। কোনো বাছাই নেই, কোনো বুদ্ধি নেই — পুরোটাই কপি। ফাইলের নাম দুইভাবে লেখা যায়, আর পার্থক্যটা হলো কোথায় খোঁজা হবে:
#include <stdio.h>
কোণা বন্ধনী মানে “সিস্টেমের header”. Compiler তার নিজের জানা ফোল্ডারগুলোতে খোঁজে — যেমন Linux-এ /usr/include. Standard library ও ইনস্টল করা library-র জন্য এটি।
#include "student.h"
উদ্ধৃতি চিহ্ন মানে “আমার প্রজেক্টের header”. GCC-সহ প্রচলিত compiler আগে যে ফাইলে #include লেখা, তার নিজের ফোল্ডারে খোঁজে; না পেলে সিস্টেমের ফোল্ডারগুলোতে যায়।
অন্য কোনো ফোল্ডারে header রাখলে compiler-কে -I flag দিয়ে ঠিকানা বলে দিতে হয়, যেমন gcc -Iinclude main.c. এই খোঁজার তালিকাকে বলে include path. Header না পেলে কী হয়, পর্ব ০৩-এ দেখেছেন — preprocessor থেমে যায়, No such file or directory.
একই header দুবার ঢুকলে কী হয়
সমস্যাটা আসে বড় প্রজেক্টে। একটি ছাত্র-ফলাফল সফটওয়্যারের কথা ভাবুন। date.h-তে তারিখের একটি struct আছে। student.h-এর দরকার জন্মতারিখ, তাই সে date.h include করে। result.h-এর দরকার পরীক্ষার তারিখ, তাই সে-ও date.h include করে। আর main.c দুটো header-ই include করে। কেউ কোনো ভুল করেনি — তবু build ভেঙে যায়। নিচের ডায়াগ্রামে দেখুন কেন:
/* date.h */ struct Date { int day, month, year; }; /* student.h */ #include "date.h" struct Student { char name[40]; struct Date birth; }; /* result.h */ #include "date.h" struct Result { int marks; struct Date exam_day; }; /* main.c */ #include "student.h" #include "result.h"
/* student.h থেকে → তার ভেতরের date.h থেকে: */ struct Date { int day, month, year; }; struct Student { char name[40]; struct Date birth; };
struct Date { int day, month, year; };
struct Student { char name[40]; struct Date birth; };
/* result.h থেকে → তার ভেতরের date.h থেকে আবার: */
struct Date { int day, month, year; };
struct Result { int marks; struct Date exam_day; };
$ gcc main.c -o result
In file included from result.h:1,
from main.c:2:
date.h:1:8: error: redefinition of 'struct Date'
#ifndef DATE_H /* DATE_H সংজ্ঞায়িত নয়? → হ্যাঁ, ভেতরে ঢোকো */ #define DATE_H /* এখন থেকে DATE_H আছে */ struct Date { int day, month, year; }; /* রাখা হলো */ #endif
struct Date { int day, month, year; };
struct Student { char name[40]; struct Date birth; };
/* result.h → date.h: DATE_H আগেই আছে, তাই #ifndef মিথ্যা — কিছুই বসল না */
struct Result { int marks; struct Date exam_day; };
/* struct Date মাত্র একবার — build সফল */
- চারটি নির্দোষ ফাইল। প্রতিটি header নিজের দরকারি জিনিস নিজে include করছে — এটাই ভালো অভ্যাস। কিন্তু খেয়াল করুন,
date.h-তে পৌঁছানোর দুটি পথ আছে। - প্রথম পথ: student.h. Preprocessor
main.c-এর প্রথম লাইনেstudent.hখুলল। তার ভেতরে#include "date.h"— তাইstruct Dateবসে গেল। এ পর্যন্ত সব ঠিক। - দ্বিতীয় পথ: result.h. এবার
result.h, যার ভেতরেও#include "date.h". Preprocessor-এর কোনো স্মৃতি নেই যে ফাইলটি আগে বসানো হয়েছে — সে আবার পুরোটা কপি করল। - Compiler-এর আপত্তি। Compiler একই translation unit-এ
struct Date-এর সংজ্ঞা দুবার দেখল —redefinitionerror. লক্ষ করুন, error দেখাচ্ছেdate.h-এ, অথচ ভুলটা আসলে include-এর গঠনে। নতুনরা এখানে প্রায়ই বিভ্রান্ত হন। - সমাধান: include guard.
date.h-এর পুরো লেখা একটি#ifndef-এর ভেতরে রাখা হলো। প্রথমবারDATE_Hনামটি সংজ্ঞায়িত নেই, তাই ভেতরে ঢুকে প্রথমেইDATE_Hসংজ্ঞায়িত করা হলো, তারপর struct রাখা হলো। - দ্বিতীয়বার কিছুই ঢুকল না।
result.hআবারdate.hখুলল, কিন্তু এবারDATE_Hআগে থেকেই আছে —#ifndefমিথ্যা, তাই#endifপর্যন্ত সবকিছু বাদ।struct Dateমাত্র একবার, build সফল। প্রতিটি header-এর প্রথম আর শেষ লাইন এভাবেই লেখা উচিত।
এই কৌশলের নাম include guard. নামটি সাধারণত ফাইলের নাম বড় হাতের অক্ষরে, .-এর জায়গায় _ দিয়ে — student.h হলে STUDENT_H. বড় প্রজেক্টে নামের সংঘাত এড়াতে সামনে প্রজেক্টের নামও যোগ করা হয়, যেমন RESULTAPP_STUDENT_H.
আরেকটি ছোট উপায় আছে — header-এর প্রথম লাইনে #pragma once. এর অর্থ “এই ফাইল একটি translation unit-এ একবারই বসাও”. GCC, Clang আর Microsoft-এর compiler (MSVC) — তিনটিই এটি বোঝে, তাই বাস্তবে খুব প্রচলিত। তবে #pragma once C স্ট্যান্ডার্ডের অংশ নয়। একই ফাইল যদি দুটি ভিন্ন পথে পৌঁছানো যায় (যেমন link করা ফোল্ডার বা নেটওয়ার্ক drive), কিছু পরিস্থিতিতে compiler বুঝতে পারে না দুটো আসলে একই ফাইল। তাই যেখানে সর্বোচ্চ portability দরকার, সেখানে মানুষ এখনো guard লেখে, কেউ কেউ দুটোই।
Include guard শুধু একটি translation unit-এর ভেতরে দ্বিতীয়বার ঢোকা আটকায়। main.c আর report.c — দুটি আলাদা ফাইল আলাদাভাবে compile হয়, তাই দুটোতেই date.h একবার করে ঢুকবে। Header-এ শুধু ঘোষণা থাকলে এতে সমস্যা নেই। কিন্তু header-এ কোনো ফাংশনের পুরো সংজ্ঞা বা ভ্যারিয়েবল রাখলে দুটি .o ফাইলে একই নাম দুবার আসে, আর linker বলে multiple definition. Guard সেটা আটকাতে পারে না। কোন জিনিস header-এ যায় আর কোনটি .c-তে, তা নিয়ে বিস্তারিত পর্ব ২৯-এ।
৩ · #define দিয়ে ধ্রুবক — const আর enum-এর সাথে তুলনা
#define-এর সবচেয়ে সরল রূপ একটি নামকে একটি লেখা দিয়ে বদলায়:
#define MAX_STUDENTS 60
#define PASS_MARK 33
int marks[MAX_STUDENTS]; /* preprocessor-এর পরে: int marks[60]; */এ ধরনের নির্দেশকে বলে macro, আর যেহেতু এটি দেখতে একটি সাধারণ নামের মতো, তাই এর পুরো নাম object-like macro. Preprocessor ফাইলের যেখানেই MAX_STUDENTS শব্দটি পায়, সেখানে 60 লিখে দেয়। একে বলে macro expansion. পর্ব ০৬-এ দেখেছেন, এভাবে magic number-কে নাম দিলে কোড পড়া সহজ হয়, আর নিয়ম বদলালে এক জায়গায় বদলালেই চলে।
কিন্তু পর্ব ০৬-এ একই কাজের জন্য const শিখেছিলেন, আর পর্ব ২২-এ enum. তাহলে কোনটি কখন? তিনটির স্বভাব পাশাপাশি দেখুন:
| প্রশ্ন | #define N 60 | const int N = 60; | enum { N = 60 }; |
|---|---|---|---|
| কে দেখে? | শুধু preprocessor; compiler দেখে শুধু 60 | Compiler — এটি টাইপসহ একটি আসল ভ্যারিয়েবল | Compiler — টাইপ int-এর একটি নামযুক্ত ধ্রুবক |
| টাইপ যাচাই | নেই, শুধু লেখা | আছে | আছে (সবসময় int) |
| Scope মানে? | না — লেখার লাইন থেকে ফাইলের শেষ পর্যন্ত সব জায়গায় | হ্যাঁ, ফাংশন বা ব্লকের ভেতরে সীমিত রাখা যায় | হ্যাঁ |
ফাইল-স্তরের array-র মাপ int a[N]; | চলে | C-তে চলে না — C-এর নিয়মে const ভ্যারিয়েবল “constant expression” নয় | চলে |
switch-এর case N: | চলে | চলে না, একই কারণে | চলে |
| Debugger-এ নাম দেখা যায়? | সাধারণত না — compile-এর আগেই নাম মুছে গেছে | হ্যাঁ | হ্যাঁ |
double বা string ধ্রুবক | চলে | চলে | না, শুধু পূর্ণসংখ্যা |
C++ জানা থাকলে চতুর্থ সারিটি অবাক করবে — C++-এ const int দিয়ে array-র মাপ দেওয়া যায়, C-তে যায় না। ফাংশনের ভেতরে int a[N]; লিখলে C99 থেকে কোডটি compile হয় ঠিকই, কিন্তু তখন সেটি আর সাধারণ array নয় — একটি “variable length array”, যাকে = {0} দিয়ে শুরুর মানও দেওয়া যায় না। আর পুরোনো C89-এ এটি সরাসরি error. সর্বশেষ C23 স্ট্যান্ডার্ডে constexpr যোগ হয়ে এই ফাঁক কিছুটা পূরণ হয়েছে, তবে সব compiler-এ এখনো সমানভাবে নেই।
অভিজ্ঞ C প্রোগ্রামাররা তাই মোটামুটি এই নিয়ম মানেন:
- পূর্ণসংখ্যার ধ্রুবক, বিশেষ করে array-র মাপ বা
case:enum— টাইপ আছে, scope আছে, debugger-এ দেখা যায়। - দশমিক বা অন্য টাইপের ধ্রুবক:
static const double VAT_RATE = 0.15; #define: যখন মানটি preprocessor-এরই লাগবে (যেমন#if MAX_STUDENTS > 100), অথবা compile করার সময় বাইরে থেকে দেওয়া হবে (gcc -DMAX_STUDENTS=80).
Object-like macro-তেও বন্ধনী ভুলবেন না। #define TOTAL 40 + 20 লিখে TOTAL * 2 হিসাব করলে compiler পায় 40 + 20 * 2 — উত্তর ৮০, ১২০ নয়। লেখা উচিত #define TOTAL (40 + 20). আর #define PASS_MARK 33;-এর মতো শেষে ; দেবেন না — if (m >= PASS_MARK) তখন হয়ে যায় if (m >= 33;), আর compiler এমন জায়গায় error দেখায় যেখানে চোখে কোনো ভুল নেই।
৪ · Function-like macro — দেখতে ফাংশন, আসলে লেখা-বদল
Macro argument-ও নিতে পারে। নামের ঠিক পরেই (মাঝে কোনো ফাঁকা জায়গা ছাড়া) বন্ধনী দিলে তৈরি হয় function-like macro:
#define SQUARE_BAD(x) x * xদেখতে একটি ফাংশনের মতো — SQUARE_BAD(5) দিলে 5 * 5, অর্থাৎ ২৫. কিন্তু মনে রাখুন, preprocessor হিসাব করে না, শুধু x-এর জায়গায় আপনি যা লিখেছেন হুবহু সেই লেখাটি বসায়। Argument-এ একটি যোগ চিহ্ন থাকলেই গল্প বদলে যায়। পর্ব ০৭-এর precedence মনে আছে তো? নিচে ধাপে ধাপে দেখুন compiler আসলে কী পায়:
- আপনার লেখা
SQUARE_BAD(a + 1)আশা: ১৬ - Preprocessor
a + 1 * a + 1শুধু লেখা বসাল - Compiler
3 + 3 + 1ফল: ৭
#define SQUARE_BAD(x) x * x int a = 3; int r = SQUARE_BAD(a + 1); /* আশা: (3+1)*(3+1) = 16 */
int a = 3; int r = a + 1 * a + 1; /* x * x ↓ ↓ a + 1 * a + 1 — বন্ধনী কোথাও নেই! */
a + 1 * a + 1 = a + (1 * a) + 1 = 3 + 3 + 1 = 7 /* 16 নয়! কোনো error নেই, কোনো warning নেই */
#define SQUARE(x) ((x) * (x)) SQUARE(a + 1) → ((a + 1) * (a + 1)) = 16 10 / SQUARE(2) → 10 / ((2) * (2)) = 2 /* বাইরের বন্ধনী না থাকলে 10 / (2) * (2) = 10 হতো */
int i = 5; int r = SQUARE(i++); /* preprocessor-এর পরে: */ int r = ((i++) * (i++)); /* i একই expression-এ দুবার বাড়ছে — undefined behavior. ফল 30? 25? 36? কোনো নিশ্চয়তা নেই। */
static inline int square(int x) { return x * x; } square(a + 1) → x = 4, ফল 16 square(i++) → x = 5, ফল 25, i হলো 6 /* ঠিক একবার */ square("hi") → compiler error: টাইপ মেলে না — ভালো!
- আপনার লেখা।
a-এর মান ৩, তাইSQUARE_BAD(a + 1)থেকে আশা করছেন ৪ × ৪ = ১৬. যে কেউ কোডটা পড়ে এটাই ভাববেন। - Preprocessor লেখা বসাল। সংজ্ঞায় আছে
x * x. দুটিx-এর জায়গাতেই বসল “a + 1” লেখাটি — চারপাশে কোনো বন্ধনী ছাড়া, কারণ preprocessor বন্ধনী যোগ করে না। এই ধাপকেই বলে macro expansion. - Compiler নিয়ম মেনে হিসাব করল। Compiler
SQUARE_BADনামটি কখনো দেখেইনি; সে পেয়েছেa + 1 * a + 1. Precedence অনুযায়ী গুণ আগে, তাই1 * a= ৩, তারপর ৩ + ৩ + ১ = ৭. কোনো error বা warning নেই — প্রোগ্রাম চুপচাপ ভুল উত্তর দেয়। - সমাধান: দুই স্তরে বন্ধনী। প্রতিটি
x-কে আলাদা বন্ধনীতে, আর পুরো সংজ্ঞাকেও একটি বাইরের বন্ধনীতে। ভেতরের বন্ধনী argument-এর যোগ-বিয়োগ রক্ষা করে, বাইরেরটি চারপাশের ভাগ-গুণ থেকে রক্ষা করে। - দ্বিতীয় ফাঁদ: দুবার হিসাব। বন্ধনী থাকলেও
SQUARE(i++)লিখলে লেখাটি দুবার বসে, ফলেi++দুবার চলে। একই expression-এ একটি ভ্যারিয়েবল দুবার বদলানো C-তে undefined behavior — ফল compiler ও optimization ভেদে যা খুশি হতে পারে। একে বলে double evaluation. - আধুনিক উত্তর:
static inlineফাংশন। আসল ফাংশনে argument ঠিক একবার হিসাব হয়, টাইপ যাচাই হয়, debugger-এ নাম দেখা যায়। আর optimization চালু থাকলে compiler ফাংশন-ডাকার খরচও তুলে দেয় — তাই গতিতে macro-র সাথে সাধারণত কোনো পার্থক্য থাকে না।
এবার নিজে চালিয়ে দেখুন। নিচের কোডে একই কাজ macro আর inline function — দুইভাবে করা হয়েছে। MAX macro-টি দুই স্তরে বন্ধনীসহ “সঠিকভাবে” লেখা, তবু একটি অবাক করা ফল দেয়:
#include <stdio.h>
#define SQUARE_BAD(x) x * x
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
static inline int max_int(int a, int b) { return a > b ? a : b; }
int main(void) {
int a = 3;
printf("SQUARE_BAD(a + 1) = %d\n", SQUARE_BAD(a + 1));
printf("SQUARE(a + 1) = %d\n", SQUARE(a + 1));
int i = 5, j = 5;
int m1 = MAX(i++, 0); /* macro */
int m2 = max_int(j++, 0); /* function */
printf("macro : m1 = %d, i = %d\n", m1, i);
printf("inline: m2 = %d, j = %d\n", m2, j);
return 0;
}প্রথম দুই লাইনে আসবে 7 আর 16 — বন্ধনীর পার্থক্য। আসল চমক শেষ দুই লাইনে। MAX(i++, 0) হয়ে যায় ((i++) > (0) ? (i++) : (0)). শর্ত যাচাইয়ে একবার i++ (৫ > ০ সত্য, i হলো ৬), তারপর উত্তর নিতে গিয়ে আবার i++ — তাই m1 = ৬ আর i = ৭. অথচ লিখেছিলেন একবার বাড়াতে! এখানে ?:-এর মাঝে একটি sequence point আছে বলে ফলটি অন্তত নির্দিষ্ট, কিন্তু ভুল। ফাংশনের বেলায় m2 = ৫, j = ৬ — ঠিক যা আশা করেছিলেন।
Macro-কে argument দেওয়ার সময় কখনো side effect আছে এমন কিছু দেবেন না — i++, getchar(), rand(), কোনো ফাংশন যা ফাইল পড়ে বা প্রিন্ট করে। এই কারণেই C জগতে প্রথা হলো macro-র নাম সব বড় হাতের অক্ষরে লেখা (MAX, SQUARE) — যাতে নাম দেখেই পাঠক সাবধান হন, “এটি ফাংশন নয়”.
৫ · কয়েক লাইনের macro আর do { } while (0) কৌশল
কখনো macro দিয়ে একাধিক কাজ করাতে ইচ্ছে হয় — ধরুন একটি বিন্দুর দুটি স্থানাঙ্ক শূন্য করা। লম্বা macro কয়েক লাইনে লিখতে চাইলে প্রতিটি লাইনের শেষে \ দিতে হয়, কারণ preprocessor-এর কাছে একটি directive মানে একটি লাইন। এখন প্রশ্ন হলো, ভেতরের কয়েকটি statement-কে কীভাবে মোড়াবেন? তিনটি চেষ্টা ধাপে ধাপে দেখুন:
#define RESET(p) (p).x = 0; (p).y = 0
if (out_of_screen)
RESET(pt);
/* preprocessor-এর পরে: */
if (out_of_screen)
pt.x = 0;
pt.y = 0; /* ← if-এর বাইরে! সবসময় চলে */
#define RESET(p) { (p).x = 0; (p).y = 0; }
if (out_of_screen)
RESET(pt);
/* preprocessor-এর পরে: */
if (out_of_screen)
{ pt.x = 0; pt.y = 0; }; /* ঠিক আছে… আপাতত */
if (out_of_screen)
RESET(pt);
else
moves++;
/* preprocessor-এর পরে: */
if (out_of_screen)
{ pt.x = 0; pt.y = 0; }; /* ← এই ; দিয়ে if শেষ */
else /* error: 'else' without a previous 'if' */
moves++;
#define RESET(p) \
do { \
(p).x = 0; \
(p).y = 0; \
} while (0)
if (out_of_screen)
do { pt.x = 0; pt.y = 0; } while (0); /* ; এখানে লাগবেই */
else
moves++; /* ঠিকঠাক compile হয় */
- মোড়ক ছাড়া — নিঃশব্দ বাগ।
if-এর পরে{ }না থাকলে শুধু পরের একটি statement শর্তের অধীনে থাকে। Macro খুলে দুটি statement হলো, তাই দ্বিতীয়টি (pt.y = 0;) সবসময় চলে। Compiler কোনো আপত্তি করে না, কারণ ব্যাকরণে কোনো ভুল নেই। { }দিয়ে মোড়ালে আপাতত চলে। এখন দুটি কাজই এক ব্লকে। শেষে একটি বাড়তি;থেকে গেল — সেটি একটি খালি statement, এখানে ক্ষতিহীন।- কিন্তু
elseএলেই ভাঙে। প্রোগ্রামাররা ফাংশনের মতো করেRESET(pt);লেখেন — শেষে;দিয়ে। ব্লকের পরের সেই;if-কে শেষ করে দেয়, ফলেelseতার জোড়া খুঁজে পায় না। Compiler error দেয় এমন লাইনে, যেখানে চোখে কোনো ভুল নেই। do { … } while (0)— একবার চলা লুপ। শর্ত0, তাই ভেতরের কাজ ঠিক একবার চলে। কিন্তু ব্যাকরণে পুরোটা একটি statement, যার শেষে;বাধ্যতামূলক — ঠিক একটি ফাংশন-ডাকার মতো।if,else, লুপ — সব জায়গায় নিরাপদ। Optimizer এই লুপটি পুরোপুরি মুছে দেয়, তাই কোনো খরচ নেই। Linux kernel-সহ অসংখ্য বড় C প্রজেক্টে কয়েক লাইনের macro এভাবেই লেখা হয়।
তাহলে macro কেন, যখন static inline আছে?
C99 থেকে C-তে inline keyword আছে। ছোট ফাংশনের সামনে static inline লিখে header-এ রাখলে compiler চাইলে ফাংশন-ডাকার জায়গায় সরাসরি ফাংশনের কোড বসিয়ে দেয় — পর্ব ০৩-এ যাকে inlining বলেছিলাম। এতে macro-র গতির সুবিধা পাওয়া যায়, কিন্তু তার কোনো ফাঁদ থাকে না:
| বৈশিষ্ট্য | Function-like macro | static inline ফাংশন |
|---|---|---|
| Argument কতবার হিসাব হয় | যতবার সংজ্ঞায় লেখা আছে | ঠিক একবার |
| বন্ধনীর ঝামেলা | প্রতিটি জায়গায় নিজেকে দিতে হয় | নেই — এটি আসল expression |
| টাইপ যাচাই | নেই | আছে, ভুল টাইপে compiler সতর্ক করে |
| Debugger-এ ধাপে ধাপে চলা | প্রায় অসম্ভব | সম্ভব (optimization বন্ধ থাকলে) |
| Error message | খোলা লেখার ভেতরে, প্রায়ই বিভ্রান্তিকর | আপনার লেখা লাইনেই |
| যেকোনো টাইপে একই কোড | পারে (MAX int, double দুটোতেই চলে) | প্রতিটি টাইপের জন্য আলাদা ফাংশন লাগে |
শেষ সারিটিই macro-র একটি আসল সুবিধা — টাইপ না জেনেই কাজ করা। তবে বেশিরভাগ বাস্তব কোডে দু-একটি টাইপই লাগে। Linux kernel-এর coding style নির্দেশিকাতেও পরিষ্কার লেখা আছে: ফাংশনের মতো দেখতে macro-র চেয়ে inline ফাংশন সাধারণত ভালো। Macro রাখুন সেই কাজগুলোর জন্য যা ফাংশন পারেই না — পরের দুটি অংশে সেগুলো দেখব।
৬ · Conditional compilation — এক কোড, আলাদা প্রোগ্রাম
Preprocessor-এর সবচেয়ে কাজের ক্ষমতা সম্ভবত এটি: শর্ত অনুযায়ী কোডের একটি অংশ রাখা বা পুরোপুরি মুছে ফেলা। একে বলে conditional compilation. সাধারণ if চলে প্রোগ্রাম চলার সময়; কিন্তু #if কাজ করে compile-এর আগে, আর যে অংশ বাদ পড়ে, সেটি compiler কখনো দেখেই না — চূড়ান্ত প্রোগ্রামে তার একটি বাইটও থাকে না।
| লেখা | অর্থ |
|---|---|
#ifdef DEBUG | DEBUG নামের macro সংজ্ঞায়িত থাকলে (মান যা-ই হোক) |
#ifndef DATE_H | সংজ্ঞায়িত না থাকলে — include guard এটাই ব্যবহার করে |
#if MAX_STUDENTS > 100 | পূর্ণসংখ্যার হিসাব সত্য হলে। অজানা নাম এখানে ০ ধরা হয় |
#if defined(_WIN32) || defined(__linux__) | কয়েকটি শর্ত একসাথে মেলাতে |
#elif, #else, #endif | নাহলে-যদি, নাহলে, শেষ — প্রতিটি #if-এর একটি #endif লাগবেই |
শর্তে ব্যবহৃত নামগুলো আসে দুই জায়গা থেকে। কিছু compiler নিজেই সংজ্ঞায়িত করে রাখে — যেমন Windows-এ (MSVC বা MinGW, ৩২ বা ৬৪ বিট যা-ই হোক) _WIN32, Linux-এ __linux__, macOS-এ __APPLE__. আর কিছু আপনি build কমান্ডে দেন: gcc -DDEBUG app.c মানে “ফাইলের একদম শুরুতে #define DEBUG 1 লেখা আছে ধরে নাও”. একই app.c থেকে কী কী আলাদা প্রোগ্রাম তৈরি হয়, দেখুন:
#include <stdio.h>
#if defined(_WIN32)
#define CLEAR_CMD "cls"
#elif defined(__linux__) || defined(__APPLE__)
#define CLEAR_CMD "clear"
#else
#error "Unknown platform: please define CLEAR_CMD"
#endif
int main(void) {
#ifdef DEBUG
printf("[debug] build date: %s\n", __DATE__);
#endif
printf("Screen clear command: %s\n", CLEAR_CMD);
return 0;
}
/* stdio.h-এর শত শত লাইন … */ int main(void) { printf("Screen clear command: %s\n", "clear"); return 0; } /* debug লাইনটির কোনো চিহ্নই নেই */
/* stdio.h-এর শত শত লাইন … */ int main(void) { printf("[debug] build date: %s\n", "Oct 16 2026"); printf("Screen clear command: %s\n", "clear"); return 0; }
/* stdio.h-এর শত শত লাইন … */ int main(void) { printf("Screen clear command: %s\n", "cls"); return 0; }
$ gcc app.c
app.c:8:6: error: #error "Unknown platform: please define CLEAR_CMD"
8 | #error "Unknown platform: please define CLEAR_CMD"
| ^~~~~
- একটিই source ফাইল। এতে দুই রকম শর্ত আছে — কোন অপারেটিং সিস্টেম (
_WIN32,__linux__), আর debug build কি না (DEBUG).__DATE__একটি তৈরি-করা macro, যা compile-এর তারিখ বসায় — অংশ ৭-এ বিস্তারিত। - Linux-এ সাধারণ build.
__linux__সংজ্ঞায়িত, তাইCLEAR_CMDহলো"clear".DEBUGনেই, তাই#ifdef DEBUGথেকে#endifপর্যন্ত সব মুছে গেল। ব্যবহারকারীর হাতে যাওয়া প্রোগ্রামে debug কোডের অস্তিত্বই নেই। - একই কম্পিউটার, শুধু
-DDEBUGযোগ। একটি flag-এর জন্য compiler এবার একটি বাড়তিprintfপেল। Source-এ এক অক্ষরও বদলাতে হলো না।__DATE__-এর জায়গায় বসেছে compile-এর দিনের তারিখ। - Windows-এ build. একই ফাইল, কিন্তু এবার
_WIN32সংজ্ঞায়িত — তাই"cls". এভাবেই একটি কোডবেস থেকে Windows, Linux, Mac আর মোবাইলের জন্য আলাদা প্রোগ্রাম তৈরি হয়। #error— নিরাপদে থেমে যাওয়া। কোনো শর্তই না মিললে preprocessor নিজেই build থামিয়ে পরিষ্কার বার্তা দেয়। চুপচাপ ভুল প্রোগ্রাম তৈরির চেয়ে এটি অনেক ভালো — সমস্যাটা ধরা পড়ে প্রোগ্রামারের টেবিলে, ব্যবহারকারীর হাতে নয়।
কোড সাময়িকভাবে “বন্ধ” রাখতে অনেকে /* … */ দিয়ে comment করেন। কিন্তু ভেতরে আগে থেকে /* */ থাকলে প্রথম */-এই comment শেষ হয়ে যায়। নিরাপদ উপায় হলো #if 0 … #endif — ভেতরে যা-ই থাকুক, পুরোটা বাদ পড়ে।
Conditional compilation-এর একটি লুকানো দাম আছে। আপনি প্রতিদিন যে build চালান, শুধু সেই শাখার কোডই compiler যাচাই করে। #ifdef _WIN32-এর ভেতরের কোডে ভুল থাকলে Linux-এ কাজ করা প্রোগ্রামার কখনো জানবেন না — যতদিন না কেউ Windows-এ build করেন। তাই পেশাদার প্রজেক্টে CI প্রতিটি platform আর প্রতিটি build ধরনে আলাদাভাবে compile করে দেখে। আর #ifdef যত বেশি, পরীক্ষা করার মতো সংস্করণও তত বেশি — তাই এটি যত কম, তত ভালো।
৭ · প্রোগ্রাম নিজের ঠিকানা বলে — __FILE__, __LINE__, __func__ ও assert
কিছু macro আপনাকে লিখতে হয় না, compiler নিজেই প্রস্তুত রাখে। এদের বলে predefined macro. সবচেয়ে কাজের কয়েকটি:
| নাম | যা দেয় | উদাহরণ |
|---|---|---|
__FILE__ | বর্তমান source ফাইলের নাম (string) | "result.c" |
__LINE__ | বর্তমান লাইন নম্বর (পূর্ণসংখ্যা) | 42 |
__DATE__, __TIME__ | Compile-এর তারিখ ও সময় (string) | "Oct 16 2026", "10:30:05" |
__STDC_VERSION__ | কোন C স্ট্যান্ডার্ডে compile হচ্ছে | C99-এ 199901L, C11-এ 201112L, C17-এ 201710L |
__func__ | বর্তমান ফাংশনের নাম (string) — macro নয়, নিচে দেখুন | "average" |
__func__ নিয়ে একটি সূক্ষ্ম কিন্তু সত্যিকারের পার্থক্য আছে। এটি C99-এ যোগ হওয়া একটি predefined identifier — macro নয়। কারণটা যৌক্তিক: preprocessor ফাংশন কী তা-ই জানে না, তাই সে ফাংশনের নাম বসাতে পারে না। কাজটি করে compiler নিজে, যেন প্রতিটি ফাংশনের শুরুতে লুকিয়ে static const char __func__[] = "average"; লেখা আছে। ব্যবহারে পার্থক্য সামান্য, কিন্তু একটি জিনিস মনে রাখুন: #ifdef __func__ লিখলে সেটি সবসময় মিথ্যা হবে।
__LINE__ আর __FILE__-এর আসল জাদু দেখা যায় macro-র ভেতরে ব্যবহার করলে। Macro খোলা হয় যেখানে ডাকা হয়েছে সেখানে, তাই __LINE__ বসায় ডাকার জায়গার লাইন নম্বর — একটি ফাংশন কখনো এটি পারত না। নিচের প্রোগ্রামে একটি ছোট LOG macro আছে, যা শুধু debug build-এ কাজ করে:
#include <stdio.h>
#define DEBUG /* delete this line for a release build */
#ifdef DEBUG
#define LOG(msg) printf("[%s:%d %s] %s\n", __FILE__, __LINE__, __func__, msg)
#else
#define LOG(msg) do { } while (0)
#endif
#define SHOW(expr) printf(#expr " = %d\n", (expr))
static int average(int total, int count) {
LOG("average() called");
if (count == 0) { LOG("count is zero, returning 0"); return 0; }
return total / count;
}
int main(void) {
SHOW(average(250, 5));
SHOW(average(10, 0));
return 0;
}আউটপুটে প্রতিটি log-এর সামনে ফাইলের নাম, লাইন নম্বর আর ফাংশনের নাম আসবে, যেমন [prog.c:13 average] average() called. অনলাইন compiler ফাইলটির নাম দেয় prog.c; নিজের কম্পিউটারে দেখবেন আপনার ফাইলের নাম। লক্ষ করুন, লাইন নম্বর ১৩ আর ১৪ — LOG যেখানে ডাকা হয়েছে সেখানকার, সংজ্ঞার লাইন ৬ নয়। এবার তৃতীয় লাইনটি মুছে আবার চালান: সব log উধাও, শুধু ফল থাকে। SHOW-এর #expr অংশটি argument-এর লেখাকে হুবহু string বানায় — সেটি অংশ ৮-এ। (বাস্তব প্রোগ্রামে log সাধারণত stderr-এ পাঠানো হয়; এখানে আউটপুটের ক্রম সহজ রাখতে printf ব্যবহার করেছি।)
assert — “এটা কখনো মিথ্যা হওয়ার কথা নয়”
Standard library-র <assert.h>-এ আছে assert, যা আসলে এই কৌশলেরই একটি পেশাদার রূপ। assert(count > 0); লিখলে প্রোগ্রাম চলার সময় শর্তটি যাচাই হয়। মিথ্যা হলে প্রোগ্রাম সঙ্গে সঙ্গে থামে, আর stderr-এ এমন কিছু ছাপে:
app: result.c:27: average: Assertion `count > 0' failed.
Aborted
শর্তের হুবহু লেখা, ফাইল, লাইন, ফাংশন — সবই এসেছে #, __FILE__, __LINE__ আর __func__ থেকে। আর release build-এ -DNDEBUG দিলে NDEBUG সংজ্ঞায়িত হয়, তখন প্রতিটি assert খুলে কিছুই হয় না — সম্পূর্ণ বাদ, কোনো খরচ নেই। CMake-এর মতো build system-এর Release মোড সাধারণত নিজে থেকেই -DNDEBUG যোগ করে।
ঠিক এই কারণে assert-এর ভেতরে কখনো আসল কাজ রাখবেন না। assert(fclose(fp) == 0); debug build-এ ঠিকঠাক চলবে, কিন্তু release build-এ পুরো লাইনটি মুছে যাবে — ফাইল আর বন্ধই হবে না। আর assert হলো প্রোগ্রামারের ভুল ধরার জন্য (“এই ফাংশনে কখনো NULL আসার কথা নয়”)। ব্যবহারকারীর ভুল ইনপুট বা ফাইল না পাওয়ার মতো স্বাভাবিক সমস্যা সামলাতে হয় সাধারণ if দিয়ে। এই পার্থক্য নিয়ে পুরো আলোচনা পর্ব ৩০-এ, এরর হ্যান্ডলিংয়ে।
৮ · # আর ## — লেখাকে string বানানো, শব্দ জোড়া দেওয়া
Macro-র ভেতরে দুটি বিশেষ অপারেটর আছে, যা শুধু preprocessor বোঝে। দুটোই “লেখা নিয়ে কাজ” — আর ঠিক এ ধরনের কাজ ফাংশন দিয়ে করা অসম্ভব।
# — stringize
Argument-এর লেখাটিকে হুবহু একটি string-এ বদলায়। SHOW(average(250, 5))-এ #expr হয়ে যায় "average(250, 5)". পাশাপাশি দুটি string C নিজেই জুড়ে দেয়, তাই #expr " = %d\n" একটি format string হয়ে যায়।
## — token pasting
দুই পাশের দুটি শব্দকে জোড়া দিয়ে একটি নতুন নাম বানায়। #define COUNTER(name) static int count_##name = 0 লিখে COUNTER(login); দিলে তৈরি হয় static int count_login = 0;
একটি বাস্তব উদাহরণ: একটি দোকানের হিসাবের প্রোগ্রামে অর্ডারের অবস্থা enum-এ রাখা আছে — PENDING, PAID, DELIVERED. Log-এ সংখ্যা 1 না দেখিয়ে নাম "PAID" দেখাতে চাইলে প্রতিটির জন্য হাতে string লিখতে হয়, আর নতুন অবস্থা যোগ করলে দুই জায়গায় বদলাতে হয়। # দিয়ে একটি তালিকা থেকেই enum আর নামের string — দুটোই তৈরি করা যায়, ফলে দুটো কখনো অমিল হয় না। এই কৌশলকে বলে “X-macro”; বড় C প্রজেক্টে বহুল ব্যবহৃত।
একটি সূক্ষ্ম নিয়ম: # বা ##-এর পাশে বসা argument আগে macro হিসেবে খোলা হয় না। তাই #define STR(x) #x দিয়ে STR(__LINE__) লিখলে পাবেন "__LINE__", লাইন নম্বর নয়। সমাধান হলো দুই ধাপের macro (#define XSTR(x) STR(x)) — প্রথম ধাপে argument খুলে যায়, দ্বিতীয় ধাপে string হয়। এখনই মুখস্থ করার দরকার নেই; শুধু জেনে রাখুন, এমন অদ্ভুত আচরণ দেখলে কারণ এটাই।
৯ · AI-এর যুগে — macro কোডে AI যেসব ভুল করে
Macro এমন একটি জায়গা যেখানে AI-এর লেখা কোড “দেখতে ঠিক” লাগে, কিন্তু বিপদ লুকিয়ে থাকে লেখা খোলার পরে। কারণ AI কোডটি একটি লেখা হিসেবে দেখে — আপনার build কমান্ডে কোন -D flag আছে, কোন header কোন পথে দুবার ঢুকছে, তা সে জানে না। পুরোনো টিউটোরিয়ালে ভরা ইন্টারনেট থেকে শেখায় AI প্রায়ই সেই যুগের অভ্যাসও নিয়ে আসে।
| AI-এর ভুল | কী হয় | কীভাবে ধরবেন |
|---|---|---|
বন্ধনী ছাড়া macro: #define AREA(r) 3.14 * r * r | AREA(x + 1) ভুল ফল দেয়, কোনো warning ছাড়াই | প্রতিটি parameter আর পুরো সংজ্ঞা বন্ধনীতে আছে কি না দেখুন |
MAX(a[i++], b) বা SQUARE(rand())-এর মতো ডাক | Double evaluation — মান দুবার বাড়ে বা দুবার আলাদা random সংখ্যা | Macro-র argument-এ ++, --, ফাংশন-ডাক খুঁজুন |
| ২০-৩০ লাইনের বিশাল macro “ফাংশন” | Error message অবোধ্য, debugger অচল, পড়া কঠিন | AI-কে বলুন static inline ফাংশনে বদলাতে |
| Header-এ include guard নেই | প্রজেক্ট বড় হলে হঠাৎ redefinition error | প্রতিটি .h-এর প্রথম ও শেষ লাইন দেখুন |
কয়েক লাইনের macro { } দিয়ে, do-while(0) ছাড়া | if/else-এর ভেতরে compile error বা নিঃশব্দ বাগ | \ দিয়ে লেখা প্রতিটি macro-র মোড়ক দেখুন |
ধ্রুবক #define-এ, শেষে ; সহ | অদ্ভুত জায়গায় error | Macro-র শেষে ; থাকলে মুছে দিন, সম্ভব হলে enum ব্যবহার করুন |
AI-এর লেখা কোডে macro দেখলে নিচের prompt ব্যবহার করুন। শেষ অনুরোধটি গুরুত্বপূর্ণ — AI-কে দিয়ে expansion হাতে লিখিয়ে নিলে সে নিজেই প্রায়ই নিজের ভুল ধরে ফেলে, আর আপনি সেটি gcc -E দিয়ে মিলিয়ে নিতে পারেন।
Macro-ওয়ালা কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এখানে ভাষার নকশার পেছনের সিদ্ধান্তগুলো দেখব।
প্র ০১C কেন compiler-এর ভেতরেই এসব না রেখে আলাদা একটি “লেখা-বদলের” ধাপ রাখল?
উত্তরটা লুকিয়ে আছে ১৯৭০-এর দশকের বাস্তবতায়। C তৈরি হচ্ছিল খুব সীমিত মেমরির কম্পিউটারে, যেখানে একটি বড় compiler পুরোটা একসাথে মেমরিতে রাখাও কঠিন ছিল। তাই কাজ ছোট ছোট আলাদা প্রোগ্রামে ভাগ করা ছিল স্বাভাবিক — Unix-এর দর্শনই ছিল “একটি প্রোগ্রাম, একটি কাজ, ভালোভাবে”.
- আলাদা ধাপ মানে সরল compiler: ফাইল জোড়া দেওয়া আর নাম বদলানোর কাজ একটি সরল লেখা-প্রক্রিয়াকারীকে দিলে compiler-কে শুধু খাঁটি C নিয়েই ভাবতে হয়। Dennis Ritchie নিজেও লিখেছেন, preprocessor শুরুতে ছিল ভাষার বাইরের একটি ঐচ্ছিক সংযোজন —
#includeআর সাধারণ#defineনিয়ে, অনেকটা অন্য ভাষার file-inclusion সুবিধার আদলে। - ভাষা না বদলে ক্ষমতা বাড়ানো: Platform-ভেদে আলাদা কোড, ধ্রুবকের নাম, debug সুবিধা — এগুলো ভাষার ব্যাকরণে না ঢুকিয়েই পাওয়া গেল। পরে নতুন চাহিদা এলে preprocessor-এ নতুন directive যোগ করলেই চলত।
- দাম: যেহেতু preprocessor C বোঝে না, তাই টাইপ, scope, precedence — কোনো নিয়মই তাকে বাঁধে না। আজকের পর্বের প্রতিটি ফাঁদ এই নকশারই প্রত্যক্ষ ফল।
পরে const, enum, inline এসে preprocessor-এর অনেক কাজ নিরাপদভাবে compiler-এর ভেতরে নিয়ে গেছে। কিন্তু পুরোনো কোটি কোটি লাইন কোড চালু রাখতে হয় বলে preprocessor সরানো আর সম্ভব নয় — একটি ভাষার নকশা-সিদ্ধান্ত প্রায় চিরকাল থেকে যায়।
মূল উপলব্ধি: সীমিত সম্পদে সরল ও আলাদা ধাপ একটি বুদ্ধিমান সিদ্ধান্ত ছিল; কিন্তু সেই সরলতার দাম দিতে হয় দশকের পর দশক। প্রকৌশলে আজকের সুবিধাজনক সমাধান কাল দায় হয়ে উঠতে পারে — তাই নকশার সময়েই ভাবতে হয় সেটি কত দিন টিকবে।
প্র ০২Java, Go-এর মতো আধুনিক ভাষা macro বাদ দিল, আর Rust রাখল ভিন্ন রূপে। কেন?
C-এর macro-র মূল সমস্যা হলো তা “অন্ধ” — কোডের গঠন না বুঝে লেখা বদলায়। পরের প্রজন্মের ভাষাগুলো এর দুই রকম উত্তর দিয়েছে।
- পুরোপুরি বাদ (Java, Go): এদের নকশাকাররা মনে করেছেন, macro কোডকে এমন রূপ দেয় যা পড়ে বোঝা যায় না —
SQUARE(x)দেখে বোঝার উপায় নেই এটি ফাংশন নাকি লেখা-বদল। তাই ধ্রুবকের জন্য টাইপসহconst, ছোট ফাংশনের জন্য compiler-এর নিজস্ব inlining, আর platform-ভেদের জন্য আলাদা ফাইল (Go-তে_windows.go,_linux.goনামের ফাইল) দেওয়া হয়েছে। - নিরাপদ macro (Rust, Lisp পরিবার): এসব ভাষার macro লেখা নয়, কোডের গঠন নিয়ে কাজ করে। Rust-এর
macro_rules!argument-কে একটি সম্পূর্ণ expression হিসেবে দেখে, তাইa + 1-এর চারপাশে আলাদা বন্ধনী লাগে না। আর macro-র ভেতরে বানানো ভ্যারিয়েবল বাইরের একই নামের ভ্যারিয়েবলের সাথে গুলিয়ে যায় না — এই গুণকে বলে “hygiene”. Scheme ভাষায় এই ধারণাটি অনেক আগেই প্রতিষ্ঠিত হয়েছিল। - অন্য পথ (C++, Zig): C++ preprocessor রেখেছে C-এর সাথে মিল রাখতে, কিন্তু template,
constexprআরinlineদিয়ে বেশিরভাগ কাজের নিরাপদ বিকল্প দিয়েছে। Zig compile-এর সময়েই সাধারণ কোড চালানোর ব্যবস্থা (comptime) রেখে macro-র প্রয়োজনই কমিয়েছে।
লক্ষ করুন, কোনো ভাষাই macro-র চাহিদা অস্বীকার করেনি — platform বাছাই, কোড তৈরি, compile-এর সময় হিসাব লাগবেই। পার্থক্য শুধু সেই ক্ষমতা কতটা নিরাপদ মোড়কে দেওয়া হয়েছে।
মূল উপলব্ধি: একটি শক্তিশালী কিন্তু বিপজ্জনক সুবিধার মুখোমুখি হলে ভাষার নকশাকাররা হয় সেটি সরিয়ে বিকল্প দেন, নয়তো এমন মোড়ক দেন যাতে ভুল করা কঠিন হয়। AI-এর যুগে দ্বিতীয় পথটি আরও মূল্যবান — যে কোড ভুলভাবে লেখাই কঠিন, তা AI-ও ভুলভাবে কম লেখে।
প্র ০৩বড় C/C++ প্রজেক্ট build হতে এত সময় লাগে কেন, আর header-এর সাথে এর সম্পর্ক কী?
মনে করুন, একটি প্রজেক্টে ১,০০০টি .c ফাইল, আর প্রায় সবাই একই বড় header include করে — যার ভেতরে আরও header, সব মিলিয়ে হাজার হাজার লাইন। প্রতিটি .c একটি আলাদা translation unit, আর include guard শুধু একটি unit-এর ভেতরে পুনরাবৃত্তি আটকায়। তাই সেই একই হাজার হাজার লাইন ১,০০০ বার পড়া, preprocess আর parse হয়। অনেক বড় প্রজেক্টে compiler-এর সময়ের বড় অংশ যায় আপনার কোডে নয়, header-এ।
- ঢেউয়ের মতো rebuild: একটি জনপ্রিয় header-এ একটি comment বদলালেও, যে সব ফাইল সেটি সরাসরি বা ঘুরিয়ে include করে, সবাইকে নতুন করে compile হতে হয়। Build system জানে না পরিবর্তনটি অর্থহীন ছিল।
- প্রচলিত প্রতিকার: শুধু যা লাগে তাই include করা (“include what you use”); header-এ পুরো struct-এর বদলে শুধু নাম ঘোষণা (
struct Student;) দেওয়া, যখন শুধু pointer লাগে; আর precompiled header — যেখানে বড় header একবার প্রক্রিয়া করে ফলটি সংরক্ষণ করা হয়। - মূল সমাধানের চেষ্টা — C++20 modules: Module একবার compile হয়ে একটি প্রস্তুত, বাইনারি রূপে থাকে। যে ফাইল সেটি
importকরে, সে লেখা কপি না করে সেই প্রস্তুত তথ্য পড়ে। আর macro module-এর বাইরে ছড়িয়ে পড়ে না, ফলে এক header-এর#defineঅন্যের কোড নষ্ট করতে পারে না। C ভাষায় এখনো modules নেই, তবে C++-এর অভিজ্ঞতা থেকে বোঝা যায় সমস্যাটি কতটা গুরুতর।
মূল উপলব্ধি: “কপি করে বসাও” — একটি সরল নকশা ছোট প্রজেক্টে নিখুঁত, কিন্তু আকার বাড়লে খরচ গুণিতক হারে বাড়ে। সিস্টেম নকশায় সবসময় জিজ্ঞেস করুন: এটি ১০ গুণ, ১,০০০ গুণ বড় হলে কী হবে?
প্র ০৪এত সমালোচনার পরও কোথায় macro-ই সবচেয়ে সঠিক হাতিয়ার?
Macro-র সমালোচনা মানে এই নয় যে এটি অচল। কিছু কাজ আছে যা C-তে শুধু preprocessor পারে, কারণ সেগুলো compile-এর আগের বা কোডের লেখা নিয়ে কাজ।
- Platform ও build-ভেদ: Windows-এ এক API, Linux-এ আরেক —
#ifdef _WIN32ছাড়া উপায় নেই। Debug build-এ বাড়তি যাচাই, release-এ শূন্য খরচ — এটিও শুধু conditional compilation পারে। - ডাকার জায়গার তথ্য:
__FILE__,__LINE__সেই জায়গার মান দেয় যেখানে লেখা আছে। একটি ফাংশন নিজের লাইন জানে, ডাকার লাইন নয়। তাইassertআর logging সিস্টেম প্রায় সবসময় macro-র ওপর দাঁড়ানো। - কোডের লেখা থেকে string:
#exprদিয়ে শর্তের হুবহু লেখা দেখানো — ফাংশন সেটি কখনো জানতে পারে না। - পুনরাবৃত্তি দূর করা: X-macro দিয়ে একটি তালিকা থেকে enum, নামের string আর টেবিল — একসাথে তৈরি, যাতে কোনোটি অমিল না হয়।
- ছোট, প্রমাণিত সহায়ক: যেমন
#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))— যেকোনো টাইপের array-র উপাদান-সংখ্যা। (সাবধান: pointer দিলে ভুল ফল দেয়, পর্ব ১৮-এর array decay মনে করুন।)
লক্ষ করুন, তালিকার প্রায় সবকিছুই হয় compile-এর আগের সিদ্ধান্ত, নয়তো কোডের লেখা নিয়ে কাজ। সাধারণ হিসাব-নিকাশ এখানে নেই — সেটি ফাংশনের কাজ।
মূল উপলব্ধি: ভালো প্রকৌশলী কোনো টুলকে “ভালো” বা “খারাপ” বলেন না; তিনি জানেন কোন টুল কোন সমস্যার জন্য একমাত্র সমাধান, আর কোথায় তা অপ্রয়োজনীয় ঝুঁকি। Macro ব্যবহারের আগে নিজেকে জিজ্ঞেস করুন: “এটা কি ফাংশন, enum বা const দিয়ে করা যায়?” উত্তর “হ্যাঁ” হলে সেটাই করুন।
প্র ০৫Macro থাকলে debugging আর code review — মানুষ বা AI, দুজনের জন্যই কেন কঠিন হয়?
মূল কারণ: আপনি যে কোড পড়ছেন আর compiler যে কোড পাচ্ছে, দুটি আলাদা। এই ফাঁকের মধ্যেই সব সমস্যা।
- Debugger-এর অন্ধত্ব: Debugger compile হওয়া কোড দেখে।
MAX_STUDENTSনামটি সেখানে নেই, শুধু60. একটি বড় macro এক লাইনে খোলে, তাই সেটির ভেতরে ধাপে ধাপে চলা যায় না। (GCC-তে-g3দিলে macro-র সংজ্ঞা debug তথ্যে রাখা হয়, আর GDB-তেmacro expandদিয়ে দেখা যায় — কিন্তু খুব কম মানুষ এটি জানেন।) - বিভ্রান্তিকর error: Error আসে খোলা লেখার ভেতর থেকে। আধুনিক GCC ও Clang “in expansion of macro …” লিখে সাহায্য করে, তবু লম্বা macro-তে বার্তাটি প্রায়ই অবোধ্য।
- অদৃশ্য কোড:
#ifdef-এর যে শাখা আপনার build-এ বাদ পড়ে, সেটি compiler কখনো যাচাই করে না। Reviewer যে কোড পড়ছেন, তার কতটুকু আসলে চলবে — তা নির্ভর করে build কমান্ডের-Dflag-এর ওপর, যা কোডে লেখা নেই। - AI-এর বিশেষ সমস্যা: AI সাধারণত একটি বা কয়েকটি ফাইল দেখে। Macro-র সংজ্ঞা অন্য header-এ থাকলে, বা build system কোন flag দেয় তা না জানলে, AI ধরে নেয় — আর সেই ধারণা ভুল হতে পারে।
MAX(a, b)দেখে সে ফাংশন ভেবেMAX(i++, j)লিখে ফেলতে পারে।
প্রতিকার: সন্দেহ হলে gcc -E দিয়ে আসল লেখা দেখুন, আর AI-কে সেই খোলা কোডটিই দিন; macro যত সম্ভব কম ও ছোট রাখুন; CI-তে সব build ধরনে compile করুন।
মূল উপলব্ধি: যে কোড পড়া আর যে কোড চলে — দুটো যত কাছাকাছি, যাচাই তত সহজ। Macro এই দূরত্ব বাড়ায়। AI-এর যুগে যখন বেশিরভাগ কোড অন্য কেউ (বা কিছু) লিখছে, তখন “যা দেখছি তাই চলছে” — এই নিশ্চয়তা আগের চেয়ে অনেক বেশি মূল্যবান।
অনুশীলন
বেশিরভাগ কাজ হাতে-কলমে হিসাব করা আর preprocessor-এর ফল চোখে দেখা।
-
Preprocessor-এর কাজ দেখুন: উপরের
macro_trap.cকোডটি নিজের কম্পিউটারে রেখে চালানgcc -E macro_trap.c | tail -n 15.main-এর ভেতরেSQUARE_BAD,SQUARE,MAXনামগুলো খুঁজে পান কি?পাবেন না। কোনো macro-র নামই থাকে না — তাদের জায়গায় আছে খোলা লেখা:
a + 1 * a + 1,((a + 1) * (a + 1))আর((i++) > (0) ? (i++) : (0)).#defineলাইনগুলোও উধাও। এটাই প্রমাণ যে compiler macro সম্পর্কে কিছুই জানে না। আরও লক্ষ করুন,max_intফাংশনটি নামসহ রয়ে গেছে — সেটি আসল C কোড। Windows-এtailনা থাকলে আউটপুট একটি ফাইলে রেখে (gcc -E macro_trap.c -o out.i) ফাইলের শেষ অংশ দেখুন। -
হাতে expansion করুন:
#define HALF(x) x / 2আর#define DOUBLE(x) (x) + (x).int n = 10;হলে (ক)HALF(n + 4)(খ)3 * DOUBLE(n)(গ)HALF(DOUBLE(n))— প্রতিটির মান কত? আগে খাতায় হিসাব করুন।(ক)
n + 4 / 2= ১০ + ২ = ১২ (আশা ছিল ৭)।(খ)
3 * (n) + (n)= ৩০ + ১০ = ৪০ (আশা ছিল ৬০) — ভেতরের বন্ধনী আছে, কিন্তু বাইরের বন্ধনী নেই।(গ) ভেতর থেকে খোলে:
(n) + (n) / 2= ১০ + ৫ = ১৫ (আশা ছিল ১০)।তিনটিতেই কোনো warning আসবে না। সঠিক রূপ:
#define HALF(x) ((x) / 2)আর#define DOUBLE(x) ((x) + (x))— অথবা আরও ভালো, দুটিstatic inlineফাংশন। -
Header গোয়েন্দা: AI আপনাকে
config.hদিল, যাতে আছে#define BUFFER 256;,struct Config { int port; };আরint request_count = 0;. Include guard নেই। এতে কয়টি সমস্যা দেখছেন, আর কোনটি guard দিলেও থেকে যাবে?তিনটি সমস্যা। (১)
BUFFER-এর শেষে;—char buf[BUFFER];হয়ে যাবেchar buf[256;];, অদ্ভুত error. (২) Guard না থাকায় দুই পথে include হলেstruct Config-এর redefinition error. (৩) Header-এ ভ্যারিয়েবলের সংজ্ঞাint request_count = 0;— দুটি.cফাইল এটি include করলে linkermultiple definitionবলবে।Guard দিলে দ্বিতীয়টি ঠিক হয়, কিন্তু তৃতীয়টি থেকে যায়, কারণ guard শুধু একটি translation unit-এর ভেতরে কাজ করে। সমাধান: header-এ
extern int request_count;(শুধু ঘোষণা), আর একটি.cফাইলে আসল সংজ্ঞা — পর্ব ২৯-এ বিস্তারিত। -
Macro থেকে ফাংশনে (কোড লিখুন): নিচের macro-টিকে একটি
static inlineফাংশনে বদলান, তারপরCLAMP(i++, 0, 100)আর আপনার ফাংশনের ফল তুলনা করুন:#define CLAMP(v, lo, hi) ((v) < (lo) ? (lo) : (v) > (hi) ? (hi) : (v))একটি সম্ভাব্য সমাধান:
static inline int clamp_int(int v, int lo, int hi) { if (v < lo) return lo; if (v > hi) return hi; return v; }i = 50হলে macro-তেvতিনবার পর্যন্ত লেখা আছে: প্রথম তুলনায়i৫১, দ্বিতীয় তুলনায় ৫২, আর ফল নিতে গিয়ে আরেকবার — ফল ৫২,iহয়ে যায় ৫৩. ফাংশনে ফল ৫০,i৫১. Macro-তে একটি argument যত বেশিবার ব্যবহার হয়, double (এখানে triple!) evaluation-এর ঝুঁকি তত বেশি।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকই header দুবার include হলে guard-এর কাজ ঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- প্রথম
#include "date.h"— preprocessor#ifndef DATE_Hযাচাই করে DATE_Hসংজ্ঞায়িত নেই, তাই ভেতরে ঢোকে#define DATE_H— নামটি এখন সংজ্ঞায়িতstruct Datetranslation unit-এ বসে- দ্বিতীয়
#include "date.h"—#ifndefমিথ্যা,#endifপর্যন্ত সব বাদ
Preprocessor সম্পর্কে কোন কথাটি সঠিক?
- সে ভ্যারিয়েবলের টাইপ দেখে macro খোলে
- সে compiler-এর আগে চলে, আর শুধু লেখা বদলায়
- সে প্রোগ্রাম চলার সময় macro খোলে
- ফাংশনের ভেতরে লেখা #define শুধু সেই ফাংশনেই কাজ করে
#define DOUBLE(x) x + x হলে 3 * DOUBLE(2)-এর মান কত?
- ১২
- ১০
- ৮
- ৬
Header-এর শুরুতে #ifndef STUDENT_H / #define STUDENT_H আর শেষে #endif লেখার উদ্দেশ্য কী?
- একই .c ফাইলে header দুবার ঢুকে redefinition error হওয়া আটকানো
- সব .c ফাইল মিলিয়ে linker-এর multiple definition error আটকানো
- প্রোগ্রামকে দ্রুত চালানো
- Header ফাইল না পেলে error লুকানো
#define MAX(a, b) ((a) > (b) ? (a) : (b)) — বন্ধনী সবই আছে। তবু MAX(n++, 10) লেখা বিপজ্জনক কেন?
- Precedence-এর কারণে তুলনা ভুল হয়
- n++ int নয়, তাই টাইপ মেলে না
- Compiler এটি compile করতে দেয় না
- n++ দুবার চলতে পারে, ফলে n ও ফল দুটোই ভুল হয়
কোডে #ifdef DEBUG … #endif-এর ভেতরে কয়েকটি printf আছে। gcc app.c (কোনো -D ছাড়া) দিয়ে build করলে কী হয়?
- printf-গুলো প্রোগ্রামে থাকে, কিন্তু চলার সময় এড়িয়ে যায়
- ওই অংশটি compile-এর আগেই মুছে যায়, প্রোগ্রামে থাকেই না
- DEBUG সংজ্ঞায়িত নেই বলে compile error হয়
- printf-গুলো সবসময় চলে
Release build-এ -DNDEBUG দেওয়া হলো। assert(save_file(db) == 0); লাইনটির কী হবে?
- ফাইল সংরক্ষণ ব্যর্থ হলে প্রোগ্রাম থামবে
- শুধু যাচাই বাদ পড়বে, save_file তবু ডাকা হবে
- পুরো লাইনটি বাদ পড়বে — save_file ডাকাই হবে না
- Compile error হবে
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Preprocessor compiler-এর আগে চলে আর শুধু লেখা বদলায় — টাইপ, scope, precedence কিছুই বোঝে না।
- প্রতিটি header-এ include guard (বা
#pragma once); guard শুধু একটি translation unit-এর ভেতরে কাজ করে। - Function-like macro-তে দুই স্তরে বন্ধনী, argument-এ কখনো side effect নয়, কয়েক লাইনের হলে
do { } while (0). - ধ্রুবকে
enum/const, ছোট হিসাবেstatic inline; macro শুধু যেখানে অন্য কিছু পারে না — platform বাছাই, debug log,__FILE__/__LINE__. assertপ্রোগ্রামারের ভুল ধরে,NDEBUG-এ উধাও হয়; সন্দেহ হলেgcc -Eদিয়ে আসল লেখা দেখুন।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ২৮ · ফাংশন পয়েন্টার ও callbackপরবর্তী পর্বফাংশনকে ডেটার মতো পাঠানো — qsort, event handler আর plugin-এর ভিত্তি।
- C প্রোগ্রামিং · কোর্স হোম৫৬টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- Programming Languages & Compiler Designগভীরে যানLexer, token আর parsing — preprocessor ও compiler ভেতরে কীভাবে লেখা পড়ে।
- Software Engineering & Gitপাশের ট্র্যাকBuild system, CI আর code review — বড় প্রজেক্টে debug/release build কীভাবে সামলানো হয়।