এই পর্বে যা শিখবেন
- পর্ব ২৩-এর দোকানের খাতাকে
ledger.h,ledger.cআরmain.c-এ ভাগ করা — কোন জিনিস কোথায় যাবে - Header থেকে linker পর্যন্ত পুরো পথ — compiler কী দেখে, linker কী মেলায়
- Opaque pointer দিয়ে information hiding — কেন
FILE *-এর ভেতরটা আপনি কখনো দেখেন না - একটি ছোট কিন্তু আসল Makefile লেখা ও পড়া, আর Make কীভাবে ঠিক করে কী আবার বানাতে হবে
- Static ও shared library বানানো, ABI কী, আর পেশাদার প্রজেক্টের ফোল্ডার সাজানো
১ · এক ফাইলে তিন হাজার লাইন — কেন আর চলে না
পর্ব ২৩-এ নাদিয়া রহিম চাচার দোকানের জন্য একটি খাতা-প্রোগ্রাম বানিয়েছিলেন — একটিমাত্র ফাইল, প্রায় ২২০ লাইন। চাচা খুশি। খবর ছড়াল, পাশের দুটি দোকানও চাইল। এবার নতুন চাহিদা এলো একের পর এক: মাসিক রিপোর্ট, বাকির তালিকা, ফাইলে সেভ (পর্ব ২৪), দিনের শেষে SMS. কাজ ভাগ করতে নাদিয়ার বন্ধু রাফি রিপোর্টের অংশটি লেখার দায়িত্ব নিলেন।
ছয় মাস পর shop.c ফাইলটি দাঁড়াল প্রায় তিন হাজার লাইনে। তখন যা যা ঘটতে লাগল:
- খুঁজে পাওয়া যায় না। “মোট হিসাবের ফাংশনটা কোথায়?” — প্রতিবার স্ক্রল করে বা সার্চ করে খুঁজতে হয়।
- দুজন একসাথে কাজ করতে পারেন না। নাদিয়া আর রাফি একই ফাইল বদলান, তাই git-এ প্রায় প্রতিদিন দুজনের পরিবর্তনে সংঘাত (conflict) হয়।
- একটি লাইন বদলালেও পুরো ফাইল আবার compile হয়। পর্ব ০৩-এর separate compilation-এর সুবিধা পুরোপুরি হারিয়ে গেছে।
- সবাই সবকিছু ছুঁতে পারে। রাফির রিপোর্ট-কোড সরাসরি
lg.countবদলে দিল, আর নাদিয়ার হিসাব ভেঙে গেল — কেউ টেরও পেল না কোথা থেকে। - আবার ব্যবহার করা যায় না। খাতার যুক্তিটুকু একটি মোবাইল অ্যাপে লাগাতে চাইলে পুরো ফাইল থেকে কেটে আলাদা করতে হবে।
একটি বাড়ির কথা ভাবুন। রান্নাঘর, শোবার ঘর, বসার ঘর — প্রতিটি আলাদা ঘর, প্রতিটির একটি দরজা। মেহমান আসেন বসার ঘরে; রান্নাঘরের কোন তাকে লবণ আছে তা তাঁর জানার দরকার নেই। আপনি রান্নাঘরের তাক নতুন করে সাজালেও মেহমান টেরই পাবেন না। এক ফাইলের তিন হাজার লাইন যেন দেয়ালহীন একটি বাড়ি — চুলা, বিছানা, সোফা সব এক হলঘরে; পাঁচজন থাকতে শুরু করলেই বিশৃঙ্খলা।
সমাধান হলো প্রোগ্রামকে module-এ ভাগ করা। প্রতিটি মডিউল একটি নির্দিষ্ট দায়িত্ব নেয়, আর তার দুটি অংশ থাকে: বাইরের দরজা, অর্থাৎ interface — অন্যরা কী ব্যবহার করতে পারবে; আর ঘরের ভেতর, অর্থাৎ implementation — কাজটা আসলে কীভাবে হয়। পর্ব ২৩-এ শেখা separation of concerns এতদিন ছিল ফাংশনের স্তরে; এবার সেটি উঠে আসছে ফাইলের স্তরে।
C-তে একটি মডিউল সাধারণত দুটি ফাইলের জোড়া: ledger.h = interface (দরজা — কোন টাইপ আর কোন ফাংশন পাওয়া যাবে) আর ledger.c = implementation (ঘরের ভেতর — ফাংশনগুলোর আসল কোড)। যে মডিউল খাতা ব্যবহার করবে, সে শুধু #include "ledger.h" লেখে; ledger.c সে কখনো দেখে না।
খুঁজে পাওয়া সহজ
মোটের হিসাবে ভুল? সোজা ledger.c খুলুন। ফাইলের নামই ঠিকানা।
একসাথে কাজ
নাদিয়া ledger.c, রাফি report.c — আলাদা ফাইল, তাই সংঘাত কম।
দ্রুত build
একটি ফাইল বদলালে শুধু সেটিই আবার compile হয়, বাকিরা আগের .o নিয়ে বসে থাকে।
টেস্ট ও পুনর্ব্যবহার
খাতা-মডিউলকে আলাদা করে টেস্ট করা যায়, অন্য প্রোগ্রামেও হুবহু লাগানো যায়।
মান ও গতি২ · খাতাকে তিন ফাইলে ভাগ — ledger.h, ledger.c, main.c
পর্ব ২৩-এ নাদিয়া প্রোগ্রামটিকে তিন স্তরে ভেবেছিলেন: UI (মেনু, প্রশ্ন, ছাপা), logic (ledger_ ফাংশনগুলো) আর ডেটা। সেই নকশাই এখন ফাইলের ভাগ ঠিক করে দেয় — আর logic স্তরে কোনো printf/scanf নেই বলেই ভাগটা এত পরিষ্কার।
প্রথম ফাইল — interface. এখানে আছে শুধু সেটুকু, যা অন্য ফাইলের জানা দরকার:
/* ledger.h - public interface of the ledger module */
#ifndef LEDGER_H
#define LEDGER_H
#include <stddef.h> /* for size_t */
typedef enum { PAY_CASH, PAY_BKASH, PAY_DUE } PayMethod;
typedef struct {
char item[32];
int qty;
long price; /* paisa: 72.50 Tk = 7250 */
PayMethod pay;
} Sale;
typedef struct {
Sale *items;
size_t count, capacity;
} Ledger;
void ledger_init(Ledger *lg);
int ledger_add(Ledger *lg, Sale s); /* 0 = ok, -1 = no memory */
long ledger_total(const Ledger *lg, int pay); /* pay = -1 means all */
void ledger_free(Ledger *lg);
#endif /* LEDGER_H */দ্বিতীয় ফাইল — implementation. ফাংশনগুলোর আসল কোড, আর একটি নিজস্ব helper, যাকে বাইরের কেউ দেখতে পাবে না:
/* ledger.c - how the ledger actually works */
#include <stdlib.h>
#include "ledger.h" /* own header: compiler checks they match */
static int grow(Ledger *lg) /* private: invisible outside this file */
{
size_t ncap = lg->capacity ? 2 * lg->capacity : 2;
Sale *t = realloc(lg->items, ncap * sizeof *t);
if (t == NULL) return -1;
lg->items = t;
lg->capacity = ncap;
return 0;
}
void ledger_init(Ledger *lg) { lg->items = NULL; lg->count = lg->capacity = 0; }
int ledger_add(Ledger *lg, Sale s)
{
if (lg->count == lg->capacity && grow(lg) != 0) return -1;
lg->items[lg->count++] = s;
return 0;
}
long ledger_total(const Ledger *lg, int pay)
{
long sum = 0;
for (size_t i = 0; i < lg->count; i++)
if (pay < 0 || lg->items[i].pay == (PayMethod)pay)
sum += lg->items[i].qty * lg->items[i].price;
return sum;
}
void ledger_free(Ledger *lg) { free(lg->items); ledger_init(lg); }তৃতীয় ফাইল — UI. এটি খাতা ব্যবহার করে, কিন্তু realloc বা grow সম্পর্কে কিছুই জানে না:
/* main.c - talks to the user, uses the ledger only through ledger.h */
#include <stdio.h>
#include "ledger.h"
static int ui_menu(void); /* private to main.c */
static void ui_add(Ledger *lg);
static void ui_totals(const Ledger *lg);
int main(void)
{
Ledger lg;
ledger_init(&lg);
int choice;
while ((choice = ui_menu()) != 0) {
if (choice == 1) ui_add(&lg);
else if (choice == 4) ui_totals(&lg);
else puts("No such option.");
}
ledger_free(&lg);
return 0;
}
/* ui_menu, ui_add, ui_totals: same as in lesson 23 */খেয়াল করুন দুটি ছোট বিষয়। #include <stdio.h>-এ কোণ বন্ধনী মানে “system-এর ফোল্ডারে খোঁজো”, আর #include "ledger.h"-এ উদ্ধৃতিচিহ্ন মানে “আগে আমার প্রজেক্টের ফোল্ডারে খোঁজো”। আর ledger.c নিজের header নিজেই include করে — এতে header-এর prototype আর .c-এর সংজ্ঞায় কোনো অমিল থাকলে compiler সঙ্গে সঙ্গে ধরে ফেলে।
এবার build. পর্ব ০৩-এ দেখেছেন, -c মানে “শুধু .o পর্যন্ত যাও, link কোরো না”:
gcc -Wall -Wextra -g -c ledger.c # ledger.c -> ledger.o
gcc -Wall -Wextra -g -c main.c # main.c -> main.o
gcc main.o ledger.o -o shop # linker joins them -> shopকিন্তু main.c compile করার সময় compiler তো ledger.c দেখেইনি — তাহলে ledger_add ডাকাটা সে মানল কীভাবে? আর শেষে linker কীভাবে ঠিক ফাংশনটি খুঁজে পেল? নিচের ডায়াগ্রামে পুরো পথটি ধাপে ধাপে দেখুন:
- main.c
#include "ledger.h"ডাকে ledger_add - Translation unit
main.c + ledger.hpreprocessor জুড়েছে - main.o
U ledger_add“আমার দরকার” - ledger.o
T ledger_add“আমার কাছে আছে” - Linker
shopদুটি মিলিয়ে দিল
#include <stdio.h> #include "ledger.h" int main(void) { Ledger lg; ledger_init(&lg); Sale s = { "Rice 1kg", 2, 7250, PAY_CASH }; ledger_add(&lg, s); ...
/* stdio.h থেকে আনা কয়েকশো লাইন ... */ typedef enum { PAY_CASH, PAY_BKASH, PAY_DUE } PayMethod; typedef struct { ... } Sale; typedef struct { ... } Ledger; void ledger_init(Ledger *lg); int ledger_add(Ledger *lg, Sale s); /* শুধু ঘোষণা, শরীর নেই */ ... int main(void) { ... ledger_add(&lg, s);
U ledger_add U = undefined: দরকার, কিন্তু আমার কাছে নেই U ledger_free U ledger_init T main T = text: এই ফাংশনের কোড আমার ভেতরে আছে t ui_menu ছোট হাতের t = static, শুধু এই ফাইলের
t grow static: linker-এর কাছে অদৃশ্য T ledger_add এই সেই কোড, যা main.o খুঁজছে T ledger_free T ledger_init T ledger_total U free U realloc এগুলো আসবে C library থেকে
$ gcc main.o ledger.o -o shop main.o: U ledger_add ⟷ ledger.o: T ledger_add ✓ ledger.o: U realloc ⟷ libc: realloc ✓ $ ./shop 1) Add 2) List 4) Totals 0) Exit
$ gcc main.o -o shop /usr/bin/ld: main.o: in function `main': main.c:(.text+0x2b): undefined reference to `ledger_init' main.c:(.text+0x5e): undefined reference to `ledger_add' collect2: error: ld returned 1 exit status
- ধাপ ১ — ডাক দেওয়া।
main.cখাতায় একটি বিক্রি যোগ করতেledger_addডাকে। কিন্তু এই ফাংশনের কোড অন্য ফাইলে।main.c-এর হাতে আছে শুধু একটি লাইন:#include "ledger.h". - ধাপ ২ — Header জুড়ে দেওয়া। Preprocessor
ledger.h-এর পুরো লেখা হুবহুmain.c-এর ভেতরে বসিয়ে দেয়। এই জোড়া লাগানো পূর্ণ লেখার নাম translation unit — compiler একবারে ঠিক এটুকুই দেখে, এর বাইরে কিছু নয়। সেখানেledger_add-এর prototype আছে, কিন্তু শরীর নেই। - ধাপ ৩ — Compiler-এর বিশ্বাস। Prototype দেখে compiler যাচাই করে ডাকটি ঠিক আছে কি না — দুটি argument, প্রথমটি
Ledger *, দ্বিতীয়টিSale. তারপর সেmain.o-তে একটি চিরকুট রেখে দেয়: “ledger_addদরকার, পরে কেউ জোগাড় করে দেবে।” Object file-এর এই নামগুলোকে বলে symbol. - ধাপ ৪ — অন্য টুকরো। একেবারে আলাদাভাবে
ledger.ccompile হয়েledger.oহলো। এর ভেতরেledger_add-এর আসল কোড আছে (বড় হাতেরT).growআছে ছোট হাতেরtনিয়ে —staticবলে সে শুধু এই ফাইলের, বাইরের কেউ তাকে জুড়তে পারবে না। - ধাপ ৫ — Linker মেলায়। Linker সব
.o-র “দরকার” তালিকা আর “আছে” তালিকা মিলিয়ে দেখে।main.o-এরU ledger_addমিললledger.o-এরT ledger_add-এর সাথে। সব মিলে গেলে তৈরি হয়shop. - ধাপ ৬ — না মিললে। Link কমান্ডে
ledger.oদিতে ভুলে গেলে “দরকার” তালিকার নামগুলোর জোড়া পাওয়া যায় না — আর আসে পর্ব ০৩-এর পরিচিতundefined reference. Compile ধাপ কিন্তু সফল হয়েছিল, কারণ prototype দেখে compiler সন্তুষ্ট ছিল। Header হলো প্রতিশ্রুতি; প্রতিশ্রুতি রক্ষা হলো কি না, যাচাই করে linker.
.c ফাইল = প্রতিশ্রুতি রক্ষা (linker যাচাই করে)
৩ · Header-এর নিয়ম — কী থাকবে, কী কখনো থাকবে না
একটি header অনেকগুলো .c ফাইলে include হয়। খাতার header ঢুকবে main.c-এ, রাফির report.c-এ, পরে হয়তো sms.c-তেও। তাই নিয়ম একটাই: header-এ এমন কিছু রাখা যাবে না, যা প্রতিটি ফাইলে কপি হলে সমস্যা করে। Declaration (“এই নামে একটি জিনিস আছে”) যত জায়গায় খুশি থাকতে পারে; কিন্তু definition (“এই সেই জিনিস, এর জন্য মেমরি নাও”) পুরো প্রোগ্রামে থাকবে ঠিক একবার।
| জিনিস | কোথায় | কেন |
|---|---|---|
ফাংশনের prototype — int ledger_add(Ledger *lg, Sale s); | ✅ header | শুধু ঘোষণা; বারবার থাকলেও ক্ষতি নেই |
typedef, struct, enum-এর গঠন | ✅ header | টাইপ মেমরি নেয় না, শুধু নকশা |
#define ধ্রুবক — #define MAX_ITEM 32 | ✅ header | লেখা বদলানোর নির্দেশ মাত্র |
extern int ledger_debug; | ✅ header | “আছে, অন্য কোথাও” — ঘোষণা |
| ফাংশনের পূর্ণ definition (শরীরসহ) | ❌ শুধু .c | দুটি ফাইলে ঢুকলে একই ফাংশনের দুটি কপি |
ভ্যারিয়েবল — int ledger_debug = 0; | ❌ শুধু .c | প্রতিটি ফাইলে আলাদা ভ্যারিয়েবল তৈরি হতে চায় |
static helper ও শুধু-নিজের ভ্যারিয়েবল | ❌ শুধু .c | এগুলো ইচ্ছা করেই গোপন — দরজায় টাঙানোর কিছু নয় |
ভুলটি করলে কী হয়? ধরুন রাফি সুবিধার জন্য header-এ লিখে দিলেন int ledger_debug = 0;. main.c আর report.c — দুটিই header include করে, তাই দুটি .o-তেই একটি করে ledger_debug তৈরি হলো। প্রতিটি ফাইল আলাদাভাবে compile হয়, তাই compiler কিছুই টের পায় না। ধরা পড়ে link করার সময়:
/usr/bin/ld: report.o:(.bss+0x0): multiple definition of `ledger_debug'; main.o:(.bss+0x0): first defined here
collect2: error: ld returned 1 exit status
ইচ্ছা করে ভাগ করা global — extern
পর্ব ১৩-এ দেখেছেন, global ভ্যারিয়েবল যত কম, তত ভালো। তবু কখনো সত্যিই একটি মান সব মডিউলের দেখা দরকার — যেমন “debug বার্তা ছাপব কি না”। সঠিক নিয়ম: header-এ extern দিয়ে ঘোষণা, আর ঠিক একটি .c ফাইলে আসল সংজ্ঞা।
/* in ledger.h : a promise, no memory */
extern int ledger_debug;
/* in ledger.c : the one and only real variable */
int ledger_debug = 0;ফাইলের নিজস্ব জিনিস — static
উল্টো দিকে, grow-এর মতো helper বা ui_menu-এর মতো ফাংশন অন্য কারও জানার দরকার নেই। পর্ব ১৩-এর static ও extern অংশে দেখেছেন, ফাংশনের বাইরে static লিখলে নামটি শুধু সেই ফাইলের হয়ে যায় — একে বলে internal linkage. এতে দুটি লাভ: অন্য ফাইল ভুল করে এটিকে ডাকতে পারে না, আর দুটি মডিউলে একই নামের helper (যেমন দুই ফাইলেই static int grow) থাকলেও কোনো সংঘাত হয় না।
অভ্যাস করে নিন: .c ফাইলের প্রতিটি ফাংশন আর ফাইল-স্তরের ভ্যারিয়েবল ডিফল্টভাবে static লিখুন। Header-এ যেটি দেওয়ার সিদ্ধান্ত নেবেন, শুধু সেটি থেকে static সরান। দরজা খোলা রাখা একটি সচেতন সিদ্ধান্ত হওয়া উচিত, ভুলবশত নয়।
একই header দুবার ঢুকলে — include guard
রাফির report.h-ও ledger.h include করে (রিপোর্টে Ledger টাইপ লাগে)। এখন main.c যদি দুটিই include করে, তাহলে ledger.h-এর লেখা ঢুকবে দুবার — আর একই typedef struct {…} Sale; দুবার দেখে compiler বলবে redefinition. এর সমাধান পর্ব ২৭-এর preprocessor অংশ থেকে আপনার চেনা: include guard. ledger.h-এর প্রথম দুই লাইন আর শেষ লাইন আবার দেখুন:
#ifndef LEDGER_H /* first time: LEDGER_H is not defined, so enter */
#define LEDGER_H /* now it is defined */
/* ... types and prototypes ... */
#endif /* second time: the whole body is skipped */অনেক compiler #pragma once-ও বোঝে — এক লাইনেই একই কাজ। প্রধান compiler-গুলো মানে, তবে এটি C standard-এর অংশ নয়। যেটিই নিন, প্রতিটি header-এ একটি guard থাকা বাধ্যতামূলক অভ্যাস।
বৃত্তাকার include থেকে সাবধান। ledger.h যদি report.h include করে, আর report.h include করে ledger.h — guard থাকায় অসীম লুপ হবে না, কিন্তু একটি ফাইল অন্যটির টাইপ দেখার আগেই ব্যবহার করতে চাইবে, আর আসবে unknown type name. সমাধান: নির্ভরতা এক দিকে রাখুন (রিপোর্ট খাতার ওপর নির্ভর করে, খাতা রিপোর্টের ওপর নয়)। শুধু পয়েন্টার লাগলে পুরো header না টেনে একটি forward declaration লিখুন: typedef struct Ledger Ledger;
৪ · Information hiding — ভেতরটা লুকিয়ে রাখা
অংশ ১-এর সেই সমস্যাটি মনে করুন: রাফির রিপোর্ট-কোড সরাসরি lg.count বদলে দিয়েছিল। ফাইল ভাগ করার পরও এটি সম্ভব! কারণ Ledger struct-এর পুরো গঠন ledger.h-এ লেখা আছে — যে-ই header include করে, সে-ই items, count, capacity দেখতে ও বদলাতে পারে। দরজা আছে, কিন্তু দেয়ালে কাচ লাগানো আর জানালা খোলা।
আরও বড় সমস্যা: নাদিয়া খাতাকে অ্যারে থেকে linked list-এ (পর্ব ৩৩) বদলাতে চাইলে, যে দশটি ফাইল সরাসরি lg.items[i] পড়ে, দশটিই বদলাতে হবে।
১৯৭২ সালে David Parnas একটি প্রবন্ধ লেখেন — “On the Criteria To Be Used in Decomposing Systems into Modules”. তাঁর যুক্তি ছিল: প্রোগ্রামকে কাজের ধাপ অনুযায়ী ভাগ করবেন না; ভাগ করুন সিদ্ধান্ত অনুযায়ী — যে সিদ্ধান্ত ভবিষ্যতে বদলাতে পারে (যেমন “ডেটা কীভাবে সাজানো থাকবে”), সেটি একটি মডিউলের ভেতরে লুকিয়ে রাখুন। এই নীতির নাম information hiding. পঞ্চাশ বছর পরও সফটওয়্যার নকশার এটি অন্যতম ভিত্তি।
C-তে এই লুকানোর কৌশলের নাম opaque pointer (অস্বচ্ছ পয়েন্টার)। Header-এ struct-এর শুধু নাম থাকে, গঠন থাকে .c ফাইলে। বদলটা দেখুন:
/* BEFORE: everyone sees and can touch the fields
typedef struct { Sale *items; size_t count, capacity; } Ledger; */
/* AFTER: only the name is public */
typedef struct Ledger Ledger;
Ledger *ledger_new(void); /* NULL if no memory */
void ledger_delete(Ledger *lg);
int ledger_add(Ledger *lg, Sale s);
long ledger_total(const Ledger *lg, int pay);
size_t ledger_count(const Ledger *lg); /* read-only access */
const Sale *ledger_get(const Ledger *lg, size_t i);#include <stdlib.h>
#include "ledger.h"
struct Ledger { /* the full shape lives ONLY here */
Sale *items;
size_t count, capacity;
};
Ledger *ledger_new(void) { return calloc(1, sizeof(Ledger)); }
void ledger_delete(Ledger *lg) { if (lg) free(lg->items); free(lg); }
size_t ledger_count(const Ledger *lg) { return lg->count; }typedef struct Ledger Ledger; লাইনটি compiler-কে বলে: “struct Ledger নামে একটি টাইপ আছে।” কিন্তু তার ভেতরে কী আছে বা আকার কত, তা বলে না। এমন টাইপকে বলে incomplete type. এর পয়েন্টার রাখা যায় (পয়েন্টারের আকার সবসময় জানা), কিন্তু এর field পড়া বা এর ভ্যারিয়েবল বানানো যায় না। ধাপে ধাপে দেখুন main.c কী পারে আর কী পারে না:
main.c → দেখে: struct Ledger নামে কিছু একটা আছে
জানে না: আকার কত, ভেতরে কী
ledger.c → দেখে: struct Ledger { Sale *items; size_t count, capacity; }
main.c:14:7: error: invalid use of incomplete typedef 'Ledger' 14 | lg->count = 0; | ^~ (পুরোনো GCC-তে: dereferencing pointer to incomplete type)
main.c:9:12: error: storage size of 'lg' isn't known 9 | Ledger lg; | ^~ আকার না জানলে কত byte জায়গা নেবে, compiler বুঝবে কীভাবে?
Ledger *lg = ledger_new(); /* খাতা বানায় ledger.c */ if (lg == NULL) return 1; ledger_add(lg, s); printf("%zu sales\n", ledger_count(lg)); ledger_delete(lg); /* ফেরতও দেয় ledger.c */
struct Ledger {
Sale *items;
size_t count, capacity;
long cash_total; /* নতুন: মোট আগেই হিসাব করা */
};
$ make
gcc -Wall -Wextra -g -c ledger.c ← শুধু এটি
gcc main.o ledger.o report.o -o shop
- দরজায় শুধু নাম।
ledger.h-এ আছেtypedef struct Ledger Ledger;— একটি নাম, গঠন নয়।main.cজানে “খাতা” নামে কিছু আছে; কয়টি field, কোন ক্রমে, কত byte — কিছুই জানে না। পূর্ণ গঠন আছে শুধুledger.c-তে। - Field-এ হাত দেওয়া — বন্ধ। রাফির পুরোনো অভ্যাস:
lg->count = 0;. এখন compiler থামিয়ে দেয়, কারণcountনামে কোনো field আছে কি না তা সে জানেই না। আগে যে ভুল নীরবে হিসাব নষ্ট করত, এখন সেটি compile-এর সময়ই ধরা পড়ে। - নিজে খাতা বানানো — বন্ধ।
Ledger lg;লিখলে compiler-কে stack-এ জায়গা নিতে হবে, কিন্তু আকারই অজানা। তাই খাতা বানানোর দায়িত্বও মডিউলের:ledger_new()heap-এ জায়গা নিয়ে পয়েন্টার দেয়। এটি opaque pointer-এর একটি দাম — প্রতিটি খাতার জন্য একটিmalloc/calloc. - সঠিক পথ — দরজার হাতল। পয়েন্টার রাখা যায়, কারণ পয়েন্টারের আকার সবসময় জানা। সব কাজ হয় interface-এর ফাংশন দিয়ে: যোগ, গোনা, পড়া, মুছে ফেলা। পড়ার জন্য
ledger_countআছে, কিন্তুcountসরাসরি বদলানোর কোনো ফাংশন নেই — তাই সংখ্যাটি কখনো ভুল হতে পারে না। - পুরস্কার — ভেতরটা স্বাধীন। নাদিয়া struct-এ
cash_totalযোগ করলেন।main.cবাreport.c-এর কোনো লাইন বদলাতে হলো না, কারণ তারা কখনো field দেখেইনি। শুধুledger.cআবার compile আর link — ঠিক এটাই Parnas-এর প্রতিশ্রুতি।
এবার নিজে চালিয়ে দেখুন। অনলাইন compiler একটিমাত্র ফাইল নেয়, তাই নিচে তিনটি “ফাইল” একটি ফাইলে পরপর রাখা হয়েছে — আর কৌশলটি হলো main-কে রাখা হয়েছে struct-এর গঠনের আগে. ফলে main লেখার মুহূর্তে compiler সত্যিই শুধু নামটি জানে, ঠিক আলাদা main.c-এর মতো:
#include <stdio.h>
#include <stdlib.h>
/* ---- ledger.h: the only part main() can see ---- */
typedef struct Ledger Ledger; /* opaque: name only, no fields */
Ledger *ledger_new(void);
void ledger_add(Ledger *lg, long paisa);
long ledger_total(const Ledger *lg);
void ledger_delete(Ledger *lg);
int main(void) { /* ---- main.c ---- */
Ledger *lg = ledger_new();
if (lg == NULL) return 1;
ledger_add(lg, 14500); ledger_add(lg, 15600);
printf("Total: %ld paisa\n", ledger_total(lg));
/* printf("%d\n", lg->count); */ /* try: remove the comment marks */
ledger_delete(lg);
return 0;
}
struct Ledger { long sum; int count; }; /* ---- ledger.c: hidden ---- */
Ledger *ledger_new(void) { return calloc(1, sizeof(Ledger)); }
void ledger_add(Ledger *lg, long p) { lg->sum += p; lg->count++; }
long ledger_total(const Ledger *lg) { return lg->sum; }
void ledger_delete(Ledger *lg) { free(lg); }আউটপুট আসবে Total: 30100 paisa — অর্থাৎ ৩০১ টাকা, পর্ব ২৩-এর সেই দুটি বিক্রির যোগফল। এবার ১৪ নম্বর লাইনের /* আর */ মুছে আবার চালান: compiler বলবে invalid use of incomplete typedef 'Ledger'. main struct-এর গঠন দেখার আগেই লেখা, তাই count বলে কিছু আছে কি না সে জানে না। calloc ব্যবহার করা হয়েছে কারণ সে মেমরি শূন্য দিয়ে ভরে দেয় — তাই sum আর count শুরুতেই ০, আলাদা init লাগে না। আর ledger_new NULL দিলে main থেমে যায় — মেমরি না পাওয়ার সম্ভাবনা interface-এই লেখা আছে।
এই কৌশল আপনি প্রথম দিন থেকেই ব্যবহার করছেন, হয়তো খেয়াল করেননি। পর্ব ২৪-এর FILE *fp = fopen(...) — FILE-এর ভেতরে কী আছে, C standard কখনো বলেনি। প্রতিটি অপারেটিং সিস্টেমের library নিজের মতো করে সাজায়, আর আপনি শুধু fopen, fgets, fclose ব্যবহার করেন। তাই একই কোড Windows আর Linux দুই জায়গাতেই চলে।
৫ · Make — শুধু যা বদলেছে, তা-ই আবার বানানো
অংশ ২-এ তিনটি কমান্ড হাতে লিখে build করেছিলেন। রাফির report.c যোগ হলে চারটি, SMS যোগ হলে পাঁচটি। আর প্রতিবার ভাবতে হবে: “কোনটি বদলেছি? কোনটি আবার compile করতে হবে?” একটি ভুলে যাওয়া মানে পুরোনো .o দিয়ে তৈরি প্রোগ্রাম — যেখানে আপনার নতুন সংশোধনটাই নেই। এই হিসাবের কাজটি মানুষের নয়, একটি build system-এর। সবচেয়ে পুরোনো আর এখনো বহুল ব্যবহৃত build system হলো Make.
Make পড়ে একটি ফাইল — Makefile. এতে প্রতিটি নিয়মের তিনটি অংশ:
🎯 Target
Target — যা বানাতে চান। কোলনের বাঁ পাশে: ledger.o:
🧱 Prerequisite
Prerequisite — যা থেকে বানানো হয়। কোলনের ডান পাশে: ledger.c ledger.h
🛠️ Recipe
Recipe — বানানোর কমান্ড। পরের লাইনে, TAB দিয়ে শুরু: gcc -c ledger.c
দোকানের খাতা প্রজেক্টের একটি ছোট কিন্তু আসল Makefile দেখুন (রাফির report.c আর report.h-সহ):
# Makefile - shop ledger (every recipe line starts with a TAB)
CC = gcc
CFLAGS = -Wall -Wextra -g
OBJS = main.o ledger.o report.o
shop: $(OBJS)
$(CC) $(OBJS) -o shop
main.o: main.c ledger.h report.h
$(CC) $(CFLAGS) -c main.c
ledger.o: ledger.c ledger.h
$(CC) $(CFLAGS) -c ledger.c
report.o: report.c report.h ledger.h
$(CC) $(CFLAGS) -c report.c
clean:
rm -f shop $(OBJS)
.PHONY: cleanলাইন ধরে পড়ুন। উপরের তিনটি লাইন ভ্যারিয়েবল — $(CFLAGS) লিখলে সেখানে বসে যায় -Wall -Wextra -g, পর্ব ০৩-এর সেই প্রতিদিনের flag. পরের নিয়মগুলো বলছে: shop তৈরি হয় তিনটি .o থেকে; প্রতিটি .o তৈরি হয় তার .c আর যে header-গুলো সে include করে, সেগুলো থেকে। clean কোনো ফাইল নয়, একটি কাজের নাম — তাই .PHONY দিয়ে Make-কে জানিয়ে রাখা হলো। টার্মিনালে শুধু make লিখলে Make প্রথম target (shop) বানাতে চেষ্টা করে।
TAB, space নয়! Recipe লাইনের শুরুতে অবশ্যই একটি TAB অক্ষর থাকতে হবে। চারটি space দেখতে হুবহু এক, কিন্তু Make মানবে না:
Makefile:7: *** missing separator. Stop.
অনেক editor TAB চাপলে নিজে থেকেই space বসিয়ে দেয়, আর AI বা ওয়েবসাইট থেকে কপি করলেও প্রায়ই TAB হারিয়ে যায়। VS Code-এ নিচের status bar-এ “Spaces: 4” লেখা থাকলে Makefile-এর জন্য সেটি বদলে নিন। ওপরের “কপি” বোতাম দিয়ে নিলে আসল TAB-ই পাবেন।
Make কীভাবে ঠিক করে কী আবার বানাতে হবে
Make-এর পুরো বুদ্ধি একটি সহজ প্রশ্নে: “target কি তার কোনো prerequisite-এর চেয়ে পুরোনো?” প্রতিটি ফাইলের সাথে অপারেটিং সিস্টেম শেষ বদলানোর সময় (timestamp) রাখে। Prerequisite target-এর চেয়ে নতুন হলে বুঝতে হবে target বানানোর পর সেটি বদলেছে — তাই আবার বানাও। না হলে বাদ। ধরুন সকাল ১০টায় সব build করা ছিল, তারপর ১০:৩০-এ আপনি শুধু ledger.c-তে একটি সংশোধন করে সেভ করলেন। এবার make:
- শুরুর অবস্থা। সকাল ১০টায় সব source লেখা, ১০:০৫-এ সব
.o, ১০:০৬-এshop. প্রতিটি target তার prerequisite-এর চেয়ে নতুন। এখনmakeচালালে সে বলে “up to date” — কিছুই করার নেই। - আপনি শুধু
ledger.cবদলালেন। ১০:৩০-এ সেভ করলেন। অপারেটিং সিস্টেম ফাইলটির timestamp বদলে দিল। এবারmakeলিখলেন। Make-এর লক্ষ্য প্রথম targetshop; কিন্তু তার আগে সেshop-এর প্রতিটি prerequisite একে একে পরীক্ষা করবে। main.o— বাদ।main.o(১০:০৫) তার তিনটি prerequisite —main.c,ledger.h,report.h(সব ১০:০০) — এর চেয়ে নতুন। মানেmain.oবানানোর পর এগুলোর কোনোটি বদলায়নি। Compile বাদ। প্রথম ধাপটি বাঁচল।ledger.o— আবার বানাও।ledger.c(১০:৩০) এখনledger.o(১০:০৫)-এর চেয়ে নতুন। Make recipe চালাল:gcc -Wall -Wextra -g -c ledger.c. নতুনledger.o-এর সময় হলো ১০:৩১.report.o— বাদ।report.c,report.h,ledger.h— কোনোটিই বদলায়নি। দ্বিতীয় ধাপটিও বাঁচল। খেয়াল করুন, আমরা বদলেছিledger.c, header নয় — তাই header-এর ওপর নির্ভরশীল ফাইলগুলোও নিরাপদে বাদ পড়ল।shop— আবার link.ledger.o(১০:৩১) এখনshop(১০:০৬)-এর চেয়ে নতুন, তাই link করতেই হবে। মোট চলল দুটি কমান্ড — একটি compile আর একটি link; দুটি compile বাদ পড়ল। তিন ফাইলে লাভ সামান্য, কিন্তু হাজার ফাইলের প্রজেক্টে এটাই ঘণ্টাকে সেকেন্ডে নামিয়ে আনে। এর নাম incremental build.
Prerequisite ভুলে গেলে Make চুপচাপ ভুল করে। ধরুন main.o-এর লাইনে ledger.h লিখতে ভুলে গেলেন। এরপর Sale struct-এ একটি field যোগ করলেন। Make দেখবে main.c বদলায়নি, তাই main.o পুরোনো আকারের Sale নিয়েই থেকে যাবে, আর ledger.o জানবে নতুন আকার। কোনো error নেই — শুধু অদ্ভুত, ভুল ফল। এমন সন্দেহ হলে make clean তারপর make দিয়ে সব নতুন করে বানান। বড় প্রজেক্টে GCC-র -MMD -MP flag দিয়ে compiler নিজেই প্রতিটি ফাইলের header-তালিকা লিখে দেয়, যাতে হাতে লিখতে না হয়।
প্রতিদিনের Make কমান্ড
| কমান্ড | কী করে |
|---|---|
make | প্রথম target (shop) বানায় — শুধু যা দরকার |
make ledger.o | শুধু একটি নির্দিষ্ট target বানায় |
make clean | তৈরি করা সব ফাইল মুছে দেয় — পরের make সব নতুন করে বানাবে |
make -j4 | একসাথে সর্বোচ্চ ৪টি recipe চালায়। main.o, ledger.o, report.o একে অপরের ওপর নির্ভর করে না, তাই তিনটি একসাথে compile হতে পারে |
make -n | কী কী চালাবে শুধু দেখায়, আসলে চালায় না — শেখার জন্য চমৎকার |
বড় প্রজেক্টে প্রতিটি .o-এর আলাদা নিয়মের বদলে একটি pattern rule লেখা হয়: %.o: %.c, যার recipe-তে $< মানে প্রথম prerequisite আর $@ মানে target. AI-এর Makefile-এ দেখলে চিনতে পারলেই যথেষ্ট।
Make লিখেছিলেন Stuart Feldman, ১৯৭৬ সালে Bell Labs-এ — যেখানে C আর Unix-এরও জন্ম। আর TAB-এর গল্প? Feldman পরে বলেছিলেন, শুরুতে সহজ কিছু একটা করেছিলেন বলে TAB বেছে নেন; কয়েক সপ্তাহের মধ্যে প্রায় এক ডজন ব্যবহারকারী হয়ে যায়, তাদের Makefile ভাঙতে চাননি — আর সেই সিদ্ধান্ত আজও বহাল। একটি ছোট সিদ্ধান্ত কীভাবে পঞ্চাশ বছরের interface হয়ে যায়, এটি তার সেরা উদাহরণ।
CMake — সব প্ল্যাটফর্মের জন্য একটি বর্ণনা
Makefile মূলত Unix-ধাঁচের পরিবেশের জন্য; Windows-এর Visual Studio নিজের ধরনের প্রজেক্ট ফাইল চায়। বড় C ও C++ প্রজেক্ট তাই প্রায়ই CMake ব্যবহার করে। CMake নিজে build করে না — আপনি একবার প্রজেক্টের বর্ণনা লেখেন, আর CMake প্রতিটি প্ল্যাটফর্মের জন্য উপযুক্ত build ফাইল (Makefile, Ninja ফাইল বা Visual Studio প্রজেক্ট) তৈরি করে দেয়। একে বলে build generator:
cmake_minimum_required(VERSION 3.13)
project(shop_ledger C)
add_library(ledger STATIC src/ledger.c src/report.c)
target_include_directories(ledger PUBLIC include)
add_executable(shop src/main.c)
target_link_libraries(shop PRIVATE ledger)cmake -S . -B build # read CMakeLists.txt, generate build files in build/
cmake --build build # run the real build (make, ninja, or MSVC)CMake header-এর নির্ভরতা নিজেই হিসাব রাখে। তবু শুরুতে Make শিখুন — ভেতরের ধারণাটি সেখানেই সবচেয়ে পরিষ্কার।
৬ · Library — মডিউলকে প্যাকেট করে বিলি করা
পাশের দোকানের জন্য আরেকজন প্রোগ্রামার একটি SMS-রিপোর্ট টুল বানাতে চান, যেটিও খাতা ব্যবহার করবে। সোর্স কোড কপি করে দিলে আপনার পরের সংশোধন তাঁর কাছে পৌঁছাবে না। সাধারণ পথ হলো মডিউলকে একটি library বানিয়ে দেওয়া — compile করা কোড আর সাথে header. পর্ব ০৩-এ দেখেছেন library দুইভাবে জোড়া যায়: static আর dynamic. এবার নিজে বানিয়ে দেখি:
- ar
libledger.a.o-গুলো এক বান্ডিলে - Static link
-lledgerকোড কপি হয় shop-এ - Shared
libledger.soশুধু নাম লেখা থাকে - চালানোর সময়
loaderখুঁজে এনে জোড়ে - আপডেট
v1 → v2সুবিধা আর ঝুঁকি
$ gcc -Wall -Wextra -g -c ledger.c report.c $ ar rcs libledger.a ledger.o report.o $ ar t libledger.a # ভেতরে কী আছে? ledger.o report.o
$ gcc main.c -L. -lledger -o shop -L. → library খোঁজার তালিকায় এই ফোল্ডারও যোগ করো -lledger → "lib" + ledger + ".so" বা ".a" নামের ফাইল খোঁজো $ rm libledger.a $ ./shop # তবু চলে — কোড shop-এর ভেতরে ঢুকে গেছে
$ gcc -fPIC -Wall -Wextra -g -c ledger.c report.c $ gcc -shared -o libledger.so ledger.o report.o $ gcc main.c -L. -lledger -o shop $ ldd ./shop libledger.so => not found libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6
$ ./shop ./shop: error while loading shared libraries: libledger.so: cannot open shared object file: No such file or directory $ LD_LIBRARY_PATH=. ./shop # loader-কে বলা হলো এই ফোল্ডারেও খোঁজো 1) Add 2) List 4) Totals 0) Exit
✓ বাগ ঠিক করা ledger_total → shop, sms_report … সবাই পরের বার খুললেই পায় ✗ Sale-এ নতুন field যোগ → পুরোনো shop ধরে নেয় Sale 56 byte, নতুন library ধরে নেয় 64 byte → ভুল হিসাব সমাধান: ভেতরটা opaque রাখা, নইলে নতুন version নম্বর (libledger.so.2)
arদিয়ে বান্ডিল। Static library আসলে কয়েকটি.oফাইলের একটি আর্কাইভ — অনেকটা zip-এর মতো।ar rcsমানে: ফাইলগুলো ঢোকাও বা বদলাও (r), আর্কাইভ না থাকলে বানাও (c), আর linker-এর জন্য সূচি লেখো (s). নামের নিয়ম: শুরুতেlib, শেষে.a(Windows-এর MSVC-তে.lib).- Static link.
-L.বলে “library এই ফোল্ডারেও খোঁজো”, আর-lledgerবলে “libledgerনামের library জোড়ো” —libআর শেষাংশ linker নিজেই যোগ করে।-lসবসময় সেই ফাইলগুলোর পরে লিখুন, যাদের এটি লাগে। Linker শুধু দরকারি অংশshop-এর ভেতরে কপি করে; এরপর.aমুছে ফেললেও প্রোগ্রাম চলে। - Shared library.
-fPICদিয়ে এমন কোড বানানো হয় যা মেমরির যেকোনো ঠিকানায় বসে চলতে পারে, তারপর-sharedদিয়ে তৈরি হয় shared library — Linux-এ.so, Windows-এ.dll, Mac-এ.dylib. এবার link করার সময়shop-এ কোড কপি হয় না, শুধু লেখা থাকে “আমারlibledger.soলাগবে”। একই ফোল্ডারে.aআর.soদুটিই থাকলে GCC সাধারণত.so-কেই বেছে নেয়। - চালানোর মুহূর্তে জোড়া। প্রোগ্রাম খোলার সময় অপারেটিং সিস্টেমের loader library-টি খুঁজে এনে জোড়ে। সে খোঁজে নির্দিষ্ট system ফোল্ডারে; আপনার প্রজেক্ট-ফোল্ডারে নয়। তাই পর্ব ০৩-এর “
.dllnot found”-এর Linux সংস্করণ দেখা গেল। পরীক্ষার জন্যLD_LIBRARY_PATHদিয়ে ফোল্ডার জানানো যায়; আসল ইনস্টলে library যায় system-এর library ফোল্ডারে। - আপডেট — সুবিধা ও ঝুঁকি।
ledger_total-এ একটি বাগ ঠিক করে নতুন.soদিলেন — যত প্রোগ্রাম এটি ব্যবহার করে, সবাই নতুন করে compile ছাড়াই সংশোধন পেয়ে গেল। কিন্তুSalestruct-এ একটি field যোগ করলে পুরোনোshopএখনো পুরোনো আকার ধরে নিয়ে মেমরি পড়বে — নতুন library-র সাথে তার হিসাব মিলবে না। এটাই ABI ভাঙা।
| প্রশ্ন | Static (.a) | Shared (.so / .dll) |
|---|---|---|
| কোড কোথায় থাকে | প্রতিটি প্রোগ্রামের ভেতরে কপি | একটি আলাদা ফাইলে, সবাই ভাগ করে |
| প্রোগ্রাম অন্য কম্পিউটারে নিলে | একা চলে | library-ও সাথে লাগবে |
| বাগ ঠিক করলে | প্রতিটি প্রোগ্রাম আবার link করতে হয় | শুধু library ফাইল বদলালেই হয় |
| বড় ঝুঁকি | পুরোনো, অনিরাপদ কপি বহু প্রোগ্রামে থেকে যাওয়া | ABI ভেঙে পুরোনো প্রোগ্রাম নষ্ট হওয়া |
ABI — compile হয়ে যাওয়া কোডের চুক্তি
Header হলো সোর্স কোডের স্তরে চুক্তি (API): কোন নাম, কোন টাইপ। কিন্তু compile হওয়ার পর আরেকটি চুক্তি তৈরি হয়, যাকে বলে ABI — struct-এর আকার কত byte, কোন field কত নম্বর byte থেকে শুরু, ফাংশনে argument কোন register-এ যায়, symbol-এর নাম কী। ধরুন ৬৪-bit Linux-এ পুরোনো shop compile হয়েছিল এই ধারণায় যে Sale ৫৬ byte, আর pay field শুরু হয় ৪৮ নম্বর byte থেকে। নতুন library-তে price-এর পরে একটি long discount যোগ হলো — এখন Sale ৬৪ byte, আর pay সরে গেছে ৫৬ নম্বর byte-এ। পুরোনো প্রোগ্রাম সেটা জানে না, তাই ভুল জায়গা থেকে ভুল মান পড়ে — কোনো error ছাড়াই। তাই shared library-র লেখকেরা খুব সাবধানে ABI রক্ষা করেন: struct-কে opaque রাখেন (তখন আকার শুধু library জানে), পুরোনো ফাংশন না বদলে নতুন ফাংশন যোগ করেন, আর ABI ভাঙতেই হলে library-র নামে নতুন version নম্বর দেন (যেমন libledger.so.1 থেকে libledger.so.2), যাতে পুরোনো প্রোগ্রাম পুরোনো version-ই খোঁজে।
৭ · প্রজেক্টের ফোল্ডার, README আর git
ফাইল বাড়তে থাকলে একটি ফোল্ডারে সব ফেলে রাখাও আরেক রকম এলোমেলো। পেশাদার C প্রজেক্টে একটি প্রচলিত সাজানো আছে; যেকোনো GitHub প্রজেক্ট খুললে প্রায় এরকমই দেখবেন:
shop-ledger/
├── README.md কী, কীভাবে build, কীভাবে চালাবেন
├── Makefile
├── .gitignore build/ যেন git-এ না যায়
├── include/
│ ├── ledger.h public interface — বাইরের জগৎ শুধু এটুকু দেখে
│ └── report.h
├── src/
│ ├── ledger.c
│ ├── report.c
│ └── main.c
├── tests/
│ └── test_ledger.c ledger মডিউলের নিজস্ব টেস্ট
└── build/ .o, libledger.a, shop — তৈরি হওয়া জিনিস
📂 include/ ও src/
Header আলাদা থাকলে বোঝা যায় কোনটি দরজা আর কোনটি ভেতর। compile-এর সময় -Iinclude flag বলে দেয় header কোথায় খুঁজতে হবে।
🏗️ build/
যা কিছু তৈরি হয়, সব এক জায়গায়। পরিষ্কার করতে পুরো ফোল্ডার মুছলেই হয়, আর সোর্সের সাথে মিশে যায় না।
তৈরি হওয়া জিনিস🧪 tests/
খাতা-মডিউলে কোনো printf/scanf নেই, তাই কীবোর্ড ছাড়াই একটি ছোট প্রোগ্রাম দিয়ে unit test করা যায়। মডিউল ভাগের এটি বড় পুরস্কার।
📝 README.md
প্রজেক্টের প্রথম পাতা: এটি কী করে, কী লাগবে, কীভাবে build ও চালাতে হয়, ফোল্ডারগুলো কী। ছয় মাস পরের আপনিও এর পাঠক।
দলিলএবার পুরো প্রজেক্টকে git-এর অধীনে আনুন। মডিউল ভাগের পর git আরও কাজের — নাদিয়া src/ledger.c আর রাফি src/report.c বদলালে তাঁদের পরিবর্তন আলাদা ফাইলে, তাই merge প্রায় সবসময় নিজে থেকেই হয়ে যায়। একটি নিয়ম মনে রাখুন: git-এ রাখুন যা মানুষ লেখে (source, header, Makefile, README), রাখবেন না যা মেশিন বানায় (.o, .a, shop).
git init
echo "build/" > .gitignore
git add README.md Makefile .gitignore include src tests
git commit -m "Split ledger into modules: ledger, report, main"Branch, merge, pull request আর দলগতভাবে কাজের পুরো পদ্ধতি শিখতে দেখুন ABCL TECH-এর Software Engineering & Git কোর্স। Module ভাগ আর git — দুটি একসাথেই দলগত কাজের ভিত্তি।
৮ · AI-এর যুগে মডুলার প্রোগ্রাম
AI-কে “একটি দোকানের হিসাবের প্রোগ্রাম লেখো” বললে প্রায় সবসময় একটি লম্বা ফাইল আসে — সব ফাংশন, সব global, একটি বিশাল main. একবার চালানোর জন্য ঠিক আছে; কিন্তু সেটিই যদি প্রজেক্টের ভিত হয়, ছয় মাস পর আপনি সেই দেয়ালহীন বাড়িতে আটকে যাবেন। আর “ফাইলে ভাগ করো” বললে সে প্রায়ই এমন ভুল করে, যা ধরা পড়ে শুধু link বা build-এর সময়।
AI সাধারণত যেসব মডিউল-ভুল করে
| AI-এর ভুল | আপনি যা দেখবেন | ঠিক উপায় |
|---|---|---|
| সব কোড একটি ফাইলে, অনেক global | চলে, কিন্তু বদলানো ও টেস্ট করা কঠিন | দায়িত্ব অনুযায়ী মডিউল আর পরিষ্কার interface চান |
| Header-এ ভ্যারিয়েবল বা ফাংশনের সংজ্ঞা | multiple definition of (link-এর সময়) | Header-এ extern বা prototype, সংজ্ঞা একটি .c-তে |
| Include guard নেই | redefinition of struct বা typedef | প্রতিটি header-এ #ifndef guard |
| দুটি header একে অপরকে include করে | unknown type name | নির্ভরতা এক দিকে; দরকারে forward declaration |
| Makefile-এ TAB-এর বদলে space | missing separator. Stop. | Recipe লাইনের শুরুতে আসল TAB |
| Makefile-এ header prerequisite নেই | header বদলালেও পুরোনো .o থেকে যায় — অদ্ভুত বাগ | প্রতিটি .o-এর পাশে তার header, অথবা -MMD -MP |
Helper ফাংশনে static নেই | দুই মডিউলে একই নাম হলে multiple definition | public নয় এমন সবকিছু static |
AI-কে মডিউল ভাগ করতে বলার সবচেয়ে ভালো উপায়: আগে interface, পরে implementation. কোড দেওয়ার আগে AI-কে শুধু header-এর নকশা দিতে বলুন — সেটি আপনি পড়ে অনুমোদন দেবেন। কোনো ফাংশন public হবে কি না, সেই সিদ্ধান্ত আপনার, AI-এর নয়:
AI-এর মডিউল ভাগ হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১C-তে header আর source আলাদা কেন? নতুন ভাষাগুলো (Go, Rust, Python) এই ব্যবস্থা বাদ দিল কেন, আর কী দিয়ে বদলাল?
C-এর header ব্যবস্থা কোনো সুচিন্তিত “module system” নয় — এটি ১৯৭০-এর দশকের সীমাবদ্ধতার ফল। তখনকার কম্পিউটারে মেমরি এত কম ছিল যে compiler একবারে একটিমাত্র ফাইল দেখতে পারত। অন্য ফাইলের ফাংশন কেমন, তা জানানোর সবচেয়ে সহজ উপায় ছিল: ঘোষণাগুলো একটি আলাদা ফাইলে লিখে preprocessor দিয়ে লেখা হুবহু কপি করে বসানো। #include আসলে একটি “কপি-পেস্ট” নির্দেশ, এর বেশি কিছু নয়।
এই সরল সমাধানের দাম আমরা আজও দিই:
- একই কথা দুবার লেখা: prototype header-এ, সংজ্ঞা
.c-তে — দুটি মিলিয়ে রাখার দায়িত্ব প্রোগ্রামারের। - ধীর build: একটি বড় header একশো
.cফাইলে include হলে compiler তাকে একশো বার পড়ে। C++-এ এই সমস্যা আরও তীব্র। - ভঙ্গুরতা: include-এর ক্রম, guard ভুলে যাওয়া, একটি header-এর macro অন্য header-এর অর্থ বদলে দেওয়া।
নতুন ভাষাগুলো compiler-কে নিজেই মডিউল বুঝতে শিখিয়েছে। Go-তে একটি ফোল্ডার একটি package, আর বড় হাতের অক্ষরে শুরু হওয়া নামই বাইরে দৃশ্যমান — Total public, total private. Rust-এ pub শব্দ দিয়ে ঠিক করা হয় কী বাইরে যাবে, আর compiler নিজেই ঘোষণা খুঁজে নেয়। Python-এ প্রতিটি ফাইলই একটি module. এমনকি C++ ২০২০ সালের standard-এ নিজস্ব modules যোগ করেছে। কোনো ক্ষেত্রেই আলাদা header হাতে লিখতে হয় না।
তবু C-এর ব্যবস্থা টিকে আছে, কারণ এর একটি অপ্রত্যাশিত গুণ আছে: header একটি সাধারণ টেক্সট ফাইল, যা যেকোনো ভাষা পড়তে পারে। Python, Rust বা Go থেকে C library ডাকার সময় এই header-ই হয় যোগাযোগের সেতু। C-এর ABI আজ কার্যত সব ভাষার মধ্যে “সাধারণ ভাষা”।
মূল উপলব্ধি: ভাষা বদলায়, কিন্তু ধারণা একই থাকে — interface আর implementation আলাদা রাখা, আর কী বাইরে দেখা যাবে তা সচেতনভাবে ঠিক করা। C-তে এটি আপনি হাতে করেন, তাই ধারণাটি সবচেয়ে পরিষ্কার দেখা যায়।
প্র ০২FILE * কেন opaque pointer-এর সবচেয়ে বিখ্যাত উদাহরণ? এই কৌশল থেকে “স্থায়ী API” তৈরির কী শিক্ষা পাওয়া যায়?
C standard বলে FILE একটি object type, যা একটি stream নিয়ন্ত্রণের তথ্য রাখে — কিন্তু তার ভেতরে কোন field আছে, কখনো বলে না। ফলে Windows, Linux, macOS আর ছোট embedded সিস্টেমের library প্রত্যেকে নিজের মতো করে FILE সাজিয়েছে: কোথাও buffer-এর আকার আলাদা, কোথাও thread-এর তালা (lock) আছে, কোথাও নেই। (কিছু library-তে struct-টি header-এ দেখা গেলেও কোনো portable প্রোগ্রাম তার field ছোঁয় না।) অথচ পঞ্চাশ বছর আগে লেখা fopen/fgets/fclose-এর কোড আজও সব জায়গায় compile হয়।
কেন এটি সম্ভব হলো? কারণ ব্যবহারকারী কখনো ভেতরের ওপর নির্ভর করেননি। এখান থেকে স্থায়ী API তৈরির কয়েকটি শিক্ষা:
- যা দেখাবেন, তা চিরকালের জন্য: একটি field public করলে কেউ না কেউ সেটি ব্যবহার করবে, আর তখন সেটি বদলানো মানে তাদের কোড ভাঙা। তাই শুরুতে যত কম দেখানো যায়।
- তৈরি ও ধ্বংস মডিউলের হাতে:
fopenতৈরি করে,fcloseমুছে দেয়। ব্যবহারকারী কখনোsizeof(FILE)জানতে চায় না — তাই library আকার বাড়ালেও কেউ ভাঙে না। - পড়ার জন্য ফাংশন:
ftell,feof,ferror— field না দেখিয়ে প্রশ্নের উত্তর দেওয়া।
একই কৌশল বড় সিস্টেমেও। Windows API-তে প্রায় সবকিছু একটি HANDLE; Unix-এ খোলা ফাইল মানে শুধু একটি সংখ্যা (file descriptor), আর আসল তথ্য থাকে kernel-এর ভেতরে। SQLite-এর sqlite3 *, OpenSSL-এর নতুন সংস্করণগুলোর বহু struct — সবই opaque. OpenSSL আগে অনেক struct খোলা রেখেছিল; পরে সেগুলো লুকাতে গিয়ে বহু প্রোগ্রামকে কোড বদলাতে হয়েছিল — শুরুতে লুকানো কত সস্তা, তার একটি শিক্ষা।
দামও আছে: প্রতিটি অবজেক্টের জন্য heap allocation, প্রতিটি field পড়তে একটি ফাংশন ডাক (যা compiler সাধারণত inline করতে পারে না, কারণ কোড অন্য ফাইলে)। খুব দ্রুত চলা দরকার এমন ছোট struct-এ (যেমন একটি বিন্দুর x, y) এই দাম অপ্রয়োজনীয়।
মূল উপলব্ধি: একটি API-র আয়ু নির্ভর করে সে কতটা কম প্রতিশ্রুতি দেয় তার ওপর। যা লুকানো, তা স্বাধীনভাবে বদলানো যায়; যা দেখানো, তা চিরকালের দায়।
প্র ০৩Make-এর timestamp ধারণা প্রায় পঞ্চাশ বছরের পুরোনো। এর সীমাবদ্ধতা কোথায়, আর আধুনিক build tool সেগুলো কীভাবে সামলায়?
Make-এর মূল ধারণা অসাধারণ সরল: “target কি prerequisite-এর চেয়ে পুরোনো?” এই একটি প্রশ্ন দিয়ে সে ১৯৭৬ সাল থেকে অসংখ্য প্রজেক্ট build করছে। কিন্তু সরলতার কিছু সীমা আছে:
- সময় মানেই পরিবর্তন নয়: একটি ফাইল খুলে কিছু না বদলে আবার সেভ করলেন — timestamp বদলাল, কিন্তু বিষয়বস্তু একই। Make তবু সব নির্ভরশীল ফাইল আবার বানাবে। উল্টোটাও হয়: git দিয়ে একটি পুরোনো সংস্করণে ফিরলে ফাইলের সময় এমন হতে পারে যে Make ভুল করে কিছু বাদ দেয়।
- ঘড়ির গরমিল: নেটওয়ার্ক ড্রাইভে বা দুটি মেশিনের মধ্যে ঘড়ি না মিললে “কোনটি নতুন” প্রশ্নের উত্তরই ভুল হয়। Make তখন “clock skew detected” সতর্কবার্তা দেয়।
- নির্ভরতা হাতে লেখা: অংশ ৫-এ দেখেছেন, একটি header লিখতে ভুলে গেলে Make নীরবে ভুল করে। আর recipe বদলালে (যেমন
CFLAGS-এ-O2যোগ করলেন) Make সেটা ধরে না, কারণ Makefile নিজে prerequisite নয়। - বড় প্রজেক্টে ধীরতা: লাখ লাখ ফাইলের প্রজেক্টে শুধু “কী বদলেছে” খুঁজতেই Make অনেক সময় নেয়।
আধুনিক tool-গুলো এসব সামলায় ভিন্ন উপায়ে। Ninja (Google Chrome-এর build দ্রুত করতে তৈরি) নিজে খুব কম সিদ্ধান্ত নেয় — CMake-এর মতো generator তার ফাইল বানায়, আর Ninja শুধু দ্রুত চালায়; সে কোন কমান্ড দিয়ে কোন ফাইল বানানো হয়েছিল তাও মনে রাখে। Bazel-এর মতো tool সময়ের বদলে ফাইলের বিষয়বস্তুর hash (একটি ছোট আঙুলের ছাপ) তুলনা করে — বিষয়বস্তু না বদলালে কিছুই আবার বানায় না, আর একই hash-এর ফল অন্য মেশিন থেকেও ধার নিতে পারে। ccache একই ধারণায় compile-এর ফল জমা রাখে।
মূল উপলব্ধি: “কী বদলেছে” প্রশ্নটি সহজ শোনালেও এর উত্তর কতটা নির্ভুল, তার ওপর নির্ভর করে build কতটা দ্রুত আর কতটা বিশ্বাসযোগ্য। সময় হলো সস্তা কিন্তু অনুমান-নির্ভর উত্তর; hash হলো দামি কিন্তু নিশ্চিত উত্তর — আবার সেই পরিচিত trade-off.
প্র ০৪ABI স্থিতিশীলতা নিয়ে Linux distribution-গুলো এত চিন্তিত কেন? একটি library-র struct বদলানো কেন পুরো অপারেটিং সিস্টেমের সমস্যা হতে পারে?
Ubuntu বা Debian-এর মতো একটি Linux distribution হাজার হাজার প্রোগ্রাম আর library একসাথে সাজিয়ে দেয়। এর বেশিরভাগই shared library ব্যবহার করে। ধরুন একটি জনপ্রিয় image library শত শত প্রোগ্রামে ব্যবহৃত হয়। তার নতুন সংস্করণে একটি public struct-এ একটি field যোগ হলো। এখন:
- নতুন library-র সাথে compile হওয়া প্রোগ্রাম ঠিক চলবে।
- পুরোনো library-র সাথে compile হওয়া প্রোগ্রাম চলবে ঠিকই, কিন্তু ভুল offset থেকে মেমরি পড়বে — হয় crash, নয়তো নীরব ভুল ফল, এমনকি নিরাপত্তা-ত্রুটি।
তাই ABI ভাঙা মানে সেই library ব্যবহারকারী প্রতিটি প্রোগ্রাম আবার build করতে হবে। Debian-এর মতো distribution-এ এর একটি নামই আছে — “library transition”, যেখানে নির্ভরশীল সব প্যাকেজ নতুন করে build করে একসাথে ছাড়া হয়। এটি সময়সাপেক্ষ, আর এর মধ্যে কিছু প্যাকেজ আটকে যায়।
সমস্যা সামলানোর কৌশলগুলো:
- soname: Library-র নামে major version থাকে (যেমন
libssl.so.3). ABI ভাঙলে নম্বর বাড়ে, আর পুরোনো ও নতুন সংস্করণ পাশাপাশি থাকতে পারে। - Symbol versioning: GNU C library (glibc) একই ফাংশনের একাধিক সংস্করণ ভেতরে রেখে দেয়, যাতে বহু বছর আগে compile হওয়া প্রোগ্রাম পুরোনো আচরণটাই পায়। এজন্যই পুরোনো Linux প্রোগ্রাম নতুন সিস্টেমেও সাধারণত চলে।
- Opaque struct: আকার লুকানো থাকলে field যোগ করা ABI ভাঙে না।
Linux kernel-এর ক্ষেত্রেও একই চিন্তা। Linus Torvalds-এর বহুল উদ্ধৃত নিয়ম — “we don't break userspace”: kernel ব্যবহারকারী প্রোগ্রামের জন্য যে interface দেয় (system call), তা বদলে পুরোনো প্রোগ্রাম ভাঙা চলবে না। মজার ব্যাপার, kernel-এর ভেতরের driver-দের জন্য কোনো স্থির ABI নেই — সেটি ইচ্ছা করেই, যাতে ভেতরটা স্বাধীনভাবে উন্নত করা যায়। বাইরে কঠোর, ভেতরে স্বাধীন — ঠিক information hiding.
মূল উপলব্ধি: compile হয়ে বিলি হওয়া কোড আর ফেরত আনা যায় না। তাই যে interface অন্যের মেশিনে চলা প্রোগ্রাম ব্যবহার করে, তার প্রতিটি পরিবর্তন একটি প্রতিশ্রুতি ভাঙার প্রশ্ন — আর বড় সিস্টেমের সুনাম তৈরি হয় এই প্রতিশ্রুতি রক্ষা করেই।
প্র ০৫ভালো মডিউল-সীমানা কীভাবে কয়েকজন মানুষ (আর AI) কে একসাথে, সংঘাত ছাড়া কাজ করতে দেয়?
১৯৬৮ সালে Melvin Conway একটি পর্যবেক্ষণ লেখেন, যা এখন “Conway's law” নামে পরিচিত: একটি প্রতিষ্ঠান যে সিস্টেম বানায়, তার গঠন হয় প্রতিষ্ঠানের যোগাযোগের কাঠামোর প্রতিচ্ছবি। উল্টো দিক থেকে দেখলে: আপনি যদি চান কয়েকজন স্বাধীনভাবে কাজ করুক, তাহলে কোডের সীমানা এমনভাবে টানুন যাতে প্রত্যেকের কাজ একটি মডিউলের ভেতরে থাকে, আর মডিউলগুলোর মধ্যে কথা হয় শুধু interface দিয়ে।
দোকানের খাতায় এটি কীভাবে কাজ করে দেখুন। প্রথমে নাদিয়া আর রাফি মিলে ledger.h চূড়ান্ত করলেন — এটিই তাঁদের চুক্তি। তারপর:
- নাদিয়া
ledger.c-এর ভেতরটা যতবার খুশি বদলান; চুক্তি না ভাঙলে রাফির কিছু যায় আসে না। - রাফি
report.cলিখতে পারেনledger.cশেষ হওয়ার আগেই — শুধু header দেখে, আর দরকারে একটি নকল (stub) খাতা বানিয়ে টেস্ট করে। - git-এ তাঁদের পরিবর্তন আলাদা ফাইলে, তাই merge-সংঘাত প্রায় হয় না। Code review-ও সহজ: header বদলালে সবাই দেখবেন,
.cবদলালে শুধু মালিক।
AI-এর ক্ষেত্রে এই নীতি আরও শক্তিশালী। AI একবারে সীমিত পরিমাণ লেখা “মাথায় রাখতে” পারে (context window). তিন হাজার লাইনের একটি ফাইলে সে প্রায়ই দূরের কোনো অংশ ভুলে যায় আর এমন পরিবর্তন করে যা অন্যত্র কিছু ভেঙে দেয়। কিন্তু আপনি যদি বলেন “এই header মেনে শুধু report.c লেখো”, তাহলে তার কাজের সীমা ছোট আর পরিষ্কার — ভুলের সুযোগ কম, আর যাচাই করাও সহজ, কারণ interface আগে থেকেই আপনার অনুমোদিত। একাধিক AI সহকারী, বা আপনি ও AI, আলাদা মডিউলে একসাথে কাজ করতে পারেন।
উল্টো চিত্রও সত্যি: সীমানা ভুল জায়গায় টানলে (যেমন দুটি মডিউল একে অপরের ভেতরের তথ্য সারাক্ষণ চায়) প্রতিটি পরিবর্তনে দুজনকে বসে আলোচনা করতে হয় — তখন দুজন মিলে একজনের চেয়েও ধীরে কাজ হয়।
মূল উপলব্ধি: মডিউল-সীমানা শুধু কোডের নকশা নয়, কাজ ভাগের নকশাও। ভালো interface হলো একটি চুক্তি, যা মানুষ আর মেশিন দুজনকেই স্বাধীনভাবে কিন্তু নিরাপদে কাজ করতে দেয়।
অনুশীলন
বেশিরভাগ কাজ ভাবা আর পর্যবেক্ষণের — শেষটি ছাড়া কোড লিখতে হবে না।
-
Header না source? নিচের প্রতিটি লাইন কোথায় যাবে —
ledger.hনাকিledger.c? (ক)long ledger_total(const Ledger *lg, int pay);(খ)static int grow(Ledger *lg) { … }(গ)int ledger_debug = 0;(ঘ)typedef struct Ledger Ledger;(ঙ)extern int ledger_debug;(ক) header — prototype, শুধু ঘোষণা; অন্যদের ডাকার জন্য দরকার।
(খ)
.c— শরীরসহ সংজ্ঞা, আরstaticমানেই “শুধু এই ফাইলের”। Header-এ রাখলে প্রতিটি ফাইলে একটি করে আলাদা কপি তৈরি হতো।(গ)
.c— মান বসানো ভ্যারিয়েবল মানে সংজ্ঞা; header-এ গেলেmultiple definition.(ঘ) header — opaque pointer-এর জন্য নামটুকু public. পূর্ণ
struct Ledger { … }যাবে.c-তে।(ঙ) header —
externশুধু প্রতিশ্রুতি দেয়, মেমরি নেয় না; (গ)-এর সাথে জোড়া বাঁধে। -
Make-এর মতো ভাবুন: অংশ ৫-এর Makefile ধরে উত্তর দিন — (ক) শুধু
ledger.hবদলালেmakeকোন কোন কমান্ড চালাবে? (খ) শুধুmain.cবদলালে? (গ) কিছু না বদলেmake cleanতারপরmakeকরলে?(ক)
ledger.hতিনটি.o-রই prerequisite, তাই তিনটিই আবার compile হবে, তারপর link — মোট চারটি কমান্ড। এজন্যই header কম বদলানো ভালো, আর header-এ শুধু দরকারি জিনিস রাখা উচিত।(খ) শুধু
main.oআবার compile, তারপর link — দুটি কমান্ড।(গ)
cleanসব.oআরshopমুছে দেয়, তাই পরেরmakeতিনটি compile আর একটি link চালাবে — পুরো build. নিজে মেলাতে চাইলেmake -nদিয়ে আগে থেকে দেখে নিন। -
Opaque pointer নিয়ে পরীক্ষা: অংশ ৪-এর
opaque_demo.c-তে (ক) ১৪ নম্বর লাইনের মন্তব্যচিহ্ন মুছে চালান। (খ) এবার সেটি আবার মন্তব্য করে,main-এর ভেতরেLedger x;লিখে চালান। (গ) শেষেstruct Ledger { … };লাইনটি কেটেmain-এর উপরে বসান, আর (ক) আবার চেষ্টা করুন। তিনবার কী দেখলেন, আর কেন?(ক)
invalid use of incomplete typedef 'Ledger'—mainstruct-এর গঠন জানে না, তাইcountপড়তে পারে না।(খ)
storage size of 'x' isn't known— আকার অজানা, তাই ভ্যারিয়েবল বানানো যায় না। এজন্যইledger_new()লাগে।(গ) এবার কোনো error নেই,
countছাপা হয় (2). গঠন দেখা গেলেই লুকানো শেষ। আসল প্রজেক্টে এটাই হয় যখন কেউ struct-কে.cথেকে header-এ সরিয়ে দেয় — information hiding এক লাইনেই হারিয়ে যায়। তাই opaque struct কখনো header-এ নয়। -
নিজের একটি মডিউল (কোড লেখার কাজ): পর্ব ১৩-এর টোকেন মেশিনটি (
next_token) একটি মডিউলে রূপ দিন:token.h,token.c,main.cআর একটি Makefile. শর্ত: শেষ নম্বরটি বাইরে থেকে বদলানো যাবে না, আর একটিtoken_reset()ফাংশন থাকবে।একটি সম্ভাব্য নকশা:
token.h: include guard, আর দুটি prototype —int token_next(void);ওvoid token_reset(int start);. কোনো ভ্যারিয়েবল নেই।token.c:#include "token.h", তারপর ফাইল-স্তরেstatic int last = 100;—staticবলে অন্য কোনো ফাইল একে ছুঁতে পারবে না; দুটি ফাংশনই একে ব্যবহার করে।main.c:token_next()তিনবার ডেকে ছাপায়, তারপরtoken_reset(500)করে আবার ডাকে।- Makefile:
tokens: main.o token.o, আরmain.oওtoken.oদুটিরই prerequisite-এtoken.h; recipe-তে TAB; একটিclean.
যাচাই:
main.c-তেlast = 0;লিখে দেখুন — compiler বলবে'last' undeclared. আরextern int last;লিখে চেষ্টা করলে compile হবে, কিন্তু link-এর সময়undefined reference to `last'— কারণstaticনামটি linker-এর কাছেই পৌঁছায় না।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি নতুন মডিউল যোগ করার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- 📝
ledger.h-এ interface ঠিক করা — টাইপ আর prototype - 🔧
ledger.c-এ implementation লেখা, helper-গুলোstatic - 📐 প্রতিটি
.cআলাদাভাবে compile করে.oবানানো - 🧵 Linker দিয়ে সব
.oজুড়েshopতৈরি - 🧪 চালিয়ে টেস্ট করা, তারপর git-এ commit
নিচের কোনটি header ফাইলে রাখা উচিত নয়?
int ledger_add(Ledger *lg, Sale s);int ledger_debug = 0;typedef enum { PAY_CASH, PAY_BKASH } PayMethod;extern int ledger_debug;
main.c সফলভাবে compile হলো, কিন্তু gcc main.o -o shop দিতেই এলো undefined reference to `ledger_add'. সবচেয়ে সম্ভাব্য কারণ কী?
ledger.hখুঁজে পাওয়া যায়নি- Header-এ prototype নেই
- Link কমান্ডে
ledger.oদেওয়া হয়নি - Header-এ include guard নেই
Header-এ শুধু typedef struct Ledger Ledger; আছে, struct-এর গঠন ledger.c-তে। main.c-তে কোনটি করা যাবে?
Ledger *lg = ledger_new();Ledger lg;lg->count++;malloc(sizeof(Ledger))
Makefile চালাতেই এলো Makefile:7: *** missing separator. Stop. — কী ঠিক করবেন?
CFLAGSলাইন যোগ করা.PHONYযোগ করা- Target-এর নাম বদলানো
- ৭ নম্বর লাইনের শুরুর space মুছে একটি TAB বসানো
শুধু ledger.c বদলে make চালালেন (অংশ ৫-এর Makefile). কী ঘটবে?
- তিনটি
.cফাইলই আবার compile, তারপর link - শুধু
ledger.ccompile, তারপর link - শুধু
ledger.ccompile, link লাগবে না - কিছুই না — “up to date”
একটি shared library-র নতুন সংস্করণে public struct Sale-এ একটি field যোগ হলো। পুরোনো, আবার compile না করা প্রোগ্রামের কী হতে পারে?
- কোনো সমস্যা নেই, নতুন field স্বয়ংক্রিয়ভাবে সামলানো হয়
- প্রোগ্রাম খোলার সময় loader পরিষ্কার error দেখাবে
- পুরোনো আকার ধরে মেমরি পড়বে — crash বা নীরব ভুল ফল (ABI ভাঙা)
- প্রোগ্রাম নিজের ভেতরে রাখা পুরোনো কপিটিই চালাবে
AI একটি Makefile দিল, যেখানে main.o: main.c লেখা — কিন্তু main.c তো ledger.h include করে। সমস্যা কী?
ledger.hবদলালেmain.oআবার compile হবে না — নীরব, অদ্ভুত বাগ- Make সঙ্গে সঙ্গে error দেবে
- Compiler
ledger.hখুঁজে পাবে না - কোনো সমস্যা নেই, header লেখা ঐচ্ছিক
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- মডিউল = interface (
.h) + implementation (.c); অন্যরা শুধু header include করে। - Header-এ শুধু ঘোষণা আর include guard; সংজ্ঞা একটিই, কোনো একটি
.c-তে। নিজস্ব জিনিসstatic, ইচ্ছা করে ভাগ করা globalextern. - Opaque pointer struct-এর ভেতরটা লুকায় — বাইরে কেউ নির্ভর না করলে ভেতরটা নিশ্চিন্তে বদলানো যায়।
- Make timestamp মিলিয়ে শুধু বদলানো অংশ আবার বানায়; recipe-তে TAB, আর প্রতিটি
.o-এর পাশে তার header. - Library:
.aকপি হয়,.so/.dllখোলার সময় জোড়ে; shared library-তে public struct বদলানো মানে ABI ভাঙা।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৩০ · এরর হ্যান্ডলিং — ভুলের জন্য প্রস্তুত থাকাপরবর্তী পর্বমডিউলের ফাংশন কীভাবে ব্যর্থতা জানাবে, আর ডাকনেওয়ালা কীভাবে সামলাবে।
- C প্রোগ্রামিং · কোর্স হোম৫৬টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- Software Engineering & Gitপাশের ট্র্যাকBranch, merge, code review — মডিউলে ভাগ করা প্রজেক্টে দলগতভাবে কাজ।
- System Designগভীরে যানএকই interface-চিন্তা যখন ফাইল ছেড়ে পুরো সার্ভিস আর সিস্টেমের স্তরে ওঠে।
- Operating Systemsপাশের ট্র্যাকLoader কীভাবে shared library খুঁজে মেমরিতে জোড়ে।