এই পর্বে যা শিখবেন
- মেমরিতে একটি স্ট্রিং বাইট ধরে ধরে কেমন দেখায়, আর
'\0'কেন এত গুরুত্বপূর্ণ - ১৯৭০-এর দশকে C কেন এই নকশা বেছে নিয়েছিল, আর আজও তার কী দাম দিতে হয়
<string.h>-এর প্রতিদিনের ফাংশনগুলো — কোনটি নিরাপদ, কোনটি ফাঁদ- Buffer overflow কীভাবে ঘটে, আর মাপ জেনে লেখার পেশাদার অভ্যাস
- বাংলা লেখায় বাইট, code point আর চোখে দেখা অক্ষরের পার্থক্য — আর AI-এর স্ট্রিং-কোড যাচাইয়ের চেকলিস্ট
১ · স্ট্রিং আসলে কী — char-এর সারি আর শেষের শূন্য
আপনার ফোনে bKash থেকে একটি SMS এলো: “You have received Tk 500”. আপনার চোখে এটি একটি বাক্য। কিন্তু মেমরিতে এটি শুধু পাশাপাশি রাখা কিছু বাইট — প্রতিটি অক্ষরের জন্য একটি নম্বর (পর্ব ০৫-এ দেখেছেন, 'Y'-এর ASCII নম্বর ৮৯)। প্রোগ্রামিংয়ে এমন লেখাকে বলে string.
Python বা Java-তে string একটি আলাদা টাইপ — সে নিজেই জানে সে কত লম্বা। C-তে এমন কিছু নেই। C-এর স্ট্রিং মানে স্রেফ char-এর একটি array (পর্ব ১৬), আর একটি চুক্তি: লেখা যেখানে শেষ, তার ঠিক পরের বাইটে থাকবে শূন্য। এই শূন্য বাইটটির নাম null terminator, কোডে লেখা হয় '\0'.
ক্লাসের রোল-কলের খাতার কথা ভাবুন। খাতার ওপরে কোথাও লেখা নেই “আজ ৩৭ জন”। বরং শিক্ষক শেষ নামের নিচে একটি লাল দাগ টেনে দেন। যিনিই খাতা পড়বেন, তাঁকে ওপর থেকে নামতে নামতে লাল দাগ পর্যন্ত যেতে হবে। দাগটি মুছে গেলে? পাঠক পরের পাতার পুরোনো নামগুলোও পড়ে ফেলবেন — কেউ টেরও পাবে না। C-এর স্ট্রিংয়ে '\0' হলো সেই লাল দাগ।
নিচের ঘোষণাটি দেখুন: char city[8] = "Dhaka";. এখানে ৮টি বাইটের জায়গা নেওয়া হলো, কিন্তু লেখা মাত্র ৫ অক্ষরের। মেমরিতে আসলে কী আছে, আর strlen কীভাবে দৈর্ঘ্য বের করে — ধাপে ধাপে দেখুন:
| i (কোন ঘরে আছে) | |
| city[i] | |
| এ পর্যন্ত গোনা অক্ষর |
- ৮টি ঘর বরাদ্দ। Compiler পাশাপাশি ৮টি বাইট দিল, তারপর
"Dhaka"-র ৫টি অক্ষর প্রথম ৫ ঘরে কপি করল। প্রতিটি ঘরের নিচে অক্ষরটির নম্বর hex-এ লেখা — 'D' মানে আসলে0x44. বাকি ঘরগুলো C-এর নিয়মে শূন্য দিয়ে ভরে যায়। - শেষ-চিহ্ন
'\0'. ঘর ৫-এ বসেছে শূন্য বাইট — এটিই null terminator, লেখার শেষ। ঘর ৬ আর ৭-ও শূন্য, কিন্তু এগুলো শুধু খালি জায়গা। খেয়াল করুন:sizeof(city)হলো ৮ — এটি জায়গার মাপ। লেখার দৈর্ঘ্য কোথাও লেখা নেই! strlenহাঁটা শুরু করল। দৈর্ঘ্য জানার একমাত্র উপায় হলো শুরু থেকে গোনা। ঘর ০-তে 'D' — শূন্য নয়, তাই গুনে সামনে এগোয়। গোনা: ১।- ঘর ১: 'h'. এখনো শূন্য নয়। গোনা: ২।
strlenজানে না সামনে কত ঘর আছে — সে শুধু পরের ঘরটি দেখে। - ঘর ২: 'a'. গোনা: ৩।
- ঘর ৩: 'k'. গোনা: ৪।
- ঘর ৪: 'a'. গোনা: ৫।
- ঘর ৫:
'\0'— থামো! শূন্য পাওয়া মাত্রstrlenথেমে ফেরত দিল ৫. ঘর ৬ আর ৭ সে কখনো দেখেনি। এখন ভাবুন: ঘর ৫-এ যদি শূন্য না থাকত?strlenথামত না — array পেরিয়ে পাশের মেমরিতে ঢুকে যেত, যতক্ষণ না কোথাও কোনো শূন্য বাইট পায়। একে বলে buffer over-read.
sizeof আর strlen আলাদা প্রশ্নের উত্তর দেয়। sizeof city বলে “কত বাইট জায়গা নেওয়া হয়েছে” — compile করার সময়ই জানা, তাই কোনো খরচ নেই। strlen(city) বলে “এখন কত অক্ষরের লেখা আছে” — প্রোগ্রাম চলার সময় প্রতিবার গুনে বের করতে হয়। আর নিয়মটি মুখস্থ রাখুন: N অক্ষরের লেখার জন্য কমপক্ষে N + 1 বাইট জায়গা লাগে। ওই বাড়তি ১ বাইটটি '\0'-এর।
শূন্য-সংক্রান্ত চারটি জিনিস নতুনরা প্রায়ই গুলিয়ে ফেলেন। এগুলোর পার্থক্য একবার পরিষ্কার করে নিই:
| কোডে লেখা | আসলে কী | মেমরিতে মান |
|---|---|---|
'\0' | Null character — স্ট্রিংয়ের শেষ-চিহ্ন | একটি বাইট, মান ০ |
'0' | শূন্য অঙ্ক — যে অক্ষর আপনি কিবোর্ডে টাইপ করেন | একটি বাইট, মান ৪৮ (0x30) |
"" | খালি স্ট্রিং — কোনো অক্ষর নেই, শুধু শেষ-চিহ্ন | ১ বাইট: শুধু '\0' |
NULL | Null pointer — “কোনো ঠিকানা নেই” (পর্ব ১৭) | একটি ঠিকানা, অক্ষর নয় |
একটি সূক্ষ্ম ফাঁদ: char code[5] = "Dhaka"; C-তে বৈধ! ৫টি অক্ষর ৫টি ঘরে ঢুকে যায়, কিন্তু '\0'-এর জায়গা থাকে না — compiler সেটি চুপচাপ বাদ দেয়। ফলে code আর স্ট্রিং নয়, শুধু ৫টি অক্ষরের array. অনেক compiler এখানে কোনো সতর্কবার্তাও দেয় না (C++ অবশ্য এটিকে সরাসরি error ধরে)। নিরাপদ অভ্যাস: মাপ নিজে না লিখে char code[] = "Dhaka"; লিখুন — compiler নিজেই ৬ বাইট নেবে।
২ · কেন এমন নকশা — ১৯৭০-এর দশকের হিসাব আর আজকের দাম
“দৈর্ঘ্যটা কোথাও লিখে রাখলেই তো হতো!” — প্রশ্নটি যৌক্তিক। আসলে বিকল্প নকশাটি তখনও ছিল। লেখার শুরুতে একটি ঘরে দৈর্ঘ্য রাখা, তারপর অক্ষরগুলো — একে বলে length-prefixed string. C-এর পূর্বসূরি ভাষা BCPL ঠিক এটাই করত, আর Pascal-এর অনেক পুরোনো রূপও।
Dennis Ritchie তাঁর “The Development of the C Language” প্রবন্ধে এই সিদ্ধান্তের কথা লিখেছেন। BCPL-এ স্ট্রিংয়ের প্রথম বাইটে থাকত অক্ষরের সংখ্যা। কিন্তু তার পরের ভাষা B (আর পরে C) বেছে নেয় শেষে একটি বিশেষ চিহ্ন রাখার পথ। Ritchie-র যুক্তি ছিল দুটি: এক বাইটে দৈর্ঘ্য রাখলে কোনো লেখা ২৫৫ অক্ষরের বেশি হতে পারে না, আর তাঁর অভিজ্ঞতায় শেষ-চিহ্ন দিয়ে কাজ করা গোনা হিসাব রাখার চেয়ে সুবিধাজনক মনে হয়েছিল। সেই সময়ের কম্পিউটারে মেমরি ছিল মাত্র কয়েক ডজন কিলোবাইট — প্রতিটি বাইটই ছিল দামি।
এই নকশার সুবিধা
বাড়তি খরচ মাত্র ১ বাইট, দৈর্ঘ্যের কোনো সীমা নেই। লেখার মাঝখানের যেকোনো ঠিকানাও নিজেই একটি বৈধ স্ট্রিং — city + 2 মানে “aka”, কোনো কপি ছাড়াই। হার্ডওয়্যারের খুব কাছাকাছি, খুব সরল।
এর দাম
দৈর্ঘ্য জানতে প্রতিবার পুরো লেখা হাঁটতে হয়। লেখার ভেতরে শূন্য বাইট রাখা যায় না, তাই ছবি বা ফাইলের মতো বাইনারি ডেটা “স্ট্রিং” হিসেবে চলে না। আর সবচেয়ে বড় দাম — জায়গার মাপ লেখার সাথে থাকে না, তাই কোনো ফাংশন নিজে জানে না কতটুকু লেখা নিরাপদ।
গতি ও নিরাপত্তার ঝুঁকিপ্রথম দামটি একটু হিসাব করে দেখি। strlen-এর কাজ লেখার দৈর্ঘ্যের সমানুপাতিক — ১০ অক্ষরে ১০ ধাপ, ১০ লক্ষ অক্ষরে ১০ লক্ষ ধাপ। Time complexity-র ভাষায় এটি O(n). Python-এ len(s) সাথে সাথে উত্তর দেয়, কারণ দৈর্ঘ্য আগে থেকেই লেখা থাকে।
এর একটি মজার পরিণতি আছে। ধরুন আপনি একটি লুপে বারবার strcat দিয়ে লেখার শেষে নতুন শব্দ জুড়ছেন। প্রতিবার strcat-কে আগে শুরু থেকে হেঁটে শেষটা খুঁজতে হয়। লেখা যত বড় হয়, প্রতিটি জোড়া তত ধীর হয়। প্রোগ্রামার Joel Spolsky ২০০১ সালে এর নাম দেন “Shlemiel the painter's algorithm” — এক রংমিস্ত্রির গল্প থেকে, যে রাস্তায় দাগ টানে কিন্তু রঙের কৌটা রেখে আসে শুরুর জায়গায় — তাই প্রতিদিন তাকে আরও বেশি হাঁটতে হয়।
২০১১ সালে FreeBSD-এর প্রবীণ ডেভেলপার Poul-Henning Kamp একটি প্রবন্ধ লেখেন, যার শিরোনাম “The Most Expensive One-byte Mistake” — “সবচেয়ে দামি এক বাইটের ভুল”। তাঁর মতে, দৈর্ঘ্য না রেখে শেষ-চিহ্ন রাখার সিদ্ধান্তটি পরের দশকগুলোতে নিরাপত্তা-ত্রুটি, ধীর কোড আর বাড়তি হার্ডওয়্যার-কৌশলে বিপুল খরচ ঘটিয়েছে। সবাই এই মূল্যায়নের সাথে পুরোপুরি একমত নন, কিন্তু প্রশ্নটি আজও আলোচিত। ভাবনার প্রশ্ন ১-এ আমরা এটি নিয়ে গভীরে যাব।
৩ · দুই রকম ঘোষণা — char s[] বনাম char *p
কোডে দুটি সোজা ডাবল কোটের মাঝে সরাসরি লেখা টেক্সট — যেমন "Dhaka" — হলো একটি string literal. Compiler এই লেখাটি প্রোগ্রাম ফাইলের ভেতরে একটি বিশেষ অংশে রেখে দেয়, যেটি প্রোগ্রাম চলার সময় সাধারণত read-only memory হিসেবে চিহ্নিত থাকে — পড়া যায়, বদলানো যায় না। এবার দেখুন, একই literal দিয়ে দুই রকম ঘোষণার মধ্যে কত বড় পার্থক্য:
char s[] = "Dhaka"; // নিজের array — literal থেকে ৬ বাইট কপি
s[0] = 'd'; // ঠিক আছে: s এখন "dhaka"
char *p = "Dhaka"; // p শুধু literal-এর ঠিকানা রাখে, কপি নয়
p[0] = 'd'; // undefined behavior — প্রায়ই Segmentation fault
const char *q = "Dhaka"; // সঠিক অভ্যাস: const লিখে দিন
q[0] = 'd'; // এবার compiler নিজেই error দেবে
অফিসের নোটিশ বোর্ডে একটি ছাপানো নোটিশ টাঙানো আছে। char s[] = "Dhaka"; মানে নোটিশটি নিজের খাতায় টুকে নেওয়া — খাতায় যত খুশি কাটাকাটি করুন, কেউ কিছু বলবে না। char *p = "Dhaka"; মানে খাতায় শুধু লিখে রাখা “নোটিশ বোর্ডের তৃতীয় কাগজটি”। এখন যদি সেই ঠিকানা ধরে গিয়ে নোটিশের ওপর কলম চালান — দারোয়ান (অপারেটিং সিস্টেম) এসে আপনাকে বের করে দেবে। এটিই Segmentation fault.
| প্রশ্ন | char s[] = "Dhaka"; | char *p = "Dhaka"; |
|---|---|---|
| লেখাটি কোথায়? | নিজের array-তে (সাধারণত stack-এ) একটি কপি | Read-only অংশে; p একটি pointer, শুধু ঠিকানা রাখে |
| অক্ষর বদলানো যায়? | হ্যাঁ | না — চেষ্টা করলে undefined behavior |
sizeof | ৬ (৫ অক্ষর + '\0') | pointer-এর মাপ — 64-bit সিস্টেমে সাধারণত ৮ |
| পরে অন্য লেখা বসানো | s = "Khulna"; চলবে না — array-তে সরাসরি মান বসানো যায় না; কপি করতে হয় | p = "Khulna"; চলবে — p এখন অন্য literal-এর দিকে তাকায় |
কেন C ভাষা literal বদলানোকে বৈধ রাখেনি? কারণ compiler একই লেখার একাধিক literal-কে একটিমাত্র কপিতে মিলিয়ে দিতে পারে। আপনার কোডে দশ জায়গায় "Dhaka" থাকলে মেমরিতে হয়তো একটিই আছে। একজন সেটি বদলালে বাকি নয় জায়গাও নিঃশব্দে বদলে যেত! তাই নিয়ম সহজ: literal-এর ঠিকানা রাখলে সবসময় const char * লিখুন। তাহলে ভুলটি প্রোগ্রাম চলার সময় নয়, compile করার সময়ই ধরা পড়বে।
৪ · string.h — প্রতিদিনের যন্ত্রপাতি আর == কেন কাজ করে না
স্ট্রিং নিয়ে কাজের ফাংশনগুলো থাকে standard library-র string.h header-এ। এদের সবার ভেতরে একটি লুপ আছে, যা '\0' খোঁজে — ঠিক যেমন strlen-এ দেখলেন। মুখস্থ করার দরকার নেই; চিনে রাখুন কোনটির কী কাজ, আর কোনটির গায়ে লাল পতাকা:
| ফাংশন | কাজ | ফেরত দেয় | সাবধানতা |
|---|---|---|---|
strlen(s) | দৈর্ঘ্য গোনা | '\0'-এর আগে কত বাইট | শেষ-চিহ্ন না থাকলে সীমা পেরিয়ে পড়ে |
strcpy(dst, src) | src-কে dst-এ কপি | dst | 🚩 dst-এর মাপ জানে না |
strncpy(dst, src, n) | সর্বোচ্চ n বাইট কপি | dst | 🚩 '\0' না-ও বসাতে পারে (অংশ ৫) |
strcat(dst, src) | dst-এর শেষে src জোড়া | dst | 🚩 dst-এর মাপ জানে না |
strcmp(a, b) | দুটি লেখা তুলনা | ঋণাত্মক, ০ বা ধনাত্মক | “সমান” মানে ০ — 1 নয়! |
strchr(s, c) | প্রথম অক্ষর c খোঁজা | সেই জায়গার ঠিকানা, না পেলে NULL | NULL যাচাই করে তারপর ব্যবহার |
strstr(s, "sub") | লেখার ভেতরে আরেকটি লেখা খোঁজা | ঠিকানা বা NULL | একই |
strcspn(s, "\n") | প্রথম কোন ঘরে তালিকার কোনো অক্ষর আছে | সেই ঘরের নম্বর | newline সরাতে দারুণ (অংশ ৭) |
snprintf(buf, size, ...) | মাপ মেনে লেখা তৈরি (stdio.h) | পুরো লেখার জন্য দরকারি দৈর্ঘ্য | ✅ কপি ও জোড়ার নিরাপদ পথ |
লেখা মেলাতে == কেন ভুল
পর্ব ১৮-তে দেখেছেন, array-র নাম বেশিরভাগ জায়গায় তার প্রথম ঘরের ঠিকানায় পরিণত হয় — একে বলে array decay. তাই if (answer == "yes") লিখলে C জিজ্ঞেস করে, “answer-এর ঠিকানা আর এই literal-এর ঠিকানা কি এক?” — প্রায় কখনোই এক নয়, যদিও দুটিতেই হুবহু “yes” লেখা। এটি দুটি বাড়ির ঠিকানা মিলিয়ে দেখার মতো, ভেতরে একই আসবাব আছে কি না নয়। ভাগ্য ভালো হলে compiler সতর্ক করে: comparison with string literal results in unspecified behavior.
লেখা মেলানোর সঠিক যন্ত্র strcmp. এটি দুটি লেখা বাঁ থেকে একসাথে হাঁটে, প্রথম যেখানে অমিল পায় সেখানে থামে। দেখুন "Dhaka" আর "Dhanmondi"-র তুলনা:
| i | |
| a[i] (ASCII) | |
| b[i] (ASCII) | |
| সিদ্ধান্ত |
- দুটি লেখা, দুটি আলাদা জায়গায়।
strcmp(a, b)দুজনকে ঘর ০ থেকে একসাথে পড়বে। প্রশ্ন একটাই: প্রথম কোন ঘরে দুজনের অক্ষর আলাদা? - ঘর ০: 'D' আর 'D'. দুটির নম্বরই ৬৮ — সমান, তাই পরের ঘরে যায়।
- ঘর ১: 'h' আর 'h'. সমান।
- ঘর ২: 'a' আর 'a'. এখনো সমান। লক্ষ করুন,
strcmpকখনো দৈর্ঘ্য আগে মাপে না — সে শুধু অমিল বা'\0'খোঁজে। - ঘর ৩: 'k' আর 'n' — প্রথম অমিল! 'k'-এর নম্বর ১০৭, 'n'-এর ১১০. প্রথমটি ছোট, তাই
strcmpসাথে সাথে থেমে একটি ঋণাত্মক সংখ্যা ফেরত দেয়। বাকি অক্ষরগুলো (“a” আর “mondi”) সে কখনো দেখেই না। - ফলের অর্থ। ঋণাত্মক মানে অভিধানের ক্রমে a আগে, ধনাত্মক মানে b আগে, ০ মানে হুবহু এক। কত ঋণাত্মক, তা standard বলে না — তাই
== -1নয়, সবসময়< 0,== 0,> 0দিয়ে যাচাই করুন। আর মনে রাখুন, এটি ASCII-র ক্রম: বড় হাতের 'Z' (৯০) ছোট হাতের 'a' (৯৭)-এর আগে আসে।
এবার এই পর্যন্ত যা দেখলেন, একটি প্রোগ্রামে মিলিয়ে নিন। এখানে sizeof বনাম strlen, == বনাম strcmp, আর মাপ মেনে লেখা তৈরির snprintf — তিনটিই আছে:
#include <stdio.h>
#include <string.h>
int main(void) {
char city[8] = "Dhaka";
char copy[8] = "Dhaka";
const char *p = city, *q = copy;
char msg[16];
printf("sizeof = %zu, strlen = %zu\n", sizeof city, strlen(city));
printf("p == q -> %d (compares addresses)\n", p == q);
printf("strcmp(p, q) -> %d (compares letters)\n", strcmp(p, q));
printf("Dhaka before Dhanmondi? %d\n", strcmp(city, "Dhanmondi") < 0);
int need = snprintf(msg, sizeof msg, "Hi %s, welcome to ABCL TECH", city);
if (need >= (int)sizeof msg)
printf("cut! needed %d, kept \"%s\"\n", need, msg);
return 0;
}প্রথম লাইনে দেখবেন জায়গা ৮, কিন্তু লেখা ৫। city আর copy-তে হুবহু একই লেখা, তবু p == q দেয় 0 — কারণ দুটি আলাদা জায়গায় আছে। strcmp দেয় 0, অর্থাৎ লেখা সমান। শেষ অংশটি সবচেয়ে গুরুত্বপূর্ণ: msg-এ মাত্র ১৬ বাইট, অথচ পুরো বাক্যের দরকার ৩০টি অক্ষর। snprintf সর্বোচ্চ ১৫টি অক্ষর লিখে নিজেই '\0' বসায় — তাই কোনো overflow হয় না। সাথে ফেরত দেয় পুরো লেখার জন্য আসলে কত লাগত (৩০)। ফেরত মান জায়গার সমান বা বেশি হলেই বুঝবেন লেখা কেটে ছোট করা হয়েছে, আর সেটি হাতেনাতে ধরতে পারবেন। চালালে একটি warning-ও দেখবেন: directive output truncated. এটি GCC-র উপহার — লেখাগুলো কোডেই আছে বলে compiler আগেই হিসাব করে ফেলেছে যে ১৬ বাইটে এই বাক্য আঁটবে না। ব্যবহারকারীর ইনপুটের ক্ষেত্রে compiler এটি জানতে পারে না, তাই ফেরত মান যাচাই করা আপনার দায়িত্ব।
৫ · বিপদ: strcpy, strcat আর buffer overflow
strcpy একটি খুব বিশ্বস্ত কর্মচারী — এতটাই বিশ্বস্ত যে সে কোনো প্রশ্ন করে না। তাকে বলা হয় “src থেকে dst-এ কপি করো”, আর সে '\0' না পাওয়া পর্যন্ত কপি করতেই থাকে। dst-এ কত জায়গা আছে, সেটি জানার কোনো উপায়ই তার নেই — কারণ C-এর স্ট্রিংয়ের সাথে মাপ থাকে না। strcat-এরও একই অবস্থা।
নিচের ডায়াগ্রামে একটি লগইন প্রোগ্রামের অংশ দেখুন। ধরা যাক মেমরিতে name-এর ঠিক পরেই role বসেছে। (বাস্তবে কোন ভ্যারিয়েবল কোথায় বসবে, তা compiler ঠিক করে — তাই এটি “যা ঘটতে পারে” তার একটি উদাহরণ, নিশ্চিত ফল নয়।)
| role-এ লেখা | |
| strlen(name) |
- দুটি ভ্যারিয়েবল, পাশাপাশি।
char name[8];এখনো খালি — ভেতরে garbage value (?)।char role[8] = "user";— সাধারণ ব্যবহারকারী। প্রোগ্রাম পরেroleদেখে ঠিক করবে কাকে কতটুকু ক্ষমতা দেবে। - সাধারণ ইনপুট: “Rafi”.
strcpy(name, input);৪টি অক্ষর আর'\0'— মোট ৫ বাইট, ৮ ঘরে আরামে আঁটে।role-এ কেউ হাত দেয়নি। এভাবে হাজারবার পরীক্ষা করলেও প্রোগ্রামটি নিখুঁত মনে হবে। - এবার একজন আক্রমণকারী লিখলেন: “XXXXXXXXadmin”. ১৩টি অক্ষর।
strcpyপ্রথম ৮টি X দিয়েname-এর সব ঘর ভরে ফেলল।name-এ আর'\0'-এর জায়গা নেই — কিন্তুstrcpyথামবে না, কারণ সে এখনো'\0'পায়নি। - সীমা পেরিয়ে লেখা। বাকি “admin” আর
'\0'গিয়ে পড়ল ঠিক পাশের ঘরগুলোতে — অর্থাৎrole-এর ওপর। কোনো error নেই, কোনো সতর্কতা নেই। এটিই buffer overflow. - ফলাফল। প্রোগ্রাম
roleপড়ে দেখল “admin” — আক্রমণকারী শুধু একটি নাম লিখে প্রশাসকের ক্ষমতা পেয়ে গেলেন! আরname-এর ভেতরে কোনো শেষ-চিহ্ন নেই, তাইstrlen(name)পাশের মেমরিতে ঢুকে ১৩ গুনল। C-এর ভাষায় পুরো ঘটনাটি undefined behavior — অন্য বিন্যাসে crash হতে পারত, অথবা এর চেয়েও খারাপ কিছু। - সমাধান: মাপ জানিয়ে দেওয়া।
snprintf(name, sizeof name, "%s", input);— এবার ফাংশন জানে ৮ বাইটের বেশি লেখা যাবে না। সে ৭টি X লিখে শেষ ঘরে নিজেই'\0'বসাল। নাম কেটে ছোট হলো, কিন্তুroleনিরাপদে “user”-ই রইল।
বাস্তবের আক্রমণ আরও ভয়ংকর। ফাংশনের local array থাকে stack-এ, আর তার কাছেই থাকে return address — ফাংশন শেষে প্রোগ্রাম কোথায় ফিরবে (পর্ব ১৩)। লম্বা ইনপুট দিয়ে সেই ঠিকানাটি মুছে নিজের পছন্দের ঠিকানা বসাতে পারলে আক্রমণকারী প্রোগ্রামের নিয়ন্ত্রণই নিয়ে নিতে পারেন। ১৯৯৬ সালে Phrack ম্যাগাজিনে Aleph One-এর লেখা “Smashing the Stack for Fun and Profit” প্রবন্ধটি এই কৌশল ধাপে ধাপে ব্যাখ্যা করে, আর তারপর এ ধরনের আক্রমণ ব্যাপকভাবে ছড়িয়ে পড়ে। পর্ব ১০-এ দেখা ১৯৮৮ সালের Morris worm-ও একই শ্রেণির একটি দুর্বলতা কাজে লাগিয়েছিল।
আজকের compiler আর অপারেটিং সিস্টেম কিছু পাহারা বসিয়েছে। যেমন অনেক Linux সিস্টেমে GCC ডিফল্টভাবে stack-এ একটি গোপন মান রাখে — একে বলে stack canary, খনির খাঁচার সেই ক্যানারি পাখির নামে, যে বিষাক্ত গ্যাসে সবার আগে মারা যেত। ফাংশন শেষে মানটি বদলে গেছে দেখলে প্রোগ্রাম সাথে সাথে বন্ধ হয়ে যায়, আর দেখায় *** stack smashing detected ***. কিন্তু এগুলো শেষ রক্ষা, সমাধান নয় — সব ক্ষেত্রে কাজ করে না। আসল সমাধান হলো overflow হতে না দেওয়া।
strncpy-এর ফাঁদ — নাম শুনে নিরাপদ, আসলে নয়
অনেকে ভাবেন, strcpy-এর নিরাপদ রূপ হলো strncpy, কারণ সে একটি মাপ নেয়। কিন্তু এর আচরণ অদ্ভুত। লেখা যদি জায়গার সমান বা বড় হয়, strncpy ঠিক n বাইট কপি করে থেমে যায় — '\0' বসায় না। আবার লেখা ছোট হলে বাকি সব ঘর শূন্য দিয়ে ভরে, অকারণে সময় খরচ করে।
char team[6];
strncpy(team, "Bangladesh", sizeof team); // কপি হলো "Bangla" — ৬টি অক্ষর
printf("%s\n", team); // '\0' নেই! পাশের আবর্জনাও ছাপা হতে পারে
strncpy(team, "Bangladesh", sizeof team - 1);
team[sizeof team - 1] = '\0'; // নিজের হাতে শেষ-চিহ্ন বসাতে হয়
এই অদ্ভুত আচরণের কারণ ঐতিহাসিক। strncpy মূলত তৈরি হয়েছিল পুরোনো Unix-এর নির্দিষ্ট মাপের ঘরের জন্য (যেমন ফাইলের নামের ১৪ বাইটের ঘর), যেখানে নাম পুরো ঘর জুড়ে থাকলে শেষ-চিহ্নের দরকারই ছিল না। সাধারণ স্ট্রিং কপির জন্য এটি কখনো বানানো হয়নি। তাই এর চেয়ে snprintf সহজ ও নির্ভরযোগ্য।
৬ · নিরাপদ অভ্যাস — সবসময় মাপ জেনে লিখুন
Buffer overflow ঠেকানোর মূল কথা একটাই: যে ফাংশন লিখবে, তাকে জায়গার মাপ জানাতে হবে। নিচের অভ্যাসগুলো পেশাদার C কোডে প্রতিদিনের নিয়ম:
কপি
snprintf(dst, sizeof dst, "%s", src); — কখনো সীমা পেরোয় না, সবসময় '\0' বসায়।
জোড়া
snprintf(full, sizeof full, "%s %s", first, last); — একবারে পুরো লেখা তৈরি, কোনো ধাপে মাপ হারায় না।
ইনপুট
fgets(buf, sizeof buf, stdin) — gets নয়, সীমাহীন scanf("%s")-ও নয় (পর্ব ১০)।
যাচাই
snprintf-এর ফেরত মান >= sizeof dst হলে লেখা কাটা পড়েছে — সিদ্ধান্ত নিন কী করবেন।
ফাংশনের ভেতরে sizeof-এর ফাঁদ। পর্ব ১৮-তে দেখেছেন, array ফাংশনে পাঠালে আসলে যায় শুধু তার ঠিকানা। তাই void greet(char name[])-এর ভেতরে sizeof name দেবে pointer-এর মাপ (সাধারণত ৮), array-র নয়! সমাধান: মাপটি আলাদা প্যারামিটার হিসেবে পাঠান — void greet(char *name, size_t size). fgets আর snprintf ঠিক এই কারণেই মাপ চেয়ে নেয়।
অনেক বড় প্রতিষ্ঠান এই নিয়মগুলো লিখিত নীতিতে পরিণত করেছে। যেমন Microsoft-এর Security Development Lifecycle-এ strcpy, strcat, gets-এর মতো ফাংশন “নিষিদ্ধ” তালিকায় আছে; Git প্রজেক্টের কোডেও একটি banned.h ফাইল আছে, যা strcpy, strcat-এর মতো ফাংশন ব্যবহার করলেই compile আটকে দেয়। আর সন্দেহ হলে পর্ব ০৩-এর sanitizer চালান: gcc -fsanitize=address দিয়ে compile করলে প্রথম overflow-এর মুহূর্তেই প্রোগ্রাম থেমে দেখিয়ে দেয় কোন লাইনে, কত বাইট পেরিয়েছে।
জায়গা কত? লেখা কত?
'\0'-এর ঘরটি কোথায়?৭ · লাইন পড়া — fgets আর শেষের newline
পর্ব ১০-এ শিখেছেন, ব্যবহারকারীর ইনপুট পড়ার নিরাপদ পথ হলো fgets — পুরো একটি লাইন, নির্দিষ্ট সীমার মধ্যে। কিন্তু একটি ছোট ঝামেলা আছে: fgets ব্যবহারকারীর চাপা Enter-টিও, অর্থাৎ '\n', লেখার শেষে রেখে দেয়। ফলে “Dhaka” টাইপ করলে buffer-এ আসে "Dhaka\n" — আর strcmp(buf, "Dhaka") আর 0 দেয় না!
সমাধানটি এক লাইনের, এবং এখন আপনি এর প্রতিটি অংশ বুঝবেন:
buf[strcspn(buf, "\r\n")] = '\0';
strcspn(buf, "\r\n") বলে, “buf-এর কোন ঘরে প্রথম '\r' বা '\n' আছে?” সেই ঘরে '\0' বসালেই লেখা সেখানে শেষ। কোনো newline না থাকলে strcspn লেখার দৈর্ঘ্যই ফেরত দেয় — তখন আগে থেকে থাকা '\0'-এর ওপরই আবার '\0' বসে, কোনো ক্ষতি নেই। '\r' রাখা হয়েছে কারণ Windows-এ তৈরি ফাইলে লাইনের শেষে থাকে "\r\n" — দুটিই মুছতে হয়।
নিচের প্রোগ্রামটি দুটি লাইন পড়ে — একটি ইংরেজি “Dhaka”, একটি বাংলা “ঢাকা” (পাতায় চালালে ইনপুট আগে থেকেই দেওয়া আছে)। প্রতিটি লাইনের newline সরানোর আগে ও পরে দৈর্ঘ্য, আর ভেতরের প্রতিটি বাইট hex-এ দেখায়:
#include <stdio.h>
#include <string.h>
int main(void) {
char line[32];
while (fgets(line, sizeof line, stdin) != NULL) {
size_t before = strlen(line);
line[strcspn(line, "\r\n")] = '\0';
printf("%zu -> %zu bytes:", before, strlen(line));
for (size_t i = 0; line[i] != '\0'; i++)
printf(" %02X", (unsigned char)line[i]);
printf("\n");
}
return 0;
}প্রথম লাইন: newline-সহ ৬ বাইট, সরানোর পর ৫ — 44 68 61 6B 61, অর্থাৎ D, h, a, k, a. দ্বিতীয় লাইনটিই আসল চমক: চোখে মাত্র দুটি অক্ষর “ঢা” আর “কা”, অথচ newline সরানোর পর ১২ বাইট! প্রতিটি E0 A6 .. দলই একটি Unicode অক্ষরের তিন বাইট (পর্ব ০৫-এর UTF-8 নিয়ম)। (unsigned char) লেখা হয়েছে যাতে E0-এর মতো বড় বাইট ঋণাত্মক সংখ্যা হয়ে ভুল না ছাপে। আর লাইনটি ৩১ বাইটের বেশি হলে? fgets ৩১ বাইট পড়ে থামবে, বাকিটা পরের ডাকে আসবে — overflow কখনো হবে না।
৮ · বাংলা লেখা — বাইট, code point আর চোখের অক্ষর
পর্ব ০৫-এ দেখেছেন, UTF-8-এ ইংরেজি অক্ষর ১ বাইট, কিন্তু বাংলা অক্ষর ৩ বাইট — 'অ' মানে E0 A6 85. C-এর স্ট্রিং ফাংশনগুলো এসব কিছুই জানে না। তাদের কাছে স্ট্রিং মানে শুধু বাইটের সারি, শেষে একটি শূন্য। তাই বাংলা লেখায় “দৈর্ঘ্য” শব্দটির অন্তত তিনটি আলাদা অর্থ দাঁড়ায়:
| লেখা | বাইট (strlen) | Code point | চোখে দেখা অক্ষর |
|---|---|---|---|
Dhaka | ৫ | ৫ | ৫ |
| অ | ৩ | ১ | ১ |
| ঢাকা (ঢ + া + ক + া) | ১২ | ৪ | ২ |
| আমি (আ + ম + ি) | ৯ | ৩ | ২ |
| ক্ষ (ক + ্ + ষ) | ৯ | ৩ | ১ |
চোখে দেখা একটি “অক্ষর” — যেমন “কা” বা যুক্তাক্ষর “ক্ষ” — Unicode-এর ভাষায় একটি grapheme cluster, যা একাধিক code point নিয়ে গঠিত হতে পারে। এবার দেখুন, কেউ যদি ইংরেজির মতো করে বাংলা লেখা উল্টাতে চান, তাহলে কী হয়:
- “ঢাকা” মেমরিতে। চারটি code point — ঢ, া (আ-কার), ক, া — প্রত্যেকে তিন বাইট করে, মোট ১২ বাইট, তারপর
'\0'. প্রতিটি ঘরের নিচে লেখা আছে বাইটটি কোন code point-এর অংশ। strlenগুনল ১২. সে শুধু শূন্য বাইট খোঁজে, অক্ষর চেনে না। তাই কোনো ফর্মে “নাম সর্বোচ্চ ১০ অক্ষর” নিয়মstrlenদিয়ে যাচাই করলে চার অক্ষরের একটি বাংলা নামও বাতিল হয়ে যেতে পারে!s[0]কোনো অক্ষর নয়। ইংরেজিতেs[0]মানে প্রথম অক্ষর। এখানেs[0]হলো0xE0— 'ঢ'-এর তিন টুকরোর প্রথমটি মাত্র। এটি একা ছাপালে স্ক্রিনে আসে ভাঙা চিহ্ন।- ভুল ১: বাইট ধরে উল্টানো। ইংরেজির জন্য লেখা “reverse” ফাংশন এখানে ১২টি বাইটের ক্রম উল্টে দিল। এখন সারির শুরুতে
BE— যা UTF-8-এ কেবল “আগের অক্ষরের অংশ” হতে পারে। ফলে বেশিরভাগ জায়গায় আসে � চিহ্ন, আর মাঝখানে কাকতালীয়ভাবে দুটি সম্পূর্ণ অচেনা অক্ষর তৈরি হয়েছে — একটি তিব্বতি, একটি আরবি লিপির! - ভুল ২: code point ধরে উল্টানো। এবার বাইটগুলো তিনটি করে দল বেঁধে উল্টানো হলো। প্রতিটি টুকরো বৈধ, কিন্তু ফল “াকাঢ” — শুরুতেই একটি আ-কার, যার আগে কোনো ব্যঞ্জন নেই। বাংলায় এমন লেখা অর্থহীন; অনেক ফন্ট এখানে একটি ডটেড বৃত্ত দেখায়।
- সঠিক: চোখের অক্ষর ধরে উল্টানো। “ঢা” আর “কা” — এই দুটি grapheme cluster-কে অটুট রেখে উল্টালে পাওয়া যায় “কাঢা”. এর জন্য Unicode-এর নিয়ম জানতে হয়, যা C-এর
string.hজানে না। বাস্তবে এ কাজের জন্য ICU-এর মতো বিশেষ library ব্যবহার করা হয়।
এই জ্ঞান কাজে লাগে প্রতিদিনের সফটওয়্যারে। ডেটাবেসে নামের ঘর ২০ বাইটের হলে একটি বাংলা নাম মাঝখান থেকে কাটা পড়ে শেষে ভাঙা অক্ষর থেকে যেতে পারে — পরে সেটি পর্ব ০৫-এর mojibake হয়ে দেখা দেয়। মোবাইলে SMS-এর হিসাবও এর একটি পরিচিত উদাহরণ: শুধু ইংরেজি অক্ষরের একটি SMS-এ ১৬০টি অক্ষর ধরে, কিন্তু বাংলা থাকলে ফোনকে অন্য encoding ব্যবহার করতে হয়, আর এক SMS-এ ধরে মাত্র ৭০টি।
C-এর স্ট্রিং ফাংশন বাংলা লেখা বহন করতে পারে — কপি, তুলনা, ফাইলে লেখা, সবই ঠিকঠাক হয়, কারণ বাইটগুলো অক্ষত থাকে। কিন্তু অক্ষর বোঝার কাজ — গোনা, কাটা, উল্টানো, বড়-ছোট হাতের অক্ষর বদলানো — বাইট ধরে করলে বাংলা ভেঙে যায়। নিয়ম: বাংলা লেখাকে একটি অখণ্ড বাইটের প্যাকেট হিসেবে নাড়াচাড়া করুন; ভেতরে কাটাছেঁড়া করতে হলে Unicode-জানা library ব্যবহার করুন।
৯ · AI-এর যুগে স্ট্রিং — যেখানে AI সবচেয়ে বেশি ভুল করে
AI-কে “C-তে নাম জোড়ার একটা ফাংশন লেখো” বললে কয়েক সেকেন্ডে কোড আসবে, আর সেটি সাধারণ ইনপুটে চলবেও। সমস্যা হলো, AI শিখেছে কয়েক দশকের পুরোনো কোড থেকে — বই, ফোরাম, টিউটোরিয়াল — যেখানে strcpy আর strcat ছিল প্রতিদিনের জিনিস। স্ট্রিংয়ের ভুল প্রায় কখনো সাধারণ পরীক্ষায় ধরা পড়ে না; ধরা পড়ে লম্বা নাম, খালি ইনপুট বা বাংলা লেখায়।
AI-এর স্ট্রিং-কোডে যেসব ভুল বারবার দেখা যায়
| AI-এর ভুল | কেন বিপদ | যা হওয়া উচিত |
|---|---|---|
strcpy / strcat / sprintf মাপ ছাড়া | লম্বা ইনপুটে buffer overflow | snprintf(dst, sizeof dst, ...) |
gets বা scanf("%s") | ইনপুটের দৈর্ঘ্যের কোনো সীমা নেই | fgets + strcspn |
if (cmd == "exit") | ঠিকানা মেলায়, লেখা নয় — প্রায় সবসময় মিথ্যা | strcmp(cmd, "exit") == 0 |
char buf[5]-এ ৫ অক্ষরের লেখা | '\0'-এর জায়গা নেই — off-by-one | মাপ = দৈর্ঘ্য + ১ |
strncpy-এর পর নিজে '\0' না বসানো | শেষ-চিহ্নহীন “স্ট্রিং” | snprintf, বা শেষ ঘরে '\0' |
| ফাংশনের local array-র ঠিকানা ফেরত দেওয়া | ফাংশন শেষে সেই জায়গা আর বৈধ নয় | ডাকা ফাংশনের কাছ থেকে buffer নেওয়া (বা পর্ব ২০-এর পদ্ধতি) |
| বাংলা/UTF-8 লেখা বাইট ধরে উল্টানো, কাটা বা গোনা | ভাঙা অক্ষর, ভুল দৈর্ঘ্য | Unicode-জানা library; অন্তত code point-এর সীমা মানা |
AI-কে স্ট্রিং-কোড লিখতে বলার সময় নিরাপত্তার শর্তগুলো প্রশ্নের ভেতরেই বলে দিন। তাহলে AI পুরোনো অভ্যাসের বদলে নিরাপদ পথ বেছে নিতে বাধ্য হয়, আর আপনি যাচাই করার একটি তালিকাও পেয়ে যান:
AI-এর স্ট্রিং-কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১শেষে শূন্য রাখা (C) বনাম শুরুতে দৈর্ঘ্য রাখা (Pascal) — কোনটি ভালো নকশা? আজ নতুন করে বানালে আপনি কোনটি বেছে নিতেন?
দুটি নকশাই একই সমস্যার দুটি সমাধান: “লেখাটি কোথায় শেষ?” পার্থক্য হলো তথ্যটি কোথায় রাখা হয়, আর কে তার দাম দেয়।
- Null-terminated (C): বাড়তি খরচ মাত্র ১ বাইট, দৈর্ঘ্যের কোনো সীমা নেই। লেখার মাঝখানের ঠিকানাও বৈধ স্ট্রিং, তাই “শেষ অংশটি” পেতে কপি লাগে না। কিন্তু দৈর্ঘ্য জানতে O(n) সময়, ভেতরে শূন্য বাইট রাখা অসম্ভব, আর জায়গার মাপ লেখার সাথে না থাকায় প্রতিটি ফাংশন অন্ধভাবে কাজ করে।
- Length-prefixed (পুরোনো Pascal): দৈর্ঘ্য সাথে সাথে জানা যায়, আর কপির আগেই মাপ মিলিয়ে দেখা যায়। কিন্তু দৈর্ঘ্য এক বাইটে রাখলে লেখা ২৫৫ অক্ষরের বেশি হতে পারে না। বড় সংখ্যা রাখতে বেশি বাইট লাগে — ১৯৭০-এর দশকের কয়েক কিলোবাইট মেমরিতে যা ছিল বিলাসিতা।
Ritchie-র সিদ্ধান্তটি তাঁর সময়ের জন্য যুক্তিসংগত ছিল। সমস্যা হলো, সিদ্ধান্তটি টিকে গেছে এমন এক যুগে, যেখানে মেমরি সস্তা আর আক্রমণকারী সর্বত্র। এজন্যই Poul-Henning Kamp একে “সবচেয়ে দামি এক বাইটের ভুল” বলেছেন।
আধুনিক ভাষাগুলো কোন পথ বেছে নিয়েছে, দেখুন: Go, Rust, Java, Python — সবাই দৈর্ঘ্য আলাদা করে রাখে, এবং দৈর্ঘ্যের জন্য পুরো একটি শব্দ (৮ বাইট পর্যন্ত) খরচ করে, তাই সীমার সমস্যাও নেই। অনেক বড় C প্রজেক্টও নিজস্ব “string” কাঠামো বানিয়ে নিয়েছে — যেমন Redis ডেটাবেসের SDS, যেখানে দৈর্ঘ্য আর জায়গার মাপ লেখার ঠিক আগে রাখা থাকে, কিন্তু শেষে '\0'-ও রাখা হয় যাতে সাধারণ C ফাংশনেও চলে।
আজ নতুন করে বানালে প্রায় সব প্রকৌশলী দৈর্ঘ্য রাখার পক্ষে যাবেন। কিন্তু আসল শিক্ষাটি সাধারণ: যে তথ্য বারবার লাগে, সেটি একবার রেখে দিলে বারবার হিসাব করতে হয় না — ডেটাবেসের index থেকে CPU-র cache পর্যন্ত সবখানে এই চিন্তা।
মূল উপলব্ধি: নকশার সিদ্ধান্ত তার যুগের সীমাবদ্ধতার ফসল। ভালো প্রকৌশলী জিজ্ঞেস করেন — “যে শর্তে এটি বেছে নেওয়া হয়েছিল, সেই শর্ত কি আজও আছে?”
প্র ০২এত দশক পরেও নিরাপত্তা-ত্রুটির এত বড় অংশ কেন C-এর মেমরি আর স্ট্রিং থেকে আসে? ভালো প্রোগ্রামাররা কি ভুল করেন না?
করেন — এবং সেটাই মূল কথা। সমস্যাটি ব্যক্তির নয়, নকশার। C-এর স্ট্রিংয়ে তিনটি জিনিস একসাথে ঘটে:
- মাপ ডেটার সাথে থাকে না। জায়গার মাপ মনে রাখার দায়িত্ব পুরোপুরি প্রোগ্রামারের। একটি বড় প্রজেক্টে লাখ লাখ লাইন, হাজার হাজার স্ট্রিং-অপারেশন — প্রতিটিতে নির্ভুল থাকার সম্ভাবনা বাস্তবে শূন্যের কাছাকাছি।
- ভুলের কোনো তাৎক্ষণিক লক্ষণ নেই। ডায়াগ্রামে দেখলেন, overflow-এর পর প্রোগ্রাম দিব্যি চলতে থাকে। সাধারণ পরীক্ষায় কখনো ধরা পড়ে না; ধরা পড়ে যখন কেউ ইচ্ছা করে লম্বা ইনপুট পাঠায়।
- ডেটা আর নিয়ন্ত্রণ পাশাপাশি থাকে। Stack-এ ব্যবহারকারীর লেখা আর return address একই এলাকায়। ডেটা উপচে পড়লে সেটি প্রোগ্রামের পরের গন্তব্যই বদলে দিতে পারে।
প্রতিরক্ষার স্তরও তৈরি হয়েছে: stack canary, প্রতিবার মেমরির ঠিকানা এলোমেলো করা (ASLR), stack-এর ডেটাকে কোড হিসেবে চালাতে না দেওয়া, sanitizer, fuzzing (লাখ লাখ এলোমেলো ইনপুট দিয়ে প্রোগ্রাম পরীক্ষা)। প্রতিটি আক্রমণকে কঠিন করেছে, কিন্তু আক্রমণকারীরাও নতুন কৌশল বের করেছেন।
এজন্যই বড় প্রতিষ্ঠানগুলো এখন সমস্যার মূলে যাচ্ছে। Microsoft আর Google-এর প্রকৌশলীরা নিজেদের বিশ্লেষণে জানিয়েছেন, তাঁদের বড় পণ্যগুলোর গুরুতর নিরাপত্তা-ত্রুটির বড় অংশ মেমরি-সংক্রান্ত। ফলে Android, Windows আর Linux kernel-এর কিছু অংশে নতুন কোড Rust-এর মতো memory-safe ভাষায় লেখা শুরু হয়েছে, যেখানে স্ট্রিং নিজের মাপ জানে আর সীমা পেরোলে প্রোগ্রাম নিরাপদে থেমে যায়।
মূল উপলব্ধি: যে ভুল “সাবধান থাকলেই এড়ানো যায়”, বড় সিস্টেমে তা ঘটবেই। নিরাপদ সিস্টেম বানাতে মানুষের সতর্কতার ওপর নয়, নকশার ওপর ভরসা করতে হয় — ভুলটাকেই অসম্ভব করে দিতে হয়।
প্র ০৩Java, Python বা Go-তে string কীভাবে রাখা হয়? সেখানে string কেন বদলানো যায় না (immutable)?
আধুনিক প্রায় সব ভাষায় string-এর ভেতরের চেহারা মোটামুটি এক: লেখার বাইটগুলোর ঠিকানা + দৈর্ঘ্য, কখনো সাথে আরও কিছু তথ্য। যেমন Go-র string আসলে দুটি ঘর — একটি pointer আর একটি দৈর্ঘ্য; এমন জোড়াকে বলে fat pointer. Rust-এর String-এ থাকে pointer, দৈর্ঘ্য আর কতটুকু জায়গা নেওয়া আছে তার মাপ (capacity) — এই তিনটি তথ্য।
দ্বিতীয় বড় সিদ্ধান্তটি হলো immutable string — Java, Python, Go, JavaScript-এ একবার তৈরি string-এর কোনো অক্ষর বদলানো যায় না। “বদলাতে” চাইলে আসলে নতুন একটি string তৈরি হয়। কেন?
- নিরাপদে ভাগাভাগি: দশটি ভ্যারিয়েবল একই string দেখলেও কেউ কারও ক্ষতি করতে পারে না — অংশ ৩-এর literal-সমস্যার উল্টো সমাধান।
- নিরাপত্তা: একটি ফাইলের নাম যাচাই করার পর অন্য কেউ চুপিচুপি সেটি বদলে দিতে পারে না।
- একাধিক thread: একসাথে অনেক কাজ চললেও (পর্ব ৪৯) পড়ার সময় তালার দরকার নেই।
- Hash-এর জন্য: dictionary-র key হিসেবে string নিশ্চিন্তে ব্যবহার করা যায়, কারণ মান কখনো বদলাবে না (পর্ব ৩৭)।
এর দামও আছে। লুপে বারবার s = s + "x" করলে প্রতিবার পুরো লেখার নতুন কপি তৈরি হয় — সেই Shlemiel-এর সমস্যার আরেক রূপ। এজন্য Java-তে StringBuilder, Python-এ "".join(list) — অনেকগুলো টুকরো আগে জমিয়ে শেষে একবারে জোড়া।
আর C? C-তে আপনি নিজেই সব সিদ্ধান্ত নেন — বদলানো যাবে কি না (const), কত জায়গা, কে মুছবে। এই স্বাধীনতাই C-কে অপারেটিং সিস্টেম লেখার উপযোগী করেছে, আবার এটিই ভুলের উৎস।
মূল উপলব্ধি: “string” শুনতে সরল, কিন্তু প্রতিটি ভাষার string আসলে একগুচ্ছ নকশা-সিদ্ধান্ত — মাপ কোথায়, বদলানো যাবে কি না, encoding কী। একটি নতুন ভাষা শেখার সময় এই তিনটি প্রশ্ন করলেই তার string-এর অর্ধেক বুঝে যাবেন।
প্র ০৪“এই লেখায় কয়টি অক্ষর?” — এত সহজ প্রশ্নের উত্তর Unicode-এ এত কঠিন কেন? বাংলার জন্য এর অর্থ কী?
কারণ “অক্ষর” শব্দটির অন্তত তিনটি আলাদা অর্থ আছে, আর কম্পিউটার কোনটি গুনছে তা না জানলে উত্তর ভুল হবেই:
- বাইট: মেমরিতে কত জায়গা। C-এর
strlenএটাই গোনে। “ক্ষ” = ৯ বাইট। - Code point: Unicode তালিকার কয়টি নম্বর। “ক্ষ” = ক + ্ (হসন্ত) + ষ = ৩টি। Python-এর
len()এটি গোনে। - Grapheme cluster: মানুষ চোখে যাকে একটি অক্ষর বলে। “ক্ষ” = ১টি।
আরও জটিলতা আছে। Java আর JavaScript-এর length গোনে UTF-16-এর ইউনিট — তাই একটি ইমোজির দৈর্ঘ্য সেখানে ২ দেখাতে পারে। Swift ভাষা আবার ডিফল্টভাবে grapheme cluster গোনে, যা মানুষের ধারণার সবচেয়ে কাছে, কিন্তু গোনাটা ধীর — কারণ প্রতিবার Unicode-এর নিয়ম মেনে হিসাব করতে হয়। কোনো ভাষাই “ভুল” নয়; প্রত্যেকে আলাদা প্রশ্নের উত্তর দিচ্ছে।
বাংলার যুক্তাক্ষর বিশেষভাবে কঠিন। হসন্ত দিয়ে দুটি ব্যঞ্জন জুড়ে কখন একটি দৃশ্যমান অক্ষর তৈরি হবে, তা নির্ভর করে ফন্ট আর Unicode-এর নিয়মের ওপর — আর Unicode-এর grapheme-এর নিয়মও সময়ের সাথে হালনাগাদ হয়েছে। তাই দুটি সফটওয়্যার একই বাংলা শব্দে আলাদা “অক্ষর-সংখ্যা” দেখাতে পারে।
ব্যবহারিক পরিণতি সর্বত্র: ফর্মে “নাম সর্বোচ্চ ২০ অক্ষর” কোন অর্থে? ডেটাবেসের ঘর বাইটে মাপা, না অক্ষরে? লেখা ছোট করে “…” দেখাতে গেলে কোথায় কাটবেন যাতে যুক্তাক্ষর ভেঙে না যায়? SMS-এ বাংলা লিখলে কেন বেশি খরচ? প্রতিটি প্রশ্নের উত্তর আলাদা স্তরে।
মূল উপলব্ধি: “দৈর্ঘ্য” বলার আগে জিজ্ঞেস করুন — বাইট, code point, না চোখের অক্ষর? বাংলাভাষী প্রোগ্রামার হিসেবে এই পার্থক্য বোঝা আপনার একটি বাস্তব সুবিধা — বহু ইংরেজিভাষী প্রোগ্রামার এই ভুলগুলো টেরই পান না।
প্র ০৫strncpy যদি ফাঁদ হয়, তাহলে “নিরাপদ কপি” ফাংশনটি কেমন হওয়া উচিত? strlcpy নিয়ে বিতর্কটি কী ছিল?
একটি ভালো কপি-ফাংশনের কাছে আমরা তিনটি জিনিস চাই: (১) কখনো সীমা পেরোবে না, (২) সবসময় '\0' বসাবে, (৩) লেখা কাটা পড়লে সেটি জানাবে। strcpy প্রথমটি মানে না, strncpy দ্বিতীয়টি মানে না, আর কেউই তৃতীয়টি করে না।
১৯৯০-এর দশকের শেষে OpenBSD প্রজেক্ট এই তিন শর্ত মেনে strlcpy আর strlcat নামে দুটি ফাংশন আনে। এরা সবসময় শেষ-চিহ্ন বসায়, আর ফেরত দেয় পুরো লেখার দৈর্ঘ্য — ঠিক snprintf-এর মতো, যাতে কাটা পড়া ধরা যায়। BSD সিস্টেম আর macOS এগুলো গ্রহণ করে।
কিন্তু Linux-এর প্রধান C library, glibc, বহু বছর এগুলো নিতে রাজি হয়নি। আপত্তিগুলো ছিল চিন্তার মতো: কাটা পড়াটাও একটি ভুল — “C:\Users\Rafi\Secret” কেটে “C:\Users\Rafi” হলে প্রোগ্রাম ভুল ফাইলে কাজ করবে; ফেরত মান যাচাই না করলে strlcpy সেই ভুল লুকিয়ে ফেলে। তাছাড়া ফেরত মান হিসাব করতে উৎস লেখাটি পুরোটা পড়তে হয়, যদিও কপি হয় অল্প। শেষ পর্যন্ত ২০২৩ সালে glibc 2.38-এ এই দুটি ফাংশন যোগ হয় — প্রায় পঁচিশ বছরের বিতর্কের পর।
এর মধ্যে snprintf সব জায়গায় পাওয়া যায় (C99 থেকে standard-এর অংশ) এবং একই কাজ করে। তাই এই কোর্সে আমরা snprintf ব্যবহার করছি। তবে ফাংশন যেটিই হোক, আসল শিক্ষা হলো: “কাটা পড়েছে কি না” যাচাই করে সিদ্ধান্ত নেওয়া — বাদ দেওয়া, ব্যবহারকারীকে জানানো, নাকি বড় জায়গা নেওয়া (পর্ব ২০)।
মূল উপলব্ধি: “নিরাপদ” কোনো ফাংশনের নাম নয়, একটি অভ্যাস। Overflow ঠেকানো প্রথম ধাপ; কাটা পড়াকে ভুল হিসেবে ধরে ঠিকভাবে সামলানো দ্বিতীয় — এবং সমান জরুরি ধাপ।
অনুশীলন
প্রথম তিনটিতে কোড লিখতে হবে না — খাতা-কলমে মেমরির ছবি এঁকে ভাবুন।
-
জায়গা বনাম লেখা: 64-bit সিস্টেমে নিচের প্রতিটির
sizeofআরstrlenকত? (ক)char a[] = "BD";(খ)char b[10] = "BD";(গ)const char *c = "BD";(ঘ)char d[] = "";(ক) sizeof ৩ (B, D,
'\0'), strlen ২। (খ) sizeof ১০ — আপনি নিজে মাপ দিয়েছেন, বাকি ৭টি ঘর শূন্য; strlen ২। (গ) sizeof ৮ — এটি pointer-এর মাপ, লেখার নয় (32-bit সিস্টেমে ৪); strlen ২। (ঘ) sizeof ১ — খালি স্ট্রিংয়েও শেষ-চিহ্নের একটি বাইট লাগে; strlen ০।শিক্ষা:
sizeofজায়গা মাপে,strlenলেখা মাপে — আর pointer-এর ক্ষেত্রেsizeofলেখার সম্পর্কে কিছুই বলে না। -
strcmp-এর মন পড়ুন: প্রতিটির ফল ঋণাত্মক, ০ না ধনাত্মক? (ক)
strcmp("apple", "Apple")(খ)strcmp("abc", "abcd")(গ)strcmp("10", "9")(ঘ)strcmp("Dhaka", "Dhaka")(ক) ধনাত্মক — প্রথম ঘরেই 'a' (৯৭) বনাম 'A' (৬৫); ছোট হাতের অক্ষরের নম্বর বড়।
(খ) ঋণাত্মক — তিন ঘর সমান, ঘর ৩-এ
'\0'(০) বনাম 'd' (১০০)। শেষ-চিহ্নও তুলনায় অংশ নেয়, তাই ছোট লেখা আগে আসে।(গ) ঋণাত্মক — '1' (৪৯) বনাম '9' (৫৭)। লেখা হিসেবে “10” আসে “9”-এর আগে! এজন্যই কম্পিউটারে ফাইলের তালিকায় file10 অনেক সময় file9-এর আগে দেখায়। সংখ্যা হিসেবে তুলনা করতে চাইলে আগে লেখাকে সংখ্যায় বদলাতে হয়।
(ঘ) ০ — হুবহু এক।
-
বাগ শিকার: নিচের কোডে অন্তত চারটি সমস্যা খুঁজুন, আর নিরাপদ রূপ লিখুন।
char first[6], full[10]; strncpy(first, "Shakib", sizeof first); strcpy(full, first); strcat(full, " Al Hasan"); if (full == "Shakib Al Hasan") puts("match");- ১. “Shakib” ঠিক ৬ অক্ষর,
first-এ ৬ ঘর —strncpy৬টি অক্ষর কপি করে,'\0'বসায় না। - ২. তাই
strcpy(full, first)শেষ-চিহ্ন খুঁজতেfirst-এর সীমা পেরিয়ে পড়ে। - ৩. “Shakib Al Hasan” ১৫ অক্ষর +
'\0'= ১৬ বাইট, অথচfull-এ মাত্র ১০ —strcatনিশ্চিত overflow ঘটায়। - ৪.
full == "..."ঠিকানা মেলায়, লেখা নয় — কখনো “match” ছাপবে না।
নিরাপদ রূপ:
char full[32]; snprintf(full, sizeof full, "%s %s", "Shakib", "Al Hasan"); if (strcmp(full, "Shakib Al Hasan") == 0) puts("match"); - ১. “Shakib” ঠিক ৬ অক্ষর,
-
বাংলা গুনুন (কোড লিখুন): UTF-8-এ প্রতিটি code point-এর প্রথম বাইট ছাড়া বাকি বাইটগুলো
10xxxxxxধরনের — অর্থাৎ(b & 0xC0) == 0x80সত্য হয় এমন বাইট। এই তথ্য দিয়ে একটি ফাংশন লিখুন যা একটি স্ট্রিংয়ের code point গোনে। “ঢাকা”-র জন্য কত পাবেন — আর সেটি কি চোখের অক্ষরের সংখ্যা?size_t count_code_points(const char *s) { size_t n = 0; for (; *s != '\0'; s++) if (((unsigned char)*s & 0xC0) != 0x80) /* "পরের অংশ" বাইট নয় */ n++; return n; }প্রতিটি code point-এর ঠিক একটি “শুরুর” বাইট থাকে, তাই শুরুর বাইটগুলো গুনলেই code point-এর সংখ্যা পাওয়া যায়। “ঢাকা”-র জন্য ফল ৪ — ১২ বাইটের চেয়ে অনেক ভালো, কিন্তু চোখের অক্ষর মাত্র ২টি (ঢা, কা)। সেটি গুনতে হলে কোন code point আগের অক্ষরের সাথে জোড়া লাগে (যেমন আ-কার, হসন্ত), সেই Unicode নিয়ম লাগবে। একটি ধাপ এগোলেন, পুরো পথ নয় — আর এটাই বাস্তবের Unicode-কাজের চেহারা।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমব্যবহারকারীর কাছ থেকে নিরাপদে একটি নাম পড়ার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- 📦 নির্দিষ্ট মাপের buffer ঘোষণা —
char name[64]; - ⌨️ মাপসহ পড়া —
fgets(name, sizeof name, stdin) - 🛑 ফেরত মান
NULLকি না যাচাই — ইনপুট শেষ হয়ে গেছে কি না - ✂️ শেষের newline সরানো —
name[strcspn(name, "\r\n")] = '\0'; - ✅ লেখা যাচাই করে তারপর ব্যবহার — খালি কি না, তুলনা
strcmpদিয়ে
char s[10] = "Rafi"; — sizeof s আর strlen(s) কত?
- ৫ আর ৫
- ১০ আর ৪
- ১০ আর ৫
- ৪ আর ৪
ব্যবহারকারী “yes” লিখলেন, কিন্তু if (answer == "yes") মিথ্যা হলো। কেন?
- বড় হাতের আর ছোট হাতের অক্ষরের পার্থক্যের জন্য
- শুধু শেষের newline-এর জন্য
- == লেখা নয়, দুটি ঠিকানা তুলনা করে
- C-তে == স্ট্রিংয়ের ওপর কাজই করে না, তাই সবসময় 0
char t[6]; strncpy(t, "Bangladesh", sizeof t); — এরপর t-এর অবস্থা কী?
- “Bangla” — ৬টি অক্ষর, কিন্তু কোনো '\0' নেই
- “Bangl” আর শেষে '\0' — নিরাপদ স্ট্রিং
- পুরো “Bangladesh” কপি হয়ে overflow
- t খালি স্ট্রিং হয়ে যায়
UTF-8-এ strlen("অ") কত ফেরত দেয়?
- ১, কারণ এটি একটি অক্ষর
- ২
- ৪, '\0'-সহ
- ৩
char *p = "Dhaka"; p[0] = 'd'; — কী হবে?
- p হয়ে যাবে “dhaka”, কোনো সমস্যা নেই
- Undefined behavior — প্রায়ই Segmentation fault
- Compiler সবসময় error দেবে
- C নিজে থেকে literal-এর একটি নতুন কপি বানিয়ে বদলাবে
ব্যবহারকারীর দেওয়া input লেখাটি char name[20]-এ কপি করার সবচেয়ে নিরাপদ উপায় কোনটি?
- strcpy(name, input);
- strncpy(name, input, 20);
- snprintf(name, sizeof name, "%s", input);
- sprintf(name, "%s", input);
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- C-এর স্ট্রিং =
char-এর array + শেষে'\0'. দৈর্ঘ্য কোথাও লেখা থাকে না; N অক্ষরের জন্য N + 1 বাইট লাগে। sizeofমাপে জায়গা,strlenমাপে লেখা — আরstrlenপ্রতিবার শুরু থেকে হেঁটে গোনে।- Literal বদলানো যায় না: বদলাতে চাইলে
char s[], শুধু পড়তেconst char *. লেখা মেলাতেstrcmp, কখনো==নয়। strcpy,strcat,getsজায়গা মাপে না — buffer overflow. সবসময় মাপ জানান:snprintf,fgets;strncpy-এর'\0'-ফাঁদ মনে রাখুন।- বাংলা UTF-8 লেখায় বাইট ≠ code point ≠ চোখের অক্ষর. বাইট ধরে কাটা, গোনা বা উল্টানো লেখা ভেঙে দেয়।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ২০ · ডায়নামিক মেমরি — দরকারমতো জায়গা নেওয়াপরবর্তী পর্বmalloc, free আর যেকোনো দৈর্ঘ্যের স্ট্রিংয়ের জন্য জায়গা তৈরি।
- C প্রোগ্রামিং · কোর্স হোম৫৬টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- Cybersecurityগভীরে যানBuffer overflow থেকে আধুনিক আক্রমণ ও প্রতিরক্ষা — আক্রমণকারীর চোখে মেমরি।
- Data Structures & Algorithmsপাশের ট্র্যাকস্ট্রিং খোঁজা, তুলনা আর সাজানোর দ্রুত অ্যালগরিদম।