সরাসরি পর্বে যান
পর্ব ১৪ · ৫৬-এর মধ্যে · মডিউল ২

স্কোপ ও স্টোরেজ ক্লাস — কোথায় দেখা যায়, কতক্ষণ বাঁচে

Scope, Lifetime, Linkage & Storage Classes in C

Taught in Bangla, this lesson separates where a variable is visible from how long it lives: block and file scope, the four storage durations, static locals, linkage with static and extern, auto, register and _Thread_local, and why returning the address of a local breaks.

এই পর্বে যা শিখবেন

  • যেকোনো ভ্যারিয়েবল দেখে তিনটি প্রশ্নের উত্তর দেওয়া — কোথায় দেখা যায়, কতক্ষণ বাঁচে, কে কে দেখে
  • একই নামের কয়েকটি ভ্যারিয়েবল থাকলে কোন লাইন কোনটিকে দেখছে, নিশ্চিতভাবে বলা
  • static, extern, auto, register — প্রতিটির আসল অর্থ ও আজকের ব্যবহার
  • Linker কীভাবে কয়েকটি ফাইলের নাম মিলিয়ে জোড়া দেয়, আর “multiple definition” কেন হয়
  • মেমরির কোন অংশে কোন ভ্যারিয়েবল থাকে, আর local-এর ঠিকানা ফেরত দেওয়া কেন মারাত্মক

১ · দুটি আলাদা প্রশ্ন — কোথায় দেখা যায়, কতক্ষণ বাঁচে

পর্ব ১৩-এ ফাংশন শেখার সময় scope আর lifetime-এর সাথে প্রথম পরিচয় হয়েছে। এই পর্বে বিষয়টি পুরোপুরি খুলে দেখব, কারণ বড় প্রোগ্রামের অনেক রহস্যময় বাগের মূলে এই দুটি ধারণা। শুরু করি একটি সাধারণ উদাহরণ দিয়ে।

“রাজু” নামটি ধরুন। বাড়িতে “রাজু” বললে সবাই বোঝে আপনার ছোট ভাইকে। পাড়ার চায়ের দোকানে “রাজু” বললে বোঝায় দোকানির ছেলেকে। আর পুরো শহরে “রাজু” বললে কেউ কিছু বুঝবে না, যদি না সে শহরের সবার চেনা কেউ হয়। একই নাম, কিন্তু কোথায় বলছেন তার ওপর নির্ভর করে কাকে বোঝানো হচ্ছে। এটিই scope.

এবার আরেকটি প্রশ্ন। ক্লাসের রোল কলের খাতা বছর শেষে ফেলে দেওয়া হয়, কিন্তু স্কুলের ভর্তি রেজিস্টার থাকে বহু বছর। কোন খাতা কতদিন টিকে থাকে — এটি সম্পূর্ণ আলাদা প্রশ্ন। প্রোগ্রামে এর নাম lifetime বা storage duration.

C-তে প্রতিটি ভ্যারিয়েবলের জন্য তিনটি প্রশ্ন করা যায়, আর তিনটির উত্তর আলাদা হতে পারে:

Scope — কোথায় দেখা যায়

কোডের কোন অংশে নামটি লিখলে compiler চেনে। এটি লেখার বিষয় — প্রোগ্রাম না চালিয়েই কোড পড়ে বলা যায়।

compile করার সময়ের প্রশ্ন

Storage duration — কতক্ষণ বাঁচে

প্রোগ্রাম চলার সময় মেমরির বাক্সটি কখন তৈরি হয় আর কখন মুছে যায়। একে lifetime-ও বলে।

চলার সময়ের প্রশ্ন

Linkage — অন্য ফাইল দেখে কি না

একাধিক .c ফাইল থাকলে একটি নাম অন্য ফাইল থেকে ব্যবহার করা যায় কি না। এটি linker-এর প্রশ্ন।

link করার সময়ের প্রশ্ন
মূল ধারণা

Scope হলো নামের বৈশিষ্ট্য, আর storage duration হলো বাক্সের বৈশিষ্ট্য। নাম আর বাক্স এক জিনিস নয়। একটি বাক্স বেঁচে থাকতে পারে, অথচ তার নাম এই মুহূর্তে দেখা যাচ্ছে না — যেমন একটি static local ভ্যারিয়েবল, যার ফাংশনের বাইরে থেকে নাম ধরে ডাকা যায় না, কিন্তু মান ঠিকই টিকে থাকে। আবার নাম দেখা যাচ্ছে, অথচ বাক্স মরে গেছে — এমন ঘটনাই dangling pointer.

যেকোনো ঘোষণা দেখলে এই তিনটি প্রশ্ন নিজেকে করার অভ্যাস করুন। ভ্যারিয়েবলটি কোথায় লেখা (ফাংশনের ভেতরে না বাইরে) আর তার সামনে কোন শব্দ আছে (static, extern বা কিছু না) — এই দুটি দেখলেই তিনটি উত্তর পাওয়া যায়। এই শব্দগুলোকে একসাথে বলে storage class specifier.

২ · Scope-এর এলাকা — block, ফাইল আর আরও দুটি

C-এর নিয়মে scope চার রকম। প্রতিদিন কাজে লাগে প্রথম দুটি; বাকি দুটি জানা থাকলে পরীক্ষায় আর কোড পড়তে সুবিধা হয়।

Scopeকোথা থেকে কোথা পর্যন্তউদাহরণ
Block scopeঘোষণার লাইন থেকে সেই { }-এর শেষ পর্যন্তlocal ভ্যারিয়েবল, ফাংশনের parameter, for (int i …)-এর i
File scopeঘোষণার লাইন থেকে পুরো ফাইলের শেষ পর্যন্তসব ফাংশনের বাইরে লেখা ভ্যারিয়েবল ও ফাংশনের নাম
Function scopeপুরো ফাংশনের ভেতরে, লেখার আগে থেকেওশুধু goto-র label (যেমন cleanup:)
Function prototype scopeশুধু prototype-এর বন্ধনীর ভেতরেint area(int w, int h);-এর w, h

শেষ দুটি নিয়ে দুই লাইন। Label-এর scope পুরো ফাংশন, তাই goto cleanup; লিখে নিচের দিকে লাফ দেওয়া যায়, যদিও label-টি তখনো লেখা হয়নি (পর্ব ৩০-এ এর ভালো ব্যবহার দেখবেন)। আর prototype-এর parameter নাম শুধু পড়ার সুবিধার জন্য — int area(int, int); লিখলেও চলে, নামগুলো বন্ধনী শেষ হতেই হারিয়ে যায়।

Shadowing — ভেতরের নাম বাইরেরটিকে ঢেকে দেয়

একই নাম দুই স্তরে ঘোষণা করলে compiler কোনো error দেয় না। নিয়ম একটাই: নাম খোঁজা হয় ভেতর থেকে বাইরের দিকে, প্রথম যেটি পাওয়া যায় সেটিই। ভেতরের নামটি বাইরেরটিকে ঢেকে দেয় — একে বলে shadowing. পর্ব ১৩-এ একটি সহজ উদাহরণ দেখেছেন। এবার একটু কঠিন একটি: এখানে global, parameter, ভেতরের block আর লুপ — চার জায়গায় x. প্রতিটি printf কোন x দেখছে, আগে নিজে অনুমান করুন, তারপর “পরে” চাপুন।

1#include <stdio.h>23int x = 10;                  // file scope45void show(int x) {           // parameter6    printf("A %d\n", x);7    {8        int x = 30;          // block9        printf("B %d\n", x);10    }11    printf("C %d\n", x);12}1314int main(void) {15    show(20);16    for (int x = 0; x < 1; x++)17        printf("D %d\n", x);18    printf("E %d\n", x);19    return 0;20}
কোন x এখন বেঁচে আছে
ফাইলের x
parameter x
block-এর x
লুপের x
“x” লিখলে পাবেন
  1. প্রোগ্রাম শুরুর আগেই ফাইলের x = 10 তৈরি হয়ে আছে। এটি file scope-এর নাম, তাই নিচের সব ফাংশন একে দেখতে পারে — যদি না কেউ তাকে ঢেকে দেয়।
  2. show(20) — A 20. Parameter-ও একটি local নাম, তার scope পুরো ফাংশনের শরীর। তাই ফাংশনের ভেতরে x মানে parameter, মান 20. ফাইলের x মুছে যায়নি, শুধু এই নামে তাকে আর পাওয়া যাচ্ছে না।
  3. ভেতরের block — B 30. নতুন {-এর ভেতরে তৃতীয় একটি x = 30. এখন সবচেয়ে ভেতরেরটিই জেতে। Parameter-টি বেঁচে আছে, কিন্তু ঢাকা পড়েছে।
  4. } পেরিয়ে — C 20. Block শেষ, তার x-ও শেষ। আবার parameter-টি দেখা যাচ্ছে। খেয়াল করুন, ভেতরের 30 বাইরের 20-কে একটুও বদলায়নি — এরা সম্পূর্ণ আলাদা দুটি বাক্স।
  5. লুপ — D 0. show ফিরে গেছে, তার parameter-ও মুছে গেছে। for-এর বন্ধনীতে ঘোষণা করা x-এর scope শুধু লুপটুকু। ভেতরে x মানে লুপেরটি, মান 0.
  6. লুপের পরে — E 10. লুপের x শেষ। main-এ এখন আর কোনো local x নেই, তাই compiler বাইরে খুঁজে ফাইলেরটি পেল। প্রোগ্রাম ঠিকঠাক চলল, কিন্তু পড়তে গিয়ে মাথা ঘুরে যাওয়ার মতো।
  7. -Wshadow চালু করলে। এই flag -Wall বা -Wextra-এর অংশ নয়, আলাদা করে দিতে হয়। দিলে compiler তিনটি জায়গাই দেখিয়ে দেয়। বড় প্রজেক্টে অনেক দল এটি চালু রাখে, কারণ shadowing থেকে এমন বাগ হয় যেখানে আপনি ভাবছেন global বদলাচ্ছেন, অথচ আসলে বদলাচ্ছে একটি local কপি।

Shadowing-এর সবচেয়ে পরিচিত বাগ: if-এর ভেতরে ভুল করে আবার int লিখে ফেলা — if (ok) { int result = 5; }. বাইরে আগে থেকে একটি result থাকলে ভেতরে তৈরি হলো নতুন একটি, যা }-এ মরে গেল। বাইরেরটি যেমন ছিল তেমনই রইল। কোড দেখে মনে হয় সব ঠিক, ফল আসে ভুল।

৩ · Storage duration — চার রকম আয়ু

এবার দ্বিতীয় প্রশ্ন: বাক্সটি কতক্ষণ বাঁচে। C-এর নিয়মে আয়ু চার রকম।

ধরনকখন জন্মকখন মৃত্যুকারা
AutomaticBlock-এ ঢোকার সময়Block থেকে বের হওয়ার সময় (})সাধারণ local ভ্যারিয়েবল, parameter
Staticmain শুরুর আগে, একবারইপ্রোগ্রাম শেষ হলেglobal ভ্যারিয়েবল, static লেখা যেকোনো ভ্যারিয়েবল
Allocatedmalloc ডাকলেfree ডাকলেheap-এর block — পর্ব ২০-এ
Threadএকটি thread শুরু হলেসেই thread শেষ হলে_Thread_local — প্রতি thread-এর নিজস্ব কপি

সাধারণ local ভ্যারিয়েবলকে বলে automatic variable, কারণ এর জন্ম-মৃত্যু নিজে থেকেই হয়। পর্ব ১৩-এর call stack মনে করুন: ফাংশন ডাকলে একটি stack frame তৈরি হয়, local ভ্যারিয়েবলগুলো থাকে সেই frame-এর ভেতরে, আর ফাংশন ফিরলে পুরো frame একসাথে মুছে যায়। অন্যদিকে static variable stack-এ থাকেই না — থাকে প্রোগ্রামের স্থায়ী এলাকায়। নিচের ডায়াগ্রামে দুটিকে পাশাপাশি দেখুন, বিশেষ করে বাঁ দিকের “Stack” সারিটি:

1#include <stdio.h>23void serve(void) {4    int x = 0;            // automatic5    static int count;     // static, শুরুতে 06    x++;7    count++;8    printf("x=%d count=%d\n", x, count);9}1011int main(void) {12    serve();13    serve();14    serve();15    return 0;16}
মেমরিতে এখন
Stack-এ frame
x (frame-এর ভেতরে)
count (স্থায়ী এলাকায়)
  1. main শুরু। Stack-এ আছে শুধু main-এর frame. কিন্তু count এর আগেই তৈরি হয়ে গেছে — স্থায়ী এলাকায়, মান 0. মান না লিখলেও static ভ্যারিয়েবল 0 দিয়ে শুরু হয়, এটি C-এর নিশ্চিত নিয়ম।
  2. প্রথম ডাক। serve-এর frame তৈরি হলো main-এর ওপরে, সাথে জন্ম নিল x = 0. লাইন ৫ count-কে নতুন করে তৈরিও করে না, 0-ও করে না — static-এর শুরুর মান বসে পুরো প্রোগ্রামে একবারই।
  3. দুটিই বাড়ল। x = 1, count = 1. ছাপা হলো x=1 count=1.
  4. প্রথম ডাক শেষ। }-এ পৌঁছাতেই serve-এর পুরো frame মুছে গেল, x-ও তার সাথে। count frame-এ ছিল না, তাই সে 1 নিয়ে বসে আছে।
  5. দ্বিতীয় ডাক। একেবারে নতুন frame, নতুন x — আগেরটির কোনো স্মৃতি নেই, আবার 0 থেকে শুরু হয়ে 1. count 1 থেকে 2.
  6. আবার frame মুছল। Stack আবার শুধু main-এ ফিরে গেল। প্রতিটি ডাকের জন্য stack-এ জায়গা নেওয়া আর ছাড়া — এটি এত দ্রুত যে প্রায় খরচই নেই। এজন্যই automatic ভ্যারিয়েবল ডিফল্ট।
  7. তৃতীয় ডাক। একই গল্প: x প্রতিবার 1-এ আটকে, count হলো 3.
  8. প্রোগ্রাম শেষ। main ফিরল, তার frame মুছল, আর প্রোগ্রাম শেষ হতেই অপারেটিং সিস্টেম পুরো মেমরি ফেরত নিল — count-ও। “Static” মানে চিরকাল নয়; মানে প্রোগ্রাম যতক্ষণ.

শুরুর মান — static পায় শূন্য, automatic পায় আবর্জনা

পর্ব ০৬-এ দেখেছেন, মান না দেওয়া local ভ্যারিয়েবলের ভেতরে থাকে garbage value — stack-এর ওই জায়গায় আগে যা ছিল তাই। কিন্তু static আয়ুর ভ্যারিয়েবলের ক্ষেত্রে নিয়ম উল্টো: মান না দিলে C নিশ্চয়তা দেয় যে সংখ্যা হবে 0, দশমিক হবে 0.0, pointer হবে NULL, আর array বা struct-এর প্রতিটি ঘরও তাই। নিচের প্রোগ্রামে শুধু নিশ্চিত মানগুলোই ছাপা হচ্ছে — মান না দেওয়া local ছাপানোর চেষ্টা আমরা ইচ্ছে করেই করছি না, কারণ সেটি পড়াই undefined behavior.

#include <stdio.h>

int g_count;                 /* global, no initialiser: 0 */
static double g_rate;        /* file-private: 0.0 */
static int table[5];         /* every element: 0 */

int calls(void) {
    static int n;            /* static local: 0, set once */
    return ++n;
}

int main(void) {
    int local = 0;           /* automatic: we MUST give a value */
    static int *p;           /* static pointer: NULL */
    calls();
    calls();
    printf("g_count=%d g_rate=%.1f table[4]=%d\n", g_count, g_rate, table[4]);
    printf("calls=%d local=%d p is NULL: %s\n", calls(), local, p == NULL ? "yes" : "no");
    return 0;
}

আউটপুট হবে g_count=0 g_rate=0.0 table[4]=0 আর calls=3 local=0 p is NULL: yes. local ছাড়া বাকি পাঁচটিকে (g_count, g_rate, table, n, p) আমরা কোনো মান দিইনি, তবু ফল নিশ্চিত, কারণ সবগুলো static আয়ুর। local-কে কিন্তু নিজে 0 দিতে হয়েছে; = 0 মুছে দিলে -Wall সাধারণত সতর্ক করে যে মান না দিয়েই ব্যবহার হচ্ছে, আর ছাপা সংখ্যাটি হতে পারে যেকোনো কিছু। এর পেছনের কারণ সহজ: static ভ্যারিয়েবলের জায়গা প্রোগ্রাম শুরুর সময় একবার তৈরি হয়, তাই একবার শূন্য করার খরচ সামান্য। Automatic ভ্যারিয়েবল প্রতি ডাকে তৈরি হয় — প্রতিবার শূন্য করলে প্রোগ্রাম ধীর হতো, তাই C সেই কাজ আপনার ওপর ছেড়ে দিয়েছে।

আরেকটি পার্থক্য জেনে রাখা জরুরি: static ভ্যারিয়েবলের শুরুর মান হতে হবে compile করার সময়ই জানা একটি ধ্রুবক। static int n = 5; চলে, কিন্তু static int n = get_value(); C-তে error, কারণ এই মান main শুরুর আগেই প্রোগ্রাম ফাইলে বসিয়ে রাখতে হয়। (C++-এ এটি চলে — সেখানে নিয়ম আলাদা।)

৪ · Static local — যে ভ্যারিয়েবল মনে রাখে

হাসপাতালের বহির্বিভাগে (OPD) সকালে টিকিট কাউন্টারে লম্বা লাইন। কাউন্টারের মানুষটি প্রতিবার একটি সিরিয়াল নম্বর দেন — আগেরটির ঠিক পরেরটি। জরুরি বিভাগের কাউন্টারে আলাদা আরেকটি সিরিয়াল চলে, তার শুরু অন্য নম্বর থেকে। দুই কাউন্টারের কেউই অন্যের খাতায় হাত দেন না।

প্রোগ্রামে এই কাজের জন্য সবচেয়ে মানানসই হলো static local: নাম দেখা যায় শুধু ফাংশনের ভেতরে (scope ছোট), কিন্তু মান বেঁচে থাকে পুরো প্রোগ্রাম জুড়ে (আয়ু দীর্ঘ)। নিচের প্রোগ্রামে আরও দুটি জিনিস খেয়াল করুন — দুই ফাংশনেই last নামে ভ্যারিয়েবল আছে, আর main-এ একটি local total বাইরের global total-কে ঢেকে দিয়েছে:

#include <stdio.h>

int total = 0;                    /* file scope: tickets issued */

int opd_serial(void)       { static int last = 100; total++; return ++last; }
int emergency_serial(void) { static int last = 900; total++; return ++last; }

int main(void) {
    int a = opd_serial();
    int b = opd_serial();
    int c = emergency_serial();
    printf("OPD: %d %d, Emergency: %d\n", a, b, c);
    printf("Tickets issued (global total): %d\n", total);
    int total = a + b + c;        /* shadows the global from here on */
    printf("Sum of serials (local total): %d\n", total);
    return 0;
}

আউটপুট: OPD: 101 102, Emergency: 901, তারপর Tickets issued (global total): 3, শেষে Sum of serials (local total): 1104. দুই ফাংশনের দুটি last সম্পূর্ণ আলাদা বাক্স — নাম এক হলেও প্রত্যেকের scope নিজের ফাংশনে, তাই কোনো সংঘাত নেই। main-এর শেষ দিকে int total লেখার পর থেকে “total” মানে local-টি; ঠিক তার আগের লাইনেও ছিল global-টি। Scope শুরু হয় ঘোষণার জায়গা থেকে, block-এর শুরু থেকে নয়। এবার একটি পরীক্ষা করুন: opd_serial-এর static শব্দটি মুছে আবার চালান। এবার OPD-তে দুবারই 101 আসবে, কারণ last প্রতি ডাকে নতুন করে 100 থেকে শুরু হবে।

Static local কখন ভালো, কখন বিপদ

ভালো ব্যবহারের তালিকা ছোট কিন্তু কাজের: সিরিয়াল বা ID তৈরি, একবারই করতে হয় এমন প্রস্তুতি (“প্রথমবার ডাকা হলে টেবিল বানাও, পরে আর নয়”), ফাংশন কতবার ডাকা হলো তার হিসাব। প্রতিটি ক্ষেত্রেই সুবিধা হলো, স্মৃতিটি ফাংশনের ভেতরে লুকানো — বাইরের কেউ ভুল করে বদলাতে পারে না, যা global-এর চেয়ে অনেক নিরাপদ।

কিন্তু একটি দাম আছে। Static local মানে ফাংশনটির একটি গোপন স্মৃতি আছে, যা সবাই ভাগ করে নেয়। একই ইনপুটে ফাংশন একেক বার একেক ফল দেয়। দুটি thread একসাথে opd_serial() ডাকলে দুজনই একই নম্বর পেতে পারে (পর্ব ৪৯)। আর ফাংশনটি নিজের কাজ শেষ হওয়ার আগে আবার ডাকা হলে আগের কাজের অবস্থা নষ্ট হতে পারে — অর্থাৎ ফাংশনটি আর reentrant থাকে না। Standard library-র strtok এই সমস্যার বিখ্যাত উদাহরণ; ভাবনার প্রশ্নে বিস্তারিত দেখবেন।

৫ · Linkage — static, extern আর linker-এর হিসাব

বাস্তবের প্রোগ্রাম অনেকগুলো .c ফাইলে ভাগ করা থাকে, আর পর্ব ০৩-এ দেখেছেন প্রতিটি ফাইল আলাদাভাবে compile হয়ে .o হয়, শেষে linker সব জোড়া দেয়। তখন তৃতীয় প্রশ্নটি জরুরি হয়ে ওঠে: একটি ফাইলের নাম অন্য ফাইল দেখতে পায় কি না? উত্তর তিন রকম:

  • External linkage: সব ফাইল দেখতে পারে। ফাংশনের বাইরে লেখা সাধারণ global ভ্যারিয়েবল আর সাধারণ ফাংশন — ডিফল্টভাবে এই দলে।
  • Internal linkage: শুধু নিজের ফাইল দেখে। ফাংশনের বাইরে static লিখলে এটি হয়।
  • No linkage: linker-এর সাথে কোনো সম্পর্কই নেই — সব local ভ্যারিয়েবল, এমনকি static local-ও।

এখানেই static শব্দের দ্বিতীয় অর্থ। ফাংশনের বাইরে static আয়ু বদলায় না (global-এর আয়ু এমনিতেই static), বদলায় শুধু linkage — “এই নাম আমার ফাইলের নিজস্ব।” আর extern লিখে বলা হয়: “এই নামের জিনিস আছে, তবে অন্য কোথাও তৈরি — আমি শুধু ব্যবহার করব।”

Declaration বনাম definition

extern int total; হলো declaration — compiler-কে শুধু নাম আর টাইপ জানায়, কোনো মেমরি নেয় না। int total = 0; হলো definition — আসল বাক্সটি এখানেই তৈরি হয়। Declaration যত খুশি থাকতে পারে, কিন্তু পুরো প্রোগ্রামে definition থাকবে ঠিক একটি। এই নিয়মকে বলে one definition rule.

Linker আসলে কী দেখে? প্রতিটি .o ফাইলের ভেতরে একটি symbol-এর তালিকা থাকে, যা nm নামের টুল দিয়ে দেখা যায়। নিচে দুটি ফাইলের একটি ছোট প্রোগ্রাম — “পরে” চেপে linker-এর চোখ দিয়ে দেখুন:

a.ctotal, calls, log_call, add
a.o — বাইরে খোলাT add · B total
a.o — শুধু ভেতরেরb calls · t log_call
b.cmain, extern total
b.o — দরকারU add · U total · U printf
b.o — আছেT main
Linker: প্রতিটি U-এর জন্য একটি T/B/D খোঁজেনা পেলে undefined reference, দুটি পেলে multiple definition
a.cহিসাবের ফাইল
int total = 0;               /* definition, external linkage */
static int calls;            /* internal: শুধু a.c-র */

static void log_call(void) { calls++; }   /* গোপন helper */

void add(int x) {
    log_call();
    total += x;
}
$ nm a.oঠিকানার কলাম বাদ দিয়ে দেখানো
T add        /* বড় হাতের T: code, বাইরে খোলা */
b calls      /* ছোট হাতের b: bss, শুধু এই ফাইলের */
t log_call   /* ছোট হাতের t: code, শুধু এই ফাইলের */
B total      /* বড় হাতের B: bss, বাইরে খোলা */
b.cমূল প্রোগ্রাম
#include <stdio.h>

extern int total;            /* declaration: আছে অন্য কোথাও */
void add(int x);             /* ফাংশনের prototype-ও declaration */

int main(void) {
    add(5);
    add(7);
    printf("total = %d\n", total);
    return 0;
}
$ nm b.oU মানে “আমার দরকার, কিন্তু আমার কাছে নেই”
U add
T main
U printf
U total
$ gcc a.o b.o -o app && ./appসব মিলেছে
U total  (b.o)  →  B total  (a.o)   ✔
U add    (b.o)  →  T add    (a.o)   ✔
U printf (b.o)  →  libc             ✔
total = 12
ভুল ১: b.c-তেও লিখলেন int total = 5;দুটি definition
/usr/bin/ld: b.o: multiple definition of `total';
a.o: first defined here
collect2: error: ld returned 1 exit status
ভুল ২: b.c-তে লিখলেন extern int calls;static জিনিস বাইরে থেকে চাওয়া
/usr/bin/ld: b.o: undefined reference to `calls'
collect2: error: ld returned 1 exit status
  1. a.c — দুই রকম নাম। total আর add সাধারণ global, তাই external linkage. calls আর log_call-এর সামনে static, তাই এরা a.c-র ঘরোয়া জিনিস। log_call-এর মতো ছোট সাহায্যকারী ফাংশনকে static রাখাই পেশাদার অভ্যাস।
  2. a.o-র symbol তালিকা। nm-এর অক্ষর পড়তে শিখুন: T = code, D = মান দেওয়া data, B = শূন্য বা মান না দেওয়া data (BSS). বড় হাতের অক্ষর মানে বাইরে খোলা, ছোট হাতের মানে শুধু এই ফাইলের। static লেখার ফলেই calls আর log_call ছোট হাতের হয়েছে।
  3. b.c — ব্যবহারকারী ফাইল। extern int total; কোনো মেমরি নেয় না — শুধু compiler-কে বলে, “total নামে একটি int আছে, বিশ্বাস করো।” Compiler বিশ্বাস করে কোড বানায়, আর আসল ঠিকানাটি খুঁজে বের করার দায়িত্ব রেখে দেয় linker-এর জন্য।
  4. b.o-র তালিকা। তিনটি U — “undefined”, অর্থাৎ “এগুলো আমার দরকার, কিন্তু আমার কাছে নেই।” main আছে T হিসেবে। খেয়াল করুন, calls বা log_call-এর কোনো উল্লেখই নেই — b.c এদের অস্তিত্বই জানে না।
  5. Linker মেলায়। প্রতিটি U-এর জন্য সে বাকি ফাইলগুলোতে বড় হাতের একটি মিল খোঁজে। total আর add পাওয়া গেল a.o-তে, printf standard library-তে। সব মিলেছে, তাই প্রোগ্রাম তৈরি হলো, আর চালালে 5 + 7 = 12 ছাপা হলো।
  6. ভুল ১ — দুটি definition. b.c-তেও int total = 5; লিখলে দুটি ফাইলেই বড় হাতের total. Linker বুঝতে পারে না কোনটি আসল — multiple definition. মনে রাখবেন, compiler এখানে কিছু ধরতে পারে না, কারণ সে একবারে একটি ফাইলই দেখে। এই ভুল ধরা পড়ে শুধু link করার সময়।
  7. ভুল ২ — গোপন জিনিস চাওয়া। b.c থেকে extern int calls; লিখে ব্যবহার করলে linker খুঁজবে বড় হাতের calls — কিন্তু a.o-তে আছে শুধু ছোট হাতের। ফল: undefined reference. এটিই static-এর পাহারা — ফাইলের ভেতরের জিনিস বাইরে থেকে ধরা যায় না।

Header-এ extern, একটি .c-তে definition

একটি global কয়েকটি ফাইলে লাগলে প্রতিটি ফাইলে আলাদা করে extern লেখা ঝুঁকিপূর্ণ — কোথাও টাইপ ভুল হলে compiler ধরতে পারবে না। পেশাদার নিয়মটি এরকম:

/* config.h  — সবাই এটি include করে */
extern int g_verbose;          /* declaration only */

/* config.c  — ঠিক একটি ফাইলে */
#include "config.h"
int g_verbose = 0;             /* the one definition */

সবাই config.h include করে বলে সবার কাছে একই declaration যায়, আর config.c নিজেও header include করায় compiler মিলিয়ে দেখতে পারে যে definition আর declaration-এর টাইপ এক। বিস্তারিত পর্ব ২৯-এ, মডুলার প্রোগ্রামে। দুটি বিপজ্জনক ভুল এখনই জেনে রাখুন:

  • Header-এ int g_verbose = 0; লেখা: যত .c ফাইল header include করবে, ততগুলো definition তৈরি হবে — linker দেবে multiple definition.
  • Header-এ static int g_verbose; লেখা: এটি আরও খারাপ, কারণ কোনো error আসে না! প্রতিটি .c ফাইল নিজের একটি গোপন কপি পায়। এক ফাইল মান বদলায়, অন্য ফাইল পুরোনো মান দেখে — নীরব বাগ।

পুরোনো Unix compiler-গুলো int total;-এর মতো মান ছাড়া global (C-এর ভাষায় tentative definition) কয়েকটি ফাইলে থাকলেও চুপচাপ একটিতে মিলিয়ে দিত — একে বলা হতো “common” symbol. ফলে অনেক পুরোনো প্রজেক্ট না জেনেই এই ভুলের ওপর চলত। ২০২০ সালে GCC 10 ডিফল্ট বদলে -fno-common করে দেয়, আর হঠাৎ বহু পুরোনো প্রজেক্ট multiple definition error দিতে শুরু করে। শিক্ষা: “চলছে” আর “নিয়ম মেনে চলছে” এক কথা নয়।

৬ · auto, register আর _Thread_local — বাকি তিন শব্দ

“C-এর storage class কয়টি?” — পরীক্ষার প্রস্তুতিতে এটি একটি পরিচিত প্রশ্ন। পুরোনো বইয়ের উত্তর চারটি: auto, register, static, extern. আধুনিক C-তে আছে আরও একটি — _Thread_local (আর ব্যাকরণের সুবিধার জন্য typedef-কেও এই দলে গোনা হয়; C23-এ constexpr-ও এই তালিকায় যোগ হয়েছে)। আজকের দিনে auto আর register প্রায় কেউ লেখে না, কিন্তু অর্থ জানা দরকার।

auto — পুরোনো অর্থ

“এটি automatic ভ্যারিয়েবল।” কিন্তু ফাংশনের ভেতরের ভ্যারিয়েবল এমনিতেই automatic, তাই auto int x; আর int x; হুবহু এক। এজন্য শব্দটি দশকের পর দশক অব্যবহৃত পড়ে ছিল।

C89 থেকে C17

auto — C23-এর নতুন কাজ

C23 মান থেকে auto x = 3.5; লিখলে compiler শুরুর মান দেখে নিজেই টাইপ ঠিক করে (এখানে double) — একে বলে type inference. C++ ২০১১ সালেই auto-কে এই কাজে লাগিয়েছিল; C23 সীমিতভাবে সেই পথে হেঁটেছে। পুরোনো অর্থটিও C23-তে বাতিল হয়নি।

C23 · পর্ব ৪৭

register — ইতিহাসের অনুরোধ

“ভ্যারিয়েবলটি CPU-র register-এ রাখার চেষ্টা করো।” আজকের compiler নিজেই এই সিদ্ধান্ত মানুষের চেয়ে ভালো নেয়, তাই অনুরোধটি সাধারণত উপেক্ষিত হয়। একটি নিয়ম কিন্তু এখনো খাটে: register ভ্যারিয়েবলের ঠিকানা (&x) নেওয়া যায় না — compile error.

C++17 শব্দটির এই অর্থ বাদ দিয়েছে

_Thread_local — প্রতি thread-এর নিজস্ব

C11-এ যোগ হয়েছে। Static-এর মতো দীর্ঘ আয়ু, কিন্তু প্রতিটি thread পায় নিজের আলাদা কপি। C23-এ একে thread_local নামেও লেখা যায়। ব্যবহার দেখবেন পর্ব ৪৯-এ।

C11 · C23-এ thread_local

auto শব্দটি এসেছে C-এর পূর্বসূরি B ভাষা থেকে, যা Ken Thompson Bell Labs-এ তৈরি করেছিলেন। B-তে local ভ্যারিয়েবল ঘোষণা করতে auto লেখা বাধ্যতামূলক ছিল, কারণ B-তে int-এর মতো কোনো টাইপ শব্দই ছিল না। বাইরের ভ্যারিয়েবল ব্যবহার করতে লেখা হতো extrn. C টাইপ যোগ করার পর auto অপ্রয়োজনীয় হয়ে যায়, কিন্তু পুরোনো কোড চালু রাখার জন্য রেখে দেওয়া হয়। প্রায় পঞ্চাশ বছর পর C23 সেই অলস শব্দটিকে নতুন কাজ দিয়েছে।

৭ · মেমরির মানচিত্রে কে কোথায় — আর local-এর ঠিকানার ফাঁদ

এ পর্যন্ত যা শিখলেন, সব একটি ছবিতে আনা যায়। প্রোগ্রাম চালালে অপারেটিং সিস্টেম তাকে কয়েকটি ভাগে সাজানো একটি মেমরি-এলাকা দেয় — একে বলে memory map. পর্ব ২০-এ এটি বিস্তারিত দেখবেন; এখানে শুধু “কোন ভ্যারিয়েবল কোন ভাগে” প্রশ্নটির উত্তর:

মেমরির ভাগকী থাকেআয়ু
Textফাংশনের machine code; পাশের read-only অংশে string literal ও const globalপুরো প্রোগ্রাম
Dataশূন্য ছাড়া অন্য মান দেওয়া global ও static: int rate = 15;পুরো প্রোগ্রাম
BSSমান না দেওয়া বা 0 দেওয়া global ও static: static int calls;পুরো প্রোগ্রাম, শুরুতে সব শূন্য
Heapmalloc দিয়ে নেওয়া blockfree পর্যন্ত
Stackautomatic local, parameter, ফেরার ঠিকানাBlock বা ফাংশন শেষ পর্যন্ত

BSS নিয়ে একটি মজার তথ্য: static int big[1000000]; লিখলেও আপনার প্রোগ্রাম ফাইল প্রায় একটুও বড় হয় না। কারণ ফাইলে লেখা থাকে শুধু “এত বাইট শূন্য জায়গা লাগবে”, আর প্রোগ্রাম খোলার সময় অপারেটিং সিস্টেম সেই জায়গা শূন্য দিয়ে ভরে দেয়। অন্যদিকে Data অংশের প্রতিটি মান ফাইলের ভেতরে আসলেই লেখা থাকে।

ফাংশন শেষ, বাক্সও শেষ — কিন্তু ঠিকানা রয়ে গেল

এখন একটি ক্লাসিক বাগ বোঝার সব উপকরণ আপনার হাতে। ফাংশন যদি নিজের local ভ্যারিয়েবলের ঠিকানা ফেরত দেয়, তাহলে কী হয়? & আর * চিহ্নের পুরো ব্যাখ্যা আসবে পর্ব ১৭-এ; এখন শুধু জানুন, &t মানে “t-এর ঠিকানা” আর *p মানে “p যে ঠিকানা ধরে আছে, সেখানে যা আছে”। ধাপে ধাপে দেখুন:

1#include <stdio.h>23int *make_token(void) {4    int t = 101;          // automatic5    return &t;            // বাগ!6}78void noisy(void) {9    int junk = 999;10    (void)junk;11}1213int main(void) {14    int *p = make_token();15    noisy();16    printf("%d\n", *p);   // ?17    return 0;18}
Stack-এর একটি ঘর
make_token-এর frame
ঘরটিতে এখন
p ধরে আছে
  1. Compiler আগেই সাবধান করছে। GCC এই ভুলটি সাধারণত নিজেই ধরে: function returns address of local variable. Warning উপেক্ষা না করার আরও একটি কারণ। তবে ঠিকানাটি একটু ঘুরিয়ে ফেরত দিলে (যেমন আগে একটি pointer-এ রেখে) compiler অনেক সময় আর ধরতে পারে না।
  2. make_token চলছে। তার frame-এ t = 101. এই মুহূর্তে &t একটি বৈধ ঠিকানা।
  3. ফাংশন ফিরল। Frame মুছে গেল, t-এর আয়ু শেষ। কিন্তু p-এর ভেতরে সেই ঘরের ঠিকানা এখনো লেখা আছে — যেমন কেউ বাসা ছেড়ে চলে গেলেও পুরোনো ঠিকানা লেখা চিরকুট আপনার পকেটে রয়ে যায়। “মুছে যাওয়া” মানে মান শূন্য হওয়া নয়; মানে ঘরটি এখন যে কেউ ব্যবহার করতে পারে।
  4. নতুন ভাড়াটে এল। noisy() ডাকা হলো, তার frame বসল stack-এর ঠিক একই জায়গায়। খুব সম্ভব junk = 999 বসেছে সেই ঘরেই, যেটির ঠিকানা p ধরে আছে।
  5. *p পড়া — undefined behavior. ফল হতে পারে 999, 101, আবর্জনা, বা crash. Compiler-এর সংস্করণ, optimization বা কম্পিউটার বদলালেই ফল বদলায়। সবচেয়ে ভয়ের ব্যাপার, অনেক সময় “ঠিকঠাক” 101 আসে — পরীক্ষায় পাস করে যায়, তারপর আসল ব্যবহারে ভেঙে পড়ে।
  6. তিনটি সঠিক সমাধান। (ক) static int t — আয়ু দীর্ঘ, কিন্তু সব ডাক একই বাক্স ভাগ করে। (খ) malloc দিয়ে heap-এ জায়গা নেওয়া — যে ডাকল সে-ই পরে free করবে (পর্ব ২০)। (গ) সবচেয়ে পরিষ্কার: যে ডাকছে সে-ই জায়গা দেবে — void make_token(int *out). পর্ব ১৯-এ string ফেরত দেওয়ার সময় এই প্রশ্নটি আবার আসবে।
এক লাইনের নিয়ম

কোনো ঠিকানা যতক্ষণ ব্যবহার করবেন, ঠিকানার বাক্সটির আয়ু অন্তত ততক্ষণ হতে হবে। Pointer-এর প্রায় সব বাগ এই নিয়ম ভাঙার ফল — আর নিয়মটি বুঝতে লাগে ঠিক এই পর্বের storage duration.

৮ · Global ভ্যারিয়েবল — সুবিধাজনক, কিন্তু দামি

নতুনরা প্রায়ই global ভালোবাসেন: parameter পাঠানোর ঝামেলা নেই, যেকোনো ফাংশন থেকে সরাসরি পড়া-লেখা যায়। ৫০ লাইনের প্রোগ্রামে এতে ক্ষতিও কম। কিন্তু প্রোগ্রাম ৫০ হাজার লাইনে পৌঁছালে একই সুবিধা পরিণত হয় সবচেয়ে বড় মাথাব্যথায়।

একটি অফিসের কথা ভাবুন, যেখানে সবার ব্যবহারের জন্য একটি হোয়াইটবোর্ড আছে। যে কেউ লিখতে পারে, যে কেউ মুছতে পারে। একদিন সকালে দেখা গেল বোর্ডে লেখা মিটিংয়ের সময় ভুল। কে বদলেছে? কখন? কেন? খুঁজতে হবে পুরো অফিস। Global ভ্যারিয়েবল ঠিক এই হোয়াইটবোর্ড।

  • লুকানো নির্ভরতা: int calc_fee(int days) দেখে মনে হয় ফল নির্ভর করে শুধু days-এর ওপর। কিন্তু ভেতরে যদি global g_rate পড়া হয়, তাহলে ফাংশনের মাথা মিথ্যা বলছে। একটি ফাংশন বদলালে অন্য কোথাও কিছু ভাঙে, আর কেউ বুঝতে পারে না দুটির মধ্যে সম্পর্ক কী।
  • পরীক্ষা করা কঠিন: ফাংশনটি পরীক্ষা করতে হলে আগে সব প্রাসঙ্গিক global ঠিক অবস্থায় আনতে হয়, পরীক্ষা শেষে আবার আগের অবস্থায় ফেরাতে হয়। একটি পরীক্ষার ফল নির্ভর করে আগের পরীক্ষা কী রেখে গেছে তার ওপর (পর্ব ৩২)।
  • একসাথে কাজ: দুটি thread একই global একসাথে বদলালে data race — ফল অনিশ্চিত (পর্ব ৪৯)।
  • নামের সংঘাত: C-তে সব external নাম একটিমাত্র বিশাল তালিকায় থাকে। আপনার count আর অন্য কারও library-র count মিলে গেলে linker error, বা আরও খারাপ, ভুল জিনিসের সাথে জোড়া।

তাহলে কী করবেন? নিচের ক্রমে ভাবুন — প্রথম যেটি কাজ করে, সেটিই নিন:

  1. ডেটা দরকার হলে parameter দিয়ে পাঠান, ফল দরকার হলে return দিয়ে নিন।
  2. অনেকগুলো সম্পর্কিত মান একসাথে ঘুরলে একটি struct বানিয়ে তার ঠিকানা পাঠান (পর্ব ২২)।
  3. একটি ফাংশনের নিজের স্মৃতি দরকার হলে static local.
  4. একটি ফাইলের কয়েকটি ফাংশনের মধ্যে ভাগ করতে হলে file-scope static.
  5. সত্যিই পুরো প্রোগ্রামে লাগলে তবেই external global — header-এ extern, একটি .c-তে definition, আর নামের আগে প্রজেক্টের prefix.

পরীক্ষা ও ইন্টারভিউতে — বিশ্ববিদ্যালয়ের পরীক্ষা, সরকারি বা ব্যাংকের প্রোগ্রামার পদের পরীক্ষা আর কোডিং ইন্টারভিউয়ের জন্য এই বিষয়ে যেমন প্রশ্নের উত্তর তৈরি রাখা ভালো:

  • মান না দেওয়া global আর local-এর শুরুর মান কী? Global ও static: 0. Automatic local: অনির্দিষ্ট (garbage), পড়া undefined behavior.
  • Static local আর global-এর পার্থক্য কী? দুটির আয়ুই পুরো প্রোগ্রাম; কিন্তু static local-এর scope শুধু নিজের block, global-এর scope পুরো ফাইল (এবং linkage external).
  • extern int x; কি মেমরি নেয়? না, এটি শুধু declaration; definition থাকে অন্য কোনো একটি জায়গায়।
  • register ভ্যারিয়েবলের ঠিকানা নেওয়া যায়? না — & দিলে compile error.
  • ফাংশন থেকে local array-র ঠিকানা ফেরত দিলে কী হয়? Dangling pointer, ব্যবহার করলে undefined behavior; সমাধান static, malloc অথবা caller-এর দেওয়া buffer.

৯ · AI-এর যুগে scope ও storage

AI-কে “একটা ছোট inventory প্রোগ্রাম লিখে দাও” বললে খুব দ্রুত চলে এমন কোড পাবেন। কিন্তু খেয়াল করলে দেখবেন, scope আর storage নিয়ে কিছু ভুল বারবার ফিরে আসে — কারণ AI শিখেছে ইন্টারনেটের লাখো ছোট উদাহরণ থেকে, যেখানে “চলছে” ছিল যথেষ্ট, “বড় হলেও টিকবে” কেউ ভাবেনি।

AI সাধারণত যেসব scope ও storage-ভুল করে

AI-এর ভুলকেন সমস্যাকী চাইবেন
সব ডেটা global-এ রাখা (int items[100]; int count; ফাইলের মাথায়)লুকানো নির্ভরতা, পরীক্ষা কঠিন, thread-এ বিপদstruct বানিয়ে parameter দিয়ে পাঠানো
Local array-র ঠিকানা ফেরত দেওয়া (char buf[64]; … return buf;)Dangling pointer — কখনো চলে, কখনো ভাঙেcaller-এর দেওয়া buffer, নয়তো malloc ও স্পষ্ট মালিকানা
সাহায্যকারী ফাংশনে static না দেওয়াঅপ্রয়োজনে সব নাম বাইরে খোলা, নামের সংঘাতের ঝুঁকিফাইলের বাইরে না লাগলে static
Header-এ definition রাখা (int g_debug = 0;)দুটি ফাইল include করলেই multiple definitionheader-এ extern, একটি .c-তে definition
Static local দিয়ে buffer ফেরত দেওয়া, অথচ সেটি না জানানোপরের ডাকে আগের ফল মুছে যায়; thread-safe নয়ফাংশনের মন্তব্যে স্পষ্ট লেখা, বা caller-এর buffer
Local-এ মান না দিয়ে ব্যবহার (int sum; for … sum += x;)Garbage থেকে যোগ শুরু — ফল এলোমেলোঘোষণার সাথেই শুরুর মান

AI-এর কাছ থেকে কোড নেওয়ার সময়ই নিয়মগুলো বলে দিন। শেষ অংশটি খেয়াল করুন — প্রতিটি ভ্যারিয়েবলের scope আর আয়ু AI-কে নিজে ব্যাখ্যা করতে বললে সে নিজের ভুল অনেক সময় নিজেই ধরে ফেলে, আর আপনিও মিলিয়ে দেখতে পারেন:

C-তে একটি ছোট লাইব্রেরি-ব্যবস্থাপনা প্রোগ্রাম লিখে দাও, দুটি ফাইলে (book.c ও main.c) আর একটি header (book.h). নিয়মগুলো মেনে চলবে: ১. কোনো global ভ্যারিয়েবল রাখবে না; ডেটা struct-এ রেখে parameter দিয়ে পাঠাবে। ২. book.c-র যেসব সাহায্যকারী ফাংশন বাইরে লাগে না, সেগুলো static হবে। ৩. কোনো ফাংশন local ভ্যারিয়েবল বা local array-র ঠিকানা ফেরত দেবে না। ৪. header-এ শুধু declaration থাকবে, কোনো definition নয়। ৫. সব local ভ্যারিয়েবলের ঘোষণার সাথেই শুরুর মান দেবে। কোড দেওয়ার পর একটি টেবিল দাও: প্রতিটি ভ্যারিয়েবল ও ফাংশনের নাম, তার scope, storage duration আর linkage.

AI-এর কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১C-তে namespace নেই। তাহলে লাখো লাইনের প্রজেক্টে হাজার হাজার ফাংশনের নাম মিলে যায় না কেন?

C-তে external linkage-এর সব নাম linker-এর কাছে একটিমাত্র সমতল তালিকায় থাকে। আপনার প্রোগ্রাম, standard library আর আপনি যত library জুড়েছেন — সবার init, parse, count একই তালিকায়। দুটি মিলে গেলে হয় linker error, নয়তো ভুল ফাংশনের সাথে চুপচাপ জোড়া। C++ এই সমস্যার সমাধানে namespace এনেছে; C কখনো আনেনি, কারণ C-এর দর্শন হলো ভাষা ছোট রাখা আর linker-এর সাথে সরাসরি, অনুমানযোগ্য সম্পর্ক রাখা। তাই বড় C প্রজেক্টগুলো কয়েকটি শৃঙ্খলা মেনে চলে:

  • Prefix: প্রতিটি বাইরে-খোলা নামের আগে প্রজেক্টের নাম। SQLite-এর সব public ফাংশন শুরু হয় sqlite3_ দিয়ে (sqlite3_open, sqlite3_exec), POSIX thread-এর pthread_, GTK-এর gtk_, SDL-এর SDL_. দেখতে লম্বা, কিন্তু সংঘাত প্রায় অসম্ভব।
  • যা বাইরে লাগে না, তা static: এতে নামগুলো তালিকাতেই ওঠে না। ভালো C ফাইলে public ফাংশন হাতে গোনা, বাকি সব static.
  • Shared library-তে visibility: GCC ও Clang-এ -fvisibility=hidden দিয়ে সব নাম ডিফল্টভাবে লুকিয়ে শুধু বেছে নেওয়া কয়েকটি খোলা রাখা যায়। এতে library দ্রুত লোড হয়, আর ভুল করে ভেতরের ফাংশনের ওপর কেউ নির্ভর করতে পারে না।
  • সংরক্ষিত নাম এড়ানো: C-এর নিয়মে underscore আর বড় হাতের অক্ষর দিয়ে শুরু (_Name) বা দুটি underscore (__x) দিয়ে শুরু নাম compiler ও standard library-র জন্য সংরক্ষিত। নিজের কোডে এগুলো ব্যবহার করলে ভবিষ্যতের কোনো সংস্করণের সাথে সংঘাত হতে পারে — _Bool, _Thread_local ঠিক এভাবেই যোগ হয়েছে।

মূল উপলব্ধি: ভাষা যে সুবিধা দেয় না, শৃঙ্খলা দিয়ে তা পূরণ করা যায়। C-এর namespace হলো “prefix + static” — নিয়ম মানলে কাজ করে, না মানলে কেউ আটকায় না। বড় প্রজেক্টে নামকরণের নিয়ম তাই রুচির ব্যাপার নয়, প্রকৌশলের ব্যাপার।

প্র ০২Static local তো নিরাপদ শোনায়। তাহলে strtok-কে কেন “বিপজ্জনক” বলা হয়, আর strtok_r কেন এল?

strtok একটি string-কে টুকরো করে — যেমন "Dhaka,Khulna,Sylhet" থেকে একটি একটি শহর। প্রথম ডাকে আপনি string দেন, পরের ডাকগুলোতে দেন NULL, আর strtok “মনে রাখে” কোথায় থেমেছিল। এই স্মৃতি থাকে তার ভেতরের একটি static ভ্যারিয়েবলে — ঠিক এই পর্বের static local.

সমস্যা দেখা দেয় তিন জায়গায়:

  • নেস্টেড ব্যবহার: প্রতিটি লাইনকে কমা দিয়ে ভাঙছেন, আর ভেতরে প্রতিটি টুকরোকে আবার - দিয়ে ভাঙতে strtok ডাকলেন। ভেতরের ডাক static স্মৃতিটি নিজের মতো বদলে দিল — বাইরের লুপ হারিয়ে গেল কোথায় ছিল। দুটি কাজ একটিমাত্র স্মৃতি ভাগ করছে।
  • Thread: দুটি thread একসাথে আলাদা string ভাঙলে একে অপরের স্মৃতি নষ্ট করে।
  • Signal handler বা callback: কাজের মাঝখানে কোনো কারণে একই ফাংশন আবার ঢুকলে (re-entry) একই বিপর্যয়।

যে ফাংশন মাঝপথে আবার ডাকা হলেও ঠিক কাজ করে, তাকে বলে reentrant. সমাধানের কৌশলটি সরল: স্মৃতিটি ফাংশনের ভেতর থেকে বের করে ডাককারীর হাতে দেওয়া। POSIX-এর strtok_r একটি বাড়তি parameter নেয় — char **saveptr — যেখানে ডাককারী নিজে অবস্থা রাখে। দুটি আলাদা কাজ, দুটি আলাদা saveptr, কোনো সংঘাত নেই। একই গল্প localtime বনাম localtime_r, asctime-এর ভেতরের static buffer, আর rand-এর লুকানো অবস্থার ক্ষেত্রেও।

একটি চমৎকার উদাহরণ errno: একসময় এটি সাধারণ global ছিল। Thread আসার পর এক thread-এর error অন্য thread-এর errno মুছে দিত। তাই POSIX নিয়ম করে দিয়েছে যে প্রতিটি thread-এর errno আলাদা — আসলে এটি thread-local storage-এর বাস্তব ব্যবহার।

মূল উপলব্ধি: লুকানো অবস্থা (hidden state) একটি ফাংশনকে একসাথে একটির বেশি কাজের অযোগ্য করে দেয়। আধুনিক নকশার নিয়ম: অবস্থা থাকলে সেটি স্পষ্টভাবে parameter হয়ে ঢুকুক — তাহলে কে কোন অবস্থার মালিক, কোড দেখেই বোঝা যায়।

প্র ০৩“Global ভ্যারিয়েবল থাকলে unit test লেখা কঠিন” — কথাটি ঠিক কেন? পেশাদাররা কীভাবে এড়ান?

একটি ভালো unit test-এর তিনটি গুণ: একই ইনপুটে সবসময় একই ফল, অন্য পরীক্ষার ওপর নির্ভরতা নেই, আর যেকোনো ক্রমে চালানো যায়। Global ভ্যারিয়েবল তিনটিই ভাঙে।

ধরুন int apply_discount(int price) ভেতরে একটি global g_discount_percent পড়ে। পরীক্ষা লিখতে গিয়ে প্রথম সমস্যা — ফাংশনের মাথা দেখে আপনি জানতেই পারবেন না যে এই global ঠিক করে রাখতে হবে। দ্বিতীয় সমস্যা — একটি পরীক্ষা global-কে 10 করে রেখে গেল, পরের পরীক্ষা ধরে নিয়েছিল 0. আলাদা চালালে দুটিই পাস, একসাথে চালালে একটি ফেল। এমন “কখনো পাস, কখনো ফেল” পরীক্ষাকে বলে flaky test, আর এগুলো দলের সময় খেয়ে ফেলে। তৃতীয় সমস্যা — পরীক্ষাগুলো একসাথে কয়েকটি thread-এ চালাতে চাইলে global নিয়ে কাড়াকাড়ি শুরু হয়।

পেশাদাররা যা করেন:

  • Dependency injection: ফাংশন যা পড়ে, তা parameter হিসেবে চায় — apply_discount(price, percent). এখন পরীক্ষায় যেকোনো মান সরাসরি দেওয়া যায়।
  • Context struct: অনেকগুলো সেটিং থাকলে একটি struct config বানিয়ে তার ঠিকানা পাঠানো। SQLite-এর sqlite3 *db বা অনেক library-র “handle” ঠিক এই নকশা — প্রতিটি সংযোগের নিজের অবস্থা, কোনো global নেই। এজন্যই একই প্রোগ্রামে দুটি আলাদা ডেটাবেস একসাথে খোলা যায়।
  • অবস্থা আর হিসাব আলাদা করা: হিসাবের অংশটি pure function রাখা (শুধু ইনপুট থেকে আউটপুট), আর অবস্থা বদলের অংশটি পাতলা একটি স্তরে রাখা।

পর্ব ৩২-এ টেস্টিং শেখার সময় দেখবেন, যে কোড সহজে পরীক্ষা করা যায়, সেটি প্রায় সবসময় ভালো নকশারও কোড — কারণ দুটোই একই জিনিস চায়: স্পষ্ট ইনপুট, স্পষ্ট আউটপুট।

মূল উপলব্ধি: Global ভ্যারিয়েবল আসলে প্রতিটি ফাংশনের একটি অদৃশ্য parameter. অদৃশ্য জিনিস পরীক্ষা করা যায় না, নথিভুক্ত করা যায় না, আর ভুলে যাওয়া সহজ। “দৃশ্যমান করো” — এটিই সমাধান।

প্র ০৪Linker ঠিক কীভাবে symbol মেলায়? Library-র ক্রম বদলালে কেন হঠাৎ “undefined reference” আসে?

Linker-এর কাজকে দুই ভাগে দেখা যায়। প্রথম ভাগ — নাম মেলানো (symbol resolution): প্রতিটি .o ফাইলের symbol তালিকায় আছে “আমার কাছে আছে” (T, D, B) আর “আমার দরকার” (U). Linker সব তালিকা একসাথে করে প্রতিটি U-এর জন্য ঠিক একটি সংজ্ঞা খোঁজে। একটিও না পেলে undefined reference, একাধিক পেলে multiple definition. ছোট হাতের অক্ষরের (static) নামগুলো এই খেলায় নামেই না। দ্বিতীয় ভাগ — ঠিকানা বসানো (relocation): সব অংশ মেমরিতে কোথায় বসবে ঠিক করে, তারপর পর্ব ০৩-এ দেখা ?? ?? ?? ?? ফাঁকা জায়গাগুলোতে আসল ঠিকানা লিখে দেয়।

Static library-র ক্রমের রহস্য: .a ফাইল আসলে অনেকগুলো .o-র একটি বান্ডিল। GNU linker কমান্ড লাইন বাঁ থেকে ডানে একবার পড়ে। একটি library-তে পৌঁছালে সে শুধু সেই .o-গুলো নেয়, যেগুলো এই মুহূর্তে বাকি থাকা কোনো U পূরণ করে। তাই gcc -lmylib main.o লিখলে library আগে পড়া হয়, তখনো কোনো U নেই, তাই কিছুই নেওয়া হয় না; পরে main.o এসে U রেখে গেলে আর ফিরে দেখা হয় না — undefined reference. সঠিক ক্রম: যে ব্যবহার করে সে আগে, যাকে ব্যবহার করা হয় সে পরে — gcc main.o -lmylib.

Tentative definition ও GCC 10: এই পর্বে দেখেছেন, মান ছাড়া global (int total;) পুরোনো দিনে “common” symbol হতো, আর কয়েকটি ফাইলে থাকলেও linker চুপচাপ একটিতে মিলিয়ে দিত। এতে বাগ লুকিয়ে থাকত — দুটি ফাইল ভিন্ন টাইপ দিয়েও একই নাম ব্যবহার করতে পারত। GCC 10 থেকে ডিফল্ট -fno-common, ফলে এখন এটিও স্পষ্ট multiple definition.

আধুনিক মোড়: Link-time optimization (-flto) চালু করলে linker পুরো প্রোগ্রাম একসাথে দেখে ফাইলের সীমানা পেরিয়েও inline ও অপ্রয়োজনীয় কোড বাদ দিতে পারে। এখানেও static সাহায্য করে — compiler নিশ্চিত জানে বাইরের কেউ এই ফাংশন ডাকবে না।

মূল উপলব্ধি: Linker কোনো জাদু নয় — নামের একটি হিসাবরক্ষক। “কে দেয়, কে চায়” এই তালিকা পড়তে জানলে (nm) linker-এর প্রায় সব error কয়েক মিনিটে বোঝা যায়।

প্র ০৫অন্য ভাষাগুলো scope আর lifetime কীভাবে সামলায়? JavaScript বা Python-এর closure কি আসলে static local-এর আত্মীয়?

Scope-এর ধারণা প্রায় সব ভাষায় আছে, কিন্তু নিয়মে মজার পার্থক্য আছে:

  • JavaScript: পুরোনো var ছিল function scope-এর — if বা লুপের ভেতরে ঘোষণা করলেও পুরো ফাংশনে দেখা যেত। এতে এত বাগ হতো যে ২০১৫ সালের ES2015 (ES6) সংস্করণে let আর const আনা হয়, যা C-এর মতো block scope মানে।
  • Python: if বা for নতুন scope তৈরি করে না — লুপ শেষেও লুপের ভ্যারিয়েবল টিকে থাকে, যা C-প্রোগ্রামারদের অবাক করে। নাম খোঁজার ক্রম “LEGB”: Local, Enclosing, Global, Built-in. ফাংশনের ভেতর থেকে global বদলাতে চাইলে স্পষ্ট করে global লিখতে হয় — ভাষা নিজেই global-এর ব্যবহারে একটু বাধা দেয়।

Closure: JavaScript বা Python-এ একটি ফাংশন আরেকটি ফাংশন বানিয়ে ফেরত দিতে পারে, আর ভেতরের ফাংশন বাইরের ফাংশনের local ভ্যারিয়েবল “ধরে” রাখতে পারে। বাইরের ফাংশন শেষ হলেও সেই ভ্যারিয়েবল বেঁচে থাকে। এটি static local-এর মতোই স্মৃতিওয়ালা — তবে বড় পার্থক্য হলো প্রতিটি closure নিজের আলাদা কপি পায়। দুটি টোকেন মেশিন বানালে দুটির আলাদা গণনা; C-এর static local-এ পুরো প্রোগ্রামে একটিই। C-তে একই জিনিস করতে হয় হাতে: একটি struct-এ অবস্থা রেখে ফাংশনের সাথে তার ঠিকানা পাঠানো (পর্ব ২৮-এর callback-এ এই নকশা দেখবেন)।

Lifetime-এর প্রশ্ন: এই ভাষাগুলোতে local-এর “ঠিকানা ফেরত দেওয়া” নিরাপদ, কারণ garbage collector দেখে কোনো বস্তুর দিকে কেউ এখনো তাকিয়ে আছে কি না — আয়ু scope দিয়ে নয়, ব্যবহার দিয়ে ঠিক হয়। দাম: বাড়তি মেমরি আর মাঝে মাঝে বিরতি। Rust নিয়েছে তৃতীয় পথ — compiler-এর borrow checker compile করার সময়ই প্রমাণ করে যে কোনো reference তার বস্তুর চেয়ে বেশিদিন বাঁচবে না। এই পর্বের dangling pointer Rust-এ compile-ই হয় না।

মূল উপলব্ধি: “কোথায় দেখা যায়” আর “কতক্ষণ বাঁচে” — এই দুটি প্রশ্নের উত্তর প্রতিটি ভাষাকে দিতে হয়। C উত্তরটি আপনার হাতে ছেড়ে দেয়, Java বা Python garbage collector-এর হাতে, Rust compiler-এর প্রমাণের হাতে। C-তে এটি ভালোভাবে বুঝলে অন্য যেকোনো ভাষার নকশা বুঝতে সুবিধা হয়।

অনুশীলন

বেশিরভাগ কাজে কোড লিখতে হবে না — পড়ে, অনুমান করে, তারপর মিলিয়ে দেখাই আসল কাজ।

  1. আউটপুট অনুমান: না চালিয়ে বলুন কী ছাপা হবে — int n = 5; void f(void){ static int n; n += 2; printf("%d ", n); } int main(void){ f(); f(); printf("%d\n", n); return 0; }

    আউটপুট: 2 4 5. f-এর ভেতরের static int n global n-কে ঢেকে দেয়; মান না দেওয়ায় এটি 0 দিয়ে শুরু হয়, প্রথম ডাকে 2, দ্বিতীয় ডাকে 4. main-এ n মানে global-টি, যাকে কেউ ছোঁয়নি — তাই 5. দুটি সম্পূর্ণ আলাদা বাক্স, দুটিরই আয়ু পুরো প্রোগ্রাম, কিন্তু scope আলাদা।

  2. Symbol শিকারি: নিজের কম্পিউটারে (অথবা যেকোনো অনলাইন Linux টার্মিনালে) এই পর্বের a.c ফাইলটি লিখে gcc -c a.c আর nm a.o চালান। এবার log_call-এর সামনে থেকে static মুছে আবার দেখুন। কী বদলাল?

    প্রথমবার log_call-এর পাশে ছোট হাতের t, পরেরবার বড় হাতের T. অর্থাৎ ফাংশনটি এখন অন্য ফাইল থেকে ডাকা যাবে — আর কোনো ফাইলে একই নামে আরেকটি ফাংশন থাকলে multiple definition হবে। আরেকটি পার্থক্য চোখে পড়তে পারে: static ফাংশন কোথাও ব্যবহার না হলে -Wall সতর্ক করে (defined but not used), কিন্তু non-static হলে করে না, কারণ compiler জানে না অন্য ফাইল ডাকবে কি না। -O2 দিলে ছোট static ফাংশন অনেক সময় পুরোপুরি inline হয়ে তালিকা থেকেই উধাও হয়ে যায়।

  3. বাগ খুঁজুন: AI এই ফাংশন দিয়েছে — char *grade_text(int marks) { char buf[16]; if (marks >= 80) strcpy(buf, "A+"); else strcpy(buf, "Other"); return buf; }. সমস্যা কী, আর তিনভাবে কীভাবে ঠিক করা যায়?

    buf একটি automatic local array; ফাংশন ফিরতেই তার আয়ু শেষ। ফেরত দেওয়া ঠিকানাটি dangling. সমাধান:

    • সবচেয়ে সহজ ও নিরাপদ: এখানে লেখাগুলো স্থির, তাই সরাসরি string literal ফেরত দিন — const char *grade_text(int marks) { return marks >= 80 ? "A+" : "Other"; }. String literal-এর আয়ু static, পুরো প্রোগ্রাম।
    • Caller-এর buffer: void grade_text(int marks, char *out, size_t size) — যে ডাকে সে জায়গা দেয়।
    • static char buf[16]: কাজ করবে, কিন্তু প্রতিটি ডাক একই buffer বদলে দেবে আর thread-safe নয় — মন্তব্যে স্পষ্ট লিখতে হবে।
  4. Global সরান (কোড লিখুন): নিচের প্রোগ্রামটি এমনভাবে বদলান যাতে কোনো global না থাকে, কিন্তু আউটপুট একই থাকে — int balance = 1000; void deposit(int x){ balance += x; } void withdraw(int x){ balance -= x; } int main(void){ deposit(500); withdraw(200); printf("%d\n", balance); return 0; }

    একটি উপায় — অবস্থাটি main-এর local করে ফাংশনগুলোকে নতুন মান ফেরত দিতে বলা:

    int deposit(int bal, int x) { return bal + x; }
    int withdraw(int bal, int x) { return bal - x; }
    int main(void) { int balance = 1000; balance = deposit(balance, 500); balance = withdraw(balance, 200); printf("%d\n", balance); return 0; }

    আউটপুট আগের মতোই 1300. এখন প্রতিটি ফাংশনের মাথা দেখেই বোঝা যায় সে কী নেয়, কী দেয়; পরীক্ষাও সহজ — deposit(0, 5) সবসময় 5. অনেকগুলো অ্যাকাউন্ট হলে struct বানিয়ে তার ঠিকানা পাঠানোই পরের ধাপ, যা পর্ব ১৭ ও ২২-এর পর আরও স্বাভাবিক মনে হবে।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমপ্রোগ্রাম চালানো থেকে শেষ পর্যন্ত ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Global ও static ভ্যারিয়েবলের জায়গা তৈরি, মান না থাকলে শূন্য
  2. main-এর stack frame তৈরি
  3. একটি ফাংশন ডাকা হলো — নতুন frame ও তার automatic local
  4. ফাংশন ফিরল — তার frame ও local মুছে গেল
  5. প্রোগ্রাম শেষ — static ভ্যারিয়েবলের জায়গাও অপারেটিং সিস্টেম ফেরত নিল

একটি ফাংশনে static int n = 0; n++; printf("%d ", n); আছে। ফাংশনটি তিনবার ডাকলে কী ছাপা হবে?

  • 1 1 1
  • 1 2 3
  • 0 1 2
  • অনির্দিষ্ট — আবর্জনা মান

সব ফাংশনের বাইরে লেখা static int cache[100]; — এখানে static কী বদলায়?

  • আয়ু বাড়িয়ে পুরো প্রোগ্রাম করে
  • মানটি আর বদলানো যাবে না
  • নামটি শুধু এই ফাইলের ভেতরে সীমিত হয় (internal linkage)
  • ভ্যারিয়েবলটি stack-এ রাখা হয়

extern int total; লাইনটি সম্পর্কে কোনটি সত্য?

  • এটি declaration — কোনো মেমরি নেয় না, definition অন্য কোথাও আছে
  • এটি total-এর জন্য নতুন মেমরি তৈরি করে, মান 0
  • এটি total-কে এই ফাইলের বাইরে অদৃশ্য করে
  • এটি শুধু ফাংশনের ভেতরেই লেখা যায়

int main(void) { int x; printf("%d\n", x); } — কী ছাপা হবে?

  • সবসময় 0
  • Compile error
  • সবসময় -1
  • অনির্দিষ্ট — যেকোনো সংখ্যা আসতে পারে (undefined behavior)

a.c আর b.c দুটি ফাইলেই লেখা আছে int count = 0; (ফাংশনের বাইরে)। একসাথে build করলে কী হবে?

  • Compiler error দেবে
  • Linker “multiple definition of `count'” error দেবে
  • দুটি আলাদা count তৈরি হবে, কোনো সমস্যা নেই
  • Linker দুটিকে একটিতে মিলিয়ে দেবে

একটি ফাংশন int t = 101; return &t; করে। এই ফেরত দেওয়া ঠিকানা সম্পর্কে কোনটি ঠিক?

  • নিরাপদ, কারণ ঠিকানাটি ফাংশনের বাইরে কপি হয়ে গেছে
  • নিরাপদ, তবে মান 0 হয়ে যাবে
  • এটি dangling pointer — ব্যবহার করলে ফল অনির্দিষ্ট
  • C এটি compile করতে দেয় না

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. প্রতিটি ভ্যারিয়েবলের তিন প্রশ্ন: scope (কোথায় দেখা যায়), storage duration (কতক্ষণ বাঁচে), linkage (অন্য ফাইল দেখে কি না)।
  2. নাম খোঁজা হয় ভেতর থেকে বাইরে; ভেতরের নাম বাইরেরটিকে ঢাকে। -Wshadow দিয়ে ধরুন।
  3. Automatic local }-এ মরে আর শুরুতে আবর্জনা ধরে; static ও global বাঁচে পুরো প্রোগ্রাম, শুরুতে শূন্য।
  4. static ভেতরে = মনে রাখো, বাইরে = ফাইলের গোপন। extern = শুধু ঘোষণা; definition ঠিক একটি।
  5. Local-এর ঠিকানা কখনো ফেরত নয়; global রাখুন শেষ উপায় হিসেবে।

পরের পর্বে: আজ দেখলেন প্রতিটি ফাংশন-ডাকে নতুন frame আর নতুন automatic ভ্যারিয়েবল তৈরি হয়। এবার সেই নিয়মের সবচেয়ে সুন্দর ব্যবহার — একটি ফাংশন যখন নিজেকেই ডাকে, তখন প্রতিটি ডাক নিজের আলাদা কপি নিয়ে কীভাবে কাজ করে।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান