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

ফাইল I/O — প্রোগ্রাম বন্ধ হলেও ডেটা যেন হারিয়ে না যায়

File I/O — Text, Binary, Random Access & Crash-Safe Saving

Taught in Bangla, this lesson covers file I/O in C: opening files with fopen and checking for NULL, buffering and fclose, text and CSV files, the while(!feof) bug, binary files with fread and fwrite, random access with fseek, and crash-safe saving.

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

  • ডেটা কেন RAM-এ টেকে না, আর persistence বলতে কী বোঝায়
  • fopen-এর প্রতিটি mode কী করে, ব্যর্থ হলে কীভাবে কারণ জানবেন, আর fclose কেন বাধ্যতামূলক
  • দোকানের খাতা CSV ফাইলে লেখা ও আবার পড়া — সঠিক পড়ার লুপসহ
  • Binary ফাইল, একই মাপের record আর fseek দিয়ে সরাসরি যেকোনো record পড়া
  • লোডশেডিংয়ের মাঝেও ফাইল নষ্ট না হওয়ার কৌশল, আর AI-এর লেখা ফাইল-কোড যাচাইয়ের চেকলিস্ট

১ · RAM সব ভুলে যায় — রহিম চাচার হারানো হিসাব

পর্ব ২৩-এ আমরা মিরপুরের রহিম চাচার মুদি দোকানের জন্য একটি হিসাবের খাতা বানিয়েছিলাম। প্রোগ্রামটি চমৎকার চলে — বিক্রি যোগ হয়, মোট হিসাব হয়, নগদ আর bKash আলাদা দেখায়। কিন্তু প্রথম সপ্তাহেই চাচা ফোন করলেন:

“বাবা, সন্ধ্যায় কারেন্ট গেল, ফিরে এসে দেখি সারাদিনের সব বিক্রি উধাও! খাতায় আবার হাতে লিখতে হলো।”

দোষটা প্রোগ্রামের নয় — আমরা জেনেশুনেই এই সীমাবদ্ধতা রেখেছিলাম। প্রোগ্রামের সব ভ্যারিয়েবল, সব array, malloc দিয়ে নেওয়া সব মেমরি থাকে RAM-এ। আর RAM হলো বিদ্যুৎনির্ভর মেমরি: বিদ্যুৎ থাকলে মনে রাখে, না থাকলে সব মুছে যায়। অর্থাৎ প্রোগ্রাম শেষ, ডেটাও শেষ।

দুই রকম মেমরি

Volatile RAM vs persistent storage

  1. RAMLedger lg;দ্রুত, কিন্তু বিদ্যুৎ গেলে সব মুছে যায়
  2. ফাইলে লেখাfprintf / fwriteএই পর্বের বিষয়
  3. Disk / SSDledger.csvধীর, কিন্তু বছরের পর বছর মনে রাখে
ছবি ২৪.১ — RAM হলো দোকানদারের মুখের হিসাব, ফাইল হলো লাল মলাটের খাতা।

রহিম চাচা ক্রেতার সাথে কথা বলতে বলতে মুখে মুখে যোগ করেন — “চাল বাহাত্তর, ডিম একশো চুয়াল্লিশ…” এটা দ্রুত, কিন্তু কেউ ডাক দিলেই হিসাব গুলিয়ে যায়। তাই দিনশেষে তিনি সব লাল খাতায় লিখে রাখেন; খাতা রাতভর দোকানে পড়ে থাকলেও কাল সকালে হিসাব ঠিকই পাওয়া যায়। RAM হলো মুখের হিসাব, আর ফাইল হলো সেই খাতা। ডেটাকে প্রোগ্রামের আয়ুর চেয়ে বেশি দিন টিকিয়ে রাখার এই গুণকে বলে persistence.

একটি file আসলে disk-এ রাখা একটি নাম-দেওয়া byte-এর সারি। ফাইলের নিজের কোনো “ধরন” নেই — .csv, .txt, .jpg শুধু নামের অংশ। ভেতরের byte-গুলোর অর্থ কী, সেটা ঠিক করে যে প্রোগ্রাম ফাইলটি লেখে ও পড়ে। এই কথাটি মনে রাখুন; পুরো পর্বের ভিত্তি এটাই।

মূল ধারণা

পর্ব ১০-এ দেখেছিলেন, printf আসলে stdout নামের একটি stream-এ লেখে। ফাইলও ঠিক একই রকম stream — শুধু অন্য মাথায় স্ক্রিনের বদলে disk-এর একটি ফাইল। তাই printf-এর যমজ ভাই fprintf; আর keyboard থেকে পড়ার জন্য চেনা fgets ফাইল থেকেও একইভাবে পড়ে। এসব ফাংশনের নামের শুরুতে f মানে “এই stream-এ”.

২ · ফাইল খোলা — FILE*, mode আর NULL যাচাই

ফাইল নিয়ে যেকোনো কাজ তিন ধাপে হয়: খোলা → পড়া বা লেখা → বন্ধ করা। খোলার কাজ করে fopen. সে দুটি জিনিস চায় — ফাইলের নাম আর কী উদ্দেশ্যে খুলছেন, যাকে বলে mode. বিনিময়ে দেয় একটি FILE pointer:

FILE *f = fopen("ledger.csv", "r");
if (f == NULL) {
    perror("ledger.csv");     /* prints e.g. "ledger.csv: No such file or directory" */
    return 1;
}
/* ... read from f ... */
fclose(f);

FILE একটি struct, যার ভেতরে C library হিসাব রাখে: OS-এর দেওয়া file descriptor নম্বর, buffer কোথায়, ফাইলের কোন জায়গায় এখন আছেন, শেষে পৌঁছেছেন কি না, কোনো ভুল হয়েছে কি না। ভেতরে উঁকি দেওয়ার দরকার নেই — শুধু pointer-টি প্রতিটি ফাংশনে দিয়ে দিন।

Mode — ফাইলটি দিয়ে কী করতে চান

Modeঅর্থফাইল না থাকলেফাইল থাকলে
"r"শুধু পড়াব্যর্থ — NULLশুরু থেকে পড়ে
"w"শুধু লেখানতুন ফাইল বানায়সাথে সাথে খালি করে দেয় — আগের সব লেখা মুছে যায়
"a"শেষে যোগ করা (append)নতুন ফাইল বানায়আগের লেখা থাকে; প্রতিটি লেখা শেষে গিয়ে বসে
"r+"পড়া ও লেখা দুটোইব্যর্থ — NULLখালি করে না; যেখানে লিখবেন সেখানকার পুরোনো byte বদলায়
"w+", "a+"পড়া ও লেখানতুন ফাইল বানায়w+ খালি করে; a+ রেখে দিয়ে শেষে যোগ করে
শেষে b — "rb", "wb", "r+b"binary modeLinux ও Mac-এ কোনো পার্থক্য নেই। Windows-এ টেক্সট mode-এ \n লেখার সময় \r\n হয়ে যায় (পড়ার সময় উল্টোটা); binary mode-এ byte যেমন আছে তেমনই থাকে

"w" হলো সবচেয়ে বিপজ্জনক mode. ধরুন চাচার খাতায় এক মাসের হিসাব আছে, আর আপনি ভুল করে "a"-এর বদলে "w" লিখলেন। ফাইল খোলার মুহূর্তেই সব মুছে যাবে — একটি byte লেখার আগেই। কোনো সতর্কবার্তা আসবে না, কোনো “Undo” নেই। ফাইল খোলার কোড পড়ার সময় mode-এর অক্ষরটিই প্রথমে দেখুন।

NULL — ফাইল খুলতে না পারার সংকেত

ফাইল খোলা ব্যর্থ হওয়া খুবই সাধারণ: নামের বানান ভুল, ফাইলটি অন্য ফোল্ডারে, অনুমতি নেই, বা অন্য প্রোগ্রাম ফাইলটি আটকে রেখেছে। তখন fopen NULL ফেরত দেয়। যাচাই না করে সেই NULL দিয়ে fprintf বা fgets ডাকলে প্রোগ্রাম সাধারণত Segmentation fault দিয়ে crash করে।

কেন ব্যর্থ হলো, সেটা জানার উপায় হলো errno. ফাইল খুলতে ব্যর্থ হলে বেশিরভাগ সিস্টেমে library errno নামের একটি global ভ্যারিয়েবলে কারণের একটি সংখ্যা বসিয়ে রাখে। সংখ্যাটি নিজে পড়ার দরকার নেই — perror সেটিকে মানুষের ভাষায় stderr-এ ছেপে দেয়, যেমন ledger.csv: No such file or directory বা Permission denied. Error সামলানোর পুরো কৌশল আসবে পর্ব ৩০-এ; আপাতত নিয়ম একটাই:

প্রতিটি fopen-এর পরের লাইনেই if (f == NULL).
ব্যতিক্রম নেই।

৩ · লেখা আসলে কোথায় যায় — buffer, fflush আর fclose

আপনি লিখলেন fprintf(f, "rice,2,7250\n"); — ফাংশনটি ফেরত এলো। লেখাটি কি এখন disk-এ? সম্ভবত না। পর্ব ১০-এ শিখেছিলেন, stdio প্রতিটি অক্ষর সাথে সাথে পাঠায় না; একটি buffer-এ জমিয়ে একবারে পাঠায়, কারণ OS-কে প্রতিবার ডাকা (system call) বেশ খরচের কাজ। ফাইলের ক্ষেত্রে এই জমানো আরও বেশি: সাধারণত buffer কয়েক KB ভরলে, অথবা আপনি নিজে বললে, তবেই লেখা নড়ে।

আর OS-এর কাছে পৌঁছালেও কাজ শেষ নয় — OS নিজেও লেখাটি কিছুক্ষণ নিজের মেমরিতে রেখে পরে disk-এ পাঠায়। নিচের ডায়াগ্রামে একটি বিক্রির হিসাবের পুরো যাত্রা দেখুন, আর প্রতিটি ধাপে খেয়াল করুন: ঠিক এই মুহূর্তে প্রোগ্রাম crash করলে বা বিদ্যুৎ গেলে কী থাকবে?

  1. ভ্যারিয়েবলtotalপ্রোগ্রামের মেমরি
  2. stdio bufferfprintfএখনো প্রোগ্রামের ভেতরে
  3. OS-এর মেমরিfflushপ্রোগ্রামের বাইরে, তবু RAM-এ
  4. Diskfsyncবিদ্যুৎ ছাড়াও টেকে
এই মুহূর্তে বিপদ এলে
লেখাটি এখন কোথায়
প্রোগ্রাম crash করলে
বিদ্যুৎ চলে গেলে
  1. ধাপ ১ — শুধু ভ্যারিয়েবলে। বিক্রির হিসাব total-এ আছে, অর্থাৎ শুধু RAM-এ। প্রোগ্রাম বন্ধ হলে বা বিদ্যুৎ গেলে — দুই ক্ষেত্রেই শেষ। পর্ব ২৩-এর খাতা সবসময় এই অবস্থায় ছিল।
  2. ধাপ ২ — fprintf ফেরত এলো। লেখাটি format হয়ে stdio-র buffer-এ কপি হয়েছে। কিন্তু buffer তো আপনার প্রোগ্রামেরই মেমরি! এখন প্রোগ্রাম crash করলে (Segmentation fault, বা Task Manager থেকে বন্ধ করা) buffer-ও সাথে মুছে যাবে। fprintf সফল হওয়া মানে “disk-এ লেখা হয়েছে” নয়।
  3. ধাপ ৩ — flush. Buffer ভরে গেলে, অথবা আপনি fflush(f) বা fclose(f) ডাকলে, stdio একটি system call দিয়ে লেখাটি OS-এর হাতে দেয়। এখন প্রোগ্রাম crash করলেও ক্ষতি নেই — OS-এর কাছে আছে, সে পরে লিখে দেবে। কিন্তু OS-ও এটি কিছুক্ষণ RAM-এ রাখে, তাই ঠিক এই মুহূর্তে বিদ্যুৎ গেলে হারাতে পারে।
  4. ধাপ ৪ — disk-এ পৌঁছানো। OS সাধারণত কয়েক সেকেন্ডের মধ্যে নিজে থেকেই disk-এ লেখে। তাড়াতাড়ি নিশ্চিত হতে চাইলে Linux/Mac-এ fsync ডেকে বলতে হয় “এখনই disk-এ লেখো, লেখা শেষ হলে তবেই ফেরত এসো”. এটি standard C-র অংশ নয়, OS-এর ফাংশন; Windows-এ একই কাজের আলাদা ফাংশন আছে। এখন বিদ্যুৎ গেলেও লেখা টিকবে।
  5. ধাপ ৫ — fclose. fclose buffer-এ বাকি থাকা লেখা OS-এ পাঠায়, তারপর ফাইলটি ছেড়ে দেয়। মনে রাখুন, fclose নিজে fsync করে না — সে লেখাটি ধাপ ৩ পর্যন্ত পৌঁছে দেয়। গুরুত্বপূর্ণ ডেটার জন্য অংশ ৮-এর পুরো কৌশল লাগবে।

fclose ভুলে গেলে আসলে কী হয়?

একটি সূক্ষ্ম সত্য জেনে রাখুন: main থেকে স্বাভাবিকভাবে return করলে বা exit() ডাকলে C library নিজেই সব খোলা stream flush করে বন্ধ করে দেয়। তাই ছোট পরীক্ষামূলক প্রোগ্রামে fclose ভুলেও অনেক সময় কিছু হয় না — আর এজন্যই ভুলটা ধরা পড়ে না। কিন্তু বাস্তবে সমস্যা হয় তিন জায়গায়:

  • প্রোগ্রাম crash করলে: স্বাভাবিক বিদায় নেই, তাই flush-ও নেই। Buffer-এ থাকা শেষ কয়েক KB হিসাব হারিয়ে যায়।
  • একই ফাইল আবার পড়লে: লিখে বন্ধ না করেই একই প্রোগ্রামে ফাইলটি আবার খুলে পড়লে শেষের লেখাগুলো পাওয়া না-ও যেতে পারে — সেগুলো তখনও buffer-এ।
  • দীর্ঘ সময় চলা প্রোগ্রামে: OS একটি প্রোগ্রামকে একসাথে সীমিত সংখ্যক ফাইল খোলা রাখতে দেয়। দোকানের প্রোগ্রাম সারাদিন চলে আর প্রতিটি বিক্রিতে একটি ফাইল খুলে বন্ধ না করলে একসময় fopen ব্যর্থ হতে শুরু করবে — Too many open files.

fclose-ও একটি ফেরত মান দেয়: সফল হলে 0, ব্যর্থ হলে EOF. লেখার জন্য খোলা ফাইলে এটি যাচাই করা জরুরি, কারণ disk ভরা থাকলে সমস্যাটি প্রায়ই ধরা পড়ে শেষ flush-এর সময় — অর্থাৎ fclose-এর ভেতরে। fprintf সব সফল দেখিয়েও শেষে fclose ব্যর্থ হতে পারে।

৪ · টেক্সট ফাইল আর CSV — খাতাকে ফাইলে লেখা

প্রথম সিদ্ধান্ত: খাতাটি ফাইলে কোন চেহারায় থাকবে? সবচেয়ে সহজ উত্তর text file — যে ফাইলে শুধু পড়া যায় এমন অক্ষর আর লাইন-ভাঙা থাকে, Notepad-এ খুললেই মানুষ পড়তে পারে। আর টেবিলের মতো ডেটা টেক্সটে রাখার সবচেয়ে প্রচলিত নিয়ম CSV (Comma-Separated Values): প্রতিটি লাইনে একটি record, ঘরগুলো কমা দিয়ে আলাদা।

rice,2,7250
egg,12,1200
tea,3,1000

প্রতিটি লাইন একটি বিক্রি: পণ্যের নাম, কয়টি, আর একটির দাম পয়সায় (পর্ব ২৩-এর সিদ্ধান্ত মেনে — ৭২.৫০ টাকা = 7250 পয়সা)। এই ফাইল Excel-এ সরাসরি খোলে। টেক্সট লেখার ও পড়ার ফাংশনগুলো আপনার চেনা:

কাজফাংশনফেরত মান যা বলে
format করে লেখাfprintf(f, "%s,%d\n", ...)কয়টি অক্ষর লেখা হলো; ভুলে ঋণাত্মক
একটি স্ট্রিং হুবহু লেখাfputs(s, f)ভুলে EOF
একটি পুরো লাইন পড়াfgets(buf, size, f)সফল হলে buf; কিছু পড়া না গেলে NULL
format মিলিয়ে পড়াfscanf(f, "%d", &x)কয়টি মান পড়া গেল; কোনো মান পড়ার আগেই ফাইল শেষ হলে EOF

পর্ব ১০-এর পেশাদার নিয়মটি এখানেও খাটে: আগে fgets দিয়ে পুরো লাইন, তারপর sscanf দিয়ে ভাঙা ও যাচাই। ফাইলের একটি লাইন নষ্ট থাকলে fscanf মাঝপথে আটকে গিয়ে বাকি সব লাইনের হিসাব গুলিয়ে দিতে পারে; লাইন ধরে পড়লে নষ্ট লাইনটি বাদ দিয়ে পরেরটিতে যাওয়া যায়। নিচের প্রোগ্রাম প্রথমে তিনটি বিক্রি ফাইলে লেখে, বন্ধ করে, তারপর আবার খুলে পড়ে মোট বের করে। চালিয়ে দেখুন:

#include <stdio.h>

int main(void) {
    FILE *f = fopen("sales.csv", "w");
    if (f == NULL) { perror("sales.csv"); return 1; }
    fputs("rice,2,7250\negg,12,1200\nbad line\ntea,3,1000\n", f);
    if (fclose(f) != 0) { perror("close"); return 1; }

    f = fopen("sales.csv", "r");
    if (f == NULL) { perror("sales.csv"); return 1; }
    char line[100], item[32];
    int qty; long price, total = 0;
    while (fgets(line, sizeof line, f) != NULL) {
        if (sscanf(line, "%31[^,],%d,%ld", item, &qty, &price) != 3) continue;
        printf("%-5s x%-3d = %6ld paisa\n", item, qty, qty * price);
        total += qty * price;
    }
    fclose(f);
    printf("Total: %ld.%02ld Tk\n", total / 100, total % 100);
    return 0;
}

প্রথম অংশ "w" mode-এ ফাইল বানিয়ে চারটি লাইন লেখে — ইচ্ছা করে একটি নষ্ট লাইন (bad line) রাখা হয়েছে। fclose-এর ফল যাচাই করা হয়েছে, কারণ শেষ flush সেখানেই হয়। দ্বিতীয় অংশে fgets একটি করে লাইন আনে। %31[^,] মানে “কমা না আসা পর্যন্ত সর্বোচ্চ ৩১টি অক্ষর পড়ো” — item-এর ৩২ ঘরের সীমা পার হবে না। sscanf ৩টি মান না পেলে লাইনটি বাদ যায়, তাই নষ্ট লাইনে প্রোগ্রাম আটকায় না। মোট আসে 319.00 Tk — হাতে মিলিয়ে দেখুন: ১৪৫ + ১৪৪ + ৩০ = ৩১৯ টাকা।

CSV-এর ফাঁদ — নামের ভেতরেই যদি কমা থাকে

CSV দেখতে সহজ, কিন্তু সরলতার একটি দাম আছে। চাচা নতুন পণ্য তুললেন: Biscuit, Family Pack. ফাইলে লাইনটি হবে Biscuit, Family Pack,1,15000 — এখন কমা তিনটি, ঘর চারটি! আমাদের sscanf নাম হিসেবে পাবে শুধু Biscuit, তারপর Family Pack-কে সংখ্যা হিসেবে পড়তে গিয়ে ব্যর্থ হবে, আর পুরো বিক্রিটি নীরবে বাদ পড়বে। হিসাবে টাকা কম দেখাবে, কোনো error আসবে না।

সমাধান ১ — নিয়ম সীমিত করা

নাম লেখার সময়ই কমা নিষেধ করুন, বা কমা দেখলে অন্য চিহ্নে বদলে দিন। ছোট, নিজের নিয়ন্ত্রণে থাকা প্রজেক্টে এটাই সবচেয়ে সহজ ও নিরাপদ।

রহিম চাচার খাতার জন্য যথেষ্ট

সমাধান ২ — উদ্ধৃতিচিহ্ন

প্রচলিত CSV নিয়মে কমাওয়ালা ঘর "..."-এর ভেতরে রাখা হয়: "Biscuit, Family Pack",1,15000. কিন্তু তখন নামের ভেতরের "-কে দুবার লিখতে হয়, লাইন-ভাঙাও ঘরে ঢুকতে পারে — সঠিক parser লেখা আর সহজ থাকে না।

বড় প্রজেক্টে তৈরি library ব্যবহার করুন

আরেকটি সাধারণ ফাঁদ: Windows-এ তৈরি CSV-র লাইনের শেষে \r\n থাকে, তাই Linux-এ পড়লে শেষ ঘরে একটি অদৃশ্য \r লেগে থাকতে পারে।

৫ · while (!feof(f)) — শেষ লাইন দুবার পড়ার বিখ্যাত বাগ

ইন্টারনেটে, পুরোনো বইয়ে, এমনকি AI-এর উত্তরেও এই লুপটি প্রায়ই দেখবেন:

while (!feof(f)) {          /* "যতক্ষণ ফাইল শেষ না হয়" */
    fgets(line, sizeof line, f);
    printf("%s", line);
}

পড়তে একদম যুক্তিসঙ্গত লাগে। কিন্তু চালালে শেষ লাইনটি দুবার ছাপা হয়। কারণ লুকিয়ে আছে feof-এর আসল অর্থে: সে বলে না “সামনে আর কিছু আছে কি না” — সে বলে “আগের একটি পড়ার চেষ্টা কি ফাইলের শেষে ধাক্কা খেয়েছে?” শেষ লাইন পড়ার পরেও কোনো পড়া এখনো ব্যর্থ হয়নি, তাই EOF চিহ্নটি তখনো বসেনি। নিচে names.txt ফাইলে দুটি লাইন — rice আর egg. ধাপে ধাপে দেখুন:

1char line[32] = "?";2while (!feof(f)) {3    fgets(line, sizeof line, f);4    printf("%s", line);5}
ভ্যারিয়েবলের মান (Watch)
line
fgets ফেরত দিল
feof(f)
  1. শুরু। এখনো কিছু পড়া হয়নি, তাই EOF চিহ্ন নেই — feof দেয় 0. লুপের ভেতরে ঢুকছে।
  2. fgets প্রথম লাইন rice\n পড়ল। সফল, তাই ফেরত দিল line-এর ঠিকানা।
  3. ছাপা হলো rice. এ পর্যন্ত সব ঠিক।
  4. আবার শর্ত যাচাই। কোনো পড়া এখনো ব্যর্থ হয়নি, তাই feof = 0 — ভেতরে।
  5. দ্বিতীয় লাইন egg\n পড়া হলো। খেয়াল করুন: ফাইলে আর কিছুই বাকি নেই, কিন্তু fgets লাইন-ভাঙা পেয়েই থেমেছে, শেষের ওপারে উঁকি দেয়নি। তাই EOF চিহ্ন এখনো বসেনি।
  6. ছাপা হলো egg. ফাইলের সব লাইন শেষ — লুপেরও এখানেই থামার কথা।
  7. এখানেই গোলমাল। শর্ত যাচাই: feof এখনো 0, কারণ কোনো পড়া ব্যর্থ হয়নি। লুপ আরেকবার ভেতরে ঢুকল।
  8. fgets পড়তে গিয়ে কিছুই পেল না — ফেরত দিল NULL, আর এবার EOF চিহ্ন বসল। কিন্তু line-এ কিছু লেখা হয়নি, তাই সেখানে আগের egg\n-ই রয়ে গেছে। ফেরত মান কেউ দেখল না!
  9. printf সেই পুরোনো line ছাপল — egg দুবার. খাতায় এর মানে শেষ বিক্রি দুবার যোগ হওয়া।
  10. এবার feof = 1, লুপ শেষ। কিন্তু ক্ষতি যা হওয়ার হয়ে গেছে। ফাইল খালি হলে অবস্থা আরও খারাপ: প্রথম পাকেই line-এর আবর্জনা মান ছাপা হতো।

সমাধানটি ছোট, আর এটাই নিয়ম: পড়ার ফাংশনকেই লুপের শর্ত বানান। পড়া সফল হলে তবেই ভেতরের কাজ হবে।

while (fgets(line, sizeof line, f) != NULL) {   /* লাইন পেলে তবেই ভেতরে */
    printf("%s", line);
}
if (ferror(f)) perror("read");                   /* শেষ, নাকি disk-এ ভুল? */

তাহলে feof-এর কাজ কী? লুপ শেষ হওয়ার পরে কারণ জানা। পড়া দুই কারণে থামতে পারে — ফাইল শেষ (feof সত্য), অথবা পড়তে গিয়ে কোনো ত্রুটি (ferror সত্য), যেমন pen drive হঠাৎ খুলে ফেলা। আগে পড়ুন, ফেরত মান দেখুন, তারপর দরকার হলে জিজ্ঞেস করুন কেন থামল। fscanf বা fread-এর ক্ষেত্রেও একই নিয়ম: লুপের শর্ত হবে তাদের ফেরত মান।

৬ · Binary ফাইল — struct সরাসরি disk-এ

টেক্সটে লিখতে হলে প্রতিটি সংখ্যাকে অক্ষরে বদলাতে হয় (7250 → '7','2','5','0'), পড়ার সময় আবার অক্ষর থেকে সংখ্যায়। লাখ লাখ record হলে এই অনুবাদেই সময় যায়। বিকল্প হলো binary file: মেমরিতে struct-টি যে byte-গুলো দিয়ে তৈরি, হুবহু সেগুলোই ফাইলে ঢেলে দেওয়া। এর জন্য দুটি ফাংশন:

size_t fwrite(const void *ptr, size_t size, size_t count, FILE *f);
size_t fread (void *ptr,       size_t size, size_t count, FILE *f);
/* both return how many items (not bytes) were fully written/read */

“এই ঠিকানা থেকে size মাপের countটি জিনিস লেখো বা পড়ো।” fwrite(sales, sizeof(Sale), 100, f) একটি ডাকেই ১০০টি বিক্রি লিখে ফেলে — কোনো format string নেই, কোনো অনুবাদ নেই। দ্রুত, আর কোডও ছোট। তাহলে সবসময় binary নয় কেন? পর্ব ০৫ আর ২২-এর তিনটি শিক্ষা এখানে একসাথে ফিরে আসে:

  • Padding: compiler struct-এর member-দের মাঝে ফাঁকা byte বসায়। সেই ফাঁকা byte-গুলোও ফাইলে যায়, আর কোন compiler কতটা ফাঁক রাখবে তা নির্দিষ্ট নয়।
  • টাইপের মাপ: long ৬৪-বিট Linux ও Mac-এ ৮ byte, কিন্তু ৬৪-বিট Windows-এ ৪ byte. Linux-এ লেখা ফাইল Windows-এর প্রোগ্রাম পড়লে প্রতিটি record-এর মাপই আলাদা ধরবে।
  • Endianness: একটি বহু-byte সংখ্যার byte-গুলো কোন ক্রমে বসবে, তা CPU-ভেদে আলাদা। আজকের বেশিরভাগ কম্পিউটার ও ফোন little-endian, কিন্তু নেটওয়ার্ক প্রোটোকল আর কিছু পুরোনো বা বিশেষ যন্ত্র big-endian.

অর্থাৎ raw struct-এর binary ফাইল কার্যত একটি নির্দিষ্ট compiler, OS আর CPU-র “ব্যক্তিগত ডায়েরি”. একই মেশিনে একই প্রোগ্রামের জন্য ঠিক আছে; অন্য কোথাও পাঠালে বা প্রোগ্রামের struct বদলালে পুরোনো ফাইল ভুল পড়া হবে — কোনো error ছাড়াই। পর্ব ২২-এ দেখা serialization — প্রতিটি member নির্দিষ্ট মাপে, নির্দিষ্ট byte-ক্রমে আলাদা করে লেখা — এর পেশাদার সমাধান।

প্রশ্নটেক্সট (CSV)Raw binary (struct)
মানুষ পড়তে পারে?হ্যাঁ, Notepad-এইনা — হিজিবিজি দেখায়
অন্য মেশিন বা ভাষায় চলে?হ্যাঁনিশ্চিত নয় — padding, মাপ, endianness
গতি ও জায়গাধীর, বেশি জায়গাদ্রুত, সাধারণত কম জায়গা
সরাসরি n-তম record-এ যাওয়াকঠিন — লাইনের দৈর্ঘ্য আলাদাসহজ — সব record একই মাপের
একটি byte নষ্ট হলেসাধারণত শুধু সেই লাইনবাকি সব record-এর হিসাব সরে যেতে পারে
কখন বেছে নেবেনছোট-মাঝারি ডেটা, বিনিময়, debugবড় ডেটা, একই প্রোগ্রামের নিজস্ব ফাইল, গতি জরুরি

রহিম চাচার দিনে কয়েকশো বিক্রির জন্য CSV-ই ভালো — গতির পার্থক্য চোখেও পড়বে না। তবে binary-র একটি বিশেষ ক্ষমতা পরের অংশে কাজে লাগবে।

৭ · Random access — সরাসরি যেকোনো record-এ লাফ

এতক্ষণ আমরা ফাইল পড়েছি শুরু থেকে শেষ পর্যন্ত, একটির পর একটি — যেন ক্যাসেট টেপ। কিন্তু চাচা যদি বলেন “৫০০০ নম্বর বিক্রিটা দেখাও”? প্রথম ৪৯৯৯টি পড়ে ফেলে দিতে হবে? Random access মানে ঠিক উল্টো: ফাইলের যেকোনো জায়গায় সরাসরি চলে যাওয়া, যেমন মেট্রো রেলে আপনি সরাসরি কারওয়ান বাজারে নামেন, মাঝের প্রতিটি স্টেশনে হাঁটতে হয় না।

শর্ত একটাই: সব record একই মাপের হতে হবে। তাহলে হিসাব সোজা — n নম্বর record (০ থেকে গুনে) শুরু হয় ঠিক n × sizeof(record) byte পরে। CSV-তে এটা সম্ভব নয়, কারণ tea,3,1000 আর Biscuit Family Pack,1,15000 লাইনের দৈর্ঘ্য আলাদা। এই লাফানোর জন্য তিনটি ফাংশন:

ফাংশনকাজ
fseek(f, offset, SEEK_SET)ফাইলের শুরু থেকে offset byte দূরে যাও। SEEK_CUR দিলে এখনকার জায়গা থেকে, SEEK_END দিলে শেষ থেকে। সফল হলে 0 দেয়।
ftell(f)এখন শুরু থেকে কত byte দূরে আছি, তা একটি long সংখ্যা হিসেবে জানায়।
rewind(f)একদম শুরুতে ফিরে যাও, আর আগের ত্রুটি বা EOF চিহ্ন মুছে দাও।

ধরুন sales.bin ফাইলে ৪টি বিক্রি আছে, প্রতিটি Sale struct sizeof অনুযায়ী ৩২ byte (৬৪-বিট Linux-এ: নামের ১৬, int-এর ৪, padding ৪, long-এর ৮)। আমরা চাই record নম্বর ৩ — অর্থাৎ চতুর্থটি:

record 0byte 0–31
record 1byte 32–63
record 2byte 64–95
record 3byte 96–127
হিসাব (৬৪-বিট Linux, sizeof(Sale) = 32)
যে কাজটি হলো
ফাইলের অবস্থান (ftell)
পড়া record (r.item)
disk থেকে পড়া byte
  1. ফাইল খোলা হলো। FILE-এর ভেতরে একটি “এখন কোথায় আছি” হিসাব থাকে — file position. শুরুতে এটি 0, অর্থাৎ প্রথম byte-এর ঠিক আগে। প্রতিটি পড়া বা লেখা এই অবস্থানকে সামনে ঠেলে দেয়।
  2. অবস্থান হিসাব। record ৩ শুরু হয় ৩ × ৩২ = ৯৬ byte পরে। এখনো disk-এ কিছুই পড়া হয়নি — এটা শুধু গুণ। array-তে a[3] খুঁজতে যেমন ঠিকানা = শুরু + ৩ × মাপ (পর্ব ১৬), এখানেও ঠিক তাই — ফাইলটি যেন disk-এ রাখা একটি array.
  3. fseek দিয়ে লাফ। অবস্থান সরাসরি 96-এ বসল। প্রথম তিনটি record পড়া হলো না, স্পর্শও হলো না। ফাইলে ৪টির বদলে ৪০ লক্ষ record থাকলেও এই লাফে প্রায় একই সময় লাগে।
  4. fread. ঠিক ৩২ byte পড়ে r-এ বসল: salt. পড়ার পর অবস্থান নিজে থেকেই ১২৮-এ সরে গেছে — পরের fread দিলে record ৪ পড়ার চেষ্টা করবে — এখানে সেটি নেই, তাই ফেরত মান হবে শূন্য।
  5. বোনাস — record গোনা। SEEK_END দিয়ে শেষে গিয়ে ftell = ১২৮, তাই record সংখ্যা = ১২৮ ÷ ৩২ = ৪. পুরো ফাইল না পড়েই জানা গেল কয়টি বিক্রি আছে। একই কৌশলে "r+b" mode-এ শুধু একটি record-এর ওপর নতুন করে লিখে সেটি বদলানো যায় — বাকি ফাইল ছুঁতে হয় না।

এবার নিজে চালিয়ে দেখুন। প্রোগ্রামটি ৪টি বিক্রি binary ফাইলে লেখে, তারপর সরাসরি record ২-এ লাফিয়ে সেটি পড়ে:

#include <stdio.h>

typedef struct { char item[16]; int qty; long price; } Sale;

int main(void) {
    Sale s[4] = {{"rice", 2, 7250}, {"egg", 12, 1200}, {"tea", 3, 1000}, {"salt", 1, 4200}};
    FILE *f = fopen("sales.bin", "w+b");
    if (f == NULL) { perror("sales.bin"); return 1; }
    if (fwrite(s, sizeof(Sale), 4, f) != 4) { perror("write"); return 1; }
    printf("record size = %zu, file size = %ld\n", sizeof(Sale), ftell(f));

    long n = 2;                                   /* third record, counting from 0 */
    Sale r;
    if (fseek(f, n * (long)sizeof(Sale), SEEK_SET) == 0 && fread(&r, sizeof(Sale), 1, f) == 1)
        printf("record %ld at byte %ld: %s x%d\n", n, n * (long)sizeof(Sale), r.item, r.qty);
    fclose(f);
    return 0;
}

"w+b" মানে নতুন ফাইল বানাও, লেখা ও পড়া দুটোই চলবে, binary mode-এ। fwrite একটি ডাকেই পুরো array লিখল; ফেরত মান ৪ কি না যাচাই করা হলো। তখন ftell জানায় ফাইলের মাপ — সাধারণ ৬৪-বিট Linux-এ record size = 32, file size = 128. তারপর fseek দিয়ে ২ × ৩২ = ৬৪ byte-এ লাফ, আর fread পড়ে আনে tea x3. লক্ষ করুন, একই ফাইলে লেখার পরে পড়ার আগে মাঝে fseek (বা fflush) থাকা C-র নিয়মেই বাধ্যতামূলক — এখানে সেটি স্বাভাবিকভাবেই আছে। Windows-এ long ৪ byte বলে মাপ ২৪ দেখাতে পারে — binary ফাইল যে মেশিননির্ভর, তার হাতে-কলমে প্রমাণ।

৮ · লোডশেডিংয়েও খাতা অক্ষত — নিরাপদে save করার কৌশল

এবার সবচেয়ে বাস্তব প্রশ্ন। চাচার প্রোগ্রাম প্রতি রাতে পুরো খাতা ledger.csv-তে save করে। সহজ কোডটি হলো fopen("ledger.csv", "w"), সব লাইন লেখা, fclose. এক রাতে ঠিক save-এর মাঝখানে কারেন্ট চলে গেল। পরদিন কী পাওয়া যাবে?

উত্তর ভয়ংকর: হয়তো একটি খালি ফাইল, হয়তো অর্ধেক লেখা ফাইল যার শেষ লাইনটি মাঝপথে কাটা। নতুন হিসাব পুরো লেখা হয়নি, আর পুরোনো হিসাব "w" খোলার মুহূর্তেই মুছে গেছে। দুটোই হারাল। একে বলে file corruption, আর এর মূল কারণ একটাই: একমাত্র কপিটির ওপরেই লেখা হচ্ছিল।

পেশাদার সমাধানটি কয়েক লাইনের, কিন্তু প্রায় সব ভালো সফটওয়্যার — text editor থেকে ডেটাবেস — কোনো না কোনো রূপে এটি ব্যবহার করে:

  1. নতুন হিসাব লিখুন একটি আলাদা temp file-এ, যেমন ledger.csv.tmp. আসল ফাইল ছোঁবেন না।
  2. সব লেখা শেষে fflush, তারপর (Linux/Mac-এ) fsync, তারপর fclose — এবং প্রতিটির ফল যাচাই করুন। কোথাও ব্যর্থ হলে temp ফাইল মুছে থেমে যান; আসল ফাইল অক্ষত থাকে।
  3. সব সফল হলে rename("ledger.csv.tmp", "ledger.csv"). এক ধাক্কায় নতুন ফাইলটি পুরোনোটির জায়গা নেয়।

কৌশলটির প্রাণ শেষ ধাপে। একই disk-এর (একই file system-এর) মধ্যে rename ডেটা কপি করে না, শুধু ফোল্ডারের নামের তালিকায় নামটি অন্য ফাইলের দিকে ঘুরিয়ে দেয়। POSIX (Linux, Mac) নিয়মে এই বদল atomic — বাইরে থেকে কেউ কখনো “অর্ধেক বদলানো” অবস্থা দেখবে না: হয় পুরোনো ফাইল, নয় নতুন ফাইল। এবার বিদ্যুৎ বিভিন্ন মুহূর্তে চলে গেলে কী থাকে, দেখুন:

disk-এ এখন যা আছে
ledger.csv
ledger.csv.tmp
এখন বিদ্যুৎ গেলে
সরাসরি লেখা — বিপজ্জনক পথএকমাত্র কপির ওপর লেখা
f = fopen("ledger.csv", "w");   /* old data erased right here */
for (...) fprintf(f, ...);         /* power cut somewhere here?  */
fclose(f);
temp + rename — নিরাপদ পথআসল ফাইল শেষ মুহূর্ত পর্যন্ত অক্ষত
f = fopen("ledger.csv.tmp", "w");
for (...) fprintf(f, ...);
fflush(f);  /* + fsync on Linux/Mac */
fclose(f);                          /* check every result! */
rename("ledger.csv.tmp", "ledger.csv");
  1. শুরুর অবস্থা। disk-এ কালকের পূর্ণ খাতা আছে, ৪০টি বিক্রি। আজ ৫৫টি বিক্রি হয়েছে, সেগুলো save করতে হবে। এখন বিদ্যুৎ গেলে আজকের হিসাব হারাবে ঠিকই, কিন্তু কালকেরটা নিরাপদ।
  2. বিপজ্জনক পথ — "w" দিয়ে খোলা মাত্র। একটি লাইন লেখার আগেই ফাইল খালি হয়ে গেছে। ঠিক এই মুহূর্তে বিদ্যুৎ গেলে কালকের ৪০টি বিক্রিও শেষ — পরদিন ০ byte-এর একটি ফাইল।
  3. বিপজ্জনক পথ — অর্ধেক লেখা। ২০টি লাইন disk-এ গেছে, শেষ লাইনটি মাঝপথে কাটা: tea,3,10. পরদিন প্রোগ্রাম এই ফাইল পড়ে চুপচাপ ভুল হিসাব দেখাবে — খালি ফাইলের চেয়েও খারাপ, কারণ ভুলটা চোখে পড়ে না।
  4. নিরাপদ পথ — temp ফাইলে লেখা চলছে। আসল ledger.csv কেউ ছোঁয়নি। এখন বিদ্যুৎ গেলে temp ফাইলটি অসম্পূর্ণ থাকবে, কিন্তু কালকের খাতা পুরোপুরি অক্ষত।
  5. নিরাপদ পথ — temp ফাইল পূর্ণ ও disk-এ পাকা। flush, fsync আর close সফল। এখন disk-এ দুটি পূর্ণ সংস্করণ — পুরোনো আর নতুন। বিদ্যুৎ গেলে কালকেরটা তো আছেই, আজকেরটাও temp নামে আছে।
  6. rename — এক ধাক্কায় বদল। নামটি নতুন ফাইলের দিকে ঘুরে গেল, পুরোনো ফাইল সরে গেল। atomic বলে মাঝামাঝি কোনো অবস্থা নেই: বিদ্যুৎ rename-এর আগে গেলে কালকেরটা, পরে গেলে আজকেরটা — কোনো অবস্থাতেই নষ্ট ফাইল নয়।

Windows নিয়ে সতর্কতা: C standard বলে না যে নতুন নামে আগে থেকে ফাইল থাকলে rename কী করবে। Linux ও Mac-এ সেটি পুরোনো ফাইলকে atomic-ভাবে প্রতিস্থাপন করে। কিন্তু Windows-এর C library-তে সাধারণত গন্তব্য ফাইল থাকলে rename ব্যর্থ হয়; তখন Windows-এর নিজস্ব ফাংশন (যেমন MoveFileEx বা ReplaceFile) ব্যবহার করতে হয়, আর তাদের নিশ্চয়তাও সব পরিস্থিতিতে Linux-এর মতো নয়। আগে পুরোনোটা মুছে তারপর rename করলে ঠিক সেই ফাঁকটুকু তৈরি হয়, যেটা এড়াতেই এত আয়োজন। Windows-এর জন্য কোড লিখলে সেই OS-এর নিয়ম আলাদা করে পড়ে নিন।

আর temp ফাইলটি অবশ্যই একই ফোল্ডারে রাখুন। অন্য disk বা অন্য partition-এ থাকলে rename হয় ব্যর্থ হয়, নয়তো (কিছু টুলে) ভেতরে ভেতরে কপি করে — তখন আর atomic থাকে না।

১৯৬৯-৭০ সাল থেকে Bell Labs-এ Ken Thompson ও Dennis Ritchie Unix নকশা করার সময় সিদ্ধান্ত নেন: ফাইল হবে শুধু byte-এর একটি সারি, ভেতরের গঠন ঠিক করবে প্রোগ্রাম, OS নয়। এই সরলতাই পরে C-র stdio হয়ে প্রায় সব ভাষার ফাইল-I/O-র আদর্শ হয়েছে — Python-এর open()-এর "r", "w", "a" mode সরাসরি এখান থেকেই এসেছে।

৯ · AI-এর যুগে — ফাইল-কোডে AI যেসব ভুল করে

AI-কে “ফাইলে ডেটা save করার কোড দাও” বললে চলনসই কোড পাবেন। কিন্তু ফাইল-কোডের ভুল সাধারণ পরীক্ষায় ধরা পড়ে না — আপনার কম্পিউটারে ফাইল সবসময় থাকে, disk ভরা থাকে না, কারেন্টও যায় না। ভুলগুলো দেখা দেয় চাচার দোকানে, তিন মাস পরে, এক ঝড়ের রাতে।

AI-এর ভুলযা ঘটবেঠিক উপায়
fopen-এর পরে NULL যাচাই নেইফাইল না থাকলে বা অনুমতি না থাকলে crashপ্রতিটি fopen-এর পরেই if (f == NULL) { perror(...); ... }
while (!feof(f)) লুপশেষ record দুবার; খালি ফাইলে আবর্জনাপড়ার ফাংশনের ফেরত মানই লুপের শর্ত
fclose নেই, বা কোনো error-পথে বাদ পড়েছেশেষ লেখা হারায়; দীর্ঘ চললে “Too many open files”প্রতিটি return-পথে বন্ধ করা; লেখার ফাইলে fclose-এর ফল যাচাই
“save” মানে struct সরাসরি fwritestruct বদলালে বা অন্য মেশিনে পুরোনো ফাইল ভুল পড়া হয়, কোনো error ছাড়াইটেক্সট (CSV/JSON) বা নিয়ম মেনে serialization; ফাইলের শুরুতে version নম্বর
আসল ফাইল "w" দিয়ে খুলে ওপরে লেখাsave-এর মাঝে crash বা বিদ্যুৎ গেলে খালি বা নষ্ট ফাইলtemp ফাইল → flush/fsync → close → rename
fscanf(f, "%s", name) — সীমা ছাড়ালম্বা নাম buffer উপচে পড়ে (buffer overflow)fgets + sscanf আর %31[^,]-এর মতো সীমা

AI-কে কাজ দেওয়ার সময় এই প্রত্যাশাগুলো শুরুতেই লিখে দিন। নির্দিষ্ট করে না বললে AI সাধারণত “সুখী পথের” (happy path) কোড দেয় — যেখানে কিছুই ভুল হয় না:

আমার C প্রোগ্রামে একটি দোকানের বিক্রির তালিকা আছে: typedef struct { char item[32]; int qty; long price; } Sale; (price পয়সায়)। দুটি ফাংশন লিখে দাও: int ledger_save(const char *path, const Sale *s, size_t n); int ledger_load(const char *path, Sale *s, size_t max, size_t *n); শর্ত: ১. ফাইল ফরম্যাট CSV: item,qty,price — item-এ কমা থাকলে save করতে অস্বীকার করবে। ২. প্রতিটি fopen, fprintf, fclose-এর ফল যাচাই করবে; ব্যর্থ হলে perror দিয়ে -1 ফেরত দেবে। ৩. save হবে নিরাপদে: আগে path.tmp-এ লেখা, fflush, fclose, তারপর rename করবে। ব্যর্থ হলে tmp মুছে দেবে, আসল ফাইল ছোঁবে না। ৪. load-এ while(!feof) চলবে না — fgets + sscanf, নষ্ট লাইন বাদ দিয়ে কয়টি বাদ গেল তা জানাবে। ৫. কোনো struct সরাসরি fwrite করবে না। শেষে বলো: Windows-এ rename-এর আচরণ কোথায় আলাদা, আর কোন কোন পরীক্ষা চালিয়ে আমি নিশ্চিত হব।

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

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

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

প্র ০১OS কেন লেখা সাথে সাথে disk-এ পাঠায় না? fsync থাকতে সবাই সবসময় সেটি ডাকে না কেন?

কারণ গতির পার্থক্য বিশাল। RAM-এ লেখা ন্যানোসেকেন্ডের কাজ; disk-এ নিশ্চিতভাবে লেখা — বিশেষ করে পুরোনো ঘূর্ণায়মান hard disk-এ — তার চেয়ে হাজার থেকে লক্ষ গুণ ধীর। OS তাই লেখাগুলো নিজের মেমরিতে (Linux-এ একে page cache বলে) জমিয়ে রাখে, তারপর একসাথে, সুবিধাজনক ক্রমে disk-এ পাঠায়। এতে তিনটি লাভ: প্রোগ্রাম অপেক্ষা না করে কাজ চালিয়ে যায়; একই জায়গায় বারবার লেখা হলে শুধু শেষটাই disk-এ যায়; আর ছোট ছোট লেখা জুড়ে বড় একটি লেখা হয়, যা disk-এর জন্য অনেক সহজ।

এর দাম হলো ঝুঁকি — লেখা “সম্পন্ন” বলে ফেরত আসার পরেও কয়েক সেকেন্ড সেটি শুধু RAM-এ থাকে। fsync সেই ঝুঁকি বন্ধ করে: “এই ফাইলের সব লেখা disk-এ পাকা না হওয়া পর্যন্ত আমাকে ফেরত দিও না।” কিন্তু প্রতিটি লেখার পরে fsync ডাকলে প্রোগ্রাম disk-এর গতিতে নেমে আসে। ডেটাবেসগুলো তাই বুদ্ধি করে: অনেক লেনদেন একসাথে জমিয়ে একটি fsync — একে বলে group commit.

আরও একটি স্তর আছে: disk নিজেও ভেতরে একটি ছোট cache রাখে, আর Linux-এ rename-এর মতো নামের বদল পাকা করতে ফোল্ডারটির ওপরও fsync লাগে — পেশাদার ডেটাবেস কোডে এই খুঁটিনাটিও থাকে।

বাস্তব সিদ্ধান্ত তাই ডেটার মূল্যের ওপর নির্ভর করে: খেলার সেটিংস হারালে ক্ষতি সামান্য, fsync লাগে না; ব্যাংকের লেনদেন হারালে সর্বনাশ, সেখানে প্রতিটি commit-এ নিশ্চয়তা চাই।

মূল উপলব্ধি: প্রতিটি স্তরের buffer গতি কেনে, আর বিনিময়ে দেয় ঝুঁকি। ভালো ইঞ্জিনিয়ার জানেন তাঁর ডেটা কোন স্তরে আছে, আর কোন ডেটার জন্য গতি ছেড়ে নিশ্চয়তা কিনতে হবে।

প্র ০২ফাইল দিয়েই যদি ডেটা রাখা যায়, তাহলে ডেটাবেসের দরকার পড়ল কেন?

রহিম চাচার একটি দোকান, একটি কম্পিউটার, দিনে কয়েকশো বিক্রি — CSV ফাইলই যথেষ্ট। এবার ভাবুন bKash-এর মতো একটি সেবা: একই মুহূর্তে হাজার হাজার মানুষ টাকা পাঠাচ্ছেন। ফাইল দিয়ে এটা করতে গেলে একে একে সমস্যা দেখা দেয়:

  • একসাথে অনেকে লিখলে: দুটি প্রোগ্রাম একই সময়ে একই ফাইলে লিখলে একজনের লেখা আরেকজনের ওপর পড়ে মুছে যেতে পারে। এর জন্য lock লাগে, আর lock ঠিকমতো লেখা কঠিন কাজ।
  • অর্ধেক কাজ: “ক-এর হিসাব থেকে ৫০০ কাটো, খ-এর হিসাবে ৫০০ যোগ করো” — মাঝখানে বিদ্যুৎ গেলে টাকা উধাও। দুটি বদল হয় একসাথে হবে, নয়তো একটিও না। ডেটাবেস একে বলে transaction.
  • খোঁজা: “গত মাসে মিরপুরের যেসব দোকানে ১০০০ টাকার বেশি বিক্রি” — ফাইলে প্রতিবার পুরোটা পড়তে হবে। ডেটাবেস index রেখে মুহূর্তে খুঁজে দেয়।
  • Crash থেকে ফেরা: আমাদের temp + rename পুরো ফাইল নতুন করে লেখে — কোটি record-এর জন্য অচল। ডেটাবেস প্রতিটি বদল আগে একটি ছোট log-এ লিখে fsync করে (write-ahead log), তারপর আসল ডেটায় বসায়। crash হলে log পড়ে অসমাপ্ত কাজ শেষ করে বা বাতিল করে।

মজার ব্যাপার: ডেটাবেসও শেষ পর্যন্ত disk-এর ফাইলেই ডেটা রাখে, আর বহু ডেটাবেস C-তে লেখা — SQLite, PostgreSQL, MySQL-এর মূল অংশ। অর্থাৎ ডেটাবেস হলো এই পর্বের কৌশলগুলোর অত্যন্ত যত্নে বানানো, বহু বছর ধরে পরীক্ষিত একটি রূপ। নিজে বানানোর বদলে সেটি ব্যবহার করাই সাধারণত বুদ্ধিমানের কাজ। এর ভেতরটা জানতে ABCL TECH-এর DBMS ও SQL কোর্স দেখুন।

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

প্র ০৩বাস্তবের সফটওয়্যার টেক্সট না binary — কোন ফরম্যাট বেছে নেয়? JSON আর Protocol Buffers-এর তুলনা থেকে কী শেখার আছে?

এই পর্বের CSV বনাম struct-এর বিতর্কই বড় পরিসরে চলছে বহু বছর ধরে। আজকের ওয়েবে সবচেয়ে প্রচলিত টেক্সট ফরম্যাট JSON: {"item": "rice", "qty": 2}. মানুষ পড়তে পারে, প্রায় সব ভাষা পড়তে পারে, ঘরের নাম সাথে থাকায় নতুন ঘর যোগ করলে পুরোনো প্রোগ্রাম সাধারণত ভাঙে না। দাম হলো — প্রতিটি record-এ ঘরের নাম বারবার লেখা, সংখ্যাকে অক্ষরে বদলানো, তাই আকারে বড় আর পড়তে ধীর।

অন্যদিকে Google-এর তৈরি Protocol Buffers একটি binary ফরম্যাট। কিন্তু এটি আমাদের raw struct-এর মতো মেশিননির্ভর নয়। এখানে প্রথমে একটি schema লেখা হয় — কোন ঘরের নম্বর কত, টাইপ কী। তারপর একটি টুল প্রতিটি ভাষার জন্য পড়া-লেখার কোড বানায়, যা byte-ক্রম ও মাপ নিজেই নির্দিষ্ট নিয়মে সামলায়। ঘরের নাম নয়, ছোট নম্বর লেখা হয়, তাই আকার ছোট ও পড়া দ্রুত; আর নতুন ঘর যোগ হলে পুরোনো প্রোগ্রাম অচেনা নম্বর এড়িয়ে যেতে পারে। দাম — ফাইল খুলে চোখে পড়া যায় না, আর schema ছাড়া ডেটার অর্থ বোঝা কঠিন।

বাস্তবে তাই দুটোই চলে: ব্রাউজার আর সার্ভারের মাঝে, কনফিগ ফাইলে, debug-এর সুবিধার জন্য সাধারণত টেক্সট; বড় কোম্পানির ভেতরের সার্ভিসগুলোর মধ্যে বিপুল পরিমাণ বার্তায় প্রায়ই binary. ছবি, ভিডিও, ডেটাবেসের ভেতরের ফাইল — সবই binary, কারণ সেখানে আকার আর গতিই মুখ্য।

সব ভালো ফরম্যাটের একটি সাধারণ গুণ: version-এর ব্যবস্থা। আজ চাচার খাতায় তিনটি ঘর; কাল “পেমেন্টের ধরন” যোগ হবে। ফাইলের শুরুতে একটি version নম্বর থাকলে নতুন প্রোগ্রাম পুরোনো ফাইলও ঠিকঠাক পড়তে পারে।

মূল উপলব্ধি: প্রশ্নটা “টেক্সট না binary” নয় — প্রশ্ন হলো “ফরম্যাটটি কি নির্দিষ্ট নিয়মে লেখা, আর ভবিষ্যতে বদলানো যায় কি না?” Raw struct এই দুটি পরীক্ষাতেই ফেল করে; JSON আর Protocol Buffers দুটোই পাস করে, ভিন্ন দামে।

প্র ০৪Unix-এ বলা হয় “সবকিছুই ফাইল” — এর মানে কী, আর এই নকশা কেন এত শক্তিশালী?

Unix-এর নকশাকাররা লক্ষ করেছিলেন, প্রোগ্রাম যেখানেই ডেটা পাঠাক — disk-এর ফাইল, terminal, printer, অন্য প্রোগ্রাম — কাজটা মূলত একই: কিছু byte পাঠানো বা আনা। তাই তাঁরা সবকিছুর জন্য একটিই দরজা রাখলেন: খোলো (open), পড়ো (read), লেখো (write), বন্ধ করো (close) — ব্যস, এই চারটি কাজ। প্রতিটি খোলা জিনিস পায় একটি file descriptor নম্বর।

  • Keyboard আর স্ক্রিন — stdin (0) আর stdout (1), পর্ব ১০-এ দেখেছেন।
  • Hardware — Linux-এ /dev ফোল্ডারে disk, terminal এমনকি “শূন্যের উৎস” /dev/null-ও ফাইলের মতো দেখায়।
  • প্রোগ্রামের মাঝে পাইপ (|) আর নেটওয়ার্ক socket (পর্ব ৪৮ ও ৫০) — এগুলোও file descriptor দিয়েই পড়া-লেখা হয়।
  • Linux-এর /proc ফোল্ডারে চলমান প্রোগ্রামের তথ্যও ফাইলের মতো পড়া যায়।

এই সরলতার কারণেই cat, grep-এর মতো ছোট টুলগুলো ফাইল, keyboard বা অন্য প্রোগ্রামের আউটপুট — সব একইভাবে পড়তে পারে, আলাদা করে কিছু শিখতে হয় না।

শক্তিটা কোথায়? আজ আপনি যে খাতার প্রোগ্রাম লিখলেন, সেটি FILE * নেয়। কাল চাইলে একই ফাংশনকে stdout দিন — রিপোর্ট স্ক্রিনে আসবে। পরশু একটি pipe দিন — রিপোর্ট সরাসরি printer বা অন্য প্রোগ্রামে যাবে। ফাংশনটি একটুও বদলাতে হলো না। পর্ব ০৩-এর শিক্ষা মনে আছে? মাঝখানে একটি সাধারণ interface বসালে M×N সমস্যা M+N হয়ে যায় — এটাও ঠিক তাই।

মূল উপলব্ধি: একটি সরল, সবার জন্য এক interface — এটাই দীর্ঘজীবী নকশার রহস্য। আপনার নিজের কোডেও ফাংশনকে নির্দিষ্ট ফাইলের নাম না দিয়ে FILE * দিন; সে অনেক বেশি জায়গায় কাজে লাগবে।

প্র ০৫ফোনের অ্যাপ save করার মাঝখানেই OS সেটি বন্ধ করে দিতে পারে — তবু ডেটা নষ্ট হয় না কেন?

মোবাইল ফোনে এটা কোনো দুর্ঘটনা নয়, নিয়মিত ঘটনা। মেমরি কম পড়লে Android বা iOS পেছনে থাকা অ্যাপ কোনো সতর্কতা ছাড়াই বন্ধ করে দেয়, ব্যাটারি হঠাৎ শেষ হয়, ব্যবহারকারী অ্যাপ সোয়াইপ করে সরিয়ে দেন। তাই মোবাইল অ্যাপের নকশাই হয় এই ধারণা নিয়ে: যেকোনো মুহূর্তে আমি মারা যেতে পারি।

তারা মূলত এই পর্বের কৌশলগুলোই ব্যবহার করে, প্রস্তুত অবস্থায়:

  • Temp + rename-এর তৈরি রূপ: Android-এ AtomicFile নামে একটি সহায়ক class আছে, যা ঠিক এই নীতিতে নতুন সংস্করণ পুরো লেখা শেষ হওয়ার পরেই পুরোনোটি প্রতিস্থাপন করে।
  • SQLite: প্রায় সব Android ও iPhone অ্যাপের ভেতরে C-তে লেখা এই ছোট ডেটাবেসটি থাকে। এটি journal বা WAL (write-ahead log) ব্যবহার করে — বদল আগে log-এ, তারপর আসল ফাইলে। মাঝপথে অ্যাপ বন্ধ হলে পরেরবার খোলার সময় SQLite নিজেই অসমাপ্ত কাজ বাতিল বা সম্পূর্ণ করে।
  • ছোট ছোট save: অ্যাপ সব কাজ শেষে একবারে না রেখে প্রতিটি পরিবর্তনের পরপরই ছোট করে রাখে। ফলে বন্ধ হলেও হারায় বড়জোর শেষ কয়েক সেকেন্ডের কাজ।
  • শুরুতে পরিষ্কার করা: খোলার সময় অ্যাপ পড়ে থাকা অসম্পূর্ণ temp ফাইল খুঁজে মুছে দেয় — অংশ ৮-এ চাচার প্রোগ্রামের জন্য যা বলেছি।

খেয়াল করুন, এখানে কেউ “crash হবে না” ধরে নেয়নি। বরং ধরে নিয়েছে crash হবেই, এবং প্রতিটি মুহূর্তে disk-এর অবস্থা যেন একটি বৈধ সংস্করণ হয় — সেভাবে কাজের ক্রম সাজিয়েছে। এই চিন্তাকে বলা যায় crash-consistency: “বিপদ ঠেকানো” নয়, “বিপদের পরেও ঠিক থাকা”.

মূল উপলব্ধি: নির্ভরযোগ্য সিস্টেম ব্যর্থতাকে অস্বীকার করে না, পরিকল্পনায় ঢুকিয়ে নেয়। প্রতিটি ধাপে নিজেকে জিজ্ঞেস করুন: “ঠিক এখন বিদ্যুৎ গেলে disk-এ কী থাকবে?”

অনুশীলন

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

  1. Mode বেছে নিন: প্রতিটির জন্য সঠিক fopen mode বলুন — (ক) প্রতিটি বিক্রি হওয়া মাত্র খাতার শেষে একটি লাইন যোগ করা (খ) আগের দিনের রিপোর্ট পড়ে স্ক্রিনে দেখানো (গ) binary ফাইলে record ৭-এর দাম বদলানো, বাকি সব অক্ষত রেখে (ঘ) প্রতি রাতে temp ফাইলে পুরো খাতা নতুন করে লেখা।

    (ক) "a" — আগের লাইন থাকবে, প্রতিটি লেখা শেষে বসবে; ফাইল না থাকলে নতুন বানাবে। (খ) "r" — শুধু পড়া; ফাইল না থাকলে NULL, যা আপনি যাচাই করে জানাবেন “আগের দিনের রিপোর্ট নেই”. (গ) "r+b" — খালি না করে পড়া-লেখা, binary mode-এ; fseek দিয়ে ৭ × sizeof(Sale)-এ গিয়ে লিখবেন। "w+b" দিলে সব মুছে যেত! (ঘ) "w" — temp ফাইল প্রতিবার নতুন, তাই খালি করাই ঠিক; আসল খাতা এতে নিরাপদ, কারণ "w" পড়ছে শুধু temp-এর ওপর।

  2. বাগ শিকার: একজন লিখেছেন int n = 0; while (!feof(f)) { fscanf(f, "%d", &x); sum += x; n++; }. ফাইলে তিনটি সংখ্যা আছে: 10 20 30 (শেষে একটি লাইন-ভাঙা)। n আর sum কত হবে, আর কেন?

    সম্ভবত n = 4, sum = 90. তৃতীয় fscanf 30 পড়ার পর থেমে যায় লাইন-ভাঙার আগে, তাই EOF চিহ্ন তখনো বসেনি। চতুর্থবার লুপে ঢুকে fscanf শুধু ফাঁকা জায়গা পেয়ে EOF ফেরত দেয়, x-এ নতুন কিছু বসায় না — পুরোনো 30 রয়ে যায়। তাই 30 দুবার যোগ হয়ে 60+30 = 90, আর গণনা ৪. গড় বের করলে আসবে ২২.৫, অথচ সঠিক গড় ২০. সমাধান: while (fscanf(f, "%d", &x) == 1) { ... }.

  3. Offset-এর অঙ্ক: একটি Student struct-এর sizeof ৪০. students.bin ফাইলের মাপ ৪০০০ byte. (ক) কয়জন ছাত্রের তথ্য আছে? (খ) রোল ৩৭-এর ছাত্র (রোল ১ থেকে শুরু, ক্রমানুসারে রাখা) কোন byte থেকে শুরু? (গ) ফাইলের মাপ ৪০১০ হলে কী সন্দেহ করবেন?

    (ক) ৪০০০ ÷ ৪০ = ১০০ জন। (খ) রোল ১ আছে record ০-তে, তাই রোল ৩৭ আছে record ৩৬-এ: ৩৬ × ৪০ = ১৪৪০ byte থেকে। “রোল” আর “record নম্বর”-এর এই এক-ঘরের পার্থক্য একটি ক্লাসিক off-by-one ফাঁদ। (গ) ৪০১০ ÷ ৪০ পূর্ণসংখ্যা নয় — শেষের ১০ byte একটি অসম্পূর্ণ record. খুব সম্ভব শেষবার লেখার সময় প্রোগ্রাম crash করেছিল বা বিদ্যুৎ গিয়েছিল। এমন ফাইল যাচাই না করে পড়লে শেষ record-এ আবর্জনা আসবে।

  4. খাতার আপগ্রেড (কোড লিখতে হবে): পর্ব ২৩-এর হিসাবের খাতায় দুটি ফাংশন যোগ করুন — প্রোগ্রাম শুরুতে ledger.csv থেকে load, আর প্রতিটি নতুন বিক্রির পরে temp + rename দিয়ে save. তিন স্তরের নকশা মেনে মেনুর কোড যেন একটুও না বদলায়। তারপর পরীক্ষা করুন: ফাইল ছাড়া শুরু, নষ্ট লাইনওয়ালা ফাইল, আর save-এর সময় Ctrl+C.

    কাঠামো: int ledger_load(Ledger *lg, const char *path) — ফাইল না থাকলে (fopen NULL আর errno == ENOENT) খালি খাতা নিয়ে সফল ধরে নিন, কারণ প্রথম দিন ফাইল থাকবেই না; অন্য কারণে ব্যর্থ হলে perror দিয়ে −১. fgets + sscanf দিয়ে প্রতিটি লাইন পড়ে ledger_add ডাকুন, নষ্ট লাইন গুনে রাখুন। int ledger_save(const Ledger *lg, const char *path) — path-এর সাথে .tmp জুড়ে (snprintf দিয়ে) লিখুন, সব ফল যাচাই, তারপর rename.

    মেনু স্তর শুধু শুরুতে load আর প্রতিটি “যোগ করুন”-এর পরে save ডাকবে — ডেটা কোথা থেকে আসছে তা সে জানে না। পর্ব ২৩-এ ডেটার স্তর আলাদা রাখার সুফল এখানেই। Ctrl+C পরীক্ষায় দেখবেন, সর্বোচ্চ একটি .tmp ফাইল পড়ে থাকে, কিন্তু ledger.csv সবসময় পূর্ণ। AI দিয়ে লেখালে উপরের প্রম্পট আর চেকলিস্ট ব্যবহার করুন।

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

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

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

  1. 📝 fopen("ledger.csv.tmp", "w") আর NULL যাচাই
  2. ✍️ সব বিক্রি temp ফাইলে লেখা
  3. 📤 fflush আর fsync — লেখা disk-এ পাকা করা
  4. 🔒 fclose আর তার ফল যাচাই
  5. 🔁 rename("ledger.csv.tmp", "ledger.csv")

মাসের হিসাব আছে এমন ledger.csv ফাইলটি কেউ fopen("ledger.csv", "w") দিয়ে খুলল, কিছু লেখার আগেই প্রোগ্রাম crash করল। ফাইলে কী থাকবে?

  • মাসের পুরো হিসাব, অক্ষত
  • কিছুই না — ফাইলটি খালি
  • অর্ধেক লেখা হিসাব
  • ফাইলটিই মুছে যাবে, অস্তিত্ব থাকবে না

FILE *f = fopen("data.txt", "r"); — ফাইলটি না থাকলে f-এর মান কী, আর কারণ জানবেন কীভাবে?

  • একটি নতুন খালি ফাইলের pointer
  • প্রোগ্রাম সাথে সাথে crash করবে
  • NULL; perror দিয়ে কারণ ছাপা যায়
  • EOF; feof দিয়ে কারণ জানা যায়

while (!feof(f)) { fgets(line, 100, f); count++; } — ৫ লাইনের ফাইলে count সাধারণত কত হবে?

  • ৬
  • ৫
  • ৪
  • লুপ কখনো থামবে না

প্রতিটি record ২৪ byte. ০ থেকে গুনলে record নম্বর ১০ পড়তে fseek-এ কোন offset দেবেন?

  • 264 (১১ × ২৪)
  • 10
  • 216 (৯ × ২৪)
  • 240 (১০ × ২৪)

Linux-এ fwrite দিয়ে struct সরাসরি লেখা একটি ফাইল Windows-এর প্রোগ্রাম ভুল পড়ছে, অথচ struct-এর কোড হুবহু এক। সবচেয়ে সম্ভাব্য কারণ?

  • Windows ফাইলটি কপি করার সময় নষ্ট করেছে
  • দুই সিস্টেমে টাইপের মাপ ও padding আলাদা, তাই struct-এর byte-বিন্যাস মেলে না
  • লেখার প্রোগ্রাম fclose করেনি
  • Windows-এ fread ধীর

temp ফাইলে লিখে rename করার কৌশলে, rename-এর ঠিক আগে বিদ্যুৎ চলে গেল। পরদিন ledger.csv-এ কী পাবেন? (Linux ধরে নিন)

  • আজকের নতুন হিসাব
  • অর্ধেক পুরোনো, অর্ধেক নতুন — নষ্ট ফাইল
  • কালকের পুরোনো হিসাব, পূর্ণ ও অক্ষত
  • একটি খালি ফাইল

fprintf(f, ...) সফলভাবে ফেরত এলো, তারপরই প্রোগ্রাম Segmentation fault দিয়ে crash করল (fclose বা fflush হয়নি)। লেখাটির কী হলো?

  • খুব সম্ভব হারিয়ে গেছে — লেখাটি তখনো প্রোগ্রামের buffer-এ ছিল
  • নিশ্চিতভাবে disk-এ আছে, কারণ fprintf সফল হয়েছিল
  • OS crash-এর সময় নিজেই buffer flush করে দেয়
  • পুরো ফাইলটি নষ্ট হয়ে গেছে

শব্দকোষ

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

মনে রাখুন

  1. RAM-এর ডেটা প্রোগ্রাম বা বিদ্যুতের সাথে মুছে যায়; টিকিয়ে রাখতে ফাইলে লিখুন। ফাইল শুধু byte-এর সারি — অর্থ দেয় আপনার প্রোগ্রাম।
  2. fopen → NULL যাচাই → কাজ → fclose. "w" খোলামাত্র ফাইল খালি করে; perror জানায় কেন খোলা গেল না।
  3. লেখা যায় buffer → OS → disk পথে; fflush/fclose OS পর্যন্ত, fsync disk পর্যন্ত পৌঁছায়।
  4. পড়ার লুপের শর্ত পড়ার ফাংশনের ফেরত মান, কখনো !feof নয়। টেক্সট (CSV) বহনযোগ্য; raw struct দ্রুত কিন্তু মেশিননির্ভর; একই মাপের record হলে fseek-এ সরাসরি লাফ।
  5. গুরুত্বপূর্ণ ফাইল সরাসরি ওপরে লিখবেন না: temp → flush/fsync → close → rename.

পরের পর্বে: ফাইলের নাম এখনো কোডের ভেতরে লেখা। পরের পর্বে প্রোগ্রামটিকে সত্যিকারের টুল বানাব — চালানোর সময়ই ফাইলের নাম আর অপশন বলে দেওয়া যাবে (argc, argv), আর দেখব printf কীভাবে যেকোনো সংখ্যক মান নিতে পারে।

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

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