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

ইনপুট-আউটপুট — প্রোগ্রাম যেভাবে কথা বলে আর শোনে

Input & Output — Streams, Buffering, printf, scanf and Safe Input

Taught in Bangla, this lesson explains C input and output: the stdin, stdout and stderr streams, redirection and pipes, output buffering, printf format strings and field widths, how scanf consumes input, its pitfalls, and safe line-based input.

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

  • stdin, stdout, stderr কী — আর <, >, | দিয়ে কীভাবে পাইপের মুখ ঘোরানো যায়
  • Buffer কেন আছে, লেখা কেন দেরিতে দেখা যায়, আর কখন fflush দরকার
  • printf দিয়ে সুন্দর করে সাজানো টেবিল ও রসিদ ছাপানো — width, precision, alignment
  • scanf ভেতরে কীভাবে ইনপুট খায়, আর তার পরিচিত ফাঁদগুলো কোথায়
  • fgets + sscanf দিয়ে নিরাপদ ইনপুট, আর AI-এর লেখা I/O কোড যাচাইয়ের চেকলিস্ট

১ · তিনটি পাইপ — stdin, stdout, stderr

একটি প্রশ্ন দিয়ে শুরু করি। আপনি printf("Hello") লিখলেন। লেখাটি স্ক্রিনে গেল। কিন্তু আপনার কোডে তো “স্ক্রিন” শব্দটি কোথাও নেই! তাহলে printf জানল কীভাবে যে লেখাটি স্ক্রিনেই পাঠাতে হবে? আর একই প্রোগ্রাম যদি একটি সার্ভারে চলে, যেখানে কোনো স্ক্রিনই নেই — তখন লেখা যায় কোথায়?

উত্তরটি C আর Unix-এর সবচেয়ে সুন্দর নকশাগুলোর একটি। আপনার প্রোগ্রাম কখনো সরাসরি কিবোর্ড বা স্ক্রিনের সাথে কথা বলে না। সে কথা বলে তিনটি stream-এর সাথে। Stream মানে byte-এর একটি ধারা, যা এক মাথা থেকে আরেক মাথায় বয়ে যায়।

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

প্রতিটি প্রোগ্রামের তিনটি জন্মগত পাইপ

The three standard streams every C program starts with

  1. stdin · 0scanf, fgetsসাধারণত কিবোর্ড থেকে
  2. আপনার প্রোগ্রামmain()পাইপের অন্য মাথা দেখে না
  3. stdout · 1printf, putsসাধারণ আউটপুট
  4. stderr · 2fprintf(stderr, …)ভুলের বার্তা
ছবি ১০.১ — প্রোগ্রাম শুরুর আগেই startup code এই তিনটি পাইপ খুলে রাখে (পর্ব ০৪-এ দেখেছিলেন)।

প্রতিটি পাইপের একটি নম্বর আছে — 0, 1 আর 2. OS এই নম্বর দিয়েই খোলা পাইপ ও ফাইলকে চেনে; এর নাম file descriptor. C-এর stdio.h এর ওপর একটি আরামদায়ক স্তর বসিয়েছে, যেখানে এদের নাম stdin, stdout আর stderr.

Streamনম্বরকাজC-তে যা দিয়ে ব্যবহার করবেন
stdin0প্রোগ্রামে ডেটা ঢোকেscanf, fgets(buf, n, stdin), getchar
stdout1আসল ফলাফল বের হয়printf, puts, putchar
stderr2ভুল, সতর্কতা, অগ্রগতির বার্তাfprintf(stderr, "..."), perror
মূল ধারণা

প্রোগ্রাম জানে না, জানার দরকারও নেই, পাইপের অন্য মাথায় কী আছে। কিবোর্ড, ফাইল, অন্য প্রোগ্রাম, এমনকি নেটওয়ার্ক — সবই তার কাছে একই রকম byte-এর ধারা। এই একটি সিদ্ধান্তের ফলে একই scanf দিয়ে কিবোর্ড থেকেও পড়া যায়, ফাইল থেকেও।

এই “সবকিছুই byte-এর ধারা” চিন্তাটি এসেছে Unix থেকে, যা ১৯৬৯ সাল থেকে Bell Labs-এ Ken Thompson আর Dennis Ritchie তৈরি করেন। তার আগের অনেক সিস্টেমে কার্ড রিডার, টেপ, প্রিন্টার — প্রতিটির জন্য আলাদা নিয়মে কোড লিখতে হতো। Unix সবকিছুকে একই চেহারা দিল, আর C-এর stdio library সেই নকশাকেই প্রায় সব অপারেটিং সিস্টেমে পৌঁছে দিল — Windows-ও এর বাইরে নয়।

২ · পাইপের মুখ ঘোরানো — redirection আর pipe

পাইপের অন্য মাথা যেহেতু প্রোগ্রামের হাতে নেই, তাই প্রোগ্রাম চালানোর সময় shell সেটি বদলে দিতে পারে — কোড একটুও না বদলে। একে বলে redirection. ধরুন আপনার report প্রোগ্রামটি scanf দিয়ে ছাত্রদের নম্বর পড়ে আর printf দিয়ে রিপোর্ট ছাপে। ৬০ জনের নম্বর হাতে টাইপ না করে একটি ফাইল থেকে দিতে চান, আর রিপোর্টটি ফাইলে রাখতে চান। নিচের কমান্ডটি ধাপে ধাপে দেখুন:

./report < marks.txt > out.txt 2> errors.txt
স্বাভাবিক অবস্থাকোনো redirection নেই
কিবোর্ড ──► [0 stdin ]           [1 stdout] ──► স্ক্রিন
                     report
                                  [2 stderr] ──► স্ক্রিন
< marks.txtইনপুটের পাইপ এখন ফাইল থেকে
marks.txt ──► [0 stdin ]         [1 stdout] ──► স্ক্রিন
                       report
                                  [2 stderr] ──► স্ক্রিন
> out.txtআউটপুটের পাইপ এখন ফাইলে
marks.txt ──► [0 stdin ]         [1 stdout] ──► out.txt
                       report
                                  [2 stderr] ──► স্ক্রিন
2> errors.txtভুলের পাইপও আলাদা ফাইলে
marks.txt ──► [0 stdin ]         [1 stdout] ──► out.txt
                       report
                                  [2 stderr] ──► errors.txt
প্রোগ্রাম চলছেকোড একটুও বদলায়নি
scanf("%d", &m);   // পড়ছে marks.txt থেকে
printf("...");     // লিখছে out.txt-এ
// report জানেই না যে কিবোর্ড বা স্ক্রিন নেই
$ cat out.txt     // পরে ফাইল খুলে রিপোর্ট দেখুন
  1. স্বাভাবিক অবস্থা। কোনো চিহ্ন না দিলে shell তিনটি পাইপই terminal-এর সাথে জোড়ে — stdin কিবোর্ডে, stdout আর stderr দুটোই স্ক্রিনে। এজন্যই আপনি সাধারণত দুটোর পার্থক্য টের পান না।
  2. < মানে “এই ফাইল থেকে খাওয়াও”। Shell প্রোগ্রাম চালু করার আগেই marks.txt খুলে stdin-এর জায়গায় বসিয়ে দেয়। এখন scanf কিবোর্ডের অপেক্ষা না করে ফাইল থেকে পড়বে। ফাইল শেষ হলে scanf পাবে EOF সংকেত।
  3. > মানে “এই ফাইলে ঢালো”। stdout এখন out.txt-এ যায়। ফাইলটি আগে থাকলে পুরোনো লেখা মুছে যায়; শেষে জোড়া দিতে চাইলে >> লিখুন।
  4. 2> মানে “২ নম্বর পাইপ এই ফাইলে”। এখানে 2 হলো stderr-এর নম্বর। চাইলে এটি না লিখে রেখে দিতে পারেন — তখন রিপোর্ট ফাইলে যাবে, কিন্তু ভুলের বার্তা স্ক্রিনেই দেখবেন। ঠিক এই সুবিধার জন্যই stderr আলাদা।
  5. প্রোগ্রাম কিছুই টের পায় না। সব জোড়া লাগানো হয়েছে চালু হওয়ার আগে। আপনার C কোডে একটি অক্ষরও বদলাতে হয়নি। এই কমান্ড Linux, Mac আর Windows-এর cmd-এ একইভাবে কাজ করে। (PowerShell-এ > আর 2> চলে, কিন্তু < চলে না; সেখানে Get-Content marks.txt | ./report লিখতে হয়।)

এবার একটি প্রোগ্রামের stdout সরাসরি আরেকটির stdin-এ জুড়ে দেওয়া যাক — মাঝে কোনো ফাইল ছাড়া। একে বলে pipe, চিহ্ন |. নিচের লাইনে report-এর আউটপুট যায় sort-এ, তার আউটপুট যায় head-এ — ফল: সবচেয়ে বেশি নম্বর পাওয়া ৩ জন।

./report < marks.txt | sort -k2 -n -r | head -3

stderr কেন আলাদা — একটি বাস্তব উদাহরণ। ধরুন marks.txt-এর ৪৫ নম্বর লাইনে “৮০”-এর জায়গায় ভুল করে “8o” লেখা। আপনার প্রোগ্রাম যদি ভুলের বার্তাটিও printf দিয়ে stdout-এ ছাপে, তাহলে সেটি চুপচাপ sort-এ ঢুকে রিপোর্টের মধ্যে মিশে যাবে — আপনি জানতেও পারবেন না। stderr-এ লিখলে রিপোর্ট পরিষ্কার থাকে, আর সতর্কবার্তা সরাসরি আপনার চোখের সামনে আসে। নিয়মটি সহজ: ফলাফল stdout-এ, ভুল ও খবর stderr-এ।

৩ · Buffer — লেখা কেন দেরিতে আসে

অনেক নতুন প্রোগ্রামার একটি অদ্ভুত অভিজ্ঞতার মুখোমুখি হন। প্রোগ্রামে printf("Loading...") লিখেছেন, তারপর একটি লম্বা হিসাব। কিন্তু হিসাব শেষ না হওয়া পর্যন্ত “Loading...” দেখাই যায় না! কিংবা প্রোগ্রাম crash করল, আর শেষ কয়েকটি printf-এর লেখা কোথাও নেই — ফলে ভুল জায়গায় বাগ খুঁজে ঘণ্টা নষ্ট।

কারণ হলো buffering. পর্ব ০৪-এ দেখেছিলেন, স্ক্রিনে কিছু লিখতে হলে প্রোগ্রামকে OS-এর কাছে অনুরোধ পাঠাতে হয় — system call. প্রতিটি system call-এ প্রোগ্রাম থেমে kernel-এর কাছে যায়, কাজ শেষে ফিরে আসে। এটি একটি সাধারণ যোগ-বিয়োগের তুলনায় অনেক গুণ বেশি সময় নেয়। তাই printf প্রতিটি অক্ষরের জন্য OS-কে না ডেকে লেখাগুলো আগে একটি buffer-এ জমায়, তারপর একবারে পাঠায়।

মিরপুর থেকে মতিঝিলের বাসের কথা ভাবুন। একজন যাত্রী উঠলেই বাস ছেড়ে দিলে প্রতিটি যাত্রীর জন্য একটি করে ট্রিপ লাগত — তেল, সময়, রাস্তা সবই নষ্ট। তাই বাস কয়েকজন যাত্রী জমা হলে ছাড়ে। যাত্রীর একটু অপেক্ষা হয়, কিন্তু সব মিলিয়ে অনেক বেশি মানুষ পৌঁছায়। Buffer ঠিক এই কাজটি করে: একটু দেরি মেনে নিয়ে মোট কাজ অনেক দ্রুত করা।

নিচের সিমুলেটরে বাঁয়ে কোড, ডানে buffer-এর ভেতরে কী জমেছে আর স্ক্রিনে কী দেখা যাচ্ছে। “পরে” চাপুন এবং খেয়াল করুন কখন buffer খালি হয়ে লেখা স্ক্রিনে যায়:

1printf("Loading");2printf("...");3printf(" done\n");4printf("Saving");5fflush(stdout);6printf("...");7/* হঠাৎ crash! */
stdout-এর buffer ও OS-কে ডাকা
buffer
system call
  1. লেখা buffer-এ জমল। printf("Loading") চলল, কিন্তু স্ক্রিন এখনো ফাঁকা! লেখাটি এখন প্রোগ্রামের নিজের মেমরিতে, buffer-এ বসে আছে। OS-কে এখনো ডাকাই হয়নি।
  2. আরও জমল। “...” এসে আগের লেখার পাশে বসল। স্ক্রিন এখনো ফাঁকা। হিসাবটি লম্বা হলে ব্যবহারকারী ভাববেন প্রোগ্রাম আটকে গেছে।
  3. \n এলো — buffer খালি হলো। আউটপুট terminal-এ যাচ্ছে বলে stdout এখন line buffered: নতুন লাইন পেলেই জমানো সব লেখা একটি system call-এ OS-এর কাছে চলে যায়। তিনটি printf, কিন্তু OS-কে ডাকা হলো মাত্র একবার।
  4. আবার জমা শুরু। “Saving”-এর শেষে \n নেই, তাই এটি buffer-এই রয়ে গেল।
  5. fflush(stdout) — জোর করে পাঠানো। Flush মানে “যা জমেছে, এখনই পাঠাও।” নতুন লাইন ছাড়াই “Saving” স্ক্রিনে চলে গেল। “Enter your age: ”-এর মতো প্রশ্ন ছাপানোর পর এটি খুব কাজের।
  6. আবার buffer-এ। “...” জমা হলো। প্রোগ্রাম স্বাভাবিকভাবে শেষ হলে (return 0 বা exit) C library নিজেই বাকি buffer পাঠিয়ে দিত।
  7. Crash — জমানো লেখা হারিয়ে গেল। প্রোগ্রাম হঠাৎ ভেঙে পড়লে (যেমন Segmentation fault) OS তাকে সাথে সাথে থামিয়ে দেয়; buffer পাঠানোর সুযোগই থাকে না। তাই স্ক্রিনে শেষ “...” কখনো দেখা যায় না। ভুল খোঁজার সময় এটি মনে রাখুন: শেষ যে লেখা দেখছেন, crash ঠিক তার পরেই হয়েছে — এমন নাও হতে পারে।

আরেকটি গুরুত্বপূর্ণ নিয়ম: buffer-এর আচরণ নির্ভর করে পাইপের অন্য মাথায় কী আছে তার ওপর।

কোথায় যাচ্ছেআচরণলেখা কখন বের হয়
stdout → terminalLine buffered\n পেলে, buffer ভরলে, fflush বা প্রোগ্রাম শেষে
stdout → ফাইল বা pipe (>, |)Fully bufferedশুধু buffer ভরলে (সাধারণত কয়েক হাজার byte), fflush বা প্রোগ্রাম শেষে — \n-এ নয়
stderrBuffer প্রায় নেইসাথে সাথে — যাতে crash-এর আগের ভুলের বার্তা হারিয়ে না যায়

এজন্যই একটি প্রোগ্রাম terminal-এ ঠিকঠাক লাইন ধরে লেখা দেখায়, কিন্তু > log.txt দিয়ে চালিয়ে crash করলে log ফাইলটি প্রায় খালি পাওয়া যায়। ডিবাগ করার বার্তা তাই stderr-এ লিখুন: fprintf(stderr, "step 3 reached\n"); — এটি সাথে সাথে বের হয়। আর একটি সুখবর: বেশিরভাগ সিস্টেমে stdin থেকে পড়ার আগে stdout-এর জমানো লেখা নিজে থেকেই পাঠিয়ে দেওয়া হয়। তবু নিশ্চিত থাকতে প্রশ্ন ছাপানোর পর fflush(stdout) দেওয়া ভালো অভ্যাস।

৪ · printf ভেতর থেকে — format string আর argument-এর জোড়া

পর্ব ০৪-এ printf-এর প্রথম পরিচয় পেয়েছিলেন — %d, %f, %c, %s. এবার দেখি ভেতরে আসলে কী ঘটে। printf-এর প্রথম argument হলো format string — একটি নকশা, যেখানে সাধারণ লেখা হুবহু ছাপা হয় আর প্রতিটি format specifier (% দিয়ে শুরু) একটি ফাঁকা ঘর, যা পরের argument দিয়ে পূরণ হবে। বাকি argument কয়টি হবে, তা আগে থেকে ঠিক নেই — এমন ফাংশনকে বলে variadic function.

0printf("%s paid %d Tk (%.1f%%)\n", name, amount, pct);1"%s"       ← argument দরকার2" paid "   ← হুবহু ছাপা3"%d"       ← argument দরকার4" Tk ("    ← হুবহু ছাপা5"%.1f"     ← argument দরকার6"%%"       ← শুধু একটি % চিহ্ন7")\n"      ← হুবহু ছাপা
Argument-এর সারি
পরের argument
printf ফেরত দেবে
  1. শুরু। printf পেল একটি format string আর তিনটি মান: name = "Rahim", amount = 500 (int), pct = 12.5 (double). সে format string বাঁ থেকে ডানে পড়বে, আর argument-এর সারি থেকে একটি একটি করে মান তুলবে।
  2. %s → প্রথম argument. “এখানে একটি লেখা বসবে।” সারির প্রথম মান name তোলা হলো, ছাপা হলো “Rahim”. এখন সারির মাথায় amount.
  3. সাধারণ লেখা হুবহু। “ paid ” অংশে কোনো % নেই, তাই কোনো argument লাগে না — যেমন আছে তেমনই ছাপা হলো।
  4. %d → পরের argument. সারি থেকে amount তোলা হলো, int হিসেবে পড়া হলো: “500”.
  5. %.1f → তার পরের argument. আগে “ Tk (” হুবহু ছাপা হলো, তারপর pct double হিসেবে পড়ে দশমিকের পর ১ ঘর রেখে: “12.5”. সারি এখন খালি।
  6. %% — argument লাগে না। শুধু একটি % চিহ্ন ছাপে। শেষে “)” আর নতুন লাইন। কাজ শেষে printf ফেরত দেয় মোট কতগুলো অক্ষর ছাপল — এখানে 26 (\n-সহ)।
  7. ধরন না মিললে কী হয়? printf("%d\n", pct) লিখলে printf একটি int খুঁজবে, অথচ দেওয়া হয়েছে double. সে argument-এর আসল ধরন দেখতে পায় না — format string-কেই বিশ্বাস করে। ফল undefined behavior: সাধারণত আবর্জনা সংখ্যা। -Wall থাকলে GCC আগেই বলে: format '%d' expects argument of type 'int', but argument 2 has type 'double'.
  8. Argument কম পড়লে? printf("%d %d\n", 5) — দ্বিতীয় %d-এর জন্য কোনো মান নেই। কিন্তু printf গুনতে পারে না কয়টি argument এসেছে; সে মেমরি বা register-এ “পরের” জায়গা থেকে যা আছে তাই পড়ে ফেলে। এই দুর্বলতাই পরে একটি বড় নিরাপত্তা-ঝুঁকির জন্ম দিয়েছে (ভাবনার প্রশ্ন ০২ দেখুন)।
format string-ই printf-এর একমাত্র নকশা।
প্রতিটি % একটি মান নেয় — ক্রমে, ধরন মিলিয়ে, সংখ্যায় সমান।

যে specifier-গুলো প্রতিদিন লাগবে

চিহ্নকোন ধরনের মানউদাহরণছাপা হবে
%dintprintf("%d", -42)-42
%uunsigned intprintf("%u", 42u)42
%ldlongprintf("%ld", 123456L)123456
%lldlong longprintf("%lld", 8801712345678LL)8801712345678
%fdouble (float-ও চলে)printf("%f", 2.5)2.500000
%.2fdouble, দশমিকের পর ২ ঘরprintf("%.2f", 2.567)2.57
%edouble, বৈজ্ঞানিক রূপেprintf("%e", 150000.0)1.500000e+05
%cএকটি অক্ষরprintf("%c", 'A')A
%sলেখা (string)printf("%s", "Dhaka")Dhaka
%xunsigned int, hexadecimal-এprintf("%x", 255)ff
%pমেমরির ঠিকানা (void *)printf("%p", (void *)&x)0x7ffd… (প্রতিবার আলাদা হতে পারে)
%%কোনো মান লাগে নাprintf("50%%")50%

%d না %lld? ফোন নম্বর 8801712345678 বা কোটি টাকার হিসাব একটি সাধারণ int-এ ধরে না (পর্ব ০৫ ও ০৬-এ দেখেছেন, int সাধারণত প্রায় ২১৪ কোটি পর্যন্ত)। long long ব্যবহার করলে ছাপাতেও %lld লাগবে। %d দিলে ভুল সংখ্যা আসবে — আর এটি AI-এর লেখা কোডে খুব পরিচিত ভুল।

৫ · সাজানো টেবিল — width, precision, alignment

মুদি দোকানের রসিদ বা bKash-এর লেনদেনের তালিকা দেখলে খেয়াল করবেন — সংখ্যাগুলো ডান দিকে, দশমিক বিন্দু একই সারিতে, নামগুলো বাঁ দিকে। এই সৌন্দর্য আসে % আর অক্ষরের মাঝখানের ছোট সংখ্যাগুলো থেকে। নিয়মটি হলো: %[চিহ্ন][width][.precision]ধরন.

Width — কত ঘর জায়গা

%6d মানে কমপক্ষে ৬ ঘর জায়গা নাও; সংখ্যা ছোট হলে বাঁ দিকে ফাঁকা বসাও। [%6d] + 42 → [ 42]. লেখা বড় হলে কাটা হয় না, জায়গা বেড়ে যায়।

সংখ্যা ডান দিকে সারি বাঁধে

Precision — দশমিকের পর কত ঘর

%.2f মানে দশমিকের পর ঠিক ২ ঘর, দরকারে গোল করে। %10.2f মানে মোট ১০ ঘর জায়গা, তার মধ্যে দশমিকের পর ২ ঘর।

টাকা-পয়সার হিসাবে অপরিহার্য

বাঁয়ে সাজানো — -

%-12s মানে ১২ ঘর জায়গা, লেখা বাঁ দিকে, ফাঁকা ডানে। পণ্যের নাম বা ব্যক্তির নাম এভাবে সাজালে কলাম সোজা থাকে।

নাম ও লেখা বাঁ দিকে

শূন্য দিয়ে ভরা — 0

%05d + 42 → 00042. রোল নম্বর, রসিদ নম্বর বা সময় (%02d:%02d → 09:05) ছাপাতে কাজে লাগে।

নির্দিষ্ট দৈর্ঘ্যের কোড

এবার সব একসাথে — একটি মুদি দোকানের রসিদ। (দামগুলো উদাহরণ মাত্র।) চালিয়ে দেখুন কলামগুলো কীভাবে সোজা দাঁড়িয়ে যায়:

#include <stdio.h>

int main(void) {
    double rice = 420.0, dal = 2 * 135.5, oil = 189.0;
    double total = rice + dal + oil;

    printf("%-12s|%4s|%10s\n", "Item", "Qty", "Amount");
    printf("------------+----+----------\n");
    printf("%-12s|%4d|%10.2f\n", "Rice 5kg", 1, rice);
    printf("%-12s|%4d|%10.2f\n", "Lentils", 2, dal);
    printf("%-12s|%4d|%10.2f\n", "Soybean Oil", 1, oil);
    printf("------------+----+----------\n");
    int n = printf("%-17s|%10.2f\n", "Total (Tk)", total);
    printf("Discount 5%%: %.2f | chars printed: %d\n", total * 0.05, n);
    return 0;
}

প্রতিটি লাইনে একই নকশা %-12s|%4d|%10.2f — নাম ১২ ঘরে বাঁয়ে, পরিমাণ ৪ ঘরে ডানে, দাম ১০ ঘরে ডানে আর দশমিকের পর ২ ঘর। তাই “Lentils” ছোট আর “Soybean Oil” বড় হলেও | চিহ্নগুলো এক সারিতে থাকে। মোট লাইনে %-17s দেওয়া হয়েছে, কারণ ১২ + ১ + ৪ = ১৭ — ঠিক প্রথম দুই কলামের সমান জায়গা। শেষ লাইনে দুটি জিনিস খেয়াল করুন: %% ছাপে একটি %, আর n-এ আছে printf-এর return value — মোট লাইনে ছাপা অক্ষরের সংখ্যা 29 (১৭ + ১ + ১০ + নতুন লাইন)। লেখা কোথাও যেতে না পারলে printf একটি ঋণাত্মক সংখ্যা ফেরত দেয়।

টাকার হিসাবে double নিয়ে সতর্কতা। %.2f শুধু ছাপার সময় গোল করে; ভেতরের মান বদলায় না। পর্ব ০৫-এ দেখেছেন, 0.1-এর মতো সংখ্যা floating-point-এ হুবহু রাখা যায় না। ব্যাংক বা মোবাইল ওয়ালেটের আসল সফটওয়্যার তাই সাধারণত টাকাকে পয়সায় (পূর্ণসংখ্যা হিসেবে) রাখে, আর শুধু দেখানোর সময় দশমিক বসায়। শেখার জন্য double ঠিক আছে, কিন্তু অভ্যাসটি মনে রাখুন।

৬ · scanf ভেতর থেকে — ইনপুট কীভাবে খাওয়া হয়

scanf হলো printf-এর উল্টো: format string দেখে stdin থেকে লেখা পড়ে, সংখ্যা বা শব্দে বদলে ভ্যারিয়েবলে রাখে। কিন্তু দুটি পার্থক্য আছে, যা না বুঝলে বারবার বিপদে পড়বেন।

প্রথম পার্থক্য: & কেন লাগে

printf("%d", age)-এ printf-কে শুধু মানটি দিলেই চলে — সে শুধু পড়বে। কিন্তু scanf-কে আপনার ভ্যারিয়েবলে লিখতে হবে। C-তে ফাংশনকে argument দিলে সে পায় মানের একটি কপি; কপিতে লিখলে আসল ভ্যারিয়েবল বদলায় না। তাই scanf-কে দিতে হয় ভ্যারিয়েবলের ঠিকানা — memory address. &age মানে “age যে ঘরে থাকে, সেই ঘরের ঠিকানা”।

দারাজে কিছু অর্ডার দিলে আপনি ডেলিভারিম্যানকে নিজের ছবি দেন না — দেন বাসার ঠিকানা। তিনি সেই ঠিকানায় গিয়ে জিনিসটি রেখে আসেন। scanf হলো সেই ডেলিভারিম্যান, আর &age হলো ঠিকানা। ঠিকানার পুরো গল্প — যাকে বলে pointer — আসবে পর্ব ১৭-এ। আপাতত নিয়মটি মনে রাখুন: int, double, char ভ্যারিয়েবলে পড়তে হলে আগে &. (লেখা রাখার char name[20]-এর নাম নিজেই ঠিকানার মতো কাজ করে, তাই সেখানে & লাগে না।)

দ্বিতীয় পার্থক্য: scanf ইনপুটকে “খায়”

ব্যবহারকারী যা টাইপ করেন, তা প্রথমে stdin-এর buffer-এ জমা হয়। scanf সেখান থেকে যতটুকু দরকার ততটুকু তুলে নেয় — বাকিটা পরের পড়ার জন্য রেখে দেয়। নিচে ব্যবহারকারী লিখলেন 25 Rahim, তারপর Enter. প্রতিটি অক্ষর কোথায় যায় দেখুন:

1int age;2char name[20];3printf("Age and name: ");4int got = scanf("%d %19s", &age, name);5printf("got=%d age=%d name=%s\n", got, age, name);
stdin-এর buffer ও ভ্যারিয়েবল
stdin buffer
age
name
got
  1. scanf অপেক্ষা করছে। প্রশ্নটি ছাপা হয়েছে (পড়ার আগে stdout-এর buffer পাঠিয়ে দেওয়া হয়)। stdin-এর buffer খালি, তাই scanf এখানে থেমে আছে — প্রোগ্রাম কিছুই করছে না, শুধু অপেক্ষা।
  2. আপনি টাইপ করছেন “25 Rahim”. অক্ষরগুলো স্ক্রিনে দেখা যাচ্ছে, কিন্তু প্রোগ্রাম এখনো কিছু পায়নি! Terminal পুরো লাইনটি নিজের কাছে ধরে রাখে, যাতে Backspace দিয়ে ভুল ঠিক করতে পারেন।
  3. Enter চাপলেন। এবার পুরো লাইন — Enter-এর অক্ষর \n-সহ — stdin-এর buffer-এ এসে পৌঁছাল: "25 Rahim\n". খেয়াল করুন, Enter-ও একটি অক্ষর।
  4. %d সংখ্যা খায়। শুরুর ফাঁকা জায়গা থাকলে বাদ দিত। তারপর '2', '5' পড়ল। স্পেস দেখে থামল, কারণ স্পেস সংখ্যার অংশ নয় — স্পেসটি buffer-এ ফেরত রাখল। “25” লেখাকে সংখ্যা 25-এ বদলে রাখল age-এর ঠিকানায়।
  5. Format-এর ফাঁকা জায়গা। format string-এ %d আর %19s-এর মাঝে একটি স্পেস আছে। এর অর্থ “যত whitespace আছে — স্পেস, ট্যাব, নতুন লাইন — সব বাদ দাও”। স্পেসটি খাওয়া হলো।
  6. %19s একটি শব্দ খায়। পরের whitespace না আসা পর্যন্ত পড়ে — “Rahim”. \n দেখে থামে, আর শেষে লেখার সমাপ্তি-চিহ্ন বসায়। 19 মানে সর্বোচ্চ ১৯টি অক্ষর, যাতে ২০ ঘরের name উপচে না পড়ে। “Rahim Uddin” লিখলে শুধু “Rahim” আসত — %s স্পেসে থামে।
  7. scanf ফেরত দেয় 2. দুটি ঘরই সফলভাবে পূরণ হয়েছে, তাই got = 2. কিন্তু দেখুন — \n এখনো buffer-এ বসে আছে! কেউ খায়নি। পরের scanf("%c", …) প্রথমেই এটিকে পাবে। পরের অংশের প্রথম ফাঁদ এটাই।
  8. ফল ছাপা হলো। got=2 age=25 name=Rahim. ব্যবহারকারী যদি “abc” লিখতেন, %d প্রথম অক্ষরেই আটকে যেত, age-এ কিছুই লেখা হতো না, আর scanf ফেরত দিত 0. ইনপুটই না থাকলে (ফাইল শেষ) ফেরত দিত EOF, যা একটি ঋণাত্মক সংখ্যা।
মূল ধারণা

scanf-এর return value হলো সত্যের একমাত্র উৎস। এটি বলে কয়টি ঘর আসলে পূরণ হয়েছে। এটি না দেখলে আপনি জানবেন না age-এ ব্যবহারকারীর সংখ্যা আছে, না আগের আবর্জনা। পেশাদার কোডে তাই সবসময়: if (scanf("%d", &age) != 1) { /* ভুল ইনপুট সামলাও */ }. (if-এর বিস্তারিত পরের পর্বে।)

৭ · scanf-এর ফাঁদ — আর পুরোনো “সমাধান” যা আসলে বিপদ

ফাঁদ ১: রয়ে যাওয়া Enter আর %c

একটি পরিচিত দৃশ্য। প্রোগ্রাম প্রথমে বয়স চায়, তারপর জিজ্ঞেস করে “Continue? (y/n)”. কিন্তু প্রশ্নটি ছাপা হয়েই উত্তর না নিয়ে প্রোগ্রাম এগিয়ে যায়! কারণ আগের অংশেই দেখলেন — বয়সের পর Enter-এর \n buffer-এ রয়ে গেছে। %d বা %s শুরুর whitespace নিজে থেকেই বাদ দেয়, কিন্তু %c দেয় না — সে যে অক্ষর পায়, তা-ই নেয়। ফলে উত্তর হিসেবে সে পেয়ে যায় \n.

scanf("%d", &age);       // buffer-এ থেকে যায় "\n"
scanf("%c", &ans);       // ans = '\n'  — অপেক্ষাই করল না!
scanf(" %c", &ans);      // সমাধান: %c-এর আগে একটি স্পেস

" %c"-এর সেই ছোট্ট স্পেসটি বলে “আগে সব whitespace বাদ দাও, তারপর প্রথম আসল অক্ষরটি নাও।” একটি অক্ষর, কিন্তু অসংখ্য শিক্ষার্থীর কয়েক ঘণ্টা বাঁচায়।

ফাঁদ ২: %s-এ সীমা নেই

char name[20]; scanf("%s", name); — ব্যবহারকারী যদি ৫০ অক্ষরের একটি শব্দ লেখেন? scanf জানে না name-এ মাত্র ২০ ঘর আছে; সে লিখতেই থাকবে, পাশের মেমরিতে থাকা অন্য ভ্যারিয়েবল মুছে দিয়ে। এর নাম buffer overflow — C-এর ইতিহাসে সবচেয়ে বেশি নিরাপত্তা-ত্রুটির উৎসগুলোর একটি। সমাধান: সবসময় সীমা লিখুন — %19s (২০ থেকে ১ কম, কারণ শেষের সমাপ্তি-চিহ্নের জন্য এক ঘর লাগে)।

ফাঁদ ৩: ভুল ইনপুটে চিরকালের লুপ

%d চাইলে ব্যবহারকারী লিখলেন “abc”. scanf 0 ফেরত দেয় এবং “abc” buffer-এ রেখে দেয়। আপনি যদি লুপে আবার scanf("%d", …) ডাকেন, সে আবার “abc” দেখে, আবার 0 দেয় — অনন্তকাল। স্ক্রিন ভরে যায় একই প্রশ্নে। return value না দেখলে আপনি এটি টেরও পাবেন না।

যে দুটি “সমাধান” কখনো ব্যবহার করবেন না

gets(name)

পুরো লাইন পড়ে, কিন্তু কত ঘর জায়গা আছে তা জানার কোনো উপায়ই এর নেই — তাই overflow ঠেকানো অসম্ভব। ১৯৮৮ সালের বিখ্যাত Morris worm ছড়ানোর একটি পথ ছিল একটি সার্ভার প্রোগ্রামের gets. শেষ পর্যন্ত C11 standard gets-কে ভাষা থেকেই সরিয়ে দেয় — standard library থেকে কোনো ফাংশন বাদ দেওয়ার ঘটনা খুবই বিরল।

বদলে: fgets

fflush(stdin)

C standard-এ fflush শুধু আউটপুট stream-এর জন্য সংজ্ঞায়িত। ইনপুটে দিলে ফল undefined behavior. কিছু পুরোনো Windows compiler-এ এটি buffer খালি করত বলে পুরোনো বই ও টিউটোরিয়ালে ছড়িয়ে পড়েছে; কিন্তু অন্য সিস্টেমে সাধারণত কিছুই হয় না।

বদলে: পুরো লাইন fgets দিয়ে পড়া

৮ · পেশাদার পথ — আগে পুরো লাইন, তারপর যাচাই

সমস্যাগুলোর মূল একটাই: scanf একসাথে দুটি কাজ করে — পড়া আর বোঝা। বোঝায় ভুল হলে পড়াটাও এলোমেলো হয়ে যায়, buffer-এ আবর্জনা পড়ে থাকে। পেশাদার সমাধান হলো কাজ দুটিকে আলাদা করা:

  1. পড়া: fgets দিয়ে Enter পর্যন্ত পুরো একটি লাইন নিজের buffer-এ পড়ুন — নির্দিষ্ট সীমার মধ্যে। এতে stdin-এ আর কোনো \n বা আবর্জনা পড়ে থাকে না।
  2. বোঝা: সেই লাইনটি sscanf (লেখা থেকে scanf) বা strtol (লেখা থেকে long সংখ্যা) দিয়ে ভাঙুন, আর ফল যাচাই করুন। ভুল হলে শুধু এই লাইনটি বাদ দিয়ে আবার চাইলেই হলো।

এই কৌশলের নাম input validation — বাইরে থেকে আসা কোনো ডেটা যাচাই না করে ব্যবহার না করা। নিচের প্রোগ্রামটি বয়স চায়। পাতায় চালালে তিনটি ইনপুট আগে থেকেই দেওয়া আছে: abc, 25 years আর 27. দেখুন প্রথম দুটি কীভাবে ধরা পড়ে:

#include <stdio.h>

int main(void) {
    char line[64];
    int age;
    char extra;

    printf("Your age: ");
    fflush(stdout);
    while (fgets(line, sizeof line, stdin) != NULL) {
        if (sscanf(line, "%d %c", &age, &extra) == 1 && age > 0 && age < 130) {
            printf("\nOK, age = %d\n", age);
            return 0;
        }
        printf("\nInvalid: %sTry again: ", line);
        fflush(stdout);
    }
    printf("\nNo more input.\n");
    return 1;
}

fgets(line, sizeof line, stdin) সর্বোচ্চ ৬৩টি অক্ষর পর্যন্ত একটি লাইন পড়ে — sizeof line নিজেই জায়গার মাপ জানিয়ে দেয়, তাই overflow অসম্ভব। ইনপুট শেষ হলে এটি NULL ফেরত দেয়, আর লুপ থামে (লুপের বিস্তারিত পর্ব ১২-এ)। আসল কৌশল "%d %c"-এ: %d একটি সংখ্যা নেয়, তারপর whitespace বাদ দিয়ে %c দেখে আর কোনো অক্ষর আছে কি না। “27” হলে শুধু সংখ্যা মেলে — ফেরত 1, গৃহীত। “abc” হলে কিছুই মেলে না — 0. “25 years” হলে সংখ্যা আর একটি বাড়তি অক্ষর দুটোই মেলে — 2, তাই বাতিল। এরপর বয়সের সীমাও (০ থেকে ১৩০) যাচাই করা হয়েছে। লেখার ধরন ঠিক হলেই হবে না, অর্থও ঠিক হতে হবে। ফলাফলে প্রশ্নের পর একটি খালি লাইন দেখবেন — কারণ এখানে ইনপুট আসছে আগে থেকে দেওয়া লেখা থেকে, terminal-এ টাইপ করলে আপনার লেখাই সেই লাইনে থাকত।

আরও কড়া যাচাই চাইলে strtol. sscanf-এর %d খুব বড় সংখ্যা (যেমন 99999999999) পেলে কী করবে, তা standard নির্দিষ্ট করে না। strtol সীমা ছাড়ালে স্পষ্ট সংকেত দেয় আর কোথায় পড়া থেমেছে তাও জানায়। ব্যাংক, পরীক্ষার ফল বা সরকারি সেবার মতো যেখানে ভুলের দাম বেশি, সেখানে এটিই মানসম্মত পথ। এর ব্যবহার pointer শেখার পর (পর্ব ১৭-১৯) আরও পরিষ্কার হবে।

ইনপুট = অপরিচিত মানুষের দেওয়া চিঠি।
আগে পুরোটা হাতে নিন (fgets), তারপর খুলে যাচাই করুন (sscanf/strtol), তারপর বিশ্বাস করুন।

৯ · AI-এর যুগে ইনপুট-আউটপুট

“একটি প্রোগ্রাম লেখো যা নাম আর নম্বর নিয়ে গ্রেড দেখাবে” — এমন অনুরোধে AI প্রায় সবসময় এমন কোড দেয় যা ঠিক ইনপুটে সুন্দর চলে। সমস্যা দেখা দেয় ভুল ইনপুটে, বড় সংখ্যায়, বা আউটপুট ফাইলে পাঠালে — যেগুলো AI নিজে পরীক্ষা করে দেখে না। কারণ সে শিখেছে কোটি কোটি পুরোনো উদাহরণ থেকে, যার অনেকগুলো শুধু “ঠিক পথ” দেখায়।

AI-এর লেখা I/O কোডে যেসব ভুল বারবার দেখা যায়

যা দেখবেনবিপদযা হওয়া উচিত
gets(name)C11 থেকে ভাষায় নেই; overflow ঠেকানো অসম্ভবfgets(name, sizeof name, stdin)
scanf("%d", &x); — ফল দেখা হয়নিভুল ইনপুটে আবর্জনা মান বা অনন্ত লুপif (scanf(...) != 1) অথবা fgets + sscanf
fflush(stdin)Undefined behavior; বেশিরভাগ সিস্টেমে কাজই করে নাপুরো লাইন fgets দিয়ে পড়া, বা " %c"
long long-এর জন্য %dভুল সংখ্যা ছাপা, undefined behavior%lld; -Wall দিলে GCC ধরে
scanf("%s", name)লম্বা ইনপুটে buffer overflowscanf("%19s", name) বা fgets
printf(msg) — ব্যবহারকারীর লেখা সরাসরিFormat string আক্রমণprintf("%s", msg)
double পড়তে scanf("%f", &d)ভুল মান; scanf-এ float আর double আলাদাscanf("%lf", &d)

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

আমি C শিখছি। একটি C প্রোগ্রাম লিখে দাও যা ব্যবহারকারীর কাছ থেকে [এখানে কী কী ইনপুট লাগবে লিখুন] নেবে এবং [কী করবে লিখুন]. শর্ত: ১. ইনপুট নেবে fgets দিয়ে পুরো লাইন পড়ে, তারপর sscanf বা strtol দিয়ে যাচাই করে। gets, fflush(stdin) একদম নয়। ২. scanf বা sscanf-এর return value সবসময় যাচাই করবে। ভুল ইনপুটে পরিষ্কার বার্তা দিয়ে আবার চাইবে। ৩. ভুলের বার্তা stderr-এ লিখবে (fprintf(stderr, ...)), আসল ফলাফল stdout-এ। ৪. প্রতিটি printf ও scanf-এ ভ্যারিয়েবলের ধরনের সাথে মিলিয়ে সঠিক format specifier দেবে (long long হলে %lld, double পড়তে %lf). ৫. কোডটি যেন gcc -std=c17 -Wall -Wextra দিয়ে কোনো warning ছাড়াই compile হয়। ৬. শেষে ৫টি পরীক্ষার ইনপুট দাও — খালি লাইন, অক্ষর, ঋণাত্মক সংখ্যা, অনেক বড় সংখ্যা, অনেক লম্বা লেখা — আর প্রতিটিতে প্রোগ্রাম কী করবে বলো।

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

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

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

প্র ০১C কেন printf-কে এমনভাবে বানাল যে ভুল ধরনের মান দিলেও ভাষা আটকায় না? আজকের compiler কীভাবে এই ফাঁক বন্ধ করে?

printf-এর ঘোষণা হলো int printf(const char *format, ...);. ওই তিনটি বিন্দু (...) মানে “এরপর যতগুলো খুশি, যে ধরনের খুশি”। ১৯৭০-এর দশকে এটি ছিল বাস্তববাদী সিদ্ধান্ত। ভাষাকে ছোট রাখতে হবে, compiler-কে ছোট মেমরির কম্পিউটারে চলতে হবে, আর একটিমাত্র সাধারণ ফাংশন দিয়ে যেকোনো সংখ্যক মান ছাপাতে হবে। ভাষায় আলাদা “print” নির্দেশ না রেখে একটি library ফাংশন — সেই নমনীয়তার দাম হলো ধরন যাচাইয়ের অনুপস্থিতি।

ভেতরে যা ঘটে: ফাংশন ডাকার সময় argument-গুলো নির্দিষ্ট register বা stack-এ সাজানো হয়, কিন্তু তাদের ধরনের কোনো লেবেল সাথে যায় না। printf format string পড়ে অনুমান করে “এখন একটি int তুলতে হবে”, “এখন একটি double”. অনুমান ভুল হলে সে ভুল জায়গা থেকে ভুল আকারের byte পড়ে। x86-64-এ যেমন double সাধারণত আলাদা ধরনের register-এ যায়, তাই %d দিয়ে double ছাপালে সম্পূর্ণ অন্য একটি register-এর আবর্জনা আসে।

ফাঁক বন্ধের চেষ্টা:

  • Compiler-এর বাড়তি বুদ্ধি: GCC ও Clang printf-এর format string নিজে পড়ে মিলিয়ে দেখে। -Wformat (যা -Wall-এর অংশ) এই warning চালু করে। নিজের লেখা ফাংশনেও __attribute__((format(printf, 1, 2))) লিখে একই সুবিধা পাওয়া যায়।
  • সীমাবদ্ধতা: format string যদি চলার সময় তৈরি হয় (যেমন ফাইল থেকে পড়া), compiler তা দেখতে পায় না — যাচাইও হয় না।
  • নতুন ভাষার পথ: Rust-এর println! আর C++20-এর std::format format string compile-এর সময়ই বিশ্লেষণ করে; ভুল ধরন দিলে প্রোগ্রাম তৈরিই হয় না।

মূল উপলব্ধি: নমনীয়তা আর নিরাপত্তার মধ্যে প্রায়ই টানাটানি চলে। C নমনীয়তা বেছেছিল, আর পরের প্রজন্মের টুল ও ভাষা সেই সিদ্ধান্তের দাম কমানোর চেষ্টা করছে। আপনার কাজ হলো warning চালু রেখে সেই সুরক্ষাটুকু ব্যবহার করা।

প্র ০২printf(name) আর printf("%s", name) — দেখতে প্রায় এক, কিন্তু প্রথমটি কেন একটি গুরুতর নিরাপত্তা-ত্রুটি?

সাধারণ নামের ক্ষেত্রে দুটোই “Rahim” ছাপবে। কিন্তু প্রথমটিতে ব্যবহারকারীর লেখা নিজেই format string হয়ে গেছে — অর্থাৎ printf-এর নকশা এখন বাইরের মানুষের হাতে। কেউ নাম হিসেবে লিখল %x %x %x %x. printf চারটি argument খুঁজবে, অথচ একটিও দেওয়া হয়নি। আগের stepper-এ দেখেছেন সে তখন কী করে — “পরের জায়গা” থেকে যা আছে তাই ছাপে। ফলে stack-এ থাকা গোপন তথ্য (পাসওয়ার্ডের টুকরো, মেমরির ঠিকানা) স্ক্রিনে বেরিয়ে আসে।

আরও ভয়ংকর হলো %n. এটি কিছু ছাপে না, বরং “এখন পর্যন্ত কয়টি অক্ষর ছাপা হলো” সেই সংখ্যা একটি ঠিকানায় লেখে। আক্রমণকারী সতর্কভাবে বানানো format string দিয়ে মেমরির নির্দিষ্ট জায়গায় নিজের পছন্দের মান লিখতে পারে — এমনকি প্রোগ্রামের নিয়ন্ত্রণ নিয়ে নিতে পারে।

এই ধরনের ত্রুটি — format string vulnerability — ১৯৯৯-২০০০ সালের দিকে ব্যাপকভাবে আলোচনায় আসে, যখন বহুল ব্যবহৃত কয়েকটি সার্ভার প্রোগ্রামে এটি পাওয়া যায়। তারপর থেকে এটি নিরাপত্তা-শিক্ষার একটি ক্লাসিক অধ্যায়।

প্রতিরোধ:

  • নিয়ম একটাই: format string সবসময় আপনার কোডে লেখা একটি স্থির লেখা হবে। বাইরের লেখা ছাপাতে printf("%s", s) বা fputs(s, stdout).
  • GCC-র -Wformat-security এমন কোড দেখলে warning দেয়; কিছু Linux distribution এটি ডিফল্টভাবে চালু রাখে।
  • অনেক আধুনিক C library %n-এর ব্যবহার সীমিত করেছে — যেমন Microsoft-এর CRT-তে এটি ডিফল্টভাবে বন্ধ।

মূল উপলব্ধি: “ডেটা” আর “নির্দেশ” কখনো মিশতে দেবেন না। ব্যবহারকারীর লেখা যখনই নির্দেশ হিসেবে ব্যাখ্যা হয় — format string, SQL query, shell কমান্ড, এমনকি AI-এর prompt — তখনই injection ধরনের আক্রমণের দরজা খুলে যায়। একই শিক্ষা, ভিন্ন ভিন্ন রূপে।

প্র ০৩Buffer এত ঝামেলা তৈরি করে — দেরিতে লেখা, crash-এ হারানো। তাহলে buffering বাদ দিলেই তো হয়?

বাদ দেওয়া যায় — setvbuf(stdout, NULL, _IONBF, 0) লিখলেই stdout-এর buffer বন্ধ। কিন্তু তখন প্রতিটি অক্ষর বা প্রতিটি printf-এর জন্য একটি করে system call হবে। আর system call সস্তা নয়। প্রতিবার CPU-কে সাধারণ প্রোগ্রামের অবস্থা থেকে kernel-এর বিশেষ অবস্থায় যেতে হয়, নিরাপত্তা যাচাই হয়, তারপর ফিরে আসতে হয়। একটি সাধারণ যোগের তুলনায় এর খরচ অনেক গুণ বেশি।

একটি হিসাব ভাবুন। একটি প্রোগ্রাম ১০ লাখ লাইনের log ফাইল লিখছে। buffer না থাকলে লাখ লাখ system call; buffer থাকলে কয়েক হাজার byte একসাথে, তাই system call অনেক কম। বাস্তবে এই পার্থক্য প্রোগ্রামকে কয়েক গুণ, কখনো দশ গুণেরও বেশি দ্রুত করতে পারে।

এটি আসলে একটি সর্বজনীন প্রকৌশল-কৌশল — latency বনাম throughput-এর trade-off. Latency মানে একটি জিনিস পৌঁছাতে কত দেরি; throughput মানে মোট কতটা কাজ হয়। Buffer latency একটু বাড়িয়ে throughput অনেক বাড়ায়। একই চিন্তা দেখবেন:

  • ডিস্কে: OS লেখা সাথে সাথে ডিস্কে না পাঠিয়ে মেমরিতে জমায়। এজন্যই USB ড্রাইভ “Safely Remove” না করে খুলে ফেললে ফাইল নষ্ট হতে পারে।
  • নেটওয়ার্কে: TCP ছোট ছোট প্যাকেট জমিয়ে পাঠাতে পারে; অনলাইন গেমের মতো কাজে যেখানে দেরি সহ্য হয় না, সেখানে এই জমানো বন্ধ রাখা হয়।
  • ডেটাবেসে: টাকা লেনদেনের মতো কাজে “লেখা শেষ” বলার আগে ডেটাবেস নিশ্চিত করে যে তথ্য সত্যিই ডিস্কে পৌঁছেছে — কারণ crash-এ হারানো চলবে না।

C library-র ডিফল্টগুলোও এই চিন্তা থেকেই: terminal-এ মানুষ দেখছে, তাই line buffered (latency কম); ফাইলে কেউ সাথে সাথে দেখছে না, তাই fully buffered (throughput বেশি); আর stderr-এর বার্তা হারানো চলবে না, তাই প্রায় buffer ছাড়া।

মূল উপলব্ধি: কোনো ডিফল্টই “সবসময় সেরা” নয় — প্রতিটি একটি নির্দিষ্ট পরিস্থিতির জন্য বেছে নেওয়া। সিস্টেম বোঝা মানে জানা কোন ডিফল্ট কেন, আর কখন তা বদলাতে হবে।

প্র ০৪নিরাপত্তা বিশেষজ্ঞরা কেন বলেন “সব ইনপুটই শত্রু”? ইনপুট যাচাই কেন নিরাপত্তার প্রথম দেয়াল?

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

ইতিহাস এর সাক্ষী। ১৯৮৮ সালের Morris worm ইন্টারনেটের প্রথম বড় worm-গুলোর একটি; এটি ছড়ানোর একটি পথ ছিল একটি সার্ভার প্রোগ্রামে gets দিয়ে পড়া — সীমা না থাকায় লম্বা ইনপুট পাঠিয়ে মেমরি উপচে দেওয়া। পরের কয়েক দশকে buffer overflow, format string, SQL injection — প্রায় সব বিখ্যাত আক্রমণের মূলে একই ভুল: যাচাই না করা ইনপুটকে বিশ্বাস করা।

ভালো যাচাইয়ের কয়েকটি স্তর:

  • দৈর্ঘ্য: সবচেয়ে আগে — কত বড় হতে পারে? (fgets-এর সীমা, %19s)
  • ধরন: সংখ্যা চাইলে সত্যিই কি শুধু সংখ্যা? বাড়তি অক্ষর আছে কি না? ("%d %c" কৌশল)
  • সীমা: বয়স ঋণাত্মক বা ৫০০ হতে পারে না; bKash-এ পাঠানোর টাকা শূন্য বা ঋণাত্মক হতে পারে না।
  • Allow-list: “কোনগুলো খারাপ” তালিকা না বানিয়ে “কোনগুলো ঠিক” তা নির্দিষ্ট করা — কারণ খারাপ ইনপুটের তালিকা কখনো শেষ হয় না।

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

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

প্র ০৫“ছোট প্রোগ্রাম, text stream আর pipe” — Unix-এর এই দর্শন ৫০ বছর পরেও কেন প্রাসঙ্গিক?

১৯৬৪ সালে Bell Labs-এর Doug McIlroy একটি নোটে লিখেছিলেন — প্রোগ্রামগুলোকে বাগানের পানির hose-এর মতো জোড়া লাগানোর উপায় থাকা উচিত, যাতে দরকারমতো আরেকটি টুকরো জুড়ে দেওয়া যায়। প্রায় এক দশক পরে, ১৯৭৩ সালে, Ken Thompson Unix-এ pipe যোগ করেন। এরপর Unix-এর প্রোগ্রামগুলো নতুন করে লেখা হলো যাতে তারা stdin থেকে পড়ে stdout-এ লেখে।

এর পেছনের দর্শনটি McIlroy-র নামে এভাবে প্রচলিত: একটি প্রোগ্রাম একটি কাজ ভালোভাবে করবে; প্রোগ্রামগুলো একসাথে কাজ করার মতো হবে; আর তারা text stream ব্যবহার করবে, কারণ সেটিই সর্বজনীন interface.

কেন শক্তিশালী? sort জানে না report কী; head জানে না sort কী। তবু ./report | sort -n | head -3 একটি নতুন টুল বানিয়ে দিল — কেউ নতুন কোড লেখেনি। ১০টি ছোট প্রোগ্রাম থেকে শত শত সংযোগ সম্ভব। পর্ব ০৩-এর M×N সমস্যার কথা মনে আছে? এটিও একই কৌশল — মাঝখানে একটি সাধারণ interface (text stream).

আজ কোথায় দেখবেন:

  • সার্ভার চালানোর কাজে প্রতিদিনের shell script আর log বিশ্লেষণ।
  • Docker ও cloud প্ল্যাটফর্ম অ্যাপের log সংগ্রহ করে মূলত stdout আর stderr থেকেই — এজন্যই “log stdout-এ লিখুন” আধুনিক সার্ভিসের একটি পরিচিত নিয়ম।
  • ডেটা pipeline আর microservice-এর নকশায় — ছোট স্বাধীন অংশ, মাঝে একটি সাধারণ ফরম্যাট (প্রায়ই JSON).

সীমাবদ্ধতাও আছে: লেখা পার্স করা ভঙ্গুর — একটি কলাম বদলালে পরের প্রোগ্রাম ভেঙে যায়। এজন্য PowerShell-এর মতো shell লেখার বদলে object পাঠায়। কিন্তু মূল চিন্তা একই থাকে।

মূল উপলব্ধি: আপনার প্রোগ্রাম যদি stdin থেকে পড়ে, শুধু ফলাফল stdout-এ আর ভুল stderr-এ লেখে, আর ঠিক exit status দেয় — তাহলে সেটি এক বিশাল পরিবারের সদস্য হয়ে যায়, যার সাথে হাজারো টুল কোনো পরিবর্তন ছাড়াই জোড়া লাগে। ভালো নকশা মানে অন্যদের সাথে মিলেমিশে কাজ করতে পারা।

অনুশীলন

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

  1. আগে অনুমান, পরে মিলান: নিচের দুটি লাইন ঠিক কী ছাপবে, কাগজে লিখুন (ফাঁকা জায়গাসহ) — printf("[%5d][%-5d][%05d]\n", 42, 42, 42); আর printf("[%8.3f][%-8s]\n", 3.14159, "Dhaka");

    প্রথম লাইন: [ 42][42 ][00042] — ৫ ঘরে ডানে, ৫ ঘরে বাঁয়ে, আর ৫ ঘরে শূন্য দিয়ে ভরা।

    দ্বিতীয় লাইন: [ 3.142][Dhaka ] — “3.142” পাঁচ অক্ষরের, ৮ ঘর পূরণ করতে আগে তিনটি স্পেস; দশমিকের পর ৩ ঘর রেখে গোল করা হয়েছে (3.14159 → 3.142). “Dhaka” বাঁয়ে, পরে তিনটি স্পেস। মিলে গেলে width আর precision আপনার আয়ত্তে।

  2. রয়ে যাওয়া Enter শিকার: একটি প্রোগ্রাম প্রথমে scanf("%d", &n), তারপর scanf("%c", &ch) চালায়। ব্যবহারকারী লিখলেন 5, Enter, A, Enter. ch-এ কী থাকবে? কী বদলালে ঠিক হবে?

    ch-এ থাকবে '\n' — প্রথম Enter-এর অক্ষর, যা %d খায়নি। দ্বিতীয় scanf তাই অপেক্ষাই করবে না, আর “A” buffer-এ পড়ে থাকবে। সমাধান: scanf(" %c", &ch) — %c-এর আগের স্পেস সব whitespace বাদ দেয়। আরও ভালো সমাধান: দুটি ইনপুটই fgets দিয়ে আলাদা লাইনে পড়া।

  3. stdout আর stderr আলাদা করে দেখুন: পাতায় দেওয়া receipt.c নিজের কম্পিউটারে compile করুন। তারপর চালান ./receipt > bill.txt. স্ক্রিনে কী এলো? bill.txt-এ কী? এবার মোট লাইনটির printf বদলে fprintf(stderr, ...) করে আবার চালান।

    প্রথমবার স্ক্রিন সম্পূর্ণ ফাঁকা থাকবে — পুরো রসিদ চলে গেছে bill.txt-এ (cat bill.txt বা Notepad দিয়ে দেখুন)। দ্বিতীয়বার মোটের লাইনটি স্ক্রিনে দেখা যাবে, বাকি রসিদ ফাইলে। এটাই redirection-এর আসল অর্থ: প্রোগ্রাম একই, শুধু পাইপের মুখ বদলেছে। আর stderr-এর বার্তা redirection-এর পরেও আপনার চোখের সামনে থাকে।

  4. AI অডিট: যেকোনো AI-কে বলুন “একটি C প্রোগ্রাম লেখো যা ছাত্রের নাম ও তিন বিষয়ের নম্বর নিয়ে গড় দেখাবে।” কোনো শর্ত দেবেন না। তারপর উপরের চেকলিস্ট দিয়ে যাচাই করুন, আর নম্বরের জায়গায় “abc” ও নামের জায়গায় “Mohammad Abdur Rahman” দিয়ে চালিয়ে দেখুন।

    সাধারণত যা পাবেন:

    • scanf("%s", name) — সীমা নেই, আর পুরো নামের শুধু প্রথম শব্দ আসে (%s স্পেসে থামে); বাকি শব্দগুলো buffer-এ থেকে পরের নম্বরের %d-কে নষ্ট করে।
    • কোনো scanf-এর return value দেখা হয়নি; “abc” দিলে গড় হিসেবে আবর্জনা সংখ্যা।
    • কখনো কখনো double পড়তে %f — তখন মান সম্পূর্ণ ভুল আসে।

    এবার এই পর্বের prompt দিয়ে আবার চেয়ে দেখুন — পার্থক্যটাই শেখার বিষয়।

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

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

ক্রমকিবোর্ডে সংখ্যা টাইপ করা থেকে ভ্যারিয়েবলে পৌঁছানো পর্যন্ত ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. ⌨️ ব্যবহারকারী “25” টাইপ করেন — terminal লাইনটি ধরে রাখে
  2. ⏎ Enter চাপা হয় — পুরো লাইন \n-সহ stdin-এর buffer-এ যায়
  3. 🔢 %d অঙ্কগুলো পড়ে লেখাকে সংখ্যায় বদলায়
  4. 📍 সংখ্যাটি &age ঠিকানায় লেখা হয়
  5. ✅ scanf ফেরত দেয় কয়টি ঘর পূরণ হলো

প্রোগ্রামের আউটপুট > result.txt দিয়ে ফাইলে পাঠানো হচ্ছে। তবু ভুলের বার্তা স্ক্রিনে দেখাতে চান। কোথায় লিখবেন?

  • stdout-এ, printf দিয়ে
  • stdin-এ
  • fprintf(stderr, ...) দিয়ে stderr-এ
  • printf-এর পর fflush(stdout) দিয়ে

long long phone = 8801712345678LL; — এটি ছাপাতে সঠিক format specifier কোনটি?

  • %d
  • %ld
  • %f
  • %lld

scanf("%d", &age)-এ & কেন লাগে?

  • এটি বলে দেয় যে ইনপুট একটি পূর্ণসংখ্যা
  • scanf-কে ভ্যারিয়েবলের ঠিকানা দিতে, যাতে সে সেখানে লিখতে পারে
  • ইনপুট ভুল হলে প্রোগ্রাম থামাতে
  • শুধু পুরোনো রীতি, না দিলেও চলে

int r = scanf("%d", &x); — ব্যবহারকারী লিখলেন abc তারপর Enter. r-এর মান কত?

  • 0
  • 1
  • 3
  • EOF

scanf("%d", &n); এর পর scanf("%c", &ch); উত্তরের অপেক্ষা না করেই এগিয়ে যায়। কারণ ও সমাধান কী?

  • stdin-এ আবর্জনা আছে; fflush(stdin) দিন
  • %c দুটি অক্ষর পড়ে; %1c দিন
  • আগের Enter-এর \n রয়ে গেছে; " %c" লিখুন
  • char-এর জন্য %s লাগে

একটি প্রোগ্রাম ./app > log.txt দিয়ে চলছিল, হঠাৎ Segmentation fault. log.txt প্রায় খালি, যদিও অনেক printf চলেছিল। সবচেয়ে সম্ভাব্য কারণ?

  • printf ফাইলে লিখতে পারে না
  • ফাইলে যাওয়ায় stdout fully buffered ছিল, জমানো লেখা crash-এ হারিয়েছে
  • Crash-এর সময় OS ফাইলটি মুছে দিয়েছে
  • printf-এর লেখা stderr-এ গিয়েছে

শব্দকোষ

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

মনে রাখুন

  1. প্রতিটি প্রোগ্রামের তিনটি পাইপ: stdin (0), stdout (1), stderr (2). ফলাফল stdout-এ, ভুলের বার্তা stderr-এ; <, >, | দিয়ে shell পাইপের মুখ ঘোরায়।
  2. আউটপুট আগে buffer-এ জমে; terminal-এ \n-এ, ফাইলে buffer ভরলে বের হয়। দরকারে fflush(stdout); crash-এ জমানো লেখা হারায়।
  3. printf format string-কে অন্ধভাবে বিশ্বাস করে — প্রতিটি %-এর জন্য সঠিক ধরনের একটি মান (%lld, %.2f, %%), আর সবসময় -Wall.
  4. scanf: & দিন, return value দেখুন, %19s-এর মতো সীমা দিন, %c-এর আগে স্পেস। gets ও fflush(stdin) কখনো নয়।
  5. নিরাপদ পথ: fgets দিয়ে পুরো লাইন, তারপর sscanf/strtol দিয়ে যাচাই। বাইরের সব ইনপুটকে সন্দেহ করুন।

পরের পর্বে: আজ বারবার দেখলেন, scanf 1 ফেরত দিলে এক কাজ, 0 দিলে আরেক কাজ করতে হবে। কিন্তু প্রোগ্রাম কীভাবে “সিদ্ধান্ত” নেয়? পরের পর্বে if, else আর switch — প্রোগ্রামকে পথ বেছে নিতে শেখানো।

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

  • পর্ব ১১ · সিদ্ধান্ত — if, else, switchপরবর্তী পর্বইনপুট যাচাইয়ের ফল দেখে প্রোগ্রাম কীভাবে পথ বেছে নেয়।
  • C প্রোগ্রামিং · কোর্স হোম৫৬টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
  • Operating Systemsগভীরে যানFile descriptor, system call আর pipe — OS-এর ভেতর থেকে stream কীভাবে কাজ করে।
  • Cybersecurityপাশের ট্র্যাকBuffer overflow, format string আর injection — যাচাই না করা ইনপুট থেকে জন্ম নেওয়া আক্রমণ।
এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান