Lesson পড়তে ৭ মিনিট লাগবে

সি (C) স্ট্রিং এবং ক্যারেক্টার (C Strings & Characters)

সি (C)-তে স্ট্রিং (string) বলতে মূলত ক্যারেক্টারগুলোর (characters) খুব সাধারণ একটি অ্যারেকেই (array) বোঝায় যার একেবারে শেষে একটি বাউন্সার (bouncer) দাঁড়িয়ে থাকে: আর তা হলো '\0'

স্ট্রিংগুলো (Strings): দ্য হার্ড ওয়ে বা সবচেয়ে কঠিন পদ্ধতি (The Hard Way)

বেশিরভাগ প্রোগ্রামিং ভাষার (languages) স্ট্রিংগুলোই (strings) মূলত নিজস্ব কিছু মেথড (methods) এবং সেফটি নেটসহ (safety nets) তৈরি হওয়া এক ধরনের বিল্ট-ইন টাইপ (built-in type) বা ব্যবস্থা। কিন্তু সি (C)-তে? সি-তে (C) যেকোনো স্ট্রিং (string) হলো char-এর খুব সাধারণ একটি অ্যারে বা array যার ভেতর একটি স্পেশাল ক্যারেক্টার (special character) থাকে — যাকে নাল টার্মিনেটর (null terminator) বা '\0' বলা হয় — এটি মূলত অ্যারের একেবারে শেষে (end) একজন বাউন্সারের (bouncer) মতো দাঁড়িয়ে থাকে এবং সবাইকে বলে যে "স্ট্রিংটি মূলত এখানেই শেষ (the string ends here)।"

সি-তে মূলত কোনো .length প্রপার্টি (property) নেই। এখানে মেমোরি (memory) ম্যানেজ বা পরিচালনা করার জন্য কোনো গারবেজ কালেক্টরও (garbage collector) নেই। এখানে আপনাকে মূলত আপনার নিজের ভরসাতেই থাকতে হবে, কারণ এখানকার প্রতিটি স্ট্রিং ফাংশনই (string function) এর মেমোরির (memory) ভেতর দিয়ে একের পর এক ক্যারেক্টার (character by character) করে সামনের দিকে এগোতে থাকে যতক্ষণ না পর্যন্ত এটি ওই '\0'-টিকে খুঁজে পায়। আর যদি এর নাল টার্মিনেটরটি (null terminator) মিসিং বা অনুপস্থিত (missing) থাকে? তাহলে এখানকার ওই ফাংশনটি (function) নিজের মতো করে সামনের দিকে এগিয়ে যেতেই থাকবে (keeps walking) — এবং আপনার ডেটার সীমানা পার হয়ে মেমোরিতে (memory) থাকা যেকোনো প্রকার গারবেজ বা আবর্জনার (garbage) ভেতরে গিয়ে পড়তে পারে।

স্ট্রিং তৈরি করা (Creating Strings)

এক্ষেত্রে আপনার কাছে মূলত প্রধান দুটি (two main) উপায় বা অপশন আছে:

  • char greeting[] = "Hello"; — এর মাধ্যমে এখানকার কম্পাইলারটি (compiler) মূলত ৬-উপাদানের (6-element) একটি অ্যারে বা array তৈরি করে (৫টি বর্ণ বা letters + '\0')
  • char greeting[6] = {'H','e','l','l','o','\0'}; — ম্যানুয়াল (manual), একের পর এক ক্যারেক্টার (character by character) করে

এখানে খেয়াল করুন যে এর সাইজটি (size) হলো ৬ (6), ৫ (5) নয়। কারণ এখানকার ওই নাল টার্মিনেটরেরও (null terminator) মূলত নিজের জন্য একটি আলাদা লকারের (own locker) প্রয়োজন পড়ে।

স্ট্রিং তৈরি করা এবং প্রিন্ট করা (Creating and Printing Strings)

#include <stdio.h>
int main() {
// কম্পাইলার (Compiler) স্বয়ংক্রিয়ভাবে (automatically) '\0' যোগ করে দেয়
char greeting[] = "Hello";
printf("String: %s\n", greeting);
printf("Length in memory: %lu bytes\n", sizeof(greeting));
printf("Char at [0]: %c\n", greeting[0]);
printf("Char at [4]: %c\n", greeting[4]);
printf("Char at [5]: %d (null terminator)\n", greeting[5]);
return 0;
}
Output
String: Hello
Length in memory: 6 bytes
Char at [0]: H
Char at [4]: o
Char at [5]: 0 (null terminator)

স্ট্রিং.এইচ (string.h) টুলবক্স বা Toolbox

যেহেতু সি স্ট্রিংগুলো (C strings) হলো মূলত সাধারণ কিছু অ্যারে (arrays), তাই এগুলোর তুলনা করার (compare) জন্য আপনি কোনো ==-কে ব্যবহার করতে পারবেন না, এমনকি এগুলোকে কনক্যাটেনেট বা জোড়া লাগানোর (concatenate) জন্যও কোনো + ব্যবহার করা যাবে না। তাই এর বদলে, আপনাকে <string.h>-এর ভেতরের কিছু নির্দিষ্ট ফাংশন (functions) ব্যবহার করতে হবে:

  • strlen(s) — এটি মূলত এর লেন্থ বা দৈর্ঘ্য (length) রিটার্ন করে বা ফেরত দেয় (তবে '\0'-কে কাউন্ট বা গণনা করে না)
  • strcpy(dest, src) — এটি মূলত src-টিকে dest-এর ভেতরে কপি (copies) করে দেয়
  • strcat(dest, src) — এটি মূলত src-টিকে dest-এর একেবারে শেষে (end) অ্যাপেন্ড বা যুক্ত (appends) করে দেয়
  • strcmp(a, b) — এটি মূলত যেকোনো দুটি স্ট্রিংয়ের (two strings) তুলনা (compares) করে: যদি তারা সমান (equal) হয় তবে এটি 0 রিটার্ন করে, a < b হলে নেগেটিভ (negative) রিটার্ন করে, এবং যদি a > b হয় তবে এটি পজিটিভ (positive) রিটার্ন করে

এখানকার প্রতিটি ফাংশনই (Each of these) মূলত যতক্ষণ পর্যন্ত ওই '\0'-টিকে খুঁজে না পায়, ততক্ষণ পর্যন্ত ক্যারেক্টার বাই ক্যারেক্টার (character by character) করে সামনের দিকে এগিয়ে যেতে থাকে। তার মানে হলো এখানকার প্রতিটি ফাংশনই O(n) — আর এর মানে হলো আপনার স্ট্রিংটি (string) যত বড় হবে, এগুলো মূলত তত বেশি সময় (time) নেবে।

কার্যকর বা অ্যাকশনে থাকা string.h ফাংশনগুলো (string.h Functions in Action)

#include <stdio.h>
#include <string.h>
int main() {
char name[] = "Alice";
char copy[20];
char full[40] = "Hello, ";
// strlen — লেন্থ বা দৈর্ঘ্যটিকে (length) বের করা
printf("Length of name: %lu\n", strlen(name));
// strcpy — একটি স্ট্রিংকে (string) কপি বা copy করা
strcpy(copy, name);
printf("Copied: %s\n", copy);
// strcat — স্ট্রিংগুলোকে (strings) জোড়া লাগানো (concatenate)
strcat(full, name);
strcat(full, "!");
printf("Full greeting: %s\n", full);
// strcmp — স্ট্রিংগুলোর (strings) তুলনা (compare) করা
printf("strcmp(\"Alice\", \"Bob\"): %d\n",
strcmp("Alice", "Bob"));
printf("strcmp(\"Alice\", \"Alice\"): %d\n",
strcmp("Alice", "Alice"));
return 0;
}
Output
Length of name: 5
Copied: Alice
Full greeting: Hello, Alice!
strcmp("Alice", "Bob"): -1
strcmp("Alice", "Alice"): 0
Note: বাফার ওভারফ্লো অ্যালার্ট বা বাফার ওভারফ্লো সতর্কতা (Buffer overflow alert)! এখানকার এই strcpy এবং strcat মূলত কখনোই চেক বা যাচাই (check) করে না যে এখানকার গন্তব্য বা ডেস্টিনেশনটি (destination) আসলে যথেষ্ট বড় (big enough) কি না। আপনি যদি ভুল করে কোনো ১০-ক্যারেক্টারের (10-character) বাফারের (buffer) ভেতর একটি ১০০-ক্যারেক্টার (100-character) বিশিষ্ট বড় স্ট্রিংকে (string) কপি বা copy করেন, তবে এটি পরবর্তীতে মেমোরির (memory) যা কিছু পাবে, তার ওপরেই অত্যন্ত আনন্দের (happily) সাথে ওভাররাইট (overwrites) করে ফেলবে — এর ফলে হয় পুরো ডেটাটি নষ্ট (corrupting data) হয়ে যেতে পারে, কিংবা এটি হয়তো প্রোগ্রামটিকে ক্র্যাশ (crashing) করে দিতে পারে, বা এমনকি কোনো বড় ধরনের সিকিউরিটি হোলও (security hole) তৈরি করতে পারে। তাই সবসময় একটি নির্দিষ্ট সাইজ লিমিট বা size limit দিয়ে এখানকার strncpy এবং strncat ফাংশনগুলোকে ব্যবহার করুন, বা আরও ভালো হয় যদি আপনি এর বদলে snprintf-কে ব্যবহার করেন।

ক্যারেক্টার ফাংশনগুলো (Character Functions)

যেকোনো সাধারণ বা একক ক্যারেক্টারগুলোর (Individual characters) জন্য <ctype.h>-এর ভেতরে তাদের নিজস্ব কিছু টুলকিট (toolkit) আছে:

  • toupper('a') মূলত 'A' রিটার্ন করে বা ফেরত দেয়
  • tolower('Z') মূলত 'z' রিটার্ন করে বা ফেরত দেয়
  • isdigit('5') মূলত নন-জিরো বা non-zero (ট্রু বা true) রিটার্ন করে
  • isalpha('x') মূলত নন-জিরো বা non-zero (ট্রু বা true) রিটার্ন করে

সবসময় মনে রাখবেন: সি-এর (C) যেকোনো char-ই হলো মূলত একটি ছোট ইনটিজার (small integer)। 'A' হলো 65, 'a' হলো 97, '0' হলো 48। তাই চাইলে আপনি সরাসরি ক্যারেক্টারগুলোর (characters) ওপরেও বিভিন্ন ধরনের গণিত বা math কষতে পারেন!

ম্যানুয়াল ইটারেশন (Manual Iteration) এবং ক্যারেক্টার ফাংশনগুলো (Character Functions)

#include <stdio.h>
#include <ctype.h>
int main() {
char message[] = "Hello, World 123!";
int letters = 0, digits = 0;
// একের পর এক ক্যারেক্টার (character by character) করে সামনের দিকে পৌঁছানো
for (int i = 0; message[i] != '\0'; i++) {
if (isalpha(message[i]))
letters++;
else if (isdigit(message[i]))
digits++;
}
printf("Letters: %d\n", letters);
printf("Digits: %d\n", digits);
// ম্যানুয়ালি বা নিজে থেকে আপারকেস বা বড় হাতের অক্ষরে (uppercase) রূপান্তর বা কনভার্ট (Convert) করা
for (int i = 0; message[i] != '\0'; i++) {
message[i] = toupper(message[i]);
}
printf("Uppercased: %s\n", message);
return 0;
}
Output
Letters: 10
Digits:  3
Uppercased: HELLO, WORLD 123!

স্ট্রিং লিটারাল (String Literals) বনাম চ্যার অ্যারে (Char Arrays)

এদের মধ্যে খুব ছোট কিন্তু গুরুত্বপূর্ণ কিছু পার্থক্য (subtle but important difference) রয়েছে:

  • char name[] = "Alice"; — এটি মূলত স্ট্যাকের (stack) ওপরে একটি পরিবর্তনযোগ্য বা মিউটেবল (mutable) অ্যারে তৈরি করে। আপনি চাইলে এখানকার ক্যারেক্টারগুলোকে (characters) খুব সহজেই পরিবর্তন (change) করতে পারেন।
  • char *name = "Alice"; — এই পয়েন্টারটি মূলত মেমোরির (memory) একটি নির্দিষ্ট বা স্পেশাল সেকশনে সেভ (stored) থাকা একটি রিড-ওনলি (read-only) স্ট্রিং লিটারালকে (string literal) পয়েন্ট (points) করে। এটিকে পরিবর্তন (Modifying) করতে যাওয়ার মানে হলো একটি আনডিফাইন্ড বিহেভিয়ার বা অনির্ধারিত আচরণের (undefined behavior) জন্ম দেওয়া।

আপনার যদি কখনোই কোনো স্ট্রিং-কে (string) মডিফাই বা পরিবর্তন (modify) করার দরকার পড়ে, তবে অবশ্যই সেটিকে অ্যারে ফরমে বা array form-এ ব্যবহার করবেন। আর যদি এটিকে শুধুমাত্র রিড (read) করার প্রয়োজন পড়ে (যেমন printf-এ পাস করানো), তবে একটি লিটারালের পয়েন্টারই (pointer to a literal) যথেষ্ট বা ঠিক আছে (fine)।

মিউটেবল বা পরিবর্তনযোগ্য (Mutable) বনাম রিড-ওনলি বা শুধুমাত্র পাঠযোগ্য (Read-Only) স্ট্রিংগুলো (Strings)

#include <stdio.h>
int main() {
// পরিবর্তনযোগ্য বা মিউটেবল (Mutable) — যা মূলত স্ট্যাকের (stack) ওপরে থাকে
char arr[] = "Hello";
arr[0] = 'J'; // ঠিক আছে (OK)!
printf("Modified array: %s\n", arr);
// রিড-ওনলি বা শুধুমাত্র পাঠযোগ্য (Read-only) — যা মূলত যেকোনো স্ট্রিং লিটারালকে (string literal) নির্দেশ বা পয়েন্ট (points) করে থাকে
const char *ptr = "Hello";
// ptr[0] = 'J'; // বিপদ বা ডেঞ্জার (DANGER): এটি একটি আনডিফাইন্ড বিহেভিয়ার বা অনির্ধারিত আচরণ (undefined behavior)!
printf("Literal pointer: %s\n", ptr);
return 0;
}
Output
Modified array: Jello
Literal pointer: Hello
চ্যালেঞ্জ

ছোট কুইজ

মেমোরিতে (memory) char s[] = "Cat"; মূলত ঠিক কতটি (How many bytes) বাইটকে দখল করে থাকে?
ArraysFunctions