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

ফাংশন — বড় কাজকে ছোট ছোট দায়িত্বে ভাগ করা

Functions — Prototypes, Pass by Value, the Call Stack, Scope & Storage Classes

Taught in Bangla, this lesson explains C functions: structure, parameters and return values, prototypes, what happens on the call stack, pass by value, scope and lifetime, static and extern, and rules for designing good functions.

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

  • ফাংশনের গঠন — return type, নাম, parameter — আর prototype ও definition-এর পার্থক্য
  • ফাংশন ডাকলে মেমরিতে আসলে কী ঘটে — call stack ধাপে ধাপে চোখে দেখা
  • Pass by value কেন, আর এর ফলে কোন ভুল হয়
  • Scope, lifetime, static ও extern — কোন নাম কোথায় দেখা যায়, কতক্ষণ বাঁচে
  • ভালো ফাংশন নকশার নিয়ম, আর AI-এর লেখা কোড ফাংশনে ভাঙার একটি চেকলিস্ট

১ · ফাংশন কেন — এক রান্নাঘরে সবাই সব কাজ করলে কী হয়

পুরান ঢাকার একটি ব্যস্ত কাচ্চি বিরিয়ানির দোকানের কথা ভাবুন। দুপুর একটা, প্রতিটি টেবিল ভরা। এখানে একজন মানুষ যদি অর্ডার নেন, রান্না করেন, প্লেট ধোন, আবার বিলও কাটেন — দোকান দশ মিনিটেই অচল হয়ে যাবে। তাই কাজ ভাগ করা থাকে।

ওয়েটার শুধু অর্ডার নেন আর একটি স্লিপে লিখে রান্নাঘরে দেন। বাবুর্চি স্লিপ দেখে রান্না করেন, খাবার ফেরত দেন। ক্যাশিয়ার প্লেটের সংখ্যা দেখে বিল বানান। কেউ কারও কাজের ভেতরের খুঁটিনাটি জানেন না, জানার দরকারও নেই। তাঁদের মধ্যে যোগাযোগের একমাত্র পথ হলো অর্ডার স্লিপ — কী যাবে, কী ফেরত আসবে। প্রোগ্রামিংয়ে প্রতিটি কর্মী হলেন একটি ফাংশন, আর অর্ডার স্লিপ হলো সেই ফাংশনের interface.

পর্ব ০২-এ আমরা শিখেছিলাম decomposition — বড় সমস্যাকে ছোট ছোট টুকরোয় ভাঙা। ফাংশন হলো C-তে সেই ভাঙাকে কোডে রূপ দেওয়ার যন্ত্র। আপনি এতদিন ধরে ফাংশন ব্যবহার করেই আসছেন: main নিজে একটি ফাংশন, আর printf-ও একটি ফাংশন, যা অন্য কেউ লিখে রেখেছেন। এই পর্বে আপনি নিজের ফাংশন বানাবেন — আর বুঝবেন ভেতরে কী ঘটে।

ফাংশনে ভাগ করার চারটি লাভ

পড়া সহজ

total = apply_vat(price); পড়েই বোঝা যায় কী হচ্ছে। ভেতরের হিসাব জানতে চাইলে তবেই ফাংশনটি খুলবেন।

নাম নিজেই ব্যাখ্যা

বারবার ব্যবহার

একবার লিখলে দশ জায়গা থেকে ডাকা যায়। VAT-এর হার বদলালে বদলাতে হবে মাত্র এক জায়গায়।

একবার লেখা, বহুবার ডাকা

আলাদা করে পরীক্ষা

ছোট ফাংশন আলাদাভাবে যাচাই করা যায় — 100 দিলে 115 আসে কি না। পুরো প্রোগ্রাম চালাতে হয় না।

unit test

দলে কাজ

Interface ঠিক করা থাকলে একজন বিলের অংশ, আরেকজন রিপোর্টের অংশ একসাথে লিখতে পারেন।

কাজের ভাগ
মূল ধারণা

ফাংশন শুধু “কোড ছোট করার কৌশল” নয়। এটি দায়িত্বের সীমানা। প্রতিটি ফাংশন একটি প্রতিশ্রুতি দেয়: “এই ইনপুট দিলে আমি এই ফল দেব।” কীভাবে দেবে, সেটি তার নিজের ব্যাপার। এই চিন্তাকে বলে abstraction — ভেতরের জটিলতা লুকিয়ে বাইরে একটি সহজ মুখ দেখানো।

২ · একটি ফাংশনের গঠন — নাম, ইনপুট আর ফলাফল

একটি ফাংশনের চারটি অংশ থাকে। নিচের ফাংশনটি একটি দামের ওপর ১৫% VAT যোগ করে মোট দাম ফেরত দেয়:

double apply_vat(double amount) {     // মাথা: return type, নাম, parameter
    double vat = amount * 0.15;       // শরীর: আসল কাজ
    return amount + vat;              // ফলাফল ফেরত
}
অংশউদাহরণেমানে
Return typedoubleফাংশন কোন ধরনের মান ফেরত দেবে
নামapply_vatযে নামে ডাকা হবে — কাজটি বোঝায় এমন নাম রাখুন
Parameterdouble amountফাংশনের ভেতরের ভ্যারিয়েবল, যেখানে পাঠানো মান এসে বসে
শরীর (body){ … }আসল কাজের কোড, শেষে return দিয়ে return value ফেরত

Parameter আর argument — একই জিনিস নয়

এই দুটি শব্দ নতুনরা প্রায়ই গুলিয়ে ফেলেন। Parameter হলো ফাংশনের সংজ্ঞায় লেখা খালি ঘর — amount. Argument হলো ডাকার সময় আসলে যে মান পাঠানো হয় — apply_vat(500.0) লিখলে 500.0 হলো argument. রেস্টুরেন্টের ভাষায়: অর্ডার স্লিপে ছাপানো “আইটেম: ____” ঘরটি parameter, আর ওয়েটার সেখানে যে “২ প্লেট কাচ্চি” লিখলেন সেটি argument.

যে ফাংশন কিছু ফেরত দেয় না — void

কিছু ফাংশনের কাজ শুধু একটি কাজ করা, কোনো মান ফেরত দেওয়া নয় — যেমন একটি রসিদ ছাপানো। এদের return type লেখা হয় void (অর্থ “কিছুই না”)। এমন ফাংশনকে বলে void function. আবার কোনো ফাংশন যদি কোনো ইনপুটই না নেয়, তবে parameter-এর জায়গায়ও void লেখা ভালো অভ্যাস, যেমন void print_header(void).

int f() আর int f(void) পুরোনো C-তে এক জিনিস ছিল না। খালি বন্ধনী মানে ছিল “parameter কী, তা বলছি না” — ফলে ভুল সংখ্যায় argument পাঠালেও compiler ধরত না। সাম্প্রতিক C23 মানে দুটিকে এক করা হয়েছে, কিন্তু পুরোনো compiler-এ নিরাপদ থাকতে (void) লেখাই ভালো।

ডাকা আর ফেরা

double total = apply_vat(500.0); — এই লাইনটি একটি function call. প্রোগ্রাম তখন main-এর কাজ থামিয়ে apply_vat-এর ভেতরে চলে যায়, কাজ শেষ করে, তারপর ঠিক ওই লাইনেই ফিরে আসে এবং ফলাফল total-এ রাখে। “ঠিক ওই লাইনেই ফেরা” কীভাবে সম্ভব? এর উত্তর অংশ ৪-এ, call stack-এ।

৩ · Prototype — compiler-কে আগে থেকে জানিয়ে রাখা

Compiler আপনার ফাইল ওপর থেকে নিচে, একবারে একটি লাইন করে পড়ে। ধরুন main ফাইলের ওপরে আছে, আর apply_vat লেখা আছে নিচে। Compiler যখন main-এর ভেতরে apply_vat(500.0) দেখে, তখন সে এই নাম কখনো শোনেনি। কী ফেরত দেয়? কয়টি argument নেয়? কিছুই জানে না।

bill.c:5:20: error: implicit declaration of function 'apply_vat' [-Wimplicit-function-declaration]

পর্ব ০৪-এ আমরা এই implicit declaration সমস্যাটি দেখেছিলাম। খুব পুরোনো C-তে compiler চুপচাপ ধরে নিত “অচেনা ফাংশন নিশ্চয়ই int ফেরত দেয়।” আমাদের ফাংশন কিন্তু double ফেরত দেয় — ফলে ভুল সংখ্যা আসত, কোনো সতর্কতা ছাড়াই। C99 থেকে নিয়মটি বাদ দেওয়া হয়েছে, আর নতুন GCC (version 14 থেকে) একে সরাসরি error হিসেবে দেখায়।

সমাধান হলো function prototype — ফাংশনের মাথাটুকু, শেষে সেমিকোলন, শরীর ছাড়া। এটি compiler-কে বলে, “এই নামের একটি ফাংশন আছে, এই ইনপুট নেয়, এই ফল দেয়। পুরো কোড পরে আসছে।”

Prototype (declaration)Definition
দেখতেdouble apply_vat(double amount);double apply_vat(double amount) { … }
কী বলে“এমন ফাংশন আছে”“ফাংশনটি এভাবে কাজ করে”
কতবার থাকতে পারেঅনেকবার (প্রতিটি ফাইলে)পুরো প্রোগ্রামে ঠিক একবার
কে ব্যবহার করেCompiler — ডাক ঠিক আছে কি না যাচাই করতেLinker — আসল কোড জোড়া দিতে

নিচের প্রোগ্রামে দুটি ফাংশন আছে — একটি ছাড় (discount) কাটে, অন্যটি VAT যোগ করে। খেয়াল করুন, ওপরে prototype আর নিচে function definition. main পড়লেই পুরো গল্পটা বোঝা যায়, ভেতরের অঙ্ক না দেখেই।

#include <stdio.h>

double apply_discount(double amount, double percent);   /* prototype */
double apply_vat(double amount);                         /* prototype */

int main(void) {
    double price = 1200.0;
    double after_discount = apply_discount(price, 10.0);
    double total = apply_vat(after_discount);
    printf("Price: %.2f\nAfter discount: %.2f\nTotal with VAT: %.2f\n",
           price, after_discount, total);
    return 0;
}

double apply_discount(double amount, double percent) {
    return amount - amount * percent / 100.0;
}

double apply_vat(double amount) {
    return amount * 1.15;
}

১২০০ টাকার জিনিসে ১০% ছাড় দিলে হয় ১০৮০, তার ওপর ১৫% VAT যোগ করলে ১২৪২. ওপরের দুটি prototype সরিয়ে দিয়ে চালিয়ে দেখুন — compiler implicit declaration নিয়ে আপত্তি করবে। আবার prototype রেখে নিচের definition সরিয়ে দিলে compile ঠিকই হবে, কিন্তু linker বলবে undefined reference to `apply_vat' — ঠিক পর্ব ০৩-এর রাফির সমস্যা। Prototype হলো compiler-এর জন্য প্রতিশ্রুতি, definition হলো linker-এর জন্য আসল জিনিস।

১৯৭৮ সালের প্রথম C বইয়ে (Kernighan ও Ritchie-এর লেখা, যাকে সবাই “K&R” বলেন) ফাংশন ঘোষণায় parameter-এর টাইপ লেখার নিয়মই ছিল না। ফলে ভুল টাইপের argument পাঠালেও কেউ ধরত না। Parameter-এর টাইপসহ prototype ধারণাটি C++ থেকে ধার করে ১৯৮৯ সালের ANSI C মানে যুক্ত করা হয়। ছোট একটি পরিবর্তন, কিন্তু এর ফলে অসংখ্য বাগ compile করার সময়েই ধরা পড়তে শুরু করে।

৪ · Call stack — ফাংশন ডাকলে মেমরিতে কী ঘটে

একটি ফাংশন আরেকটিকে ডাকে, সেটি আবার আরেকটিকে। প্রত্যেকে কাজ শেষে ঠিক যেখান থেকে ডাকা হয়েছিল সেখানেই ফেরে। প্রতিটির নিজের ভ্যারিয়েবল আছে, যা অন্যের সাথে মিশে যায় না। এই হিসাব রাখে মেমরির একটি অংশ — call stack.

বিয়েবাড়িতে প্লেট ধোয়ার পর একটির ওপর আরেকটি প্লেট রাখা হয়। নতুন প্লেট রাখা হয় সবসময় ওপরে, আর তুলতেও হয় ওপর থেকে। Call stack ঠিক এমন। প্রতিটি ফাংশন ডাক একটি নতুন প্লেট — যাকে বলে stack frame. ফাংশন শেষ হলে তার প্লেট ওপর থেকে সরিয়ে ফেলা হয়।

প্রতিটি stack frame-এ তিনটি জিনিস থাকে: ফাংশনের parameter (পাঠানো মানের কপি), তার local variable, আর return address — কাজ শেষে কোন লাইনে ফিরতে হবে। নিচের প্রোগ্রামে main ডাকে calc_total-কে, আর সে ডাকে apply_vat-কে। “পরে” চাপুন এবং ডান দিকে stack কীভাবে বাড়ে আর কমে, খেয়াল করুন:

1#include <stdio.h>23double apply_vat(double amount) {4    double vat = amount * 0.15;5    return amount + vat;6}78double calc_total(int qty, double price) {9    double subtotal = qty * price;10    return apply_vat(subtotal);11}1213int main(void) {14    double bill = calc_total(2, 50.0);15    printf("bill = %.2f\n", bill);16    return 0;17}
Call stack (ওপরে = এখন যে চলছে)
apply_vat
calc_total
main
মোট frame
  1. শুরু — শুধু main. Stack-এ এখন একটিমাত্র frame, main-এর। তার local ভ্যারিয়েবল bill-এ এখনো কিছু রাখা হয়নি। লাইন ১৪ চলতে যাচ্ছে।
  2. calc_total ডাকা হলো — নতুন frame যোগ। Argument 2 আর 50.0-এর কপি গিয়ে বসল parameter qty ও price-এ। সাথে লেখা হলো ফেরার ঠিকানা: “কাজ শেষে লাইন ১৪-তে ফিরো।” main এখন থেমে অপেক্ষা করছে।
  3. লাইন ৯ চলল। subtotal = 2 × 50 = 100. এই ভ্যারিয়েবলটি শুধু calc_total-এর frame-এ থাকে — main একে দেখতেও পায় না।
  4. apply_vat ডাকা হলো — তৃতীয় frame. subtotal-এর মান 100-এর কপি গেল amount-এ। ফেরার ঠিকানা: লাইন ১০. Stack এখন তিন তলা। ওপরেরটিই সবসময় “এখন চলছে”, নিচের দুটি থেমে আছে।
  5. লাইন ৪ চলল। vat = 100 × 0.15 = 15.
  6. return — ওপরের frame মুছে গেল। apply_vat 115 ফেরত দিল আর তার পুরো frame সরিয়ে ফেলা হলো। amount আর vat এখন আর নেই। ফেরার ঠিকানা দেখে প্রোগ্রাম ফিরল লাইন ১০-এ, আর calc_total হাতে পেল 115.
  7. আরেকটি return — আরেকটি frame মুছল। calc_total 115 ফেরত দিয়ে নিজের frame সরিয়ে ফেলল। প্রোগ্রাম ফিরল লাইন ১৪-তে, bill হলো 115. Stack আবার এক তলা।
  8. ফল ছাপা হলো। স্ক্রিনে bill = 115.00. খেয়াল করুন: যে ফাংশন সবার শেষে ঢুকেছিল (apply_vat), সে-ই সবার আগে বেরিয়েছে। এই নিয়মকে বলে LIFO — Last In, First Out.
ডাকলে frame যোগ (push), ফেরত দিলে frame মুছে যায় (pop).
যে শেষে ঢোকে, সে আগে বের হয়।

এখান থেকে দুটি গুরুত্বপূর্ণ কথা বোঝা যায়। প্রথমত, ফাংশন শেষ হলেই তার local ভ্যারিয়েবল মুছে যায় — তাই পরের বার ডাকলে আগের মান পাওয়া যায় না। দ্বিতীয়ত, stack-এর জায়গা সীমিত। Linux-এ সাধারণত ৮ MB, Windows-এ ডিফল্ট ১ MB. কোনো ফাংশন যদি না থেমে নিজেকে বা একে অপরকে বারবার ডাকতে থাকে, frame জমতে জমতে জায়গা ফুরিয়ে যায় — একে বলে stack overflow, আর প্রোগ্রাম crash করে। পর্ব ১৫-এ recursion শেখার সময় এটি আবার দেখা হবে।

বাস্তবের CPU-তে ছবিটি একটু ভিন্ন। আধুনিক 64-bit কম্পিউটারে প্রথম কয়েকটি argument সাধারণত stack-এ না গিয়ে সরাসরি CPU-র register-এ যায়, কারণ সেটি আরও দ্রুত। Optimization চালু থাকলে compiler কিছু ফাংশনের frame পুরোপুরি বাদও দিতে পারে। তবে ধারণাগতভাবে “প্রতিটি ডাকের নিজের খোপ, নিজের ভ্যারিয়েবল, নিজের ফেরার ঠিকানা” — এই মডেলটি সবসময় সঠিক থাকে, আর debugger-ও আপনাকে এভাবেই দেখায়।

৫ · Pass by value — ফাংশন পায় শুধু কপি

Call stack-এর ছবিতে একটি কথা লক্ষ করেছেন? Argument-এর কপি গিয়ে বসে parameter-এ। C-তে argument পাঠানোর এই নিয়মকে বলে pass by value. এর সবচেয়ে বিখ্যাত ফল হলো নিচের ভুলটি।

ধরুন আপনি দুটি ভ্যারিয়েবলের মান অদলবদল (swap) করার একটি ফাংশন লিখলেন। দেখতে একদম ঠিক। কিন্তু ধাপে ধাপে দেখুন কী ঘটে:

1#include <stdio.h>23void swap(int a, int b) {4    int t = a;5    a = b;6    b = t;7}89int main(void) {10    int x = 5, y = 9;11    swap(x, y);12    printf("x=%d y=%d\n", x, y);13    return 0;14}
দুটি আলাদা frame
main: x
main: y
swap: a
swap: b
swap: t
  1. main-এ দুটি ভ্যারিয়েবল। x = 5, y = 9. লক্ষ্য: এদের মান অদলবদল করে x = 9, y = 5 বানানো।
  2. swap ডাকা হলো — কপি তৈরি। নতুন frame-এ a পেল x-এর মানের কপি (5), b পেল y-এর কপি (9). এগুলো সম্পূর্ণ আলাদা ঘর — মেমরির অন্য জায়গায়।
  3. লাইন ৪: t = a = 5. পুরোনো মান সাময়িকভাবে রেখে দেওয়া হলো।
  4. লাইন ৫: a = b = 9. খেয়াল করুন, বদলাল শুধু a — x আগের মতোই 5.
  5. লাইন ৬: b = t = 5. ফাংশনের ভেতরে অদলবদল নিখুঁতভাবে হয়ে গেছে: a = 9, b = 5. কিন্তু এগুলো তো কপি!
  6. swap শেষ — frame মুছে গেল। a, b, t — তিনটিই হারিয়ে গেল, সাথে অদলবদলের সব কাজ। main-এর x আর y কেউ ছুঁয়েও দেখেনি।
  7. ফল: x=5 y=9. কিছুই বদলায়নি। ফাংশনটি পরিশ্রম করেছে ঠিকই, কিন্তু ফটোকপির ওপর। মূল কাগজ বদলাতে হলে ফাংশনকে মূল ঘরের ঠিকানা দিতে হবে — সেটিই pointer, যা পর্ব ১৭ ও ১৮-তে শিখবেন।

আপনার SSC-র মার্কশিটের একটি ফটোকপি কাউকে দিলেন। তিনি ফটোকপিতে কলম দিয়ে নম্বর বদলে দিলেন। আপনার আসল মার্কশিট কি বদলাল? না। Pass by value ঠিক এমন — ফাংশন সবসময় ফটোকপি পায়। ভালো দিকটি হলো, ফাংশন ভুল করলেও আপনার আসল ডেটা নিরাপদ থাকে।

তাহলে একটি ফাংশন থেকে দুটি ফল কীভাবে পাবেন? return দিয়ে তো একটিই মান ফেরত দেওয়া যায়। এখনকার জন্য উপায় হলো কাজটিকে দুটি ফাংশনে ভাগ করা। পরে pointer (পর্ব ১৭–১৮) আর struct (পর্ব ২২) শিখলে আরও ভালো উপায় পাবেন। একটি সতর্কতা আগেই রাখি: array পাঠালে মনে হবে ফাংশন মূল ডেটা বদলাতে পারছে। সেখানেও আসলে কপিই যায় — তবে array-র নয়, array-র ঠিকানার কপি। পর্ব ১৮-তে এর রহস্য খুলবে।

৬ · Scope ও lifetime — কোন নাম কোথায় দেখা যায়, কতক্ষণ বাঁচে

প্রতিটি ভ্যারিয়েবলের দুটি আলাদা বৈশিষ্ট্য আছে, যা নতুনরা প্রায়ই এক করে ফেলেন:

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

Scope হলো কোডের কোন অংশ থেকে নামটি ব্যবহার করা যায়। এটি লেখার বিষয় — কোড দেখেই বলা যায়।

স্থান নিয়ে প্রশ্ন

Lifetime — কতক্ষণ বাঁচে

Lifetime হলো প্রোগ্রাম চলার সময় ভ্যারিয়েবলটি মেমরিতে কতক্ষণ টিকে থাকে। এটি চলার বিষয়।

সময় নিয়ে প্রশ্ন

C-তে scope-এর দুটি প্রধান স্তর

  • Block scope: { }-এর ভেতরে ঘোষণা করা নাম শুধু সেই block-এর ভেতরেই দেখা যায় — ঘোষণার লাইন থেকে block শেষ হওয়া পর্যন্ত। ফাংশনের শরীরও একটি block, তাই ফাংশনের parameter ও local ভ্যারিয়েবল এই স্তরে পড়ে। লুপের for (int i = 0; …)-এর i-ও তাই লুপের বাইরে অচেনা — পর্ব ১২-এ যা দেখেছেন।
  • File scope: সব ফাংশনের বাইরে ঘোষণা করা নাম ঘোষণার জায়গা থেকে ফাইলের শেষ পর্যন্ত সব ফাংশন দেখতে পায়। এগুলোকেই সাধারণত global variable বলা হয়।

(C মানে আরও দুটি scope আছে — goto-র label-এর জন্য “function scope”, আর prototype-এর parameter নামের জন্য “function prototype scope”. সাধারণ ভ্যারিয়েবলের কাজে এগুলো নিয়ে ভাবতে হয় না; পরের পর্বে চারটিই পাশাপাশি দেখবেন।)

একই নাম যদি বাইরে আর ভেতরে দুই জায়গাতেই ঘোষণা করা হয়, তখন কী হয়? ভেতরেরটি বাইরেরটিকে ঢেকে দেয় — একে বলে shadowing. নিচের প্রোগ্রামে level নামে তিনটি আলাদা ভ্যারিয়েবল আছে। প্রতিটি printf কোনটিকে দেখছে, ধাপে ধাপে দেখুন:

1#include <stdio.h>23int level = 1;               // file scope45int main(void) {6    printf("A: %d\n", level);7    int level = 2;           // main-এর নিজের8    printf("B: %d\n", level);9    {10        int level = 3;       // ভেতরের block-এর11        printf("C: %d\n", level);12    }13    printf("D: %d\n", level);14    return 0;15}
কোন level বেঁচে আছে
ফাইলের level
main-এর level
block-এর level
“level” লিখলে পাবেন
  1. প্রোগ্রাম শুরুর আগেই ফাইলের level = 1 তৈরি হয়ে আছে। এটি file scope-এর ভ্যারিয়েবল, তাই পুরো প্রোগ্রাম চলাকালীন বেঁচে থাকে।
  2. লাইন ৬ — A: 1. main-এর নিজের level ঘোষণা হয় লাইন ৭-এ। লাইন ৬-এ সে এখনো জন্মায়নি — তাই এখানে level মানে ফাইলেরটি। Scope শুরু হয় ঘোষণার পর থেকে, block-এর শুরু থেকে নয়।
  3. লাইন ৭–৮ — B: 2. এবার main-এর level = 2 তৈরি হলো। এটি ফাইলের level-কে ঢেকে দিল। ফাইলেরটি মুছে যায়নি, শুধু এই নামে আর তাকে ডাকা যাচ্ছে না।
  4. লাইন ১০–১১ — C: 3. ভেতরের block-এ তৃতীয় level = 3. এখন সবচেয়ে ভেতরেরটিই জেতে। নিয়ম: compiler নাম খোঁজে ভেতর থেকে বাইরের দিকে, প্রথম যেটি পায় সেটিই।
  5. লাইন ১২–১৩ — D: 2. }-এ block শেষ, block-এর level মুছে গেল। আবার main-এরটি দেখা যাচ্ছে। শিক্ষা: একই নাম বারবার ব্যবহার করলে কোড পড়তে বিভ্রান্তি হয়। gcc-এ -Wshadow flag দিলে compiler এমন জায়গায় সতর্ক করে।

Lifetime — তিনটি প্রধান আয়ু

ধরনকখন তৈরিকখন মুছে যায়উদাহরণ
AutomaticBlock বা ফাংশনে ঢোকার সময়বের হওয়ার সময়সাধারণ local ভ্যারিয়েবল, parameter
Staticপ্রোগ্রাম শুরুর আগে, একবারপ্রোগ্রাম শেষ হলেglobal ভ্যারিয়েবল, static local
Allocatedআপনি যখন চাইবেনআপনি যখন ছাড়বেনmalloc — পর্ব ২০-তে

সাধারণ local ভ্যারিয়েবলকে বলে automatic variable, কারণ এগুলো stack frame-এর সাথে নিজে থেকে তৈরি হয় আর নিজে থেকে মুছে যায়। এর একটি বিপজ্জনক ফল আছে: ফাংশন থেকে কখনো তার local ভ্যারিয়েবলের ঠিকানা ফেরত দেবেন না — frame মুছে যাওয়ার পর সেই ঠিকানায় যা থাকবে তা আবর্জনা। Pointer শেখার সময় এই ভুলটি আবার দেখব।

৭ · static, extern আর global ভ্যারিয়েবলের বিপদ

একটি ভ্যারিয়েবল কোথায় থাকবে, কতক্ষণ বাঁচবে আর অন্য ফাইল থেকে দেখা যাবে কি না — এই তিনটি ঠিক করে তার storage class. দুটি keyword এখানে সবচেয়ে গুরুত্বপূর্ণ: static আর extern.

ফাংশনের ভেতরে static — স্মৃতিওয়ালা ভ্যারিয়েবল

সাধারণ local ভ্যারিয়েবল প্রতিবার ফাংশন ডাকলে নতুন করে জন্মায়, আগের কিছু মনে রাখে না। কিন্তু local ভ্যারিয়েবলের আগে static লিখলে সে stack-এ না থেকে প্রোগ্রামের স্থায়ী মেমরিতে থাকে। ফলে ফাংশন শেষ হলেও তার মান থেকে যায়। এটিই static variable. নিচে একই ফাংশন তিনবার ডেকে দুই রকম ভ্যারিয়েবলের তুলনা দেখুন:

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

বাস্তবে এর একটি চমৎকার ব্যবহার হলো সিরিয়াল নম্বর দেওয়া — যেমন ব্যাংকের কাউন্টারে টোকেন মেশিন। প্রতিবার বোতাম চাপলে আগের নম্বরের পরেরটি দেয়। নিচের প্রোগ্রামটি চালিয়ে দেখুন:

#include <stdio.h>

int next_token(void) {
    static int last = 100;   /* set once, remembered between calls */
    last++;
    return last;
}

int main(void) {
    for (int i = 0; i < 3; i++) {
        printf("Your token: %d\n", next_token());
    }
    return 0;
}

আউটপুটে আসবে 101, 102, 103. last ভ্যারিয়েবলটি শুধু next_token-এর ভেতর থেকে দেখা যায় (scope ছোট), কিন্তু পুরো প্রোগ্রাম চলাকালীন বেঁচে থাকে (lifetime দীর্ঘ)। এটিই static local-এর আসল শক্তি — global ভ্যারিয়েবলের মতো স্মৃতি, অথচ বাইরের কেউ ভুল করে বদলাতে পারে না। এবার static শব্দটি মুছে আবার চালান: প্রতিবার 101 আসবে, কারণ last প্রতি ডাকে নতুন করে 100 হয়ে যাবে।

ফাইলের স্তরে static, আর extern

সব ফাংশনের বাইরে static লিখলে অর্থ বদলে যায়: তখন সেটি বলে “এই ভ্যারিয়েবল বা ফাংশন শুধু এই ফাইলের নিজস্ব — অন্য ফাইল একে দেখতে পাবে না।” আর extern ঠিক উল্টো কাজ করে: “এই নামের জিনিস আছে, তবে অন্য কোনো ফাইলে তৈরি করা।” পর্ব ০৩-এর separate compilation মনে করুন — এই দুটি keyword ঠিক করে কোন নাম linker-এর কাছে দৃশ্যমান হবে। এই নিয়মকে বলে linkage.

লেখাকোথায়অর্থ
int count;ফাংশনের ভেতরেসাধারণ automatic local — প্রতি ডাকে নতুন
static int count;ফাংশনের ভেতরেস্মৃতিওয়ালা local — ডাকের মাঝে মান থাকে
int total;ফাংশনের বাইরেglobal — অন্য ফাইলও extern দিয়ে ব্যবহার করতে পারে
static int total;ফাংশনের বাইরেশুধু এই ফাইলের global — বাইরে থেকে অদৃশ্য
extern int total;যেকোনো জায়গায়ঘোষণা মাত্র — আসলটি অন্য ফাইলে আছে

static শব্দটির দুই জায়গায় দুই অর্থ — ভেতরে “আয়ু দীর্ঘ”, বাইরে “ফাইলের বাইরে গোপন”। C-এর নকশার এটি একটি পরিচিত বিভ্রান্তি; শুধু মনে রাখুন কোথায় লেখা হয়েছে সেটিই অর্থ ঠিক করে।

Global ভ্যারিয়েবল কেন বড় প্রোগ্রামে বিপদ

ছোট প্রোগ্রামে global ভ্যারিয়েবল খুব সুবিধাজনক মনে হয় — parameter পাঠানোর ঝামেলা নেই, সবাই সরাসরি ব্যবহার করে। কিন্তু প্রোগ্রাম বড় হলে এটিই সবচেয়ে বড় মাথাব্যথা হয়ে ওঠে:

  • কে বদলাল? পঞ্চাশটি ফাংশন একটি global পড়ে ও বদলায়। মান ভুল এলে সন্দেহভাজন পঞ্চাশজন — প্রতিটি খুঁজে দেখতে হবে।
  • লুকানো নির্ভরতা: ফাংশনের মাথা দেখে বোঝা যায় না সে কোন global ব্যবহার করে। Interface মিথ্যা বলে।
  • পরীক্ষা কঠিন: একই ইনপুটে ফাংশন ভিন্ন ফল দিতে পারে, কারণ ফল নির্ভর করছে global-এর তখনকার মানের ওপর।
  • একসাথে কাজ (thread): পর্ব ৪৯-এ দেখবেন, দুটি thread একই global একসাথে বদলালে ফল অনিশ্চিত হয়ে যায়।

নিয়মটি সহজ: ডেটা দরকার হলে parameter দিয়ে পাঠান, ফল দরকার হলে return দিয়ে নিন। স্মৃতি দরকার হলে আগে static local ভাবুন। Global রাখুন শেষ উপায় হিসেবে, আর রাখলে অন্তত ফাইলের স্তরে static দিয়ে তার নাগাল ছোট করুন।

৮ · ভালো ফাংশনের নকশা — পাঁচটি নিয়ম

ফাংশন লিখতে পারা আর ভালো ফাংশন লিখতে পারা — দুটি আলাদা দক্ষতা। অভিজ্ঞ প্রোগ্রামাররা নিচের নিয়মগুলো প্রায় অভ্যাসে মেনে চলেন:

১. একটি ফাংশন, একটি দায়িত্ব

এটিকে বলে single responsibility নীতি। পরীক্ষা সহজ: ফাংশনটি কী করে, এক বাক্যে বলুন। বাক্যে যদি “এবং” আসে — “ইনপুট নেয় এবং হিসাব করে এবং ফাইলে লেখে” — তবে ফাংশনটি তিনটি হওয়া উচিত। রেস্টুরেন্টের ওয়েটার রান্না করেন না; ঠিক এই কারণেই রান্নাঘর চলে।

২. নাম দিয়েই বোঝান

calc(), do_it(), func2() — এমন নাম কিছুই বলে না। ভালো নাম একটি ক্রিয়া দিয়ে শুরু হয় আর কাজটি স্পষ্ট করে: apply_vat, is_valid_phone, print_receipt. যে ফাংশন হ্যাঁ/না উত্তর দেয়, তার নাম সাধারণত is_ বা has_ দিয়ে শুরু হয়।

৩. ছোট রাখুন

Linux kernel-এর কোডিং নির্দেশিকায় লেখা আছে, ফাংশন হবে ছোট, একটি কাজ করবে, আর এক-দুই স্ক্রিনের মধ্যে ধরে যাবে। কোনো ফাংশন যদি পড়তে গিয়ে বারবার ওপরে-নিচে স্ক্রল করতে হয়, সেটি ভাঙার সময় হয়েছে। প্রতিটি ছোট অংশ আলাদা নামের ফাংশনে নিন।

৪. পারলে pure রাখুন

যে ফাংশন শুধু ইনপুট নিয়ে হিসাব করে ফল ফেরত দেয় — স্ক্রিনে কিছু লেখে না, global বদলায় না, ফাইলে হাত দেয় না — তাকে বলে pure function. apply_vat এমন একটি ফাংশন: 100 দিলে সবসময় 115, পৃথিবীর আর কিছু বদলায় না। অন্যদিকে স্ক্রিনে ছাপানো, ফাইলে লেখা বা global বদলানো হলো side effect. Side effect খারাপ নয় — এগুলো ছাড়া প্রোগ্রাম কিছুই দেখাতে পারত না। কিন্তু ভালো নকশায় হিসাবের কাজ থাকে pure ফাংশনে, আর ইনপুট-আউটপুটের কাজ থাকে আলাদা, পাতলা একটি স্তরে।

Pure

double apply_vat(double amount)
শুধু হিসাব। যেকোনো জায়গা থেকে ডাকা যায়, পরীক্ষা এক লাইনে।

ভরসা করা সহজ

Side effect আছে

void print_receipt(double total)
স্ক্রিনে লেখে। দরকারি, কিন্তু যত কম জায়গায় হয় তত ভালো।

সীমানায় রাখুন

৫. Prototype রাখুন header-এ

প্রোগ্রাম কয়েকটি ফাইলে ভাগ হলে প্রতিটি ফাইলে আলাদা করে prototype লেখা ঝামেলার, আর একটিতে বদলে অন্যটিতে ভুলে গেলে বাগ। সমাধান: সব prototype একটি header ফাইলে রাখা, আর যে ফাইলের দরকার সে #include করবে।

/* bill.h — the "order slip" other files can see */
double apply_discount(double amount, double percent);
double apply_vat(double amount);
void   print_receipt(double total);

bill.c ফাইলে থাকবে এদের definition, আর main.c শুধু #include "bill.h" লিখে ফাংশনগুলো ব্যবহার করবে। Header হয়ে যায় দুই দলের মাঝের চুক্তিপত্র। এর পূর্ণ কৌশল — include guard, Makefile, library — শিখবেন পর্ব ২৯-এ।

পরীক্ষা: ছোট ফাংশনের সবচেয়ে বড় পুরস্কার

Pure, ছোট ফাংশন পরীক্ষা করা প্রায় মজার কাজ। assert(apply_vat(100.0) == 115.0); জাতীয় এক লাইনের পরীক্ষা লিখে রাখলে, পরে কেউ ভুল করে ফাংশন বদলালে সাথে সাথে ধরা পড়ে। এমন ছোট পরীক্ষাকে বলে unit test. (দশমিক সংখ্যার তুলনায় == ঝুঁকিপূর্ণ — পর্ব ০৫ মনে করুন। যেমন bill.c-এর মতো amount * 1.15 লিখলে 100.0-এর ফল ঠিক 115.0 না হয়ে 114.99999999999999 হয়, তখন এই assert ব্যর্থ হবে। বাস্তবে ছোট একটি সহনসীমা ধরে তুলনা করা হয়।) বিশাল একটি main-এর ক্ষেত্রে এমন পরীক্ষা লেখাই যায় না — সেখানে পুরো প্রোগ্রাম চালিয়ে চোখে দেখা ছাড়া উপায় থাকে না।

৯ · AI-এর যুগে ফাংশন — বিশাল main() থেকে পরিষ্কার নকশা

AI-কে “একটি বিল হিসাবের প্রোগ্রাম লেখো” বললে প্রায়ই পাবেন একটি ৮০-১০০ লাইনের main, যেখানে ইনপুট, হিসাব, যাচাই আর ছাপা — সব একসাথে জড়ানো। প্রোগ্রামটি চলবে। কিন্তু কোনো একটি অংশ বদলাতে বা যাচাই করতে গেলেই আপনি আটকে যাবেন। মনে রাখুন, AI-এর লেখা কোড যাচাইয়ের দায়িত্ব আপনার — আর বিশাল জট পাকানো কোড যাচাই করা প্রায় অসম্ভব।

ছোট ফাংশন = ছোট ছোট প্রতিশ্রুতি।
প্রতিটি প্রতিশ্রুতি আলাদা করে যাচাই করা যায়।

ফাংশন নিয়ে AI সাধারণত যেসব ভুল করে

AI-এর ভুলকেন সমস্যাআপনি যা চাইবেন
সব কাজ এক বিশাল main-এপড়া, পরীক্ষা, বদলানো — সবই কঠিনপ্রতিটি কাজের জন্য আলাদা নামের ফাংশন
ভ্যারিয়েবল global করে দেওয়ালুকানো নির্ভরতা, কে কখন বদলাল বোঝা যায় নাParameter দিয়ে পাঠানো, return দিয়ে ফেরত
Prototype ছাড়া ফাংশন main-এর পরে লেখাImplicit declaration error বা warningওপরে prototype, বা header ফাইল
একটি ফাংশন পাঁচটি কাজ করেনাম কাজের সাথে মেলে না, পরীক্ষা অসম্ভবএক বাক্যে বোঝানো যায় এমন দায়িত্ব
হিসাবের ফাংশনের ভেতরেই printfPure নয়, অন্য জায়গায় আবার ব্যবহার করা যায় নাহিসাব আলাদা, ছাপা আলাদা
Pointer ছাড়া swap বা “দুটি ফল ফেরত”Pass by value-এর কারণে কাজই করে নাহাতে চালিয়ে মিলিয়ে দেখা

AI-কে কোড লিখতে বলার সময় কাঠামোটাও বলে দিন। নিচের prompt-টি কপি করে নিজের সমস্যা বসিয়ে ব্যবহার করুন:

একটি C প্রোগ্রাম লেখো যা একটি দোকানের বিল হিসাব করবে: জিনিসের দাম ও সংখ্যা নেবে, ১০% ছাড় কাটবে, ১৫% VAT যোগ করবে, তারপর রসিদ ছাপাবে। নিয়ম: ১. প্রতিটি কাজের জন্য আলাদা ছোট ফাংশন — প্রতিটির একটিমাত্র দায়িত্ব, ২০ লাইনের কম। ২. কোনো global ভ্যারিয়েবল নয়; ডেটা parameter দিয়ে পাঠাও, ফল return দিয়ে নাও। ৩. হিসাবের ফাংশনগুলো pure রাখো — ভেতরে printf বা scanf নয়। ৪. main-এর আগে সব ফাংশনের prototype দাও। ৫. প্রতিটি হিসাবের ফাংশনের জন্য assert দিয়ে অন্তত ২টি পরীক্ষা লেখো, যার একটি edge case (যেমন দাম 0). ৬. প্রতিটি ফাংশনের ওপরে এক লাইনে লেখো সে কী নেয়, কী ফেরত দেয়। gcc -Wall -Wextra দিয়ে যেন কোনো warning না আসে।

AI-এর লেখা ফাংশন হাতে পেলে — যাচাইয়ের চেকলিস্ট

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

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

প্র ০১C কেন সবসময় pass by value করে? মূল ভ্যারিয়েবল সরাসরি পাঠালে কি সুবিধা হতো না?

সরাসরি পাঠানোর (pass by reference) সুবিধা আছে ঠিকই — swap সহজ হতো, বড় ডেটা কপি করতে হতো না। কিন্তু C-এর নকশাকাররা বেছে নিয়েছিলেন সরলতা আর স্পষ্টতা।

  • পড়েই নিশ্চিত হওয়া যায়: C-তে f(x) দেখলে আপনি নিশ্চিত — x বদলাবে না। বদলানোর অনুমতি দিতে হলে আপনাকে নিজ হাতে f(&x) লিখে ঠিকানা দিতে হবে। ডাকার জায়গাতেই লেখা থাকে “এখানে বদল হতে পারে।” কোড review-এর সময় এটি বিরাট সুবিধা।
  • একটিই নিয়ম: C-তে আলাদা reference ব্যবস্থা নেই। “মূল জিনিস বদলাতে চাইলে ঠিকানা পাঠাও” — এই একটি নিয়ম দিয়েই সব কাজ হয়, কারণ ঠিকানা নিজেও তো একটি মান, তাই তারও কপি যায়। ভাষা ছোট থাকে, compiler সহজ থাকে।
  • মেশিনের কাছাকাছি: CPU আসলে register বা stack-এ মান কপি করেই ফাংশনকে দেয়। Pass by value তাই হার্ডওয়্যার যা করে তারই সৎ প্রতিফলন।

ইতিহাস থেকে শিক্ষা: পুরোনো Fortran-এ argument সাধারণত reference হিসেবে যেত। কিছু পুরোনো compiler-এ এর একটি কুখ্যাত ফল ছিল — ফাংশনে সরাসরি একটি সংখ্যা (যেমন 2) পাঠালে আর ফাংশন সেই parameter বদলালে, প্রোগ্রামের অন্য জায়গায় “2”-এর মানই বদলে যেতে পারত! Pass by value এই ধরনের অদ্ভুত বাগের দরজাই বন্ধ করে দেয়।

দাম কী? বড় ডেটা (যেমন ৪০০ বাইটের একটি struct) পাঠালে পুরোটাই কপি হয়, যা ধীর। তাই বাস্তবে বড় জিনিসের ঠিকানা পাঠানো হয়, আর ফাংশন যদি বদলাবে না বলে প্রতিশ্রুতি দেয়, তবে সাথে const লেখা হয় — পর্ব ১৮ ও ৪৫-এ দেখবেন। C++ পরে আলাদা “reference” যোগ করে, যা সুবিধা দিলেও ডাকার জায়গায় বদলের চিহ্ন লুকিয়ে ফেলে — ভাষা নকশায় এটিও একটি trade-off.

মূল উপলব্ধি: নিরাপদ ডিফল্ট (কপি) আর স্পষ্টভাবে চাওয়া ক্ষমতা (ঠিকানা) — ভালো নকশা বিপজ্জনক কাজকে অসম্ভব করে না, শুধু দৃশ্যমান করে।

প্র ০২একটি ফাংশন ডাকার আসল খরচ কত? এত ছোট ছোট ফাংশন লিখলে প্রোগ্রাম কি ধীর হয়ে যায় না?

প্রশ্নটি যৌক্তিক, কারণ প্রতিটি ডাকে কিছু বাড়তি কাজ হয়: argument সঠিক জায়গায় রাখা, ফেরার ঠিকানা সংরক্ষণ, নতুন frame তৈরি, ফাংশনে লাফ দেওয়া, শেষে frame সরিয়ে ফিরে আসা। আধুনিক CPU-তে এটি সাধারণত কয়েকটি instruction-এর কাজ — এক সেকেন্ডের কোটি ভাগের এক ভাগেরও কম।

তার ওপর compiler আপনার হয়ে খরচটি মুছে দেয়। Optimization চালু থাকলে (-O2) compiler ছোট ফাংশনের কোড সরাসরি ডাকার জায়গায় বসিয়ে দেয় — একে বলে inlining. তখন ডাকার খরচ শূন্য, অথচ আপনার সোর্স কোড পরিষ্কার ও ভাগ করা থাকে। C99 থেকে inline keyword আছে, তবে সেটি শুধু একটি পরামর্শ; আধুনিক compiler নিজেই বেশ ভালো সিদ্ধান্ত নেয় কোনটি inline করবে।

  • Inlining-এর দাম: একই কোড অনেক জায়গায় বসলে প্রোগ্রাম বড় হয়। বড় প্রোগ্রাম CPU-র cache-এ কম ধরে, তাতে উল্টো ধীরও হতে পারে। তাই compiler বড় ফাংশন সাধারণত inline করে না।
  • কোথায় খরচ সত্যিই গুরুত্বপূর্ণ: কোটি বার চলা একটি লুপের ভেতরের ছোট ফাংশন, বা অন্য ফাইলে থাকা ফাংশন (যা compiler দেখতে পায় না)। এর জন্য আছে link-time optimization (LTO), যা linker পর্যায়ে পুরো প্রোগ্রাম একসাথে দেখে inline করতে পারে।

পেশাদার নিয়ম: আগে পরিষ্কার, ছোট ফাংশনে লিখুন। তারপর মাপুন (profiler দিয়ে) আসলে সময় কোথায় যাচ্ছে। প্রায় সবসময় দেখা যায় সমস্যা অ্যালগরিদমে বা মেমরি ব্যবহারে, ফাংশন ডাকায় নয়। Donald Knuth-এর বিখ্যাত সতর্কবাণী — অসময়ের optimization অনেক সমস্যার মূল — এখানে পুরোপুরি খাটে।

মূল উপলব্ধি: মানুষের জন্য পরিষ্কার কোড লিখুন, মেশিনের জন্য দ্রুত কোড বানানোর কাজ compiler-কে করতে দিন। মাপার আগে গতির অজুহাতে নকশা নষ্ট করবেন না।

প্র ০৩Global ভ্যারিয়েবল থাকলে প্রোগ্রাম পরীক্ষা (testing) করা কেন এত কঠিন হয়ে যায়?

একটি ভালো পরীক্ষার মূল শর্ত হলো পুনরাবৃত্তিযোগ্যতা: একই ইনপুট দিলে প্রতিবার একই ফল। Global ভ্যারিয়েবল এই শর্তটিই ভেঙে দেয়।

ধরুন calc_bill(500) ফাংশনটি ভেতরে একটি global discount_rate পড়ে। আপনার পরীক্ষা আজ পাস করল। কাল অন্য কেউ একটি নতুন ফাংশন যোগ করলেন, যা discount_rate বদলায়। এখন পরীক্ষাগুলো কোন ক্রমে চলছে তার ওপর নির্ভর করে কখনো পাস, কখনো ফেল। এমন “মাঝে মাঝে ফেল করা” পরীক্ষা (flaky test) খোঁজা সবচেয়ে বিরক্তিকর কাজগুলোর একটি।

  • লুকানো ইনপুট: ফাংশনের আসল ইনপুট শুধু তার parameter নয়, সব global-ও। কিন্তু মাথা দেখে সেটি বোঝার উপায় নেই। পরীক্ষককে পুরো ফাংশন পড়ে খুঁজে বের করতে হয় কোন কোন global ঠিক করে রাখতে হবে।
  • পরীক্ষার মাঝে দূষণ: এক পরীক্ষা global বদলে দিলে পরের পরীক্ষা সেই বদলানো অবস্থা থেকে শুরু করে। প্রতিটি পরীক্ষার আগে সব global আবার আগের অবস্থায় আনতে হয়।
  • আলাদা করা অসম্ভব: আসল ডেটাবেস বা ফাইলের বদলে একটি নকল (mock) বসিয়ে পরীক্ষা করতে চাইলে, global থাকলে তা বদলানো কঠিন; parameter হলে শুধু অন্য একটি মান পাঠালেই হয়।

এর সমাধানের নাম হলো নির্ভরতা বাইরে থেকে দেওয়া — ফাংশন যা দরকার তা parameter হিসেবে চেয়ে নেয়। calc_bill(500, 0.10) লিখলে পরীক্ষা নিজেই বলে দেয় কোন অবস্থায় পরীক্ষা হচ্ছে। Pure function হলে তো কথাই নেই — কোনো প্রস্তুতি লাগে না, শুধু ডাকুন আর ফল মেলান।

মূল উপলব্ধি: যে কোড সহজে পরীক্ষা করা যায়, সেটি সাধারণত ভালো নকশারও কোড। পরীক্ষা লিখতে গিয়ে কষ্ট হলে সেটি নকশার একটি সতর্কসংকেত — কোথাও লুকানো নির্ভরতা আছে।

প্র ০৪Call stack কীভাবে একটি ফাংশনকে নিজেকেই ডাকার সুযোগ দেয়? আর stack overflow আসলে কী?

Call stack-এর আসল জাদু হলো: প্রতিটি ডাকের নিজের frame. একই ফাংশন দুবার ডাকা হলে দুটি আলাদা frame তৈরি হয়, প্রত্যেকটির নিজের parameter, নিজের local ভ্যারিয়েবল, নিজের ফেরার ঠিকানা। তাই একটি ফাংশন যদি নিজেকেই ডাকে, নতুন ডাকটি পুরোনোর ভ্যারিয়েবল নষ্ট করে না — পুরোনোটি নিচে থেমে অপেক্ষা করে।

এটিকে বলে recursion, পর্ব ১৫-এর বিষয়। যেমন factorial(3) ডাকে factorial(2)-কে, সে ডাকে factorial(1)-কে। Stack-এ তিনটি frame, প্রতিটিতে n-এর আলাদা মান: 3, 2, 1. সবচেয়ে ওপরেরটি উত্তর দিয়ে মুছে যায়, তারপর নিচেরটি সেই উত্তর দিয়ে নিজের হিসাব শেষ করে — ঠিক আমাদের apply_vat সিমুলেটরের মতো।

ইতিহাসের একটি মজার তথ্য: সব ভাষায় এই সুবিধা ছিল না। প্রথম দিকের Fortran-এ প্রতিটি ফাংশনের ভ্যারিয়েবল থাকত একটি নির্দিষ্ট, স্থায়ী জায়গায় — অনেকটা static-এর মতো। ফলে ফাংশন নিজেকে ডাকলে নিজের ভ্যারিয়েবলই নষ্ট করে ফেলত, তাই recursion নিষেধ ছিল। ALGOL 60-এর মতো ভাষা stack-ভিত্তিক frame জনপ্রিয় করে, আর C সেই ধারাই নেয়।

Stack overflow: Stack-এর জায়গা নির্দিষ্ট — অপারেটিং সিস্টেম শুরুতে ঠিক করে দেয় (Linux-এ সাধারণত ৮ MB, Windows-এ ১ MB). Recursion যদি কখনো না থামে (থামার শর্ত ভুল বা নেই), অথবা প্রতিটি frame-এ বিশাল local array থাকে, frame জমতে জমতে সীমা পেরিয়ে যায়। তখন প্রোগ্রাম সাধারণত Segmentation fault দিয়ে crash করে। বিখ্যাত প্রোগ্রামিং প্রশ্নোত্তর সাইট “Stack Overflow”-এর নাম এই সমস্যা থেকেই।

মূল উপলব্ধি: একটি সরল ডেটা কাঠামো — নিচে থেকে ওপরে জমা হওয়া খোপ — এক ধাক্কায় তিনটি সমস্যার সমাধান করে: কোথায় ফিরতে হবে, কার ভ্যারিয়েবল কোনটি, আর নিজেকে ডাকা। তবে সব সুবিধার মতো এরও সীমা আছে, আর সেই সীমা জানাই ভালো প্রকৌশল।

প্র ০৫ভালো ফাংশন-সীমানা কীভাবে AI-এর লেখা কোডকে যাচাইযোগ্য করে তোলে?

AI খুব দ্রুত কোড লেখে, কিন্তু তার ভুলগুলো প্রায়ই সূক্ষ্ম — একটি ভুল সূত্র, একটি উল্টো শর্ত, একটি ভুলে যাওয়া edge case. ৩০০ লাইনের একটি main হাতে পেলে এমন ভুল খোঁজা খড়ের গাদায় সুঁই খোঁজার মতো। কিন্তু একই কোড যদি পনেরোটি ছোট, নাম দেওয়া ফাংশনে ভাগ করা থাকে, তবে আপনি প্রতিটিকে আলাদা করে প্রশ্ন করতে পারেন।

  • নাম বনাম কাজ: প্রতিটি ফাংশনের নাম একটি দাবি — apply_vat মানে “VAT যোগ করি।” আপনার কাজ শুধু মিলিয়ে দেখা দাবিটি সত্য কি না। ২০ লাইনের ভেতরে এটি কয়েক মিনিটের কাজ।
  • আলাদা পরীক্ষা: Pure ফাংশন হলে আপনি নিজে দুই-তিনটি ইনপুট বেছে হাতে হিসাব মিলিয়ে নিতে পারেন। ভুল ধরা পড়লে জানেন ঠিক কোথায় — পুরো প্রোগ্রাম ঘাঁটতে হয় না।
  • ছোট সংশোধন: AI-কে বলতে পারেন “শুধু apply_discount ফাংশনটি ঠিক করো, বাকি কিছু ছোঁবে না।” সীমানা স্পষ্ট থাকলে AI-এর নতুন ভুল ঢোকানোর জায়গাও কমে যায়।
  • Interface আগে: একটি শক্তিশালী কৌশল হলো আগে নিজে prototype-গুলো লিখে AI-কে দেওয়া — “এই পাঁচটি ফাংশন এই নামে, এই ইনপুটে লিখে দাও।” নকশার সিদ্ধান্ত থাকল আপনার হাতে, খুঁটিনাটি লেখার কাজ AI-এর।

এটি আসলে সফটওয়্যার প্রকৌশলের পুরোনো শিক্ষারই নতুন রূপ। Unix-এর দর্শন — “প্রতিটি প্রোগ্রাম একটি কাজ করবে, এবং ভালোভাবে করবে” — একই কারণে টিকে আছে: ছোট, স্পষ্ট টুকরো মানুষ বুঝতে পারে, আর যা বোঝা যায় তা-ই বিশ্বাস করা যায়। পেশাদার দলে কোড review-এর সময়ও বড় পরিবর্তনকে ছোট ছোট অংশে ভাগ করে জমা দিতে বলা হয়।

মূল উপলব্ধি: AI-এর যুগে আপনার সবচেয়ে মূল্যবান দক্ষতা কোড টাইপ করা নয়, বরং সীমানা আঁকা — কোন দায়িত্ব কোথায় থাকবে, কোন interface দিয়ে তারা কথা বলবে। ভালো সীমানা থাকলে যেকোনো লেখকের কোড — মানুষ হোক বা AI — যাচাই করা যায়।

অনুশীলন

এগুলোর বেশিরভাগে কোড লিখতে হবে না — হাতে-কলমে দেখা আর ভাবাই আসল কাজ।

  1. Stack আঁকুন: main ডাকে a()-কে, a ডাকে b()-কে, b শেষ হয়ে ফেরে, তারপর a ডাকে c()-কে। প্রতিটি মুহূর্তে stack-এ কোন কোন frame আছে, খাতায় নিচ থেকে ওপরে লিখুন। সর্বোচ্চ কয়টি frame একসাথে ছিল?

    ক্রমটি হবে: [main] → [main, a] → [main, a, b] → (b ফিরল) [main, a] → [main, a, c] → (c ফিরল) [main, a] → (a ফিরল) [main]. সর্বোচ্চ ৩টি frame একসাথে ছিল। লক্ষ করুন, b আর c কখনো একসাথে stack-এ থাকে না — b-এর জায়গাটিই পরে c ব্যবহার করে। এজন্যই stack মেমরির এত সাশ্রয়ী ব্যবহার করে।

  2. Swap-এর ফাঁদ: আপনার বন্ধু লিখলেন void make_double(int n) { n = n * 2; } আর main-এ int salary = 20000; make_double(salary);. তিনি অবাক — বেতন দ্বিগুণ হলো না কেন? কারণ বলুন, আর pointer ছাড়াই ঠিক করার উপায় দিন।

    Pass by value: n হলো salary-র কপি। কপি দ্বিগুণ হয়ে ফাংশনের frame-এর সাথেই মুছে গেছে, salary আগের মতোই 20000.

    Pointer ছাড়া সমাধান: ফাংশন নতুন মান ফেরত দিক — int make_double(int n) { return n * 2; }, আর ডাকার জায়গায় salary = make_double(salary);. এখন বদলটি ডাকার লাইনেই স্পষ্ট দেখা যাচ্ছে — এটি আসলে আরও ভালো নকশা, কারণ ফাংশনটি এখন pure.

  3. ফাংশন ভাঙুন: একটি ফাংশনের বর্ণনা: “ছাত্রের নম্বর ইনপুট নেয়, যাচাই করে ০–১০০-এর মধ্যে কি না, গ্রেড বের করে, আর স্ক্রিনে রিপোর্ট ছাপায়।” একে কয়টি ফাংশনে ভাঙবেন? প্রতিটির prototype লিখুন, আর কোনগুলো pure হবে বলুন।

    একটি যুক্তিসঙ্গত ভাগ (৪টি):

    • int read_marks(void); — ইনপুট নেয় (side effect: কীবোর্ড পড়া)
    • int is_valid_marks(int marks); — ০–১০০ হলে 1, নইলে 0 (pure)
    • char grade_of(int marks); — নম্বর থেকে গ্রেড (pure)
    • void print_report(int marks, char grade); — ছাপায় (side effect)

    দুটি pure ফাংশনেই আসল যুক্তি, আর সেগুলো ইনপুট ছাড়াই পরীক্ষা করা যায় — যেমন grade_of(80), grade_of(79) সীমানার মানগুলো। main শুধু চারটিকে ক্রমে ডাকে।

  4. Static-এর পরীক্ষা (কোড): উপরের token.c-তে static int last = 100; লাইনটি ফাংশনের বাইরে, ফাইলের ওপরে নিয়ে যান (static রেখেই)। আউটপুট কি বদলাল? scope আর lifetime-এর দিক থেকে কী বদলাল?

    আউটপুট একই থাকবে — 101, 102, 103 — কারণ lifetime একই রয়ে গেছে: দুই অবস্থাতেই ভ্যারিয়েবলটি পুরো প্রোগ্রাম চলাকালীন বাঁচে।

    বদলেছে scope: এখন ফাইলের সব ফাংশন last দেখতে ও বদলাতে পারে, অথচ আগে শুধু next_token পারত। static থাকায় অন্য ফাইল এখনো দেখতে পায় না। অর্থাৎ একই আচরণ, কিন্তু ভুলের সুযোগ বেড়েছে। শিক্ষা: যে ভ্যারিয়েবল একটিমাত্র ফাংশনের দরকার, তাকে সেই ফাংশনের ভেতরেই রাখুন।

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

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

ক্রমএকটি ফাংশন ডাকার ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📝 ডাকার জায়গায় argument-এর মান হিসাব করা হয়
  2. 📥 নতুন stack frame তৈরি — parameter-এ মানের কপি, সাথে ফেরার ঠিকানা
  3. ⚙️ ফাংশনের শরীর চলে, local ভ্যারিয়েবল তৈরি হয়
  4. 📤 return দিয়ে ফলাফল ফেরত দেওয়া হয়
  5. 🗑️ Frame মুছে যায়, প্রোগ্রাম ফেরার ঠিকানায় ফিরে চলতে থাকে

double apply_vat(double amount); — শেষে সেমিকোলনসহ এই লাইনটি কী?

  • ফাংশনের definition
  • ফাংশনের prototype (declaration)
  • ফাংশন ডাকা (call)
  • একটি double ভ্যারিয়েবল ঘোষণা

void add_bonus(int s) { s = s + 500; } ডাকার পর main-এর salary (আগে 20000) কত?

  • 20500
  • 0
  • 20000
  • Compile error

একটি stack frame-এ সাধারণত কী কী থাকে?

  • Parameter, local ভ্যারিয়েবল আর ফেরার ঠিকানা
  • সব global ভ্যারিয়েবল
  • ফাংশনের পুরো machine code
  • শুধু static ভ্যারিয়েবল

একটি ফাংশনের ভেতরে static int count = 0; count++; — ফাংশনটি তিনবার ডাকার পর count কত?

  • 0
  • 1
  • 2
  • 3

ফাইলের একদম ওপরে (সব ফাংশনের বাইরে) static int total; লিখলে এর অর্থ কী?

  • ফাংশন শেষ হলেও মান থাকবে, শুধু এই অর্থে
  • ভ্যারিয়েবলটি শুধু এই ফাইলের — অন্য ফাইল থেকে দেখা যাবে না
  • সব ফাইল থেকে ব্যবহার করা যাবে
  • মানটি আর কখনো বদলানো যাবে না

AI একটি ফাংশন দিল: double get_total(...) — যা হিসাব করে, স্ক্রিনে ছাপায়, ফাইলে লেখে, আর একটি global বদলায়। কোনটি সবচেয়ে ভালো পরামর্শ?

  • চলছে যখন, যেমন আছে রেখে দেওয়া
  • নাম বদলে get_total_and_save_and_print রাখা
  • হিসাবকে একটি pure ফাংশনে আলাদা করা, ছাপা ও ফাইলে লেখা আলাদা ফাংশনে, global-এর বদলে parameter
  • আরও global ব্যবহার করে parameter কমানো

একটি ফাংশন না থেমে নিজেকেই বারবার ডাকতে থাকলে শেষে কী ঘটে?

  • Stack overflow — stack-এর জায়গা ফুরিয়ে প্রোগ্রাম crash করে
  • Compiler আগেই error দেয়, প্রোগ্রাম তৈরিই হয় না
  • প্রোগ্রাম চিরকাল চলতে থাকে, কোনো সমস্যা নেই
  • পুরোনো frame মুছে গিয়ে জায়গা খালি হয়

শব্দকোষ

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

মনে রাখুন

  1. ফাংশন হলো একটি নাম দেওয়া দায়িত্ব — return type, নাম, parameter আর শরীর নিয়ে গঠিত।
  2. ব্যবহারের আগে prototype (compiler-এর জন্য), পুরো প্রোগ্রামে ঠিক একবার definition (linker-এর জন্য)।
  3. C সবসময় pass by value — ফাংশন কপি পায়, তাই সাধারণ swap মূল ভ্যারিয়েবল বদলায় না।
  4. প্রতিটি ডাকে call stack-এ নতুন frame; ফিরলে মুছে যায়। static local stack-এ থাকে না, তাই মান মনে রাখে।
  5. ছোট, এক-দায়িত্বের, যতটা সম্ভব pure ফাংশন; global এড়িয়ে চলুন — AI-এর কোডেও এটাই যাচাই করুন।

পরের পর্বে: আজ scope, lifetime, static আর extern-এর প্রথম পরিচয় পেলেন। পরের পর্বে এগুলো পুরোপুরি খুলে দেখব — চার রকম scope ও চার রকম আয়ু, linker কীভাবে কয়েকটি ফাইলের নাম মেলায়, auto ও register-এর আসল অর্থ, আর local-এর ঠিকানা ফেরত দেওয়া কেন মারাত্মক।

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

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