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

C-তে অ্যারে আর ফাংশন — সমস্যা সমাধান

Arrays and Functions in C — Searching, Sorting, Strings, Recursion and Classic Exam Programs

Taught in Bangla, this lesson covers arrays and functions in C: declaring arrays and zero-based indexing, sum, average, maximum and linear search with loops, bubble sort, 2D arrays as matrices, strings as char arrays ending in a null character and safe input with fgets, function prototypes, call by value and why a naive swap fails, scope, and recursion with base cases.

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

  • অ্যারে ঘোষণা, মান বসানো, index 0 থেকে কেন, আর সীমার বাইরে গেলে কী বিপদ
  • পরীক্ষায় বারবার আসা অ্যারে-প্রোগ্রাম — হাতে dry run করে নিশ্চিত হওয়ার কৌশলসহ
  • 2D অ্যারে দিয়ে ম্যাট্রিক্স, আর string নিয়ে নিরাপদে কাজ
  • ফাংশনের অংশগুলো, call by value, scope আর recursion-এর ভেতরের ছবি
  • AI-এর লেখা C কোড যাচাই করার চেকলিস্ট

১ · অ্যারে — এক নামে অনেক মান

পর্ব ৩৯-এ আপনি ভ্যারিয়েবল, শর্ত আর লুপ শিখেছেন। এবার একটি বাস্তব কাজ ধরুন: একটি ক্লাসের ৫০ জন শিক্ষার্থীর ICT নম্বর রাখতে হবে, তারপর গড়, সর্বোচ্চ আর মেধাক্রম বের করতে হবে। ৫০টি আলাদা ভ্যারিয়েবল — m1, m2, … m50? লেখা যাবে, কিন্তু যোগ করতে ৫০টি নাম লিখতে হবে, আর শিক্ষার্থী ৫০০ জন হলে প্রোগ্রামটাই অচল।

সমাধান হলো array (অ্যারে): একই ধরনের অনেকগুলো মান, একটি নামে, ক্রমিক নম্বর দেওয়া ঘরে।

ট্রেনের একটি বগির কথা ভাবুন। বগির নাম “ঙ”, আর ভেতরে আসনগুলো ক্রমিক নম্বরে পাশাপাশি। টিকিটে শুধু “ঙ-১২” লিখলেই আসন খুঁজে পাওয়া যায় — প্রতিটি আসনের আলাদা নাম লাগে না। অ্যারেও তাই: নাম marks, আর ঘরের নম্বর দিয়ে নির্দিষ্ট মানটি — marks[12]. পার্থক্য একটাই: C-তে প্রথম আসনের নম্বর ১ নয়, ০.

ঘোষণা, মান বসানো আর index

int marks[5];                          /* ৫টি int-এর জায়গা, মান এখনো অজানা */
int roll[5] = {11, 12, 13, 14, 15};    /* ঘোষণার সাথেই মান */
int a[5] = {7, 3};                     /* বাকি তিনটি ঘর হবে 0 */
int b[] = {4, 8, 15, 16};              /* আকার compiler নিজে গুনে নেয়: 4 */
float price[3];
char grade[4] = {'A', 'B', 'C', 'F'};

marks[0] = 72;                         /* প্রথম ঘর */
marks[4] = 90;                         /* শেষ ঘর — marks[5] নয়! */
printf("%d", roll[2]);                 /* ছাপবে 13 */

বন্ধনীর ভেতরের সংখ্যাটি দুই জায়গায় দুই অর্থে বসে, এটিই নতুনদের প্রথম বিভ্রান্তি। ঘোষণায় int marks[5]-এর ৫ মানে মোট ঘর কয়টি। ব্যবহারে marks[4]-এর ৪ মানে কোন ঘর — একে বলে index. index শুরু ০ থেকে, তাই ৫ ঘরের অ্যারের বৈধ index ০, ১, ২, ৩, ৪ — শেষটি সবসময় আকারের চেয়ে এক কম।

মেমরিতে অ্যারে দেখতে কেমন

পর্ব ২৪-এ দেখেছেন, RAM হলো ক্রমিক নম্বর দেওয়া অসংখ্য বাইটের সারি, আর প্রতিটি বাইটের নিজস্ব memory address আছে। অ্যারের ঘরগুলো RAM-এ একটির পর একটি, কোনো ফাঁক ছাড়া বসে। সাধারণ কম্পিউটারে একটি int ৪ বাইট, তাই int roll[5] নেয় ৫ × ৪ = ২০ বাইট:

ঘরroll[0]roll[1]roll[2]roll[3]roll[4]
মান1112131415
ঠিকানা (ধরা যাক)10001004100810121016
মূল ধারণা

যেকোনো ঘরের ঠিকানা = শুরুর ঠিকানা + index × একটি ঘরের মাপ। roll[3]-এর ঠিকানা = 1000 + 3 × 4 = 1012. এজন্যই CPU অ্যারের যেকোনো ঘরে একবারের হিসাবে সরাসরি পৌঁছাতে পারে — ১০তম ঘর আর ১০ লাখতম ঘর, সময় একই। আর পাশাপাশি থাকায় একটি ঘর পড়লে পাশের ঘরগুলোও cache-এ চলে আসে — পর্ব ২৫-এর spatial locality. লুপ দিয়ে অ্যারে পড়া তাই খুব দ্রুত।

সীমার বাইরে গেলে — C পাহারা দেয় না

int a[5]; লিখে a[5] = 99; বা a[-1] লিখলে C কোনো error দেয় না। হিসাব অনুযায়ী সে শুধু পাশের মেমরিতে লিখে ফেলে — হয়তো অন্য কোনো ভ্যারিয়েবলের ঘরে। C-এর নিয়মে এর ফল নির্দিষ্ট নয়, একে বলে undefined behaviour: কখনো প্রোগ্রাম দিব্যি চলে, কখনো অন্য ভ্যারিয়েবলের মান রহস্যজনকভাবে বদলে যায়, কখনো crash. Python বা Java এমন জায়গায় সাথে সাথে error দেয়; C দেয় না, কারণ প্রতিবার পাহারা দিলে গতি কমে। তাই সীমা মানার দায়িত্ব পুরোপুরি প্রোগ্রামারের। নিয়ম মনে রাখুন: লুপ i = 0 থেকে, শর্ত i < n — কখনো i <= n নয়।

২ · লুপ দিয়ে অ্যারে — পরীক্ষার ক্লাসিক প্রোগ্রাম

অ্যারের আসল শক্তি আসে লুপের সাথে। index যেহেতু একটি সংখ্যা, তাই i ভ্যারিয়েবল দিয়ে ০ থেকে n − 1 পর্যন্ত ঘুরলেই সব ঘরে একে একে পৌঁছানো যায়। ৫টি হোক বা ৫০০০টি — কোড একই থাকে, শুধু n বদলায়। প্রায় সব অ্যারে-প্রোগ্রামের কাঠামো তিন ভাগে: পড়া (scanf লুপ) → প্রক্রিয়া (হিসাবের লুপ) → লেখা (printf লুপ)। নিচের প্রতিটি ট্যাবে একটি করে ক্লাসিক প্রোগ্রাম — পরীক্ষার খাতায় যেভাবে লেখা হয়, সেভাবেই:

#include <stdio.h>
int main()
{
    int a[100], n, i, sum = 0;
    float avg;
    printf("How many numbers? ");
    scanf("%d", &n);
    for (i = 0; i < n; i++)
        scanf("%d", &a[i]);
    for (i = 0; i < n; i++)
        sum = sum + a[i];
    avg = (float)sum / n;
    printf("Sum = %d\nAverage = %.2f\n", sum, avg);
    return 0;
}

প্রত্যাশিত আউটপুট (ইনপুট: 5, তারপর 10 20 30 40 45)

How many numbers? 5
10 20 30 40 45
Sum = 145
Average = 29.00

(float)sum না লিখলে 145 / 5 পূর্ণসংখ্যার ভাগ হয়ে দশমিক অংশ হারাত — যোগফল ১৪৬ হলে উত্তর আসত 29.00, যদিও আসল গড় 29.20. a[100] মানে সর্বোচ্চ ১০০টি সংখ্যা; n-এ ১০০-এর বেশি দিলে সীমার বাইরে লেখা হবে।

#include <stdio.h>
int main()
{
    int a[6] = {45, 12, 78, 34, 90, 23};
    int i, max = a[0], min = a[0];
    for (i = 1; i < 6; i++) {
        if (a[i] > max) max = a[i];
        if (a[i] < min) min = a[i];
    }
    printf("Largest = %d, Smallest = %d\n", max, min);
    return 0;
}

প্রত্যাশিত আউটপুট

Largest = 90, Smallest = 12

কৌশলটি হলো max-কে প্রথম মান দিয়েই শুরু করা, ০ দিয়ে নয়। সব মান ঋণাত্মক হলে (যেমন তাপমাত্রা −5, −12) max = 0 দিয়ে শুরু করলে উত্তর আসত ০ — যা তালিকাতেই নেই।

#include <stdio.h>
int main()
{
    int a[9] = {12, 7, 9, 20, 33, 18, 5, 6, 11};
    int i, even = 0, odd = 0;
    for (i = 0; i < 9; i++) {
        if (a[i] % 2 == 0)
            even++;
        else
            odd++;
    }
    printf("Even = %d, Odd = %d\n", even, odd);
    return 0;
}

প্রত্যাশিত আউটপুট

Even = 4, Odd = 5

দুটি counter শুরুতে ০, প্রতিটি মান দেখে একটি বাড়ে। একই ছাঁচে গোনা যায় “পাস কতজন” (a[i] >= 33) বা “ধনাত্মক কয়টি”।

#include <stdio.h>
int main()
{
    int a[7] = {15, 8, 23, 42, 4, 16, 31};
    int i, key = 42, pos = -1;
    for (i = 0; i < 7; i++) {
        if (a[i] == key) {
            pos = i;
            break;
        }
    }
    if (pos == -1)
        printf("%d not found\n", key);
    else
        printf("%d found at index %d (position %d)\n", key, pos, pos + 1);
    return 0;
}

প্রত্যাশিত আউটপুট

42 found at index 3 (position 4)

Linear search: শুরু থেকে একে একে মিলিয়ে দেখা, পেলেই break. pos = -1 দিয়ে শুরু করার কারণ — −১ কোনো বৈধ index নয়, তাই লুপ শেষে −১ থেকে গেলে বোঝা যায় পাওয়া যায়নি। ১০০০টি মানে সবচেয়ে খারাপ ক্ষেত্রে ১০০০বার তুলনা লাগে।

#include <stdio.h>
int main()
{
    int a[5] = {1, 2, 3, 4, 5};
    int i, n = 5, temp;
    for (i = 0; i < n / 2; i++) {
        temp = a[i];
        a[i] = a[n - 1 - i];
        a[n - 1 - i] = temp;
    }
    for (i = 0; i < n; i++)
        printf("%d ", a[i]);
    printf("\n");
    return 0;
}

প্রত্যাশিত আউটপুট

5 4 3 2 1

প্রথমের সাথে শেষ, দ্বিতীয়ের সাথে শেষের-আগে — এভাবে অর্ধেক পর্যন্ত অদলবদল। লুপ n পর্যন্ত চালালে প্রতিটি জোড়া দুবার বদলে অ্যারে আবার আগের মতো হয়ে যেত! আর তিন ভ্যারিয়েবলের swap (temp) কৌশলটি মনে রাখুন — bubble sort-এও এটিই লাগবে।

#include <stdio.h>
int main()
{
    int a[8] = {2, 5, 2, 8, 5, 2, 9, 0};
    int freq[10] = {0};          /* 0-9 প্রতিটি অঙ্কের গণনা */
    int i;
    for (i = 0; i < 8; i++)
        freq[a[i]]++;
    for (i = 0; i < 10; i++)
        if (freq[i] > 0)
            printf("%d occurs %d time(s)\n", i, freq[i]);
    return 0;
}

প্রত্যাশিত আউটপুট

0 occurs 1 time(s)
2 occurs 3 time(s)
5 occurs 2 time(s)
8 occurs 1 time(s)
9 occurs 1 time(s)

ভোটকেন্দ্রের গণনার মতো: প্রতিটি প্রার্থীর একটি ঘর, ব্যালট এলে সেই ঘরে এক যোগ। freq[a[i]]++ মানে “মানটিকেই index বানিয়ে সেই ঘর বাড়াও।” শর্ত একটাই — প্রতিটি মান ০ থেকে ৯-এর মধ্যে হতে হবে, নইলে সীমার বাইরে লেখা হবে।

Dry run — পরীক্ষার সবচেয়ে বড় অস্ত্র: খাতায় কম্পিউটার নেই। তাই প্রতিটি প্রোগ্রাম কাগজে dry run করার অভ্যাস গড়ুন — একটি টেবিলে কলাম i, a[i], sum বা max; প্রতিটি লুপ-ঘুরতিতে এক সারি। পাঁচ মিনিটের এই অভ্যাস output-প্রশ্নে নির্ভুল উত্তর দেয়, আর নিজের লেখা প্রোগ্রামের ভুলও ধরিয়ে দেয়।

৩ · Bubble sort — পাশাপাশি তুলনা করে সাজানো

মেধাতালিকা বানাতে নম্বরগুলো সাজাতে হয়। সাজানোর সবচেয়ে সহজ algorithm হলো bubble sort (পর্ব ৩৮-এ দেখেছেন, algorithm মানে ধাপে ধাপে সুনির্দিষ্ট নিয়ম)। নিয়ম মাত্র একটি: পাশাপাশি দুটি মান দেখুন; বাঁয়েরটি বড় হলে জায়গা বদলান। বাঁ থেকে ডানে একবার পুরো ঘোরাকে বলে একটি pass. প্রতিটি pass শেষে সবচেয়ে বড় মানটি পানির বুদবুদের মতো ভেসে শেষ প্রান্তে পৌঁছে যায় — নামটি সেখান থেকেই।

নিচে {5, 1, 4, 2, 8} সাজানো হচ্ছে। “পরে” চাপুন, আর প্রতিটি ধাপে কোন দুটি ঘর তুলনা হলো, সেটি খেয়াল করুন:

1for (i = 0; i < n - 1; i++)2    for (j = 0; j < n - 1 - i; j++)3        if (a[j] > a[j + 1]) {4            temp = a[j];5            a[j] = a[j + 1];6            a[j + 1] = temp;7        }
অ্যারের অবস্থা
a[0]
a[1]
a[2]
a[3]
a[4]
pass
এই pass-এ swap
  1. শুরু। অ্যারে {5, 1, 4, 2, 8}, n = 5. বাইরের লুপ (i) গোনে কয়টি pass, ভেতরের লুপ (j) পাশাপাশি জোড়াগুলো ঘুরে দেখে।
  2. Pass ১, j = 0: a[0] = 5 আর a[1] = 1. 5 > 1 — ভুল ক্রম, তাই swap. এখন {1, 5, 4, 2, 8}.
  3. Pass ১, j = 1: 5 > 4 — আবার swap. {1, 4, 5, 2, 8}. খেয়াল করুন, 5 প্রতিবার এক ঘর ডানে সরছে — বুদবুদ উঠছে।
  4. Pass ১, j = 2: 5 > 2 — swap. {1, 4, 2, 5, 8}.
  5. Pass ১, j = 3: 5 > 8? না — কিছু বদলায় না। Pass ১ শেষ: ৪টি তুলনা, ৩টি swap, আর সবচেয়ে বড় 8 শেষ ঘরে নিশ্চিতভাবে বসে গেছে। তাই পরের pass-এ শেষ ঘর আর দেখতে হবে না — এজন্যই ভেতরের শর্ত j < n - 1 - i.
  6. Pass ২ (৩টি তুলনা): 1 > 4 না; 4 > 2 হ্যাঁ — swap; 4 > 5 না। এখন {1, 2, 4, 5, 8} — শেষ দুটি ঘর নিশ্চিত।
  7. Pass ৩ (২টি তুলনা): 1 > 2 না, 2 > 4 না — একটিও swap নেই। অ্যারে আসলে সাজানো হয়ে গেছে।
  8. Pass ৪ (১টি তুলনা): 1 > 2 না। বাইরের লুপ শেষ। মোট তুলনা ৪ + ৩ + ২ + ১ = ১০, মোট swap ৪. n টি মানে সর্বোচ্চ n − 1টি pass. চালাক উন্নতি: কোনো pass-এ একটিও swap না হলে তখনই থামা — তাহলে pass ৪ লাগত না।

এবার সব একসাথে: একটি ক্লাসের নম্বর পড়ে মোট, গড়, সর্বোচ্চ, সর্বনিম্ন আর সাজানো তালিকা। ইনপুট আগে থেকে দেওয়া আছে (প্রথমে শিক্ষার্থীর সংখ্যা, তারপর নম্বরগুলো) — চালিয়ে দেখুন, তারপর হাতে মিলিয়ে নিন:

#include <stdio.h>

int main(void) {
    int m[50], n, sum = 0;
    if (scanf("%d", &n) != 1 || n < 1 || n > 50) return 1;   /* bounds check */
    for (int i = 0; i < n; i++) { scanf("%d", &m[i]); sum += m[i]; }
    int max = m[0], min = m[0];
    for (int i = 1; i < n; i++) {
        if (m[i] > max) max = m[i];
        if (m[i] < min) min = m[i];
    }
    printf("Total = %d, Average = %.2f\n", sum, (float)sum / n);
    printf("Highest = %d, Lowest = %d\n", max, min);
    for (int i = 0; i < n - 1; i++)                 /* bubble sort */
        for (int j = 0; j < n - 1 - i; j++)
            if (m[j] > m[j + 1]) { int t = m[j]; m[j] = m[j + 1]; m[j + 1] = t; }
    printf("Sorted:");
    for (int i = 0; i < n; i++) printf(" %d", m[i]);
    printf("\n");
    return 0;
}

আউটপুট: Total = 446, Average = 74.33, Highest = 90, Lowest = 58, আর Sorted: 58 64 72 77 85 90. তিনটি জিনিস খেয়াল করুন। এক, পঞ্চম লাইনে n যাচাই — ৫০-এর বেশি দিলে প্রোগ্রাম কাজই শুরু করে না, তাই অ্যারের সীমার বাইরে লেখার সুযোগ নেই। দুই, পড়ার লুপেই যোগ সেরে ফেলা হয়েছে — একটি লুপ বাঁচল। তিন, সাজানোর পর আসল ক্রম হারিয়ে যায়; রোল নম্বর অনুযায়ী তালিকাও দরকার হলে সাজানোর আগে কপি রাখতে হতো। ইনপুট বদলে নিজের ক্লাসের নম্বর দিয়ে চালিয়ে দেখুন।

৪ · 2D অ্যারে — সারি আর কলামের ম্যাট্রিক্স

পরীক্ষার ফলাফলের শিটে সারি = শিক্ষার্থী, কলাম = বিষয়। এমন টেবিলের জন্য আছে two-dimensional array: int r[3][4]; মানে ৩টি সারি, ৪টি কলাম, মোট ১২টি ঘর। কোনো ঘর ধরতে দুটি index লাগে — r[সারি][কলাম], দুটোই ০ থেকে। Excel-এর সেল যেমন “B3” (পর্ব ১০), তেমনি এখানে r[2][1] — শুধু গোনা শুরু ০ থেকে। আর দুটি index মানে দুটি লুপ: বাইরের লুপ সারি ধরে, ভেতরের লুপ কলাম ধরে।

মেমরিতে কিন্তু টেবিল বলে কিছু নেই — RAM একটিই লম্বা সারি। C তাই সারিগুলো পরপর বসায়: আগে প্রথম সারির সব ঘর, তারপর দ্বিতীয় সারি (row-major order). HSC-র ক্লাসিক প্রশ্ন — ম্যাট্রিক্স যোগ:

#include <stdio.h>
int main()
{
    int A[2][3] = {{1, 2, 3}, {4, 5, 6}};
    int B[2][3] = {{6, 5, 4}, {3, 2, 1}};
    int C[2][3], i, j;
    for (i = 0; i < 2; i++)
        for (j = 0; j < 3; j++)
            C[i][j] = A[i][j] + B[i][j];
    printf("Sum matrix:\n");
    for (i = 0; i < 2; i++) {
        for (j = 0; j < 3; j++)
            printf("%4d", C[i][j]);
        printf("\n");             /* প্রতিটি সারি শেষে নতুন লাইন */
    }
    return 0;
}

প্রত্যাশিত আউটপুট

Sum matrix:
   7   7   7
   7   7   7

ইনপুট নিতে হলে প্রথম লুপের ভেতরে scanf("%d", &A[i][j]); — কাঠামো একই। ম্যাট্রিক্স যোগের শর্ত হলো দুটির মাপ এক হতে হবে। ভেতরের printf("\n") বাইরের লুপে, ভেতরের লুপের পরে — এটি ভুল জায়গায় বসালে সব সংখ্যা এক লাইনে বা প্রতিটি আলাদা লাইনে চলে আসে, পরীক্ষায় খুব সাধারণ ভুল।

Transpose (সারিকে কলাম বানানো) — শুধু index দুটি উল্টে দিন: T[j][i] = A[i][j];. 2 × 3 ম্যাট্রিক্সের transpose হয় 3 × 2:

int A[2][3] = {{1, 2, 3}, {4, 5, 6}}, T[3][2], i, j;
for (i = 0; i < 2; i++)
    for (j = 0; j < 3; j++)
        T[j][i] = A[i][j];
/* T = {{1, 4}, {2, 5}, {3, 6}} */

৫ · String — char-এর অ্যারে আর শেষের '\0'

নাম, ঠিকানা, বাক্য — এগুলো string. C-তে আলাদা কোনো “string টাইপ” নেই; string হলো char-এর অ্যারে, আর প্রতিটি অক্ষর একটি ঘরে (তার সংখ্যামান ASCII অনুযায়ী — পর্ব ১৮)। কিন্তু অ্যারে তো নিজের দৈর্ঘ্য জানে না — তাহলে লেখা কোথায় শেষ? উত্তর: শেষে একটি বিশেষ অক্ষর বসে, '\0' (null character, যার মান ০)। char city[] = "Dhaka"; লিখলে মেমরিতে যা থাকে:

index012345
অক্ষর'D''h''a''k''a''\0'

৫টি অক্ষরের জন্য ৬টি ঘর — শেষেরটি '\0'-এর। তাই strlen(city) = 5 (অক্ষর গোনে, '\0' বাদে), কিন্তু sizeof(city) = 6 (অ্যারের মোট বাইট)। ১০ অক্ষরের নাম রাখতে অ্যারে লাগে অন্তত ১১ ঘরের।

String ইনপুট — কোনটি নিরাপদ

উপায়কী করেসমস্যা
scanf("%s", name);একটি শব্দ পড়ে (& লাগে না — অ্যারের নামই ঠিকানা)স্পেসে থামে: “Rahim Uddin” দিলে শুধু “Rahim”. সীমাও দেখে না
gets(name);পুরো লাইন পড়েঅ্যারের মাপ জানার কোনো উপায়ই নেই — বেশি লম্বা লাইন পাশের মেমরিতে উপচে পড়ে। C11 থেকে ভাষা থেকেই বাদ
fgets(name, sizeof(name), stdin);পুরো লাইন, কিন্তু সর্বোচ্চ মাপ − 1টি অক্ষরশেষে Enter-এর '\n'-ও রেখে দেয় — দরকার হলে সরাতে হয়

অনেক পুরোনো বই ও গাইডে এখনো gets() আছে, আর আধুনিক compiler-এ সেটি warning বা error দেয়। জায়গার চেয়ে বেশি ডেটা লিখে পাশের মেমরি নষ্ট করাকে বলে buffer overflow — ভাবনার প্রশ্ন ২-এ দেখবেন, এটি কত বড় বিপদ ডেকে আনতে পারে। scanf-এও সীমা দেওয়া যায়: scanf("%19s", name); (২০ ঘরের অ্যারের জন্য)।

string.h-এর চারটি কাজের ফাংশন

#include <stdio.h>
#include <string.h>
int main()
{
    char a[20] = "Dhaka", b[20];
    strcpy(b, a);                    /* b = "Dhaka" (কপি) */
    strcat(b, " City");              /* b = "Dhaka City" (জোড়া) */
    printf("%s, length = %d\n", b, (int)strlen(b));
    printf("%d\n", strcmp(a, "Dhaka") == 0);   /* সমান হলে strcmp দেয় 0 */
    return 0;
}

প্রত্যাশিত আউটপুট

Dhaka City, length = 10
1

দুটি ফাঁদ মনে রাখুন। এক, string-এ b = a; বা if (a == b) চলে না — অ্যারের নাম ঠিকানা, তাই প্রথমটি error, আর দ্বিতীয়টি অক্ষর নয়, ঠিকানা তুলনা করে। কপিতে strcpy, তুলনায় strcmp. দুই, strcpy আর strcat-ও জায়গা যাচাই করে না — b যথেষ্ট বড় কি না, সেটি আপনার দায়িত্ব। Palindrome (উল্টো পড়লেও একই, যেমন “madam”, “level”) যাচাইয়ের প্রোগ্রাম আছে অংশ ৮-এর চালানো যায় এমন উদাহরণে।

৬ · ফাংশন — একটি কাজ, একটি নাম

এতক্ষণ সব কোড main-এর ভেতরে লিখেছি। ছোট প্রোগ্রামে চলে, কিন্তু একটি স্কুলের ফলাফল সফটওয়্যারে গড় বের করার কোড যদি দশ জায়গায় লাগে? দশবার কপি করবেন, আর একটি ভুল পেলে দশ জায়গায় ঠিক করবেন? Function এই সমস্যার সমাধান: একটি নির্দিষ্ট কাজের কোডকে একটি নাম দিয়ে প্যাকেট করা, যাতে যখন খুশি নাম ধরে ডাকা যায়।

চায়ের দোকানে বললেন “এক কাপ দুধ চা, চিনি কম।” দোকানদার কীভাবে পানি ফোটান, কতটা পাতা দেন — আপনাকে জানতে হয় না। আপনি দিলেন উপকরণ (দুধ চা, চিনি কম), পেলেন ফল (এক কাপ চা)। ফাংশনও তাই: ডাকার সময় দেন argument, ফাংশন ভেতরে কাজ করে, ফেরত দেয় একটি return value. ভেতরের খুঁটিনাটি লুকানো — পর্ব ২৩-এর abstraction-এর প্রথম ধাপ এটাই।

ফাংশনের সুবিধা তিনটি: পুনর্ব্যবহার (একবার লিখে বহুবার ডাকা), আলাদা করে পরীক্ষা (gcd(48, 18) ঠিক ৬ দেয় কি না, বাকি প্রোগ্রাম ছাড়াই যাচাই করা যায়), আর ভাগ করে কাজ (modularity — বড় সমস্যাকে ছোট ছোট নামওয়ালা টুকরোয় ভাঙা)।

দুই রকম ফাংশন

Library function

C-এর সাথে তৈরি পাওয়া — printf, scanf, strlen, sqrt. ব্যবহার করতে শুধু ঠিক header file include করতে হয় (stdio.h, string.h, math.h).

library function

User-defined function

আপনি নিজে লেখেন — factorial, isPrime, average. নাম, কাজ আর ফেরত টাইপ আপনার ঠিক করা। main নিজেও একটি ফাংশন!

user-defined function

একটি ফাংশনের তিনটি অংশ

#include <stdio.h>

long power(int base, int exp);         /* ১. prototype (ঘোষণা) — শেষে ; */

int main()
{
    long p = power(2, 10);             /* ২. call — 2 আর 10 হলো argument */
    printf("2^10 = %ld\n", p);
    return 0;
}

long power(int base, int exp)          /* ৩. definition — base, exp হলো parameter */
{
    long result = 1;                   /* local variable */
    int i;
    for (i = 1; i <= exp; i++)
        result = result * base;
    return result;                     /* ফেরত মান */
}

প্রত্যাশিত আউটপুট

2^10 = 1024
  • Prototype: compiler ওপর থেকে নিচে পড়ে। main যখন power ডাকে, তখনো definition আসেনি — prototype আগেভাগে জানিয়ে রাখে “এই নামে একটি ফাংশন আছে, দুটি int নেয়, long ফেরত দেয়।” Definition main-এর ওপরে লিখলে prototype না দিলেও চলে।
  • Call: power(2, 10) — প্রোগ্রাম ফাংশনে লাফ দেয়, কাজ শেষে ফেরত মান নিয়ে এখানেই ফিরে আসে।
  • Definition: header লাইন (ফেরত টাইপ, নাম, parameter) আর { }-এর ভেতরে আসল কাজ। Parameter হলো ফাংশনের “খালি ঘর”, argument হলো ডাকার সময় সেই ঘরে দেওয়া মান।
  • return আর void: ফাংশন একবারে একটিই মান ফেরত দিতে পারে। কিছু ফেরত দেওয়ার না থাকলে (যেমন শুধু একটি লাইন ছাপা) ফেরত টাইপ লিখুন void — void printLine(int n).

Scope — কোন ভ্যারিয়েবল কোথায় দেখা যায়

power-এর ভেতরের result আর i হলো local variable — জন্ম ফাংশন ডাকার সময়, মৃত্যু ফাংশন শেষে। main এদের দেখতেই পায় না; চাইলে main-এও আলাদা একটি i রাখা যায়, দুটির মধ্যে কোনো সম্পর্ক নেই। একটি ভ্যারিয়েবলকে কোথা থেকে দেখা যায়, সেটিই তার scope. সব ফাংশনের বাইরে ঘোষণা করলে হয় global variable, যাকে সব ফাংশন দেখে ও বদলাতে পারে। সুবিধাজনক মনে হলেও বড় প্রোগ্রামে global এড়িয়ে চলাই নিয়ম — কোন ফাংশন মানটি বদলাল, খুঁজে বের করা দুঃসাধ্য হয়ে ওঠে। ফাংশনের যা লাগে, parameter দিয়ে নিন; যা দেওয়ার, return দিয়ে দিন।

ফাংশনে অ্যারে পাঠানো

float average(int a[], int n)          /* অ্যারে + তার দৈর্ঘ্য */
{
    int i, sum = 0;
    for (i = 0; i < n; i++)
        sum += a[i];
    return (float)sum / n;
}
/* main-এ: int marks[4] = {60, 70, 80, 90};
           printf("%.1f", average(marks, 4));   ছাপবে 75.0 */

অ্যারের বেলায় দুটি বিশেষ নিয়ম। এক, পুরো অ্যারে কপি হয় না — যায় শুধু শুরুর ঠিকানা। তাই ফাংশন অ্যারের ঘর বদলালে main-এর আসল অ্যারেই বদলায় (এজন্যই একটি sort(a, n) ফাংশন লেখা যায়)। দুই, ঠিকানার সাথে দৈর্ঘ্য যায় না — তাই n আলাদা করে পাঠাতে হয়। ফাংশনের ভেতরে sizeof(a) অ্যারের মাপ দেয় না, দেয় একটি ঠিকানার মাপ।

৭ · Call by value — যে swap কাজ করে না

সাধারণ ভ্যারিয়েবলের বেলায় C-এর নিয়ম হলো call by value: ফাংশন ডাকার সময় argument-এর মানটি কপি হয়ে parameter-এ যায়। ফাংশন নিজের কপি নিয়ে যা খুশি করুক, আসল ভ্যারিয়েবল অক্ষত। এর সবচেয়ে বিখ্যাত প্রমাণ — দুটি সংখ্যা অদলবদলের এই ফাংশন, যা দেখতে নিখুঁত, কিন্তু কাজ করে না:

1void swap(int x, int y) {2    int t = x;3    x = y;4    y = t;5}6int main(void) {7    int a = 10, b = 20;8    swap(a, b);9    printf("a=%d b=%d\n", a, b);10}
মেমরি (Watch)
main: a
main: b
swap: x
swap: y
swap: t
  1. main শুরু। a = 10, b = 20 — main-এর নিজের দুটি ঘর।
  2. swap(a, b) ডাকা হলো। মেমরিতে swap-এর জন্য নতুন ঘর তৈরি হলো: x পেল a-র মানের কপি 10, y পেল b-র কপি 20. খেয়াল করুন, x আর a দুটি সম্পূর্ণ আলাদা ঘর — যেন মূল মার্কশিটের ফটোকপি।
  3. t = x: t = 10. এখনো সব ঠিকঠাক।
  4. x = y: x হলো 20. বদলাল ফটোকপি — main-এর a এখনো 10.
  5. y = t: y হলো 10. swap-এর ভেতরে অদলবদল সফল — x = 20, y = 10. কিন্তু a আর b-র দিকে তাকান: একটুও নড়েনি।
  6. ফাংশন শেষ। x, y, t local ভ্যারিয়েবল — swap শেষ হতেই তাদের ঘর মুছে গেল, অদলবদলের ফলসহ। ফটোকপিতে কাটাকাটি করে মূল মার্কশিট বদলানো যায় না।
  7. আউটপুট a=10 b=20. কিছুই বদলায়নি। সমাধান দুটি: ফাংশনকে আসল ঘরের ঠিকানা দেওয়া (swap(&a, &b) আর pointer — একে বলে call by reference, C কোর্সে বিস্তারিত), অথবা অদলবদলটি main-এর ভেতরেই করা। অ্যারে কেন বদলায়, এবার বোঝা যাচ্ছে — অ্যারের বেলায় ঠিকানাই যায়।

৮ · Recursion — যে ফাংশন নিজেকে ডাকে

৪! = ৪ × ৩ × ২ × ১. কিন্তু আরেকভাবেও লেখা যায়: ৪! = ৪ × ৩! — অর্থাৎ বড় সমস্যার উত্তর একই ধরনের একটি ছোট সমস্যার উত্তর দিয়ে। ফাংশন যখন এভাবে নিজেকেই ডাকে, তাকে বলে recursion. কিন্তু “৩! = ৩ × ২!, ২! = ২ × ১!…” — কোথাও থামতে হবে। যে শর্তে ফাংশন আর নিজেকে না ডেকে সরাসরি উত্তর দেয়, সেটি base case: ১! = ১.

সিনেমা হলে অন্ধকারে বসে জানতে চান আপনি কোন সারিতে। সামনের জনকে জিজ্ঞেস করলেন, তিনি তাঁর সামনের জনকে… একদম প্রথম সারির মানুষটি জানেন “আমি ১ নম্বরে” — এটাই base case. এবার উত্তর পেছনে ফিরতে থাকে: “আমি ২”, “আমি ৩”… প্রশ্ন যায় সামনে, উত্তর ফেরে পেছনে। Recursion ঠিক এভাবেই চলে।

প্রতিটি ডাকের তথ্য (তার নিজস্ব n, কোথায় ফিরতে হবে) মেমরির একটি অংশে থাক করে রাখা হয় — একে বলে call stack, অনেকটা ক্যান্টিনে প্লেটের স্তূপের মতো: সবশেষে রাখা প্লেটটি সবার আগে তোলা হয়। factorial(4)-এর পুরো যাত্রা দেখুন:

1long factorial(int n) {2    if (n <= 1) return 1;      /* base case */3    return n * factorial(n - 1);4}5/* main-এ: */6printf("%ld", factorial(4));
Call stack (নিচে পুরোনো, ওপরে নতুন)
factorial(1)
factorial(2)
factorial(3)
factorial(4)
main
  1. main ডাকল factorial(4). n = 4, base case নয়। লাইন ৩: উত্তর হবে 4 × factorial(3) — কিন্তু factorial(3) এখনো অজানা, তাই গুণটি আধাআধি রেখে factorial(4) অপেক্ষায় থাকল।
  2. নিচে নামা: factorial(3). stack-এ নতুন একটি স্তর, তার নিজের n = 3. সেও অপেক্ষায় — 3 × factorial(2).
  3. factorial(2). আরেকটি স্তর, n = 2. এখন stack-এ চারটি ফাংশন একসাথে “জীবিত”, প্রত্যেকে নিজের আলাদা n নিয়ে।
  4. factorial(1) — base case! n ≤ 1 সত্য, তাই আর ডাকা নয়, সরাসরি 1 ফেরত। এই মুহূর্তে stack সবচেয়ে উঁচু। base case না থাকলে নামা কখনো থামত না।
  5. ওপরে ওঠা শুরু। factorial(1)-এর স্তর সরে গেল। factorial(2) তার আধাআধি গুণ শেষ করল: 2 × 1 = 2, ফেরত দিল।
  6. factorial(3): 3 × 2 = 6, ফেরত।
  7. factorial(4): 4 × 6 = 24, ফেরত। সবশেষে যে ডাকা হয়েছিল (factorial(1)), সে-ই সবার আগে শেষ হয়েছে — stack-এর নিয়ম।
  8. main পেল 24, ছাপল। নামা ৪ ধাপ, ওঠা ৪ ধাপ। পরীক্ষায় recursion-এর output প্রশ্নে ঠিক এই দুই-কলামের ছবি (নামার সময় কী জমল, ওঠার সময় কী হিসাব হলো) আঁকলেই ভুল হয় না।

Fibonacci (০, ১, ১, ২, ৩, ৫, ৮, …) আরেকটি ক্লাসিক — প্রতিটি সংখ্যা আগের দুটির যোগফল, তাই এখানে base case দুটি:

int fib(int n)
{
    if (n == 0) return 0;              /* base case ১ */
    if (n == 1) return 1;              /* base case ২ */
    return fib(n - 1) + fib(n - 2);
}
/* for (i = 0; i < 8; i++) printf("%d ", fib(i));
   ছাপবে: 0 1 1 2 3 5 8 13 */

সুন্দর, কিন্তু অপচয়ী — fib(5) হিসাব করতে fib(3) দুবার, fib(2) তিনবার আলাদাভাবে হিসাব হয়। n বড় হলে ডাকার সংখ্যা বিস্ফোরণের মতো বাড়ে (ভাবনার প্রশ্ন ৩)। একটি সাধারণ লুপে এটি অনেক দ্রুত হয়।

ক্লাসিক ফাংশন একসাথে — চালিয়ে দেখুন

নিচের প্রোগ্রামে চারটি ফাংশন: মৌলিক সংখ্যা যাচাই, recursive factorial, গসাগু (GCD — ইউক্লিডের পদ্ধতিতে, এটিও recursive), আর palindrome string যাচাই। definition-গুলো main-এর ওপরে, তাই prototype লাগেনি:

#include <stdio.h>
#include <string.h>
int isPrime(int n) {
    if (n < 2) return 0;
    for (int i = 2; i * i <= n; i++)
        if (n % i == 0) return 0;
    return 1;
}
long factorial(int n) { return (n <= 1) ? 1 : n * factorial(n - 1); }
int gcd(int a, int b) { return (b == 0) ? a : gcd(b, a % b); }
int isPalindrome(char s[]) {
    for (int i = 0, j = (int)strlen(s) - 1; i < j; i++, j--)
        if (s[i] != s[j]) return 0;
    return 1;
}
int main(void) {
    printf("isPrime(29) = %d, isPrime(21) = %d\n", isPrime(29), isPrime(21));
    printf("factorial(10) = %ld\n", factorial(10));
    printf("gcd(48, 18) = %d\n", gcd(48, 18));
    printf("madam -> %d, dhaka -> %d\n", isPalindrome("madam"), isPalindrome("dhaka"));
    return 0;
}

আউটপুট: isPrime(29) = 1, isPrime(21) = 0 (২১ = ৩ × ৭), factorial(10) = 3628800, gcd(48, 18) = 6, আর madam -> 1, dhaka -> 0. লক্ষ করুন: isPrime শুধু √n পর্যন্ত ভাগ করে দেখে (i * i <= n) — ২৯-এর জন্য ২ থেকে ২৮ পর্যন্ত ২৭টি ভাগের বদলে ২ থেকে ৫, মাত্র ৪টি। আর ০, ১ বা ঋণাত্মক সংখ্যা প্রথম লাইনেই বাদ — এই edge case-টি AI ও শিক্ষার্থী দুজনেই প্রায়ই ভোলেন। gcd: gcd(48, 18) → gcd(18, 12) → gcd(12, 6) → gcd(6, 0) = 6; ভাগশেষ ০ হওয়াই base case. isPalindrome দুই প্রান্ত থেকে দুটি index নিয়ে মাঝের দিকে এগোয়, একটি অমিল পেলেই ০. factorial(10)-এ ফলের জন্য long নেওয়া হয়েছে, কারণ ১৩! আর ৩২-বিট int-এ ধরে না (Windows-এ long-ও ৩২-বিট, তাই আরও বড় মানের জন্য long long নিন)।

সব জোড়া দিয়ে — menu-driven প্রোগ্রাম

বাস্তব প্রোগ্রাম এভাবেই দাঁড়ায়: main শুধু মেনু দেখায় আর সিদ্ধান্ত নেয়, আসল কাজ করে আলাদা ফাংশন। পরীক্ষায় “ফাংশন ব্যবহার করে মেনুভিত্তিক প্রোগ্রাম” প্রায়ই চাওয়া হয়:

#include <stdio.h>

long factorial(int n);
int isPrime(int n);
int gcd(int a, int b);

int main()
{
    int choice, x, y;
    do {
        printf("\n1. Factorial  2. Prime check  3. GCD  0. Exit\nChoice: ");
        if (scanf("%d", &choice) != 1) break;
        switch (choice) {
        case 1: printf("n = ");  scanf("%d", &x);
                printf("%d! = %ld\n", x, factorial(x)); break;
        case 2: printf("n = ");  scanf("%d", &x);
                printf(isPrime(x) ? "Prime\n" : "Not prime\n"); break;
        case 3: printf("a b = "); scanf("%d %d", &x, &y);
                printf("GCD = %d\n", gcd(x, y)); break;
        case 0: printf("Bye\n"); break;
        default: printf("Invalid choice\n");
        }
    } while (choice != 0);
    return 0;
}
/* factorial, isPrime, gcd-এর definition ওপরের প্রোগ্রামের মতোই — main-এর নিচে বসান */

নমুনা চালানো (ইনপুট: 1, 5, 3, 48 18, 0)

1. Factorial  2. Prime check  3. GCD  0. Exit
Choice: 1
n = 5
5! = 120

1. Factorial  2. Prime check  3. GCD  0. Exit
Choice: 3
a b = 48 18
GCD = 6

1. Factorial  2. Prime check  3. GCD  0. Exit
Choice: 0
Bye

৯ · সাধারণ ভুল আর পরীক্ষার প্রস্তুতি

ভুলযা ঘটেঠিক উপায়
for (i = 0; i <= n; i++)এক ঘর বেশি — সীমার বাইরে পড়া/লেখা (off-by-one error)i = 0 থেকে i < n; অথবা i = 1 থেকে i <= n হলে index a[i - 1]
int a[n]; লিখে পরে n পড়াn-এ মান আসার আগেই অ্যারে তৈরি হয় — আকার হয় আবর্জনা মানবড় নির্দিষ্ট আকার (a[100]), তারপর n পড়ে যাচাই
ফেরত টাইপ int, কিন্তু return ভুলে যাওয়াডাকার জায়গায় আবর্জনা মান — warning দেয়, error নয়প্রতিটি পথে return; -Wall চালু রাখা
prototype ছাড়া main-এর নিচে definitionআধুনিক compiler-এ error বা “implicit declaration” warningওপরে prototype, অথবা definition main-এর আগে
prototype-এর শেষে ; নেই, বা definition-এর header-এ ; আছেSyntax errorprototype-এ ; আছে, definition-এর header-এ নেই
string তুলনায় ==, কপিতে =ঠিকানা তুলনা / compile errorstrcmp, strcpy
void main()অনেক compiler মেনে নেয়, কিন্তু C মানদণ্ডে নেইint main(), শেষে return 0;

পরীক্ষা ও ইন্টারভিউতে — এ ধরনের প্রশ্ন প্রায়ই আসে:

  • অ্যারের প্রথম উপাদানের index কত? ০. int a[10]-এর শেষ উপাদান a[9].
  • int a[10] কত বাইট নেয়? int ৪ বাইট ধরলে (সাধারণ কম্পিউটারে তাই) ১০ × ৪ = ৪০ বাইট; char c[10] নেয় ১০ বাইট।
  • Recursion আর base case কী? ফাংশনের নিজেকে ডাকা হলো recursion; যে শর্তে নিজেকে না ডেকে সরাসরি উত্তর দেয়, সেটি base case — না থাকলে stack overflow.
  • Call by value কী? argument-এর কপি parameter-এ যায়; ফাংশন কপি বদলালে আসল ভ্যারিয়েবল বদলায় না।
  • char s[] = "ICT"; — strlen আর sizeof কত? strlen = ৩ (অক্ষর), sizeof = ৪ (শেষের '\0'-সহ)।

আরও গভীরে: pointer দিয়ে আসল swap (call by reference), অ্যারে আর pointer-এর গভীর সম্পর্ক, binary search, quicksort আর merge sort, memoization দিয়ে দ্রুত recursion, আর সীমার বাইরে লেখা ধরার টুল (AddressSanitizer) — সব আছে আমাদের C প্রোগ্রামিং কোর্সে। শুরু করুন অ্যারে, ফাংশন আর recursion পর্ব দিয়ে; string-এর নিরাপদ ব্যবহার পাবেন strings পর্বে.

১০ · AI-এর যুগে — AI-এর লেখা C কোড যাচাই

“HSC-র জন্য bubble sort-এর C প্রোগ্রাম দাও” — AI কয়েক সেকেন্ডে দেবে, আর বেশিরভাগ সময় সেটি চলবেও। সমস্যা অন্য জায়গায়। AI শিখেছে ইন্টারনেটের পুরোনো লেখা থেকে, যেখানে gets(), void main(), conio.h আর getch() ভরপুর। আর AI সাধারণত “সুখী পথ” পরীক্ষা করে — n = 0, ঋণাত্মক সংখ্যা, ৫০-এর বেশি শিক্ষার্থী, ফাঁকা string — এসব নিয়ে ভাবে না।

AI-এর কোডে যা দেখবেনকেন সমস্যা
gets(name)সীমা দেখে না, C11-এ বাতিল — fgets চান
int a[100] কিন্তু n যাচাই নেই১০১ দিলে সীমার বাইরে লেখা
isPrime-এ ১ বা ০-কে মৌলিক বলাn < 2 শর্ত বাদ পড়েছে
int-এ ২০-এর factorialoverflow — ভুল বা ঋণাত্মক উত্তর
গড়ে sum / nপূর্ণসংখ্যার ভাগ, দশমিক হারায়; n = 0 হলে ভাগ শূন্য দিয়ে

AI-কে শিক্ষক হিসেবে ব্যবহার করুন, উত্তরের দোকান হিসেবে নয়। নিচের prompt-টি কোড দেওয়ার সাথে পরীক্ষাও শেখাবে:

আমি HSC ICT-র জন্য C শিখছি। ফাংশন ব্যবহার করে একটি প্রোগ্রাম লিখে দাও, যা n জন শিক্ষার্থীর নম্বর অ্যারেতে পড়বে, গড় বের করবে আর bubble sort দিয়ে সাজাবে। শর্ত: ১. শুধু standard C (stdio.h, string.h) — gets(), conio.h, void main() ব্যবহার করবে না। ২. n যাচাই করবে (১ থেকে ১০০), নইলে বার্তা দেখিয়ে থামবে। ৩. প্রতিটি ফাংশনের ওপরে এক লাইনে বাংলায় লেখো সে কী করে। ৪. শেষে n = 1, সব নম্বর সমান, আর আগে থেকেই সাজানো ইনপুট — এই তিনটি পরীক্ষার জন্য প্রত্যাশিত আউটপুট দাও। ৫. bubble sort-এর প্রথম pass-টি {5, 1, 4, 2, 8} দিয়ে টেবিলে dry run করে দেখাও।

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

পরীক্ষার খাতায় AI থাকবে না, compiler-ও না। তাই সবচেয়ে কাজের অভ্যাস হলো: প্রতিদিন একটি ক্লাসিক প্রোগ্রাম খাতায় নিজে লিখুন, dry run করুন, তারপর কম্পিউটারে চালিয়ে মিলিয়ে দেখুন। AI-কে বলুন আপনার লেখা কোডের ভুল খুঁজে দিতে — উল্টোটা নয়।

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

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১মানুষ গোনে ১ থেকে, তাহলে C-র অ্যারে ০ থেকে শুরু কেন? এটা কি শুধু প্রোগ্রামারদের খেয়াল?

খেয়াল নয় — এর পেছনে যন্ত্রের যুক্তি আর গণিতের যুক্তি দুটোই আছে।

যন্ত্রের যুক্তি: অংশ ১-এ দেখেছেন, ঘরের ঠিকানা = শুরুর ঠিকানা + index × মাপ। C-তে a[i] আসলে *(a + i)-এর সংক্ষিপ্ত রূপ — “শুরুর ঠিকানা থেকে i ঘর সরে যাও।” প্রথম ঘর শুরু থেকে শূন্য ঘর দূরে, তাই তার index ০. index ১ থেকে শুরু হলে প্রতিটি ঘর ধরতে বাড়তি একটি বিয়োগ (i − 1) লাগত। ১৯৭০-এর দশকের ধীর যন্ত্রে প্রতিটি নির্দেশ মূল্যবান ছিল। C-এর পূর্বসূরি BCPL-ও ০ থেকে গুনত, C সেই ধারা নেয়।

গণিতের যুক্তি: ১৯৮২ সালে ডাচ কম্পিউটার বিজ্ঞানী Edsger W. Dijkstra একটি ছোট নোট লেখেন — “Why numbering should start at zero” (EWD831). তাঁর যুক্তি: একটি পরিসর লেখার সবচেয়ে পরিষ্কার উপায় হলো “শুরু অন্তর্ভুক্ত, শেষ বাদ”, অর্থাৎ 0 ≤ i < n. তখন পরিসরের দৈর্ঘ্য হয় সরাসরি n − 0 = n, দুটি পরিসর জোড়া লাগে কোনো ফাঁক বা ওভারল্যাপ ছাড়া, আর ফাঁকা পরিসর লেখা যায় স্বাভাবিকভাবে। আপনার প্রতিটি লুপ for (i = 0; i < n; i++) ঠিক এই রূপটিই।

সবাই কি মানে? না। FORTRAN, MATLAB, Lua, R-এ গোনা শুরু ১ থেকে — গণিতবিদ ও বিজ্ঞানীদের ম্যাট্রিক্স লেখার অভ্যাসের সাথে মিল রেখে। কিন্তু C-এর প্রভাবে C++, Java, JavaScript, Python — আজকের প্রায় সব জনপ্রিয় ভাষা ০ থেকে গোনে। দুই ধারার ভাষার মধ্যে কোড অনুবাদ করতে গিয়ে off-by-one ভুল তাই খুব পরিচিত।

মূল উপলব্ধি: index আসলে “কত নম্বর ঘর” নয়, “শুরু থেকে কত ঘর দূরে” (offset). এভাবে ভাবলে ০ থেকে গোনা আর অস্বাভাবিক লাগে না, আর i < n শর্তটিও মুখস্থ করতে হয় না।

প্র ০২gets() এত বিপজ্জনক কেন যে C ভাষা থেকেই সেটি বাদ দেওয়া হলো? একটি ইনপুট ফাংশন কীভাবে ইন্টারনেট থামিয়ে দিতে পারে?

gets(name) ফাংশন শুধু অ্যারের শুরুর ঠিকানা পায় — অ্যারে কত বড়, জানার কোনো উপায় তার নেই। ব্যবহারকারী যত লম্বা লাইন দেবেন, সব সে লিখে যাবে, অ্যারের সীমা পেরিয়ে পাশের মেমরিতেও। এটি সঠিকভাবে ব্যবহার করাই অসম্ভব — যত সাবধানেই লিখুন, ইনপুটের দৈর্ঘ্য নিয়ন্ত্রণ করেন ব্যবহারকারী, আপনি নন।

ইতিহাসের শিক্ষা: ১৯৮৮ সালের নভেম্বরে Cornell বিশ্ববিদ্যালয়ের ছাত্র Robert Tappan Morris-এর ছাড়া একটি প্রোগ্রাম — পরে যার নাম হয় “Morris worm” — তখনকার ছোট ইন্টারনেটে ছড়িয়ে পড়ে। যে কয়েকটি দুর্বলতা সে কাজে লাগিয়েছিল, তার একটি ছিল Unix-এর fingerd সার্ভিস, যা নেটওয়ার্ক থেকে আসা লেখা gets() দিয়ে পড়ত। প্রত্যাশার চেয়ে লম্বা ইনপুট পাঠিয়ে ফাংশনের ফেরার পথের তথ্য নষ্ট করা গিয়েছিল। কয়েক হাজার কম্পিউটার — তখনকার ইন্টারনেটের উল্লেখযোগ্য অংশ — অচল হয়ে পড়ে। এর পরই যুক্তরাষ্ট্রে প্রথম কম্পিউটার জরুরি সাড়াদান দল (CERT) গঠিত হয়, আর Morris সেখানকার কম্পিউটার অপরাধ আইনে দণ্ডিত প্রথম ব্যক্তিদের একজন হন।

ভাষার প্রতিক্রিয়া: C মানদণ্ড কমিটি gets()-কে প্রথমে অপ্রচলিত ঘোষণা করে, তারপর ২০১১ সালের C11 মানদণ্ড থেকে পুরোপুরি বাদ দেয় — একটি প্রতিষ্ঠিত ভাষা থেকে ফাংশন মুছে ফেলার বিরল উদাহরণ। বিকল্প fgets(buf, size, stdin), যেখানে সর্বোচ্চ মাপ বলে দিতে হয়।

আজকের অবস্থা: buffer overflow এখনো নিরাপত্তা-ত্রুটির বড় উৎস। প্রতিরক্ষা কয়েক স্তরে — compiler-এর stack protector, OS-এর মেমরি সুরক্ষা, AddressSanitizer-এর মতো পরীক্ষার টুল, আর Rust-এর মতো নতুন ভাষা, যা সীমা যাচাই নিজেই করে। পর্ব ৪৩-এ সাইবার নিরাপত্তার ব্যবহারিক দিক দেখবেন।

মূল উপলব্ধি: বাইরে থেকে আসা প্রতিটি ইনপুটকে অবিশ্বাস করুন, আর এমন ফাংশন ব্যবহার করুন যাকে সীমা বলে দেওয়া যায়। এক লাইনের অসাবধানতাও লক্ষ কম্পিউটারের সমস্যা হয়ে উঠতে পারে।

প্র ০৩Recursion এত সুন্দর — তাহলে সব লুপ recursion দিয়ে লেখা হয় না কেন?

Recursion-এর সৌন্দর্য হলো সমস্যার নিজের গঠনের সাথে কোডের মিল। গাছের মতো গঠন — ফোল্ডারের ভেতরে ফোল্ডার, পরিবারের বংশতালিকা, গাণিতিক রাশির ভেতরে রাশি — recursion দিয়ে লিখলে কোড প্রায় সংজ্ঞার মতোই পড়া যায়। হ্যানয়ের টাওয়ার বা quicksort লুপে লেখা কঠিন, recursion-এ কয়েক লাইন।

কিন্তু প্রতিটি ডাকের দাম আছে। factorial(4)-এর stepper-এ দেখেছেন, প্রতিটি ডাক call stack-এ একটি স্তর নেয়। stack-এর জায়গা সীমিত — সাধারণত কয়েক মেগাবাইট (Linux-এ প্রায়ই ৮ MB, Windows-এ ১ MB ডিফল্ট)। base case ভুলে গেলে, বা ১০ লাখ গভীরতার recursion চালালে, stack ভরে যায় — stack overflow, প্রোগ্রাম crash. প্রোগ্রামারদের বিখ্যাত প্রশ্নোত্তর ওয়েবসাইট Stack Overflow-র নামও এই ভুল থেকে।

দ্বিতীয় দাম — পুনরাবৃত্তি: সরল recursive fib(40) হিসাব করতে ফাংশনটি ডাকা হয় ৩০ কোটির বেশি বার, কারণ একই ছোট মান বারবার নতুন করে হিসাব হয়। একটি সাধারণ লুপ একই কাজ ৪০ ধাপে করে। সমাধান হলো memoization — একবার হিসাব করা ফল একটি অ্যারেতে লিখে রাখা, যা পরে dynamic programming নামে একটি পুরো কৌশলে রূপ নিয়েছে।

ভাষাভেদ: Haskell-এর মতো functional ভাষায় লুপই নেই, সব recursion — compiler “tail call” কে লুপে বদলে stack বাঁচায়। C-এর মানদণ্ড এমন নিশ্চয়তা দেয় না, যদিও optimization চালু থাকলে gcc প্রায়ই এটি করে।

মূল উপলব্ধি: যেখানে সমস্যা নিজেই recursive (গাছ, ভাগ করে জয়), সেখানে recursion; যেখানে সরল পুনরাবৃত্তি (যোগফল, গণনা), সেখানে লুপ। আর recursion লিখলে প্রথম লাইনেই base case — অভ্যাসটি পাকা করুন।

প্র ০৪Bubble sort তো ধীর — বাস্তবে কেউ ব্যবহার করে না। তাহলে HSC-তে এটিই শেখানো হয় কেন?

ধীর, সত্যি। n টি মানে bubble sort সবচেয়ে খারাপ ক্ষেত্রে প্রায় n²/২টি তুলনা করে। ১০টি মানে ৪৫টি — কিছুই না। কিন্তু ১০ লাখ মানে প্রায় ৫০,০০০ কোটি তুলনা, আধুনিক কম্পিউটারেও অনেক সময়।

দ্রুত পথ: ১৯৫৯ সালে ব্রিটিশ কম্পিউটার বিজ্ঞানী Tony Hoare (C. A. R. Hoare) মস্কোতে যন্ত্র-অনুবাদের একটি প্রকল্পে কাজ করার সময় শব্দ সাজানোর প্রয়োজনে quicksort উদ্ভাবন করেন, প্রকাশিত হয় ১৯৬১ সালে। কৌশল: একটি মান (pivot) বেছে ছোটগুলো বাঁয়ে, বড়গুলো ডানে — তারপর দুই ভাগে একই কাজ recursion দিয়ে। গড়ে লাগে প্রায় n log₂ n তুলনা — ১০ লাখ মানে মোটামুটি ২ কোটি, bubble sort-এর চেয়ে কয়েক হাজার গুণ কম। C-এর standard library-তে qsort() আছে; Python ব্যবহার করে Timsort, যা merge sort আর insertion sort-এর মিশ্রণ।

তাহলে bubble sort কেন? কারণ এর উদ্দেশ্য গতি শেখানো নয়, চিন্তার অনুশীলন। একটি ছোট প্রোগ্রামে একসাথে আসে nested loop, অ্যারে index, তুলনা, temp দিয়ে swap, আর “প্রতিটি pass-এর পর শেষ ঘর নিশ্চিত” — এই অপরিবর্তনীয় সত্যের (invariant) ধারণা। হাতে dry run করা যায়, ভুল চোখে পড়ে। Donald Knuth তাঁর বিখ্যাত বই The Art of Computer Programming-এ মন্তব্য করেছিলেন, আকর্ষণীয় নাম ছাড়া bubble sort-এর সুপারিশ করার মতো তেমন কিছু নেই — তবু শেখার প্রথম ধাপ হিসেবে এর জায়গা পাকা।

বাস্তবে ছোট তালিকায় (কয়েক ডজন মান) সরল পদ্ধতিই প্রায়ই দ্রুত — এজন্য অনেক দ্রুত sort-এর ভেতরে ছোট টুকরোয় insertion sort ব্যবহার হয়।

মূল উপলব্ধি: সহজ algorithm দিয়ে ভাবতে শিখুন, তারপর জিজ্ঞেস করুন “ডেটা ১০ লাখ হলে কী হবে?” — এই প্রশ্নই আপনাকে algorithm-এর জটিলতা (complexity) বোঝার দিকে নিয়ে যাবে।

প্র ০৫কোটি লাইনের Linux বা Windows কি একটি বিশাল main? এত বড় প্রোগ্রাম মানুষ বানায় কীভাবে?

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

ফাংশনই প্রথম abstraction: পর্ব ২৩-এ দেখেছেন, কম্পিউটার স্তরে স্তরে তৈরি, প্রতিটি স্তর নিচের জটিলতা লুকায়। প্রোগ্রামের ভেতরে এই লুকানোর প্রথম হাতিয়ার ফাংশন। printf ডাকার সময় আপনি ভাবেন না সংখ্যাকে অক্ষরে রূপান্তর, বাফার, OS-এর system call — সব printf-এর ভেতরে। আপনার isPrime-ও তাই: যিনি ব্যবহার করবেন, তাঁর শুধু জানা দরকার “সংখ্যা দিলে ১ বা ০ দেয়।”

ইতিহাস: ফাংশনের ধারণা (তখন বলা হতো subroutine) প্রায় কম্পিউটারের সমান পুরোনো। ১৯৫০-এর দশকের শুরুতে কেমব্রিজের EDSAC কম্পিউটারে David Wheeler-সহ গবেষকেরা পুনর্ব্যবহারযোগ্য subroutine-এর একটি library গড়ে তোলেন — বারবার লাগা হিসাব একবার লিখে সবাই ব্যবহার করবে। আজকের stdio.h, string.h সেই ধারণারই উত্তরসূরি।

কেন কাজ করে: একজন প্রোগ্রামার একসাথে মাথায় রাখতে পারেন সীমিত কয়েকটি জিনিস। ছোট ফাংশনের নাম যদি তার কাজ বলে দেয় (calculateGPA, isValidNID), তাহলে হাজার জন প্রোগ্রামার আলাদা আলাদা ফাংশনে কাজ করতে পারেন, প্রতিটি আলাদা করে পরীক্ষা করা যায়, আর একটি ভুল ঠিক করলে সব জায়গায় ঠিক হয়। Unix-এর দর্শনও এটাই — প্রতিটি প্রোগ্রাম একটি কাজ ভালোভাবে করবে, আর ছোট ছোট প্রোগ্রাম জুড়ে বড় কাজ হবে।

মূল উপলব্ধি: ভালো প্রোগ্রামার মানে বড় কোড লেখা নয় — বড় সমস্যাকে ঠিক জায়গায় কেটে ভালো নামের ছোট ফাংশনে ভাগ করা। আজকের menu-driven প্রোগ্রামই সেই দক্ষতার প্রথম অনুশীলন।

অনুশীলন

খাতা-কলম নিন — বেশিরভাগ কাজ dry run আর ভাবনার।

  1. Bubble sort dry run: {3, 9, 2, 7}-এ bubble sort চালান। প্রতিটি pass শেষে অ্যারে কেমন দেখায় আর প্রতিটি pass-এ কয়টি swap হলো, লিখুন।

    Pass ১: 3 > 9 না; 9 > 2 হ্যাঁ → {3, 2, 9, 7}; 9 > 7 হ্যাঁ → {3, 2, 7, 9}. swap ২টি।

    Pass ২: 3 > 2 হ্যাঁ → {2, 3, 7, 9}; 3 > 7 না। swap ১টি।

    Pass ৩: 2 > 3 না। swap ০টি। ফল {2, 3, 7, 9}, মোট তুলনা ৩ + ২ + ১ = ৬, মোট swap ৩.

  2. Output বলুন: int a[5] = {4, 8}; char s[10] = "Bangla"; থাকলে a[1] + a[3], strlen(s) আর sizeof(s) কত?

    a[1] = 8, আর a[3] = 0 (আংশিক মান দিলে বাকি ঘর ০ হয়), তাই যোগফল ৮. strlen(s) = ৬ (B-a-n-g-l-a). sizeof(s) = ১০ — ঘোষণায় ১০ ঘর চাওয়া হয়েছে, ভেতরে কী আছে তাতে sizeof বদলায় না।

  3. একটি ফাংশন লিখুন: int countVowels(char s[]), যা একটি string-এ কয়টি স্বরবর্ণ (a, e, i, o, u — ছোট হাতের) আছে ফেরত দেবে। “education” দিলে কত আসা উচিত?
    int countVowels(char s[])
    {
        int i, count = 0;
        for (i = 0; s[i] != '\0'; i++)
            if (s[i] == 'a' || s[i] == 'e' || s[i] == 'i' || s[i] == 'o' || s[i] == 'u')
                count++;
        return count;
    }

    লুপ চলে '\0' না পাওয়া পর্যন্ত — string-এর দৈর্ঘ্য আলাদা করে জানার দরকার নেই। “education”-এ e, u, a, i, o — ৫টি। বড় হাতের অক্ষরও গুনতে চাইলে ctype.h-এর tolower() দিয়ে আগে ছোট হাতের করে নিন।

  4. ভুল খুঁজুন: একজন শিক্ষার্থী লিখলেন — int sum(int a[], int n) { int i, s = 0; for (i = 0; i <= n; i++) s += a[i]; }. দুটি ভুল আছে। কী কী, আর প্রতিটির ফল কী হবে?

    ভুল ১ — off-by-one: i <= n হওয়ায় a[n]-ও পড়া হচ্ছে, যা অ্যারের বাইরে — undefined behaviour, যোগফলে আবর্জনা মান ঢুকতে পারে। ঠিক: i < n.

    ভুল ২ — return নেই: ফাংশন int ফেরত দেবে বলেছে, কিন্তু return s; লেখা নেই। ডাকার জায়গায় অনির্দিষ্ট মান আসবে। -Wall চালু থাকলে compiler সতর্ক করে: “control reaches end of non-void function”.

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

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

ক্রমmain থেকে factorial(3) ডাকলে ঘটনাগুলো যে ক্রমে ঘটে, সেভাবে সাজান (▲▼ চেপে সরান)

  1. main ডাকে factorial(3)
  2. factorial(3) ডাকে factorial(2)
  3. factorial(2) ডাকে factorial(1)
  4. factorial(1) base case — ফেরত দেয় 1
  5. factorial(2) ফেরত দেয় 2 × 1 = 2
  6. factorial(3) ফেরত দেয় 3 × 2 = 6

C-তে int a[10]; ঘোষণা করা হলে শেষ উপাদানটি কোনটি?

  • a[10]
  • a[1]
  • a[9]
  • a[11]

C-তে ২০ ঘরের char name[20]-এ স্পেসসহ পুরো একটি লাইন নিরাপদে পড়ার উপায় কোনটি?

  • gets(name);
  • fgets(name, sizeof(name), stdin);
  • scanf("%s", name);
  • name = getchar();

C-তে char s[] = "Dhaka"; হলে strlen(s) আর sizeof(s)-এর মান কত?

  • 5 আর 6
  • 5 আর 5
  • 6 আর 6
  • 6 আর 5

একটি recursive ফাংশনে base case না থাকলে কী ঘটে?

  • Compiler error দেয়, প্রোগ্রাম তৈরিই হয় না
  • ফাংশন একবার চলে থেমে যায়
  • ফাংশন স্বয়ংক্রিয়ভাবে 0 ফেরত দেয়
  • ডাকা চলতেই থাকে, call stack ভরে প্রোগ্রাম crash করে

C-তে void swap(int x, int y) { int t = x; x = y; y = t; } ফাংশন থাকলে, a = 10, b = 20 নিয়ে swap(a, b); ডাকার পর a আর b কত?

  • a = 20, b = 10
  • a = 10, b = 20
  • a = 0, b = 0
  • Compile error হয়

অ্যারে {5, 1, 4, 2, 8}-এ ছোট থেকে বড় ক্রমে bubble sort-এর প্রথম pass শেষে অ্যারেটি কেমন হবে?

  • {1, 2, 4, 5, 8}
  • {1, 5, 4, 2, 8}
  • {1, 4, 2, 5, 8}
  • {8, 5, 4, 2, 1}

শব্দকোষ

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

মনে রাখুন

  1. অ্যারে = একই ধরনের অনেক মান এক নামে, মেমরিতে পাশাপাশি; index ০ থেকে n − 1, লুপ সবসময় i < n.
  2. C সীমা পাহারা দেয় না — সীমার বাইরে যাওয়া undefined behaviour; ইনপুটের n যাচাই করুন।
  3. String = char-এর অ্যারে, শেষে '\0'; gets নয়, fgets; কপি-তুলনায় strcpy/strcmp.
  4. ফাংশন: prototype → call → definition; সাধারণ ভ্যারিয়েবল যায় কপি হয়ে (call by value), অ্যারে যায় ঠিকানা হয়ে।
  5. Recursion-এ আগে base case; প্রতিটি ডাক call stack-এ একটি স্তর — নামা আর ওঠা, দুটোই dry run করুন।

পরের পর্বে: অ্যারেতে ৫০ জনের নম্বর রাখা গেল, কিন্তু প্রোগ্রাম বন্ধ হলেই সব হারিয়ে যায়। লাখো শিক্ষার্থীর তথ্য স্থায়ীভাবে, গুছিয়ে আর দ্রুত খোঁজার উপযোগী করে রাখে ডেটাবেস — টেবিল, key আর relation দিয়ে।

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

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