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

স্ট্যান্ডার্ড লাইব্রেরি — C-এর সাথে আসা তৈরি হাতিয়ারের বাক্স

The C Standard Library — ctype, stdlib, math, time & friends

Taught in Bangla, this lesson tours the C standard library: a map of the headers, character tests in ctype.h, strtol over atoi, how random rand() really is, math.h and floating-point traps, time.h and the year 2038 problem, and limits and sizes.

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

  • “স্ট্যান্ডার্ড” লাইব্রেরি আসলে কী, কে বানায়, আর header ও library ফাইলের পার্থক্য
  • প্রধান header-গুলোর একটি মানচিত্র — কোন সমস্যার জন্য কোন বাক্স খুলবেন
  • ctype.h, stdlib.h, math.h, time.h-এর সবচেয়ে কাজের ফাংশন আর তাদের ফাঁদ
  • এলোমেলো সংখ্যা কেন সবসময় নিরাপদ নয়, আর ২০৩৮ সালে কেন অনেক যন্ত্রের ঘড়ি গোলমাল করতে পারে
  • AI-এর লেখা কোডে library-সংক্রান্ত ভুল ধরার একটি চেকলিস্ট

১ · তৈরি হাতিয়ারের বাক্স — standard library আসলে কী

একজন রাজমিস্ত্রির কথা ভাবুন। তিনি প্রতিদিন সকালে নিজে হাতুড়ি, কর্নি আর ওলন বানিয়ে কাজ শুরু করেন না। তাঁর ব্যাগে তৈরি যন্ত্রপাতি থাকে — বহু বছর ধরে হাজারো মিস্ত্রির হাতে পরীক্ষিত। তাঁর আসল দক্ষতা হলো কোন কাজে কোন যন্ত্র লাগবে, সেটা জানা।

C প্রোগ্রামারের ব্যাগটির নাম standard library. প্রথম পর্ব থেকেই আপনি এটি ব্যবহার করছেন — printf, scanf, strlen, malloc, fopen — এর কোনোটিই C ভাষার অংশ নয়! এগুলো সাধারণ ফাংশন, যা কেউ আগে থেকে লিখে compiler-এর সাথে দিয়ে রেখেছেন। এই পর্বে আমরা পুরো ব্যাগটি খুলে দেখব — কী কী আছে, আর কোন যন্ত্রের ধার কোথায় হাত কাটতে পারে।

মূল ধারণা

Library-তে কী আছে জানা মানে বাগ লেখা থেকে বাঁচা। নিজে হাতে sort, তারিখের হিসাব বা লেখা থেকে সংখ্যা বানানোর কোড লিখলে প্রায় নিশ্চিতভাবে কোনো একটি কোণার ক্ষেত্রে (edge case) ভুল থাকবে। Library-র ফাংশন কোটি কোটি প্রোগ্রামে দশকের পর দশক ধরে পরীক্ষিত।

“স্ট্যান্ডার্ড” মানে কী?

C ভাষার একটি আনুষ্ঠানিক নিয়ম-বই আছে (C standard), যা ISO-র একটি কমিটি দেখাশোনা করে। এই বইয়ের একটা বড় অংশজুড়ে লেখা — কোন কোন header থাকবে, প্রতিটিতে কোন ফাংশন, আর প্রতিটি ফাংশন ঠিক কী আচরণ করবে। অর্থাৎ স্ট্যান্ডার্ড বলে দেয় কী থাকবে; কীভাবে বানানো হবে, সেটা যে বানায় তার ব্যাপার।

এই “বানানো” রূপটিকে বলে libc — C library-র একটি বাস্তব implementation. একাধিক দল আলাদা আলাদা libc বানিয়েছে:

🐧 glibc

GNU-র C library. Ubuntu, Debian, Fedora-র মতো বেশিরভাগ Linux-এ এটিই চলে। বড়, দ্রুত, অনেক বাড়তি সুবিধাসহ।

glibc

🪶 musl

ছোট ও সরল, static linking-এর জন্য জনপ্রিয়। Docker-এ বহুল ব্যবহৃত Alpine Linux এটি ব্যবহার করে।

musl

🪟 MSVC CRT

Microsoft-এর C runtime. Windows-এ Visual Studio দিয়ে বানানো প্রোগ্রাম এটি ব্যবহার করে (আধুনিক রূপের নাম UCRT).

Windows

🔌 newlib

Microcontroller-এর জন্য বানানো হালকা C library. অনেক ARM এমবেডেড toolchain-এর সাথে এটি আসে।

newlib

সবগুলোই একই স্ট্যান্ডার্ড মেনে চলে, তাই strlen("Dhaka") সবখানে 5 দেয়। কিন্তু স্ট্যান্ডার্ড যেখানে কিছু খোলা রেখেছে (যেমন rand()-এর সর্বোচ্চ মান কত), সেখানে ফল আলাদা হতে পারে। এই পর্বে সেরকম কয়েকটি জায়গা দেখবেন।

Header আর library ফাইল — দুটো আলাদা জিনিস

পর্ব ০৩-এ toolchain-এর চার ধাপ দেখেছিলেন। সেখান থেকেই এই পার্থক্যটা পরিষ্কার হয়:

Header (.h)Library ফাইল
ভেতরে কীফাংশনের ঘোষণা (prototype), টাইপ, macroফাংশনের আসল machine code
কে ব্যবহার করেPreprocessor ও compilerLinker (আর dynamic হলে, প্রোগ্রাম খোলার সময় OS)
Linux-এ কোথায়/usr/include/stdio.hlibc.so.6, libm.so.6
না পেলে কী errorNo such file or directoryundefined reference to ...

Header হলো রেস্টুরেন্টের মেনু কার্ড — কোন খাবার পাওয়া যায় আর দাম কত, সেটা লেখা। কিন্তু মেনু কার্ড চিবিয়ে পেট ভরে না; খাবার আসে রান্নাঘর থেকে। Library ফাইল সেই রান্নাঘর। #include <math.h> লিখলে compiler মেনু দেখে খুশি হয়, কিন্তু link-এর সময় রান্নাঘর খুঁজে না পেলে linker অভিযোগ করে।

এর সবচেয়ে পরিচিত উদাহরণ math library. Linux-এ glibc-র অঙ্কের ফাংশনগুলো ঐতিহাসিক কারণে আলাদা একটি ফাইলে (libm) থাকে। তাই sqrt-এর মতো ফাংশন ব্যবহার করলে কমান্ডের শেষে -lm জুড়তে হয় — “l” মানে library, “m” মানে math:

gcc -Wall -Wextra area.c -o area -lm

এই পর্বের পাশের অনলাইন compiler-এ এটা আমরা নিজেরাই পরীক্ষা করেছি — একটি ভ্যারিয়েবলের sqrt নিতে গেলে -lm ছাড়া linker ঠিক এই error দেয়:

/usr/bin/ld: prog.c:(.text+0x4a): undefined reference to `sqrt'
collect2: error: ld returned 1 exit status

Windows-এর MSVC আর Mac-এর Clang-এ -lm লাগে না — সেখানে অঙ্কের ফাংশনও মূল library-র ভেতরেই থাকে। এজন্যই অনেকের প্রোগ্রাম Windows-এ চলে, কিন্তু Linux সার্ভারে নিলেই undefined reference to `sqrt' দেখা যায়।

১৯৭০-এর দশকে C-এর সাথে যে ফাংশনগুলো আসত, সেগুলো মূলত Unix-এর সাথে জড়িত ছিল, আর বিভিন্ন compiler নিজের মতো করে বানাত। ১৯৮৯ সালে ANSI প্রথম C স্ট্যান্ডার্ড (C89) প্রকাশ করে, ১৯৯০ সালে ISO একই নিয়ম গ্রহণ করে। সেই প্রথম স্ট্যান্ডার্ডে ছিল ১৫টি header — stdio.h, stdlib.h, string.h, ctype.h, math.h, time.h সহ। পরে C99-এ stdint.h ও stdbool.h, আর C11-এ threads.h ও stdatomic.h যোগ হয়। তবু Python বা Java-র লাইব্রেরির তুলনায় এটি এখনো খুব ছোট — কেন, সেটা ভাবনার প্রশ্নে দেখব।

২ · হেডারের মানচিত্র — কোন সমস্যায় কোন বাক্স

সব ফাংশন মুখস্থ রাখার দরকার নেই, পেশাদাররাও রাখেন না। দরকার একটি মানচিত্র — কোন ধরনের কাজের জন্য কোন header খুলতে হবে। নিচের তালিকাটি সেই মানচিত্র। শেষ কলামে লেখা, এই কোর্সের কোন পর্বে সেটি বিস্তারিত আছে।

Headerকী কাজের জন্যকোথায় দেখবেন
stdio.hইনপুট-আউটপুট: printf, fgets, fopenপর্ব ১০, ২৪
stdlib.hসাধারণ কাজ: সংখ্যায় রূপান্তর, মেমরি, sort, এলোমেলো সংখ্যা, exitএই পর্ব, পর্ব ২০
string.hস্ট্রিং ও মেমরি ব্লক: strlen, strcmp, memcpyপর্ব ১৯
ctype.hএকটি অক্ষর চেনা ও বদলানো: isdigit, toupperএই পর্ব
math.hদশমিকের অঙ্ক: sqrt, pow, floorএই পর্ব
time.hসময়, তারিখ, CPU সময় মাপাএই পর্ব, পর্ব ৩৯
limits.hপ্রতিটি টাইপের সীমা: INT_MAX, CHAR_BITপর্ব ০৫, এই পর্ব
stdint.hনির্দিষ্ট মাপের পূর্ণসংখ্যা: int32_t, uint8_t (C99)পর্ব ০৬, এই পর্ব
stdbool.hbool, true, false (C99; C23-এ এগুলো সরাসরি keyword)পর্ব ১১
stddef.hsize_t, NULL, offsetofপর্ব ১৭, ২২
assert.hassert() — “এটা সত্য হতেই হবে” যাচাইপর্ব ২৭, ৩০
errno.herrno — শেষ ব্যর্থতার কারণপর্ব ২৪, ৩০
stdarg.hযত খুশি argument নেওয়া ফাংশন (va_list)পর্ব ২৫
signal.hSignal ধরা, যেমন Ctrl+Cএই পর্ব, পর্ব ৪৮
setjmp.hএক ফাংশন থেকে অনেক দূরে লাফ (setjmp/longjmp)এই পর্ব
locale.hদেশ ও ভাষাভেদে দশমিক চিহ্ন, তারিখের ধরনএই পর্ব
threads.h, stdatomic.hথ্রেড ও atomic অপারেশন (C11, ঐচ্ছিক)পর্ব ৪৭, ৪৯

মানচিত্র হাতে পেলেন। এবার ব্যবহার করার অনুশীলন। নিচের ডায়াগ্রামে পাঁচটি বাস্তব সমস্যা আছে — প্রতিটির জন্য কোন header, কোন ফাংশন, আর কোন ফাঁদ মাথায় রাখতে হবে, ধাপে ধাপে দেখুন:

stdlib.hরূপান্তর, sort
ctype.hঅক্ষর চেনা
time.hসময় ও তারিখ
stdint.hনির্দিষ্ট মাপ
সমস্যা ১ইউজার লিখেছে "250" — সংখ্যা বানাতে হবে
/* stdlib.h */
char *end;
errno = 0;
long taka = strtol(text, &end, 10);
/* end দেখে বুঝুন পুরোটা সংখ্যা ছিল কি না */
সমস্যা ২মোবাইল নম্বরে শুধু অঙ্ক আছে তো?
/* ctype.h */
for (size_t i = 0; s[i] != '\0'; i++)
    if (!isdigit((unsigned char)s[i]))
        return 0;   /* অঙ্ক নয় এমন কিছু পাওয়া গেল */
return 1;
সমস্যা ৩১০,০০০ জন ছাত্রের নম্বর সাজাতে হবে
/* stdlib.h */
int cmp(const void *a, const void *b) {
    int x = *(const int *)a, y = *(const int *)b;
    return (x > y) - (x < y);   /* x - y নয়! */
}
qsort(marks, n, sizeof marks[0], cmp);
সমস্যা ৪রসিদে তারিখ ছাপতে হবে
/* time.h */
time_t now = time(NULL);
struct tm *t = localtime(&now);
strftime(buf, sizeof buf, "%d %b %Y", t);
সমস্যা ৫সেন্সর থেকে ঠিক ২ বাইটের মান আসে
/* stdint.h */
uint16_t reading;   /* সব যন্ত্রে ঠিক ১৬ বিট */
/* int হলে কোথাও ১৬, কোথাও ৩২ বিট হতে পারত */
  1. লেখা থেকে সংখ্যা → stdlib.h → strtol. কিবোর্ড বা ফাইল থেকে যা আসে, তা সবসময় লেখা (স্ট্রিং)। একে সংখ্যা বানাতে strtol. ফাঁদ: atoi দিয়েও কাজ হয়, কিন্তু "abc" দিলে সে চুপচাপ 0 দেয় — আসল 0 আর ভুল ইনপুট আলাদা করা যায় না। অংশ ৪-এ বিস্তারিত।
  2. একটি অক্ষর চেনা → ctype.h → isdigit. নিজে c >= '0' && c <= '9' লেখা যায়, কিন্তু অক্ষরের ধরন আরও অনেক (space, বিরামচিহ্ন, বড়-ছোট হাতের অক্ষর)। ফাঁদ: argument-এ (unsigned char) cast — কেন, সেটা অংশ ৩-এ।
  3. সাজানো → stdlib.h → qsort. নিজে bubble sort লেখার দরকার নেই। আপনি শুধু একটি তুলনার ফাংশন (comparator) দেন। ফাঁদ: return x - y; লিখলে খুব বড় বা খুব ছোট সংখ্যায় overflow হয়ে ভুল চিহ্ন আসতে পারে। ফাংশন পয়েন্টার পর্ব ২৮-এ, sort-এর ভেতরের কৌশল পর্ব ৪০-এ।
  4. তারিখ ছাপা → time.h → strftime. মাসের নাম, ১২ ঘণ্টার ঘড়ি, AM/PM — সব একটি format string-এ। ফাঁদ: struct tm-এ মাস গোনা শুরু 0 থেকে, আর বছর গোনা হয় 1900 থেকে। অংশ ৭-এ ধাপে ধাপে দেখবেন।
  5. নির্দিষ্ট মাপের সংখ্যা → stdint.h → uint16_t. ফাইল ফরম্যাট, নেটওয়ার্ক প্যাকেট বা সেন্সরের ডেটায় বাইট গুনে মিলতে হয়। int-এর মাপ যন্ত্রভেদে বদলায়, uint16_t-এর বদলায় না। নিয়ম: আগে মানচিত্রে বাক্স খুঁজুন, তারপর নিজে লেখার কথা ভাবুন।

string.h-এর ফাংশন (strlen, strcpy, strncpy, snprintf আর তাদের ফাঁদ) পর্ব ১৯-এ বিস্তারিত দেখেছেন, তাই এখানে আর পুনরাবৃত্তি করছি না। ফাইলের ফাংশনগুলো (fopen, fread, fseek) পর্ব ২৪-এ।

৩ · ctype.h — অক্ষর চেনা আর signed char-এর ফাঁদ

ctype.h-এর ফাংশনগুলো একটি অক্ষর নিয়ে প্রশ্নের উত্তর দেয় — “এটা কি অঙ্ক?”, “এটা কি space?” — অথবা অক্ষরটি বদলে দেয়। এগুলো ছোট, কিন্তু প্রায় প্রতিটি ইনপুট-যাচাইয়ের কোডে লাগে।

ফাংশনপ্রশ্ন বা কাজউদাহরণ
isdigit(c)অঙ্ক 0–9?'7' → সত্য
isalpha(c)ইংরেজি বর্ণ?'k' → সত্য
isalnum(c)অঙ্ক বা বর্ণ?'_' → মিথ্যা
isspace(c)Space, tab, newline?'\n' → সত্য
isupper(c) / islower(c)বড় না ছোট হাতের?'D' → বড়
toupper(c) / tolower(c)বড়/ছোট হাতে বদলানো'a' → 'A'; '5' → '5'

খেয়াল করুন, “সত্য” মানে এখানে শূন্য নয় এমন কোনো মান — ঠিক 1 নাও হতে পারে। তাই if (isdigit(c) == 1) লিখবেন না, লিখুন if (isdigit(c)).

সবচেয়ে বিখ্যাত ফাঁদ: ঋণাত্মক char

এই ফাংশনগুলোর argument-এর টাইপ int, আর স্ট্যান্ডার্ডের নিয়ম কড়া: মানটি হতে হবে হয় EOF, নয়তো unsigned char-এ ধরে এমন একটি মান (সাধারণত 0 থেকে 255). এর বাইরে কিছু দিলে ফল undefined behavior.

সমস্যা হলো, x86 কম্পিউটারে char সাধারণত signed — অর্থাৎ 128-এর বেশি মানের বাইট char-এ রাখলে ঋণাত্মক হয়ে যায়। আর বাংলা লেখার প্রতিটি বাইটই 128-এর বেশি! পর্ব ১৯-এ দেখেছেন, UTF-8-এ “অ” তিনটি বাইটে লেখা হয়: E0 A6 85. এবার ধাপে ধাপে দেখুন কী ঘটে:

1const char *s = "অ";   /* bytes: E0 A6 85 */2char c = s[0];         /* byte 0xE0 */3int r1 = toupper(c);4int r2 = toupper((unsigned char)c);
মান (signed char ধরে)
c
argument
অবস্থা
  1. বাইটটি ঋণাত্মক হয়ে গেল। বাইট 0xE0-এর মান 224. কিন্তু signed char রাখতে পারে −128 থেকে 127, তাই 224 সেখানে −32 হিসেবে বসে (two's complement, পর্ব ০৫)।
  2. নিয়মটি মনে করুন। toupper-এর argument হতে হবে EOF (সাধারণত −1) অথবা 0 থেকে 255. −32 এই তালিকায় নেই।
  3. Undefined behavior. অনেক library ভেতরে একটি তালিকা (table) রাখে আর table[c] দেখে উত্তর দেয়। −32 দিলে তালিকার আগের মেমরি পড়া হয় — ভুল উত্তর, বা কিছু library-র debug সংস্করণে সরাসরি crash. আরও মজার একটি সমস্যা: 0xFF বাইটটি signed char-এ হয় −1, যা হুবহু EOF-এর সমান!
  4. Cast করলেই সমাধান। (unsigned char)c আগে বাইটটিকে আবার 224 বানায়, তারপর int-এ যায়। 224 বৈধ মান, আর এটি ইংরেজি বর্ণ নয়, তাই toupper একে না বদলে ফেরত দেয়। নিয়ম: ctype-এর প্রতিটি ফাংশনে char দিলে আগে (unsigned char).

ctype বাংলা বোঝে না। এই ফাংশনগুলো একটি একটি বাইট দেখে, অক্ষর নয়। “৫” (বাংলা পাঁচ) UTF-8-এ তিন বাইট, তাই isdigit এর কোনো বাইটকেই অঙ্ক বলবে না। বাংলা অঙ্ক বা বর্ণ চিনতে হলে আগে UTF-8 থেকে code point বের করতে হয় (পর্ব ১৯-এর অংশ ৮), অথবা ICU-র মতো বড় Unicode library ব্যবহার করতে হয়। বিকাশ বা নগদের মতো অ্যাপে কেউ বাংলা অঙ্কে টাকার অঙ্ক লিখলে সেটা আলাদা করে সামলাতে হয়।

৪ · লেখা থেকে সংখ্যা — atoi কেন নয়, strtol কেন

ধরুন একটি মোবাইল রিচার্জ অ্যাপ বানাচ্ছেন। ইউজার টাকার অঙ্ক লেখেন একটি ঘরে। প্রোগ্রাম পায় একটি স্ট্রিং — "50", বা ভুল করে "5O" (শূন্যের বদলে ইংরেজি O), বা কিছুই না। এই লেখা থেকে নিরাপদে সংখ্যা বানানোই এই অংশের বিষয়। পর্ব ২৫-এ argv-র সংখ্যার জন্য strtol-এর পাহারা প্রথম দেখেছেন; এখানে পুরো ছবিটা দেখব — atoi-র সাথে তুলনা আর একই পরিবারের অন্য ফাংশন।

অনেক বই আর অনেক AI প্রথমেই দেখায় atoi — “ASCII to integer”. দেখতে সহজ, কিন্তু এর একটি বড় দুর্বলতা আছে: ভুল হলে জানানোর কোনো উপায় তার নেই।

ইনপুটatoi দেয়strtol জানায়
"42"4242, পুরোটা পড়া হয়েছে
"0"00, পুরোটা পড়া হয়েছে
"abc"0 (আসল 0-এর সাথে পার্থক্য নেই)কোনো অঙ্কই পড়া যায়নি (end == s)
"5O"5 (চুপচাপ বাকিটা ফেলে দেয়)5, কিন্তু end দেখায় বাকি আছে "O"
খুব বড় সংখ্যাUndefined behaviorLONG_MAX আর errno = ERANGE

strtol-এর তিনটি argument: স্ট্রিং, একটি char *-এর ঠিকানা (যেখানে সে জানায় কোথায় পড়া থামল), আর ভিত্তি (10 মানে দশমিক, 16 মানে hexadecimal). দশমিক সংখ্যার জন্য একই পরিবারের strtod, আর unsigned long-এর জন্য strtoul. নিচের প্রোগ্রামে পাঁচটি ইনপুট একে একে যাচাই করা হচ্ছে — ctype.h-এর isspace দিয়ে শেষের space-ও মাফ করা হয়েছে:

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <errno.h>

int main(void) {
    const char *in[] = {"42", "  -17 ", "12abc", "abc", "99999999999999999999"};
    for (int i = 0; i < 5; i++) {
        const char *s = in[i];
        char *end;
        errno = 0;
        long v = strtol(s, &end, 10);
        while (isspace((unsigned char)*end)) end++;   /* trailing spaces OK */
        printf("%-24s -> ", s);
        if (end == s)             printf("not a number\n");
        else if (errno == ERANGE) printf("too big for long\n");
        else if (*end != '\0')    printf("junk after number: \"%s\"\n", end);
        else                      printf("OK, value = %ld\n", v);
    }
    return 0;
}

তিনটি প্রশ্নে পুরো যাচাই: (১) end == s হলে একটিও অঙ্ক পড়া যায়নি — "abc". (২) errno == ERANGE হলে সংখ্যাটি long-এ ধরে না — শেষের বিশ অঙ্কের সংখ্যা। এজন্যই ডাকার আগে errno = 0 করা জরুরি; নইলে আগের কোনো ব্যর্থতার পুরোনো মান থেকে যেতে পারে। (৩) *end != '\0' হলে সংখ্যার পরে আবর্জনা আছে — "12abc". লক্ষ করুন, শুরুর space strtol নিজেই এড়িয়ে যায়, শেষেরটা আমরা isspace দিয়ে এড়ালাম, আর সেখানে (unsigned char) cast আছে। এই ধাঁচটি পর্ব ৩০-এ এরর হ্যান্ডলিংয়ে আবার কাজে লাগবে।

stdlib.h-এর আরও কিছু প্রতিদিনের যন্ত্র

ফাংশনকাজফাঁদ
abs, labs, llabsপরম মান (int, long, long long)abs(INT_MIN) undefined — ধনাত্মক রূপটি int-এ ধরে না। দশমিকের জন্য fabs (math.h).
qsort, bsearchযেকোনো array সাজানো ও সাজানো array-তে খোঁজাbsearch-এর আগে array একই comparator দিয়ে সাজানো থাকতে হবে (পর্ব ৪০, ৪১)।
malloc, calloc, realloc, freeদরকারমতো মেমরিপর্ব ২০-এ বিস্তারিত — NULL যাচাই, একবারই free.
exit, atexitপ্রোগ্রাম শেষ করা; শেষ হওয়ার আগে চালানোর ফাংশন রেজিস্টার করাexit stdio-র buffer flush করে আর atexit-এর ফাংশন চালায়; _Exit কিছুই করে না।
getenvenvironment variable পড়া, যেমন HOME, PATHনা থাকলে NULL ফেরত দেয় — যাচাই না করে ব্যবহার করলে crash.
rand, srandছদ্ম-এলোমেলো সংখ্যাফাঁদ অনেক — পরের অংশটি পুরোটাই এ নিয়ে।

৫ · rand() — এলোমেলো, কিন্তু কতটা?

লুডো খেলার একটি অ্যাপ বানাতে গেলে ছক্কা লাগবে। প্রায় সব টিউটোরিয়াল এভাবে লেখে:

srand(time(NULL));          /* একবার, প্রোগ্রামের শুরুতে */
int face = rand() % 6 + 1;  /* 1 থেকে 6 */

খেলার জন্য এটি মোটামুটি চলে। কিন্তু ভেতরে তিনটি বিষয় বোঝা দরকার।

প্রথমত, rand() আসলে এলোমেলো নয়। এটি একটি PRNG — একটি সূত্র, যা আগের মান থেকে পরের মান হিসাব করে। শুরুর মানটির নাম seed. একই seed দিলে প্রতিবার হুবহু একই ধারা আসে। srand(time(NULL)) বর্তমান সময়কে seed বানায়, তাই প্রতিবার আলাদা ধারা পাওয়া যায়। কিন্তু srand প্রতিবার rand()-এর আগে ডাকলে বিপদ — একই সেকেন্ডে সব ডাকেই একই seed, অর্থাৎ একই “এলোমেলো” সংখ্যা! নিয়ম: seed একবারই, প্রোগ্রামের শুরুতে।

দ্বিতীয়ত, % 6 সব মুখকে সমান সুযোগ দেয় না। rand() দেয় 0 থেকে RAND_MAX পর্যন্ত। স্ট্যান্ডার্ড শুধু বলে RAND_MAX অন্তত 32767; glibc-তে এটি 2147483647, MSVC-তে 32767. বোঝার সুবিধার জন্য একটি খেলনা যন্ত্র ধরি যেখানে RAND_MAX = 7, অর্থাৎ rand() আটটি মান (0–7) সমান সম্ভাবনায় দেয়:

0r = 0  -> 0 % 6 = 0 -> face 11r = 1  -> 1 % 6 = 1 -> face 22r = 2  -> 2 % 6 = 2 -> face 33r = 3  -> 3 % 6 = 3 -> face 44r = 4  -> 4 % 6 = 4 -> face 55r = 5  -> 5 % 6 = 5 -> face 66r = 6  -> 6 % 6 = 0 -> face 1 again7r = 7  -> 7 % 6 = 1 -> face 2 again
কোন মুখে কতটি r-মান
face 1
face 2
face 3
face 4
face 5
face 6
  1. আটটি সমান সম্ভাবনার মান। এই খেলনা যন্ত্রে rand() দেয় 0, 1, … 7 — প্রতিটির সম্ভাবনা ১/৮. এবার দেখি r % 6 এদের কোন মুখে পাঠায়।
  2. প্রথম ছয়টি মান ঠিকঠাক। 0 থেকে 5 প্রত্যেকে আলাদা একটি মুখে গেল। এ পর্যন্ত সব মুখের একটি করে মান — ন্যায্য।
  3. r = 6 ঘুরে এলো মুখ ১-এ। 6 % 6 = 0, তাই আবার face 1. ছয় দিয়ে ভাগশেষ নিলে ছয়ের পরের মানগুলো আবার শুরু থেকে ঘুরে আসে।
  4. r = 7 গেল মুখ ২-এ। 7 % 6 = 1. এখন মুখ ১ আর ২-এর দুটি করে মান, বাকিদের একটি করে। আট মানকে ছয় ভাগে সমান ভাগ করা যায় না — দুটি মান বাড়তি থেকে যায়।
  5. পক্ষপাত (modulo bias). মুখ ১ ও ২ পড়ে ২৫% সময়, বাকিরা ১২.৫% — দ্বিগুণ পার্থক্য! আসল RAND_MAX বড় হলে পার্থক্য ছোট হয়, কিন্তু শূন্য হয় না। আর n বড় হলে আবার বড় হয়: MSVC-তে (32768টি মান) rand() % 20000 নিলে 0–12767 সংখ্যাগুলোর সুযোগ বাকিদের দ্বিগুণ।
  6. সমাধান: বাড়তি মান ফেলে দিন। r যদি 6 বা 7 হয়, সেটা বাদ দিয়ে আবার rand() ডাকুন। এখন বাকি ছয়টি মান ছয় মুখে একটি করে — প্রতিটির সম্ভাবনা ঠিক ১/৬. এই কৌশলের নাম rejection sampling; ভালো library-গুলো ভেতরে ঠিক এটাই করে।

তৃতীয়ত, rand() নিরাপত্তার কাজে একেবারেই চলবে না। OTP, পাসওয়ার্ড রিসেট লিংক, session token, এনক্রিপশনের key — এসব বানাতে হবে এমন উৎস থেকে, যা আগে থেকে অনুমান করা যায় না। rand()-এর সূত্র সাধারণত সরল, আর seed যদি time(NULL) হয়, আক্রমণকারী শুধু আনুমানিক সময় জানলেই কয়েক হাজার seed চেষ্টা করে আপনার “গোপন” OTP বের করে ফেলতে পারে।

কাজকী ব্যবহার করবেন
ছোট খেলা, শেখার প্রোগ্রামrand(), সাথে rejection sampling
টেস্ট বা সিমুলেশন, যেখানে ফল আবার মেলাতে হবেনির্দিষ্ট seed (যেমন srand(42)), পর্ব ৪৪-এ দেখবেন
ভারী পরিসংখ্যান বা বৈজ্ঞানিক সিমুলেশনভালো মানের PRNG library, যেমন PCG বা xoshiro পরিবারের
OTP, পাসওয়ার্ড, token, keyOS-এর CSPRNG: Linux-এ getrandom() বা /dev/urandom, Windows-এ BCryptGenRandom

৬ · math.h — দশমিকের হিসাব আর তার ফাঁদ

math.h-এর প্রায় সব ফাংশন double নেয় আর double ফেরত দেয়। একই ফাংশনের float রূপের নামের শেষে f (sqrtf), long double-এর শেষে l (sqrtl).

ফাংশনকাজউদাহরণ
sqrt(x)বর্গমূলsqrt(25.0) → 5.0
pow(x, y)x-এর y ঘাতpow(2.0, 10.0) → 1024.0
fabs(x)দশমিকের পরম মানfabs(-3.5) → 3.5
floor(x)নিচের দিকের পূর্ণসংখ্যাfloor(-2.5) → −3.0
ceil(x)ওপরের দিকের পূর্ণসংখ্যাceil(2.1) → 3.0
round(x)কাছের পূর্ণসংখ্যা (ঠিক মাঝখানে হলে শূন্য থেকে দূরে)round(2.5) → 3.0
fmod(x, y)দশমিকের ভাগশেষ (% শুধু পূর্ণসংখ্যায় চলে)fmod(10.5, 3.0) → 1.5

pow(10, 2) কেন কখনো 99 হয়ে যায়

একটি বহুল আলোচিত ঘটনা: কিছু পুরোনো compiler-library জুটিতে (বিশেষ করে পুরোনো MinGW-তে) (int)pow(10, 2) লিখলে 100-এর বদলে 99 আসত। কারণটা বুঝলে ফাঁদটা চিরদিনের জন্য মনে থাকবে:

  1. pow সাধারণ ঘাতের জন্য বানানো — 2.7-এর 3.14 ঘাতও নিতে পারে। তাই ভেতরে অনেক সময় লগারিদমের মতো পথে হিসাব করে, পূর্ণসংখ্যা গুণ করে নয়।
  2. সেই পথে ফল হতে পারে 99.99999999999997 — 100-এর খুব কাছে, কিন্তু একটু কম। (Floating-point-এর সীমাবদ্ধ precision, পর্ব ০৫।)
  3. (int) cast গোল করে না, দশমিক অংশ কেটে ফেলে (truncation, পর্ব ০৮)। 99.999… কেটে হয় 99.

আধুনিক glibc-তে pow(10, 2) ঠিক 100 দেয়, কিন্তু শিক্ষাটা সব দশমিক হিসাবে খাটে। তিনটি নিয়ম মনে রাখুন:

ভুল ইনপুট দিলে math.h কী করে? sqrt(-1.0) দেয় একটি বিশেষ মান NaN (“সংখ্যা নয়”), আর pow(10.0, 400.0)-এর মতো খুব বড় ফল দেয় HUGE_VAL (সাধারণত অসীম)। প্রোগ্রাম crash করে না, কিন্তু চুপচাপ ভুল মান পরের হিসাবে ছড়িয়ে পড়ে। isnan() ও isinf() দিয়ে যাচাই করা যায়। দশমিক হিসাবের গভীর বিষয়গুলো ABCL TECH-এর Numerical Methods কোর্সে আছে।

৭ · time.h — সময়, তারিখ আর ২০৩৮ সালের সমস্যা

কম্পিউটার সময় রাখে খুব সরলভাবে: একটি নির্দিষ্ট মুহূর্ত থেকে এ পর্যন্ত কত সেকেন্ড পার হয়েছে, শুধু সেই সংখ্যাটি। সেই মুহূর্তটি হলো ১ জানুয়ারি ১৯৭০, রাত ১২টা (UTC) — নাম Unix epoch. time.h-এর মূল টাইপ ও ফাংশনগুলো এই সংখ্যাকে ঘিরে:

নামকী
time_tসময়ের সংখ্যা (সাধারণত epoch থেকে সেকেন্ড)
time(NULL)এই মুহূর্তের time_t
struct tmভাঙা রূপ: বছর, মাস, দিন, ঘণ্টা, মিনিট, সেকেন্ড, সপ্তাহের দিন
localtime / gmtimetime_t → struct tm, স্থানীয় সময়ে / UTC-তে
mktimeউল্টো পথ: struct tm → time_t, সাথে সপ্তাহের দিনও পূরণ করে
strftimestruct tm থেকে পছন্দমতো তারিখ-লেখা বানানো
difftime(t2, t1)দুটি time_t-এর পার্থক্য, সেকেন্ডে (double)
clock()প্রোগ্রাম এ পর্যন্ত কত CPU সময় খরচ করেছে; CLOCKS_PER_SEC দিয়ে ভাগ করলে সেকেন্ড

সময় অঞ্চল (time zone) নিয়ে চিন্তা নেই। বাংলাদেশের সময় UTC থেকে ৬ ঘণ্টা এগিয়ে। কিন্তু এই হিসাব আপনাকে করতে হয় না — OS-এর সেটিংস জানে আপনি কোন অঞ্চলে, আর localtime সেই অনুযায়ী স্থানীয় সময় দেয়। সার্ভারে সাধারণত সব সময় UTC-তে রাখা হয়, আর শুধু দেখানোর সময় স্থানীয় সময়ে বদলানো হয়।

কোড কতক্ষণ চলল মাপতে clock() সহজ উপায় — কাজের আগে ও পরে ডেকে পার্থক্য নিন। পর্ব ৩৯-এ Big-O-র তত্ত্ব আসল সময়ের সাথে মেলাতে এটি ব্যবহার করবেন। খেয়াল রাখুন, এটি দেয় CPU সময়, দেয়ালঘড়ির সময় নয় — প্রোগ্রাম ঘুমিয়ে থাকলে বা ইনপুটের অপেক্ষায় থাকলে সেই সময় গোনা হয় না।

strftime — format string দিয়ে তারিখ বানানো

রসিদে লিখতে চান “16 Oct 2026, 05:30 PM”. printf-এর মতোই strftime-এর format string-এ % দিয়ে শুরু হওয়া কোড বসে, আর প্রতিটি কোড struct tm-এর একটি ঘর থেকে মান নেয়। ধাপে ধাপে দেখুন লেখাটি কীভাবে তৈরি হয়:

%d %b %Y, %I :%M %p
1t.tm_year = 126;  /* 2026 - 1900 */2t.tm_mon  = 9;    /* 0 = Jan */3t.tm_mday = 16;4t.tm_hour = 17;   /* 24-hour */5t.tm_min  = 30;
এই ধাপে
কোড
ঘর থেকে
লেখা হলো
  1. %d → দিন। tm_mday থেকে, দুই অঙ্কে: 16. এখানে কোনো চমক নেই — মাসের দিন গোনা শুরু হয় 1 থেকেই।
  2. %b → মাসের সংক্ষিপ্ত নাম। tm_mon = 9, অথচ অক্টোবর দশম মাস! কারণ এখানে মাস গোনা শুরু 0 থেকে (0 = January) — যাতে মাসের নামের array-তে সরাসরি index হিসেবে বসানো যায়। tm_mon = 10 লিখলে আসত Nov — নতুনদের সবচেয়ে সাধারণ ভুল।
  3. %Y → চার অঙ্কের বছর। tm_year-এ রাখা হয় 1900-এর পর কত বছর, তাই 2026-এর জন্য 126. strftime নিজে 1900 যোগ করে 2026 লেখে। Format string-এর কমা আর space হুবহু বসে যায়।
  4. %I → ১২ ঘণ্টার ঘড়ির ঘণ্টা। tm_hour সবসময় ২৪ ঘণ্টার হিসাবে: 17. %I একে ১২ ঘণ্টায় বদলে দুই অঙ্কে লেখে: 05. (%H দিলে আসত 17.)
  5. %M → মিনিট। tm_min = 30, তাই :30. খেয়াল করুন, %m (ছোট হাতের) মানে মাসের সংখ্যা, আর %M (বড় হাতের) মানে মিনিট — এক অক্ষরের তফাতে পুরো অর্থ বদলে যায়।
  6. %p → AM না PM. আবার tm_hour দেখে: 17 ≥ 12, তাই PM. পুরো লেখা তৈরি: “16 Oct 2026, 05:30 PM”. মাসের নাম ও AM/PM ইংরেজিতে আসে, কারণ ডিফল্ট “C” locale; অন্য locale বেছে নিলে নাম বদলাতে পারে।

এবার math.h আর time.h একসাথে চালিয়ে দেখুন। খেয়াল করুন, তারিখটি আমরা নিজেরা struct tm-এ বসিয়েছি — তাই প্রোগ্রাম যেদিনই চালান, ফল একই আসবে:

#include <stdio.h>
#include <math.h>
#include <time.h>

int main(void) {
    printf("sqrt(3*3 + 4*4) = %.1f\n", sqrt(3.0 * 3.0 + 4.0 * 4.0));
    printf("round(2.5)=%.0f  floor(-2.5)=%.0f  ceil(2.1)=%.0f\n",
           round(2.5), floor(-2.5), ceil(2.1));
    printf("fmod(10.5, 3) = %.1f\n", fmod(10.5, 3.0));

    struct tm t = {0};             /* a fixed date, not "now" */
    t.tm_year = 2026 - 1900;       /* years since 1900 */
    t.tm_mon  = 9;                 /* 0 = January, so 9 = October */
    t.tm_mday = 16;  t.tm_hour = 17;  t.tm_min = 30;
    t.tm_isdst = -1;               /* "not sure about daylight saving" */
    mktime(&t);                    /* fills in tm_wday (day of week) */

    char buf[64];
    strftime(buf, sizeof buf, "%a, %d %b %Y, %I:%M %p", &t);
    printf("%s\n", buf);
    return 0;
}

প্রথম তিন লাইন math.h: বর্গমূল 5.0, round(2.5) শূন্য থেকে দূরে গিয়ে 3, floor(-2.5) নিচের দিকে গিয়ে −3 (−2 নয়!), আর দশমিকের ভাগশেষ 1.5. একটি সূক্ষ্ম কথা: এখানে সব argument সরাসরি সংখ্যা, তাই gcc compile করার সময়ই উত্তর হিসাব করে বসিয়ে দেয় — -lm লাগে না। ভ্যারিয়েবলের মান দিলে Linux-এ -lm জুড়তেই হবে। এরপর mktime আমাদের দেওয়া তারিখ থেকে সপ্তাহের দিন বের করল (%a → Fri), আর শেষ লাইনে আসে Fri, 16 Oct 2026, 05:30 PM. buf-এর মাপ sizeof buf দিয়ে দেওয়ায় strftime কখনো সীমার বাইরে লিখবে না।

২০৩৮ সালের সমস্যা

পুরোনো অনেক সিস্টেমে time_t ছিল ৩২ বিটের signed পূর্ণসংখ্যা। এর সর্বোচ্চ মান 2147483647 সেকেন্ড। Epoch থেকে এতগুলো সেকেন্ড পূর্ণ হবে ১৯ জানুয়ারি ২০৩৮, UTC সময় রাত ৩টা ১৪ মিনিট ৭ সেকেন্ডে (বাংলাদেশ সময় সকাল ৯টা ১৪ মিনিট)। এর পরের সেকেন্ডে সংখ্যাটি wraparound হয়ে সবচেয়ে ছোট ঋণাত্মক মানে চলে যাবে — ঘড়ি দেখাবে ডিসেম্বর ১৯০১!

এর নাম Y2038 সমস্যা. আধুনিক ৬৪-বিট Linux, Windows আর Mac-এ time_t ইতিমধ্যে ৬৪ বিট, যা প্রায় ২৯ হাজার কোটি বছর চলবে। ঝুঁকি মূলত ৩২-বিটের পুরোনো এমবেডেড যন্ত্রে — রাউটার, শিল্পকারখানার নিয়ন্ত্রক, গাড়ির যন্ত্রাংশ — যেগুলো বহু বছর ধরে আপডেট ছাড়াই চলে। আর ফাইল ফরম্যাট বা ডেটাবেসে সময় যদি ৩২ বিটে জমা রাখা হয়, সেটাও একই ফাঁদে পড়বে।

৮ · সীমা, মাপ আর কিছু বিশেষ যন্ত্র

limits.h আর stdint.h — সংখ্যার মাপ নিশ্চিত করা

পর্ব ০৬-এ দেখেছেন, int কত বড় হবে তা C নির্দিষ্ট করে বলে না — শুধু বলে অন্তত ১৬ বিট। তাই “আমার যন্ত্রে সীমা কত” জানতে limits.h:

নামHeaderঅর্থ
INT_MAX, INT_MINlimits.hint-এর সবচেয়ে বড় ও ছোট মান (সাধারণত 2147483647 ও −2147483648)
LONG_MAX, UINT_MAXlimits.hlong ও unsigned int-এর সীমা
CHAR_BITlimits.hএক বাইটে কত বিট (প্রায় সবখানে 8)
int32_t, uint8_t, int64_tstdint.hঠিক ৩২, ৮, ৬৪ বিটের টাইপ — সব যন্ত্রে একই মাপ
INT32_MAX, UINT8_MAXstdint.hওই টাইপগুলোর সীমা
SIZE_MAXstdint.hsize_t-এর সর্বোচ্চ মান — মেমরির মাপ হিসাবের সীমা
bool, true, falsestdbool.hসত্য-মিথ্যার টাইপ (C23-এ header ছাড়াই পাওয়া যায়)

কখন কোনটা? সাধারণ গণনায় int ঠিক আছে। কিন্তু যেখানে মাপ চুক্তির অংশ — ফাইলের ফরম্যাট, নেটওয়ার্ক প্যাকেট, হার্ডওয়্যারের রেজিস্টার, হ্যাশের হিসাব — সেখানে stdint.h-এর টাইপ। আর যোগ বা গুণের আগে overflow হবে কি না যাচাই করতে INT_MAX-এর মতো সীমা: if (a > INT_MAX - b) মানে “যোগ করলে উপচে পড়বে”।

setjmp ও longjmp — অনেক দূরে লাফ

setjmp/longjmp জোড়াটি অদ্ভুত। setjmp বর্তমান অবস্থার একটি “বুকমার্ক” রাখে। পরে অনেকগুলো ফাংশনের গভীর থেকে longjmp ডাকলে প্রোগ্রাম সরাসরি সেই বুকমার্কে ফিরে আসে — মাঝের সব ফাংশন কোনো return ছাড়াই বাতিল হয়ে যায়। অনেকটা লুডোর সাপের মুখে পড়ে এক লাফে নিচে নেমে যাওয়ার মতো।

শুনতে শক্তিশালী, কিন্তু খুব কম ক্ষেত্রেই এটা ঠিক যন্ত্র। মাঝের ফাংশনগুলোর খোলা ফাইল বন্ধ হয় না, malloc করা মেমরি free হয় না, আর লাফের পরে কিছু local ভ্যারিয়েবলের মান অনির্ধারিত হতে পারে (যদি না সেগুলো volatile হয়)। কিছু parser, interpreter আর পুরোনো library ভুল-ব্যবস্থাপনায় এটি ব্যবহার করে। আপনার নিজের কোডে বেশিরভাগ সময় পর্ব ৩০-এর return code আর goto cleanup ধাঁচই ভালো।

signal.h — Ctrl+C ধরা

Terminal-এ চলমান প্রোগ্রামে Ctrl+C চাপলে OS প্রোগ্রামকে একটি signal পাঠায় — SIGINT. ডিফল্টভাবে প্রোগ্রাম সঙ্গে সঙ্গে বন্ধ হয়। signal.h দিয়ে নিজের একটি ফাংশন (signal handler) বসানো যায়, যেমন বন্ধ হওয়ার আগে ফাইল ঠিকমতো save করতে। তবে handler-এর ভেতরে নিয়ম খুব কড়া — printf বা malloc ডাকা নিরাপদ নয়; সাধারণত শুধু একটি volatile sig_atomic_t flag বসানো হয়, আর মূল লুপ সেটা দেখে। পূর্ণ আলোচনা পর্ব ৪৮-এ।

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

  • Header ফাইল আর library-র পার্থক্য কী? — Header-এ ঘোষণা থাকে (compiler দেখে), library-তে আসল কোড থাকে (linker জোড়ে)।
  • atoi আর strtol-এর পার্থক্য? — atoi ভুল জানাতে পারে না; strtol end pointer ও errno দিয়ে ভুল ইনপুট আর overflow ধরে।
  • srand(time(NULL)) লুপের ভেতরে লিখলে কী হয়? — একই সেকেন্ডে বারবার একই seed বসে, তাই একই “এলোমেলো” সংখ্যা বারবার আসে।
  • sqrt ব্যবহার করলে Linux-এ undefined reference কেন? — math library আলাদা ফাইলে; compile কমান্ডে -lm লাগে।
  • struct tm-এ ২০২৬ সালের অক্টোবর কীভাবে লিখবেন? — tm_year = 126, tm_mon = 9.

৯ · AI-এর যুগে — library-র ব্যাপারে AI যেসব ভুল করে

AI-কে C কোড লিখতে বললে সে দুই উল্টো রকম ভুল করে। কখনো library-র তৈরি ফাংশন থাকা সত্ত্বেও নিজে নতুন করে লেখে — নিজের হাতে বানানো my_atoi, নিজের bubble sort, নিজে লিপ ইয়ারের হিসাব — আর সেগুলোতে কোণার ক্ষেত্রে বাগ থাকে। আবার কখনো library ব্যবহার করে, কিন্তু ঠিক সেই ফাঁদগুলোতে পা দেয় যা এই পর্বে দেখলেন। কারণটা সহজ: AI শিখেছে ইন্টারনেটের অগণিত টিউটোরিয়াল থেকে, আর সেখানে atoi আর rand() % n-ই সবচেয়ে বেশি দেখা যায়।

AI যা লেখেসমস্যাঠিক রূপ
নিজে লেখা parse বা sort ফাংশনঋণাত্মক সংখ্যা, খালি ইনপুট, overflow-তে বাগstrtol, qsort
int n = atoi(input);"abc" আর "0" আলাদা করা যায় নাstrtol + end pointer + errno
OTP: rand() % 1000000অনুমানযোগ্য, পক্ষপাতদুষ্টOS-এর CSPRNG (getrandom)
isdigit(s[i])বাংলা বা অন্য বাইটে ঋণাত্মক মান → UBisdigit((unsigned char)s[i])
gcc main.c (math.h ব্যবহার সত্ত্বেও)Linux-এ undefined reference to `sqrt'কমান্ডের শেষে -lm
থ্রেডের ভেতরে localtime()সব থ্রেড একটিই ভেতরের struct tm ভাগ করে, একে অপরের মান মুছে দেয়localtime_r() (POSIX)
(int)pow(10, n)কিছু library-তে এক কম আসেলুপে পূর্ণসংখ্যা গুণ

শেষ সারির আগেরটা একটু ব্যাখ্যা দাবি করে। localtime ফল রাখে library-র ভেতরের একটি static struct tm-এ, আর তার ঠিকানা ফেরত দেয়। একটি থ্রেডে এটা ঠিক আছে। কিন্তু দুটি থ্রেড একসাথে ডাকলে একজনের ফল আরেকজন মুছে দিতে পারে — এমন ফাংশনকে বলে thread-safe নয় (thread-safety পর্ব ৪৯-এ)। localtime_r-এ আপনি নিজের struct tm-এর ঠিকানা দেন, তাই কোনো ভাগাভাগি নেই।

AI-কে C কোড লিখতে বলার সময় library-র নিয়মগুলো আগেই বলে দিন। নিচের প্রম্পটটি কপি করে নিজের কাজ অনুযায়ী বদলে নিন:

আমি Linux-এ gcc -Wall -Wextra দিয়ে C17 কোড লিখছি। [এখানে কাজটি লিখুন] — এর জন্য একটি C প্রোগ্রাম লেখো। শর্ত: ১. C standard library-তে যে কাজের ফাংশন আছে, সেটা নিজে নতুন করে লিখবে না (যেমন strtol, qsort, strftime). ২. লেখা থেকে সংখ্যা বানাতে atoi নয়, strtol ব্যবহার করবে এবং end pointer ও errno যাচাই করবে। ৩. ctype.h-এর ফাংশনে char দিলে (unsigned char) cast করবে। ৪. নিরাপত্তার কাজে (OTP, token) rand() ব্যবহার করবে না। ৫. math.h লাগলে compile কমান্ডে -lm দেখাবে। ৬. কোডের পরে প্রতিটি library ফাংশনের পাশে এক লাইনে বলো: কোন header, আর এর প্রধান ফাঁদ কী।

AI-এর কোডে library ব্যবহার যাচাইয়ের চেকলিস্ট

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

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

প্র ০১Python-এর সাথে JSON, HTTP, ZIP — সবকিছু আসে। C-এর standard library এত ছোট কেন? এটা কি দুর্বলতা?

Python-এর দর্শন “batteries included” — ভাষার সাথে প্রায় সবকিছু দিয়ে দেওয়া। C-এর দর্শন উল্টো, আর তার পেছনে শক্ত যুক্তি আছে।

  • C চলে সব জায়গায়: ৬৪-বিট সার্ভার থেকে কয়েক কিলোবাইট মেমরির microcontroller পর্যন্ত। স্ট্যান্ডার্ডে যা-ই রাখা হয়, প্রতিটি যন্ত্রের libc-কে তা বানাতে হয়। যে যন্ত্রে নেটওয়ার্কই নেই, সেখানে HTTP library বাধ্যতামূলক করলে কী হবে? এজন্যই স্ট্যান্ডার্ড এমবেডেড যন্ত্রের জন্য “freestanding” নামে আরও ছোট একটি স্তর রেখেছে, যেখানে stdio.h-ও বাধ্যতামূলক নয়।
  • একবার স্ট্যান্ডার্ডে ঢুকলে আর সরানো যায় না: লক্ষ লক্ষ প্রোগ্রাম নির্ভর করে, তাই ভুল নকশাও দশকের পর দশক থেকে যায়। ১৯৮৮ সালের Morris worm যে gets-এর দুর্বলতা কাজে লাগিয়েছিল, সেটি স্ট্যান্ডার্ড থেকে সরানো হয় অনেক পরে, ২০১১ সালের C11-এ। কমিটি তাই খুব সাবধানে নতুন জিনিস নেয়।
  • বাকি কাজ করে OS ও বাইরের library: নেটওয়ার্ক, ফাইল সিস্টেমের ফোল্ডার, প্রসেস — এগুলো আসে POSIX বা Windows API থেকে। JSON-এর জন্য cJSON, HTTP-র জন্য libcurl, compression-এর জন্য zlib — সবই C-তে লেখা, আর প্রায় সব OS-এ পাওয়া যায়।

দাম আছে, অবশ্যই। C-তে কোনো স্ট্যান্ডার্ড package manager নেই, বাইরের library জোড়া ঝামেলার (পর্ব ২৯)। Python-এর মতো “এক লাইনে ওয়েব থেকে ডেটা আনা” C-তে হয় না। আবার মজার ব্যাপার, Python-এর নিজের standard library-র অনেক অংশই ভেতরে C library-র ওপর দাঁড়িয়ে — zlib, OpenSSL, libc.

মূল উপলব্ধি: Standard library-র আকার একটি নকশার সিদ্ধান্ত — বহনযোগ্যতা আর সুবিধার মধ্যে বিনিময়। যে ভাষা সবখানে চলতে চায়, তার অবশ্য-থাকা অংশ ছোট রাখতেই হয়।

প্র ০২Docker-এ Alpine Linux নিলে কোনো কোনো প্রোগ্রাম চলে না বা অন্যরকম আচরণ করে। glibc আর musl-এর পার্থক্য এখানে কীভাবে জড়িত?

Alpine Linux খুব ছোট বলে Docker image-এর জন্য জনপ্রিয়, আর এর ছোট হওয়ার একটি বড় কারণ musl libc. Ubuntu বা Debian ব্যবহার করে glibc. দুটোই C স্ট্যান্ডার্ড আর POSIX মানে, কিন্তু স্ট্যান্ডার্ডের বাইরের অংশে ও খুঁটিনাটিতে পার্থক্য আছে।

  • বাইনারি মেলে না: glibc-র বিরুদ্ধে compile করা একটি প্রোগ্রাম dynamic linking-এ glibc-র নির্দিষ্ট ফাইল ও সংস্করণ খোঁজে। Alpine-এ সেটা নেই, তাই প্রায়ই “not found” ধরনের অদ্ভুত error আসে — যদিও ফাইলটি সামনেই আছে! দুটোর ABI আলাদা।
  • বাড়তি সুবিধা: glibc-তে GNU-র নিজস্ব অনেক বাড়তি ফাংশন ও আচরণ আছে। যে কোড না জেনে সেগুলোর ওপর নির্ভর করে, musl-এ compile হয় না বা আলাদা আচরণ করে।
  • খুঁটিনাটি আচরণ: DNS নাম খোঁজার পদ্ধতি, থ্রেডের ডিফল্ট stack-এর মাপ (musl-এ অনেক ছোট), memory allocator-এর গতি, locale-এর সমর্থন — এসবে দুটো ভিন্ন। গভীর রিকার্শন যে কোড glibc-তে চলে, musl-এর ছোট থ্রেড stack-এ stack overflow হতে পারে।

musl-এর সুবিধাও স্পষ্ট: কোড সরল ও পড়া সহজ, static linking ভালোভাবে সমর্থন করে, তাই একটি ফাইলেই পুরো প্রোগ্রাম বানানো যায়। অনেক দল এজন্য musl দিয়ে static বাইনারি বানিয়ে খুব ছোট container বানায়।

মূল উপলব্ধি: “স্ট্যান্ডার্ড মেনে চলে” মানে “হুবহু একই” নয়। যতটুকু স্ট্যান্ডার্ডে লেখা, শুধু ততটুকুর ওপর ভরসা রাখলে কোড সবখানে চলে; বাকিটা যেখানে চালাবেন, সেখানে পরীক্ষা করে নিতে হয়।

প্র ০৩Y2K-তে পৃথিবী বেঁচে গিয়েছিল। Y2038 কি একই রকম সহজে মিটে যাবে? কারা সবচেয়ে ঝুঁকিতে?

Y2K সমস্যা ছিল বছরকে দুই অঙ্কে রাখা — “99”-এর পরে “00” এলে ২০০০ না ১৯০০? বিশ্বজুড়ে বিপুল অর্থ ও শ্রম খরচ করে পুরোনো সফটওয়্যার আগেভাগে ঠিক করা হয়েছিল, তাই বড় বিপর্যয় হয়নি। Y2038 একই ধরনের, কিন্তু কয়েকটি কারণে আরও জটিল।

  • সমস্যাটা লুকানো: Y2K-তে “99” চোখে দেখা যেত। Y2038-এর সমস্যা একটি টাইপের মাপে — ৩২-বিট time_t, বা ফাইল ফরম্যাট, ডেটাবেস কলাম, নেটওয়ার্ক প্রোটোকলে সময় রাখার ৩২-বিট ঘর। কোড পড়ে সহজে চোখে পড়ে না।
  • এমবেডেড যন্ত্র: সবচেয়ে বড় ঝুঁকি ৩২-বিটের ছোট যন্ত্রে — রাউটার, CCTV, শিল্পকারখানার নিয়ন্ত্রক, মিটার, গাড়ির যন্ত্রাংশ। এগুলো ১০–২০ বছর চলে, প্রায়ই কখনো আপডেট হয় না। আজ যে যন্ত্র বিক্রি হচ্ছে, তার অনেকগুলো ২০৩৮ সালেও চালু থাকবে।
  • আগেই আঘাত হানতে পারে: ভবিষ্যতের তারিখ যে হিসাব করে — যেমন ৩০ বছরের গৃহঋণের কিস্তি, দীর্ঘমেয়াদি সার্টিফিকেটের মেয়াদ — সে আজই ২০৩৮-এর পরের সময় নিয়ে কাজ করে, আর ৩২-বিটে উপচে পড়ে।

সমাধানের কাজ অনেক এগিয়েছে। ৬৪-বিট সিস্টেমে time_t আগে থেকেই ৬৪ বিট। Linux kernel ৩২-বিট যন্ত্রের জন্যও ৬৪-বিট সময়ের সমর্থন যোগ করেছে, আর glibc-তে ৩২-বিট প্রোগ্রামকেও ৬৪-বিট time_t দিয়ে compile করার ব্যবস্থা আছে। কিছু Linux distribution তাদের ৩২-বিট সংস্করণ পুরোটাই এভাবে নতুন করে বানিয়েছে। কিন্তু যে বাইনারি নতুন করে compile হবে না, আর যে ডেটা ৩২-বিটে জমা আছে, সেগুলো রয়ে যাবে।

মূল উপলব্ধি: একটি টাইপের মাপ বেছে নেওয়া আসলে ভবিষ্যতের সাথে একটি চুক্তি। ১৯৭০-এর দশকে ৩২ বিট যথেষ্ট মনে হয়েছিল; আজ যে মাপ বাছবেন, সেটাও কোনো একদিন পরীক্ষায় পড়বে।

প্র ০৪OTP বা পাসওয়ার্ডের জন্য এলোমেলো সংখ্যা কেন OS থেকে নিতে হয়? rand()-এর সমস্যা ঠিক কোথায়?

পরিসংখ্যানের জন্য “এলোমেলো” মানে সংখ্যাগুলো সমানভাবে ছড়ানো। নিরাপত্তার জন্য “এলোমেলো” মানে আরও কঠিন একটি শর্ত: আগের সব সংখ্যা দেখেও কেউ পরেরটি অনুমান করতে পারবে না। rand() প্রথম শর্তও ঠিকমতো পূরণ করে না, দ্বিতীয়টি তো নয়ই।

  • অবস্থা খুব ছোট: অনেক libc-র rand() একটি সরল সূত্র (যেমন linear congruential generator), যার পুরো ভেতরের অবস্থা কয়েক বাইট। কয়েকটি আউটপুট দেখেই অবস্থা বের করে বাকি সব ভবিষ্যদ্বাণী করা যায়।
  • Seed অনুমানযোগ্য: srand(time(NULL)) মানে seed হলো সেকেন্ডে বর্তমান সময়। আক্রমণকারী যদি জানে OTP কোন মিনিটে তৈরি হয়েছে, সে মাত্র ৬০টি seed চেষ্টা করলেই হবে।
  • বাস্তব ইতিহাস: দুর্বল এলোমেলো সংখ্যা বহুবার বড় ক্ষতি করেছে। ২০০৮ সালে Debian Linux-এর OpenSSL-এ একটি পরিবর্তনের ফলে এলোমেলোতার উৎস প্রায় নষ্ট হয়ে গিয়েছিল — প্রায় দুই বছর ধরে তৈরি হওয়া SSH ও SSL key-গুলোর প্রতিটি ধরন ও মাপের জন্য মাত্র প্রায় ৩২ হাজার সম্ভাবনার মধ্যে আটকে ছিল, আর সেগুলো সহজেই অনুমান করা যেত।

OS-এর CSPRNG আলাদা কেন? Kernel যন্ত্রের নানা অনুমান-অযোগ্য ঘটনা থেকে — কিবোর্ড ও নেটওয়ার্কের সময়ের সূক্ষ্ম তারতম্য, হার্ডওয়্যারের এলোমেলো সংকেত — এলোমেলোতা জমায়, আর তা থেকে cryptography-র মানদণ্ডে পরীক্ষিত পদ্ধতিতে সংখ্যা বানায়। Linux-এ getrandom() বা /dev/urandom, Windows-এ BCryptGenRandom. আর ছয় অঙ্কের OTP বানাতেও % 1000000 নয় — rejection sampling, কারণ পক্ষপাত নিরাপত্তায়ও ছিদ্র।

মূল উপলব্ধি: “দেখতে এলোমেলো” আর “অনুমান করা অসম্ভব” এক জিনিস নয়। নিরাপত্তার কাজে কখনো নিজে এলোমেলো সংখ্যা বানাবেন না — যাচাই করা উৎস ব্যবহার করুন।

প্র ০৫যে microcontroller-এ কোনো OS নেই, সেখানে printf বা malloc কীভাবে চলে?

ডেস্কটপে printf শেষ পর্যন্ত OS-কে system call দিয়ে বলে “এই বাইটগুলো screen-এ পাঠাও”, আর malloc OS-এর কাছে মেমরি চায়। কিন্তু একটি ছোট microcontroller-এ OS নেই, screen নেই। তাহলে?

এখানে কাজে আসে newlib-এর মতো এমবেডেড libc. ১৯৯০-এর দশকে Cygnus Solutions এটি বানায়, পরে Red Hat দেখাশোনা করে; আজ অনেক ARM এমবেডেড toolchain-এর সাথে এটি আসে, আর মেমরি বাঁচাতে এর একটি ছোট রূপ “newlib-nano” ব্যবহৃত হয়। এর নকশার চাবিকাঠি হলো, OS-নির্ভর অংশগুলো কয়েকটি ছোট ফাংশনে আলাদা করে রাখা — যেমন _write (বাইট কোথায় পাঠাবে) আর _sbrk (মেমরি কোথা থেকে বাড়াবে)।

  • _write: আপনি নিজে এটি লেখেন, যাতে বাইটগুলো UART পিন দিয়ে ল্যাপটপের serial monitor-এ যায়। তখন printf("temp=%d\n", t) সরাসরি debug-এর কাজে লাগে।
  • _sbrk: linker script-এ ঠিক করা RAM-এর একটি অংশকে heap হিসেবে দেয়। তবে অনেক এমবেডেড প্রকল্পে malloc ব্যবহারই নিষেধ — অল্প RAM-এ মেমরি টুকরো হয়ে যাওয়ার ঝুঁকি, আর সময়ের নিশ্চয়তা থাকে না।
  • খরচের হিসাব: পূর্ণ printf (দশমিক সংখ্যাসহ) কয়েক কিলোবাইট flash খেতে পারে, যা ছোট chip-এ বড় অংশ। তাই অনেকে দশমিক সমর্থন বাদ দেওয়া ছোট সংস্করণ বেছে নেন।

স্ট্যান্ডার্ড নিজেও এটা মেনে নেয়: “hosted” পরিবেশে পুরো library, আর “freestanding” পরিবেশে শুধু অল্প কয়েকটি header (যেমন stdint.h, stddef.h, limits.h) বাধ্যতামূলক। পর্ব ৫১-এ এমবেডেড C-তে এ নিয়ে আরও দেখবেন।

মূল উপলব্ধি: ভালো library-নকশা মানে যা যন্ত্র-নির্ভর, তা কয়েকটি ছোট “হুক”-এ আলাদা রাখা। এতে একই printf-এর কোড সার্ভার থেকে ঘড়ির chip পর্যন্ত সবখানে চলে।

অনুশীলন

বেশিরভাগই ভাবা আর হাতে হিসাব করার কাজ — একটিতে ছোট কোড লিখতে হবে।

  1. Header খুঁজুন: নিচের প্রতিটি কাজের জন্য কোন header ও কোন ফাংশন? (ক) একটি স্ট্রিংয়ের সব অক্ষর বড় হাতের করা (খ) ১০০টি দশমিক সংখ্যার গড়কে কাছের পূর্ণসংখ্যায় গোল করা (গ) প্রোগ্রাম শেষ হওয়ার ঠিক আগে একটি লগ ফাইল বন্ধ করা (ঘ) কোনো int যোগে overflow হবে কি না আগেই যাচাই করা।

    (ক) ctype.h → প্রতিটি অক্ষরে toupper((unsigned char)s[i]). (খ) math.h → round() বা সরাসরি long চাইলে lround(); Linux-এ -lm মনে রাখুন। (গ) stdlib.h → atexit() দিয়ে একটি ফাংশন রেজিস্টার করুন, যা fclose করবে; exit() বা main থেকে return — দুই ক্ষেত্রেই সেটা চলবে। (ঘ) limits.h → b > 0 && a > INT_MAX - b হলে যোগে উপচে পড়বে।

  2. পক্ষপাত হাতে গুনুন: ধরুন একটি যন্ত্রে RAND_MAX = 9 (অর্থাৎ ১০টি মান)। rand() % 4 নিলে 0, 1, 2, 3 প্রত্যেকে কতবার আসবে? কোনটির সম্ভাবনা কত?

    0–9 কে ৪ দিয়ে ভাগশেষ: 0→0, 1→1, 2→2, 3→3, 4→0, 5→1, 6→2, 7→3, 8→0, 9→1. তাই 0 ও 1 আসে ৩ বার করে (সম্ভাবনা ৩০%), 2 ও 3 আসে ২ বার করে (২০%). ১০-কে ৪ দিয়ে ভাগ করলে ভাগশেষ ২ — ঠিক ততগুলো ফল বাড়তি সুযোগ পায়। Rejection sampling-এ 8 ও 9 এলে ফেলে দিলে বাকি ৮টি মান চার ভাগে সমান — প্রতিটি ২৫%.

  3. struct tm পূরণ করুন: ২১ ফেব্রুয়ারি ১৯৫২, সকাল ৯টা — এই সময়ের জন্য tm_year, tm_mon, tm_mday, tm_hour-এ কী লিখবেন? "%d %b %Y, %I:%M %p" দিয়ে কী ছাপা হবে?

    tm_year = 52 (1952 − 1900), tm_mon = 1 (0 = January, তাই ফেব্রুয়ারি 1), tm_mday = 21, tm_hour = 9, tm_min = 0. ছাপা হবে 21 Feb 1952, 09:00 AM. লক্ষ করুন, এই তারিখ ১৯৭০-এর epoch-এর আগের, তাই এর time_t হবে ঋণাত্মক — বেশিরভাগ আধুনিক সিস্টেম এটা ঠিকঠাক সামলায়, কিন্তু স্ট্যান্ডার্ড তার নিশ্চয়তা দেয় না।

  4. ছোট কোড — নিরাপদ বয়স পড়া: একটি ফাংশন লিখুন int parse_age(const char *s, int *out), যা সফল হলে 1, ব্যর্থ হলে 0 ফেরত দেবে। বয়স হতে হবে 0 থেকে 150-এর মধ্যে। "25", "-3", "2five", "" আর "999999999999" দিয়ে পরীক্ষা করুন।

    উপরের parse.c-এর ধাঁচ, সাথে সীমা যাচাই:

    int parse_age(const char *s, int *out) {
        char *end;
        errno = 0;
        long v = strtol(s, &end, 10);
        if (end == s || *end != '\0') return 0;   /* no digits / junk */
        if (errno == ERANGE) return 0;            /* too big for long */
        if (v < 0 || v > 150) return 0;           /* outside valid age */
        *out = (int)v;
        return 1;
    }

    ফল: "25" → 1 (বয়স 25); "-3" → 0 (সীমার বাইরে); "2five" → 0 (আবর্জনা); "" → 0 (end == s); "999999999999" → 0 (৬৪-বিট long-এ ধরে, কিন্তু 150-এর বেশি)। সীমা যাচাই int-এ রূপান্তরের আগে করা হয়েছে বলে cast নিরাপদ।

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

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

ক্রমলেখা থেকে নিরাপদে সংখ্যা বানানোর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. errno = 0 বসানো
  2. strtol(s, &end, 10) ডাকা
  3. end == s কি না দেখা — কোনো অঙ্ক পড়া হয়েছে তো?
  4. errno == ERANGE কি না দেখা — সীমার ভেতরে তো?
  5. *end শেষ চিহ্ন '\0' কি না দেখা — পেছনে আবর্জনা নেই তো?

Linux-এ gcc calc.c -o calc চালালে undefined reference to `pow' এলো। কারণ কী?

  • math.h include করা হয়নি
  • Math library link করা হয়নি — কমান্ডে -lm লাগবে
  • pow C-তে নেই, নিজে লিখতে হবে
  • প্রোগ্রাম চলার সময় pow crash করেছে

atoi("abc") কী ফেরত দেয়?

  • −1, ভুল বোঝাতে
  • প্রোগ্রাম crash করে
  • 0, আর আসল "0"-এর সাথে পার্থক্য বোঝা যায় না
  • অক্ষরগুলোর ASCII মানের যোগফল

isalpha(s[i])-কে isalpha((unsigned char)s[i]) লেখা হয় কেন?

  • এতে ফাংশন দ্রুত চলে
  • যাতে বাংলা অক্ষরও বর্ণ হিসেবে ধরা পড়ে
  • শুধু compiler-এর warning বন্ধ করতে
  • signed char-এ ঋণাত্মক মান হলে undefined behavior হয়

একটি ব্যাংকের অ্যাপে ৬ অঙ্কের OTP বানাতে কোনটি ঠিক?

  • OS-এর CSPRNG (যেমন getrandom) থেকে, rejection sampling দিয়ে
  • srand(time(NULL)) একবার, তারপর rand() % 1000000
  • প্রতিবার srand(time(NULL)) ডেকে rand()
  • বর্তমান সময়ের সেকেন্ডের শেষ ৬ অঙ্ক

২০২৬ সালের ১৬ অক্টোবরের জন্য struct tm-এ কোনটি ঠিক?

  • tm_year = 2026, tm_mon = 10, tm_mday = 16
  • tm_year = 126, tm_mon = 9, tm_mday = 16
  • tm_year = 126, tm_mon = 10, tm_mday = 16
  • tm_year = 2026, tm_mon = 9, tm_mday = 16

৩২-বিট signed time_t ব্যবহার করা একটি পুরোনো রাউটারের ঘড়ি কবে সমস্যায় পড়বে?

  • ১ জানুয়ারি ২০০০
  • ৭ ফেব্রুয়ারি ২১০৬
  • ১৯ জানুয়ারি ২০৩৮
  • কখনোই না, time_t অসীম

শব্দকোষ

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

মনে রাখুন

  1. Standard library = ISO C-এ লেখা তালিকা; libc (glibc, musl, MSVC CRT, newlib) তার বাস্তব রূপ। Header-এ ঘোষণা, library ফাইলে কোড — Linux-এ math-এর জন্য -lm.
  2. নিজে লেখার আগে header-এর মানচিত্র দেখুন: রূপান্তর ও sort stdlib.h, অক্ষর ctype.h, অঙ্ক math.h, তারিখ time.h, মাপ stdint.h.
  3. atoi নয়, strtol — end pointer আর errno দিয়ে তিনটি যাচাই। ctype-এ সবসময় (unsigned char).
  4. rand() % n পক্ষপাতদুষ্ট, seed একবারই; নিরাপত্তার কাজে শুধু OS-এর CSPRNG. দশমিক ফল int-এ নেওয়ার আগে round().
  5. struct tm-এ মাস 0 থেকে, বছর 1900 থেকে; strftime দিয়ে লেখা। ৩২-বিট time_t ভাঙবে ১৯ জানুয়ারি ২০৩৮-এ।

পরের পর্বে: এই পর্বের প্রতিটি কোড শুরু হয়েছে #include দিয়ে, আর আমরা দেখেছি INT_MAX, EOF, RAND_MAX-এর মতো নাম। এগুলো আসলে কী? পর্ব ২৭-এ preprocessor আর macro — compile-এর আগেই কোড বদলে দেওয়ার জগৎ।

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

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