বিট-ওয়াইজ নিয়ে খেলা! (Playing with Bitwise!)

Standard

আগের পর্বঃ বিট-ওয়াইজ অপারেটর

এর আগে বিট-ওয়াইজ অপারেটরগুলো নিয়ে লিখেছিলাম। এবার এগুলোর কিছু ব্যবহার নিয়ে লিখছি যেগুলোর মাধ্যমে প্রোগ্রাম অনেক এফিসিয়েন্ট করা যায়। তবে এগুলো বোঝার জন্য বিট-ওয়াইজ অপারেটরগুলো কিভাবে কাজ করে তা জানা আবশ্যক। জানা না থাকলে আগের পর্বটি পড়তে অনুরোধ করছি।

সম্পূর্ণ লেখা

প্রাইম নাম্বার – সিভ অফ এরাটস্থেনিজ (Prime Number – Sieve of Eratosthenes)

Standard

আগের পর্বঃ প্রাইম নাম্বার – বেসিক

প্রাইম নাম্বারের বেসিক কনসেপ্টগুলো নিয়ে আগের পর্বে আলোচনা করা হয়েছে। এই পর্বে আমরা কিভাবে 1 থেকে শুরু করে n পর্যন্ত সকল প্রাইম নাম্বার বের করতে পারি তার একটা এফিসিয়েন্ট এলগোরিদম নিয়ে আলোচনা করব। এই এলগোরিদমটির নাম হল ‘সিভ অফ এরাটস্থেনিজ’।

সম্পূর্ণ লেখা

প্রাইম নাম্বার – বেসিক (Prime Number – Basic)

Standard

প্রাইম নাম্বার বা মৌলিক সংখ্যা নিয়ে গণিতচর্চার সূচনা লগ্ন থেকেই নানা গবেষণা চলে আসছে। আজও এর গুরত্ব এতটুকু হ্রাস পায়নি। কম্পিউটার ও প্রযুক্তির জগতে, বিশেষত ক্রিপ্টোগ্রাফির ক্ষেত্রে প্রাইম নাম্বার অত্যন্ত গুরুত্ব বহন করে। বিশেষ করে অনেক বড় বড় প্রাইম নাম্বারগুলোকে নানাভাবে কাজে লাগানো হয়। একারণে প্রোগ্রামিং এর ক্ষেত্রে প্রাইম নাম্বারের জ্ঞান অত্যাবশ্যক।

সম্পূর্ণ লেখা

বিট-ওয়াইজ অপারেটর (Bitwise Operator)

Standard

বিটওয়াইজ অপারেটর এবং বিটওয়াইজ অপারেশনগুলো প্রোগ্রামিং এর ক্ষেত্রে অনেক গুরুত্বপূর্ণ ভূমিকা পালন করতে পারে যার মূল কারণ হল এই অপারেটরের কাজগুলো কম্পিউটার অত্যন্ত দ্রুত সম্পন্ন করতে পারে। যোগ/বিয়োগ/গুণ/ভাগ ইত্যাদির চেয়ে একটা বিটের মান পরিবর্তন করে দেওয়া কম্পিউটারের জন্য অনেক সহজ কাজ। তাই অনেকক্ষেত্রেই বিট-ওয়াইজ অপারেটর ব্যবহারের মাধ্যমে হিসাব সম্পন্ন করলে অনেক সময় বাঁচানো যায়, তথা প্রোগ্রামের এফিশিয়েন্সি অনেকাংশে বৃদ্ধি করা যায়।

সম্পূর্ণ লেখা

স্ট্রাকচার (Structure)

Standard

স্ট্রাকচার হল কতগুলো স্ট্যান্ডার্ড ডাটা টাইপের সমষ্টিতে প্রোগ্রামারের নিজের সৃষ্টি করা ডাটা টাইপ। অন্যান্য ডাটা টাইপগুলো যেমন একটি নির্দিষ্ট ধরনের ডাটা রাখতে পারে, সেখানে স্ট্রাকচার একাধিক ধরনের একাধিক ডাটা রাখতে পারে। এই ডাটা টাইপটি প্রোগ্রামার নিজে তার প্রয়োজনমত তৈরি করে। এই ডাটা টাইপ অন্যান্য স্ট্যান্ডার্ড ডাটা টাইপের সমন্বয়ে তৈরি করা হয় বলে একে বলে Derived Data Type।

অনেক সময় কোন প্রবলেম সলভ করতে গেলে দেখা যায় যে কোন বিশেষ ধরনের বস্তুর জন্য কতগুলো করে তথ্য ভেরিয়েবলে রাখতে হয় যেগুলো দিয়ে পরবর্তিতে বিভিন্ন হিসাব করা হয়। সেই সব ক্ষেত্রে স্ট্রাকচার ব্যবহার করলে সবকিছু অনেক গোছানো থাকে। যেমন ধরা যাক একটি ক্লাসের 100 জন ছাত্রের নাম, রোল, বয়স এই তিনটা তথ্য ইনপুট নিয়ে রাখতে হবে। তাহলে তাহলে প্রতিটা ছাত্রের নামের জন্য একটা char অ্যারে, রোল এবং বয়সের জন্য দুটি int ভেরিয়েবল ডিক্লেয়ার করতে হবে। তাহলে 100 জন ছাত্রের জন্য এগুলোর প্রতিটার 100 এলিমেন্টের অ্যারে ডিক্লেয়ার করতে হবে।

[code language=”cpp”]
char name[100][30];
int roll[100], age[100];
[/code]

কিন্তু এইভাবে কাজ করা অনেক ঝামেলা। একদিকে দেখতেই কেমন যেন লাগে, তার উপর জিনিসটা গোছানো না। এছাড়া যদি এরকম অনেক ধরনের জিনিস থাকে যাদের সম্পর্কে তথ্য রাখতে হবে, অনেক হিসাব থাকে, তখন একেবারে হিজিবিজি অবস্থা হয়ে যাবে! এই ব্যাপারটাকেই অনেকখানি সুবিন্যস্ত এবং সুন্দর করে লেখা যায় স্ট্রাকচার ব্যবহার করলে। যেমন উপরের সমস্যাটার জন্যই একটা স্ট্রাকচার ডিক্লেয়ার করলে তা নিম্নরূপ হবেঃ

[code language=”cpp”]
struct student {
char name[30];
int roll;
int age;
};
[/code]

এখন সিন্ট্যাক্স দেখা যাক। প্রথমে struct দিয়ে বলা হচ্ছে যে আমরা একটা স্ট্রাকচার ডিফাইন করতে যাচ্ছি। অর্থাৎ আমাদের এই নতুন ডাটা টাইপটা কিরকম হবে। যেহেতু এটা কোন স্ট্যান্ডার্ড ডাটা টাইপ না, এটায় বিভিন্ন টাইপের ডাটা থাকতে পারে, সেহেতু কি কি ডাটা থাকবে তা বলে দিতে হবে। এরপর যে student লেখা, এটা হল আমাদের নতুন ডাটা টাইপের নাম। আমরা যখন কোন ভেরিয়েবল ডিক্লেয়ার করি তখন ভেরিয়েবলের আগে ডাটা টাইপের নাম লেখা হয়। যেমন, int i; char c; এখানে int বা char হল ডাটা টাইপের নাম। সেরকম আমাদের নতুন এই ডাটা টাইপের নাম হল student।

এরপর { } এর মধ্যে আমরা কি কি ডাটা রাখব এই নতুন টাইপের অধীনে সেটা লিখে দিলাম। আমাদের student ডাটা টাইপের মধ্যে তিনটা তথ্য রাখতে হবে, নাম, রোল এবং বয়স। তাই নতুন এই ডাটা টাইপের মধ্যে কি কি ভেরিয়েবল থাকবে, সেটি কিরকম হবে তা আমরা এখানে বলে দিলাম। এরপর একটা সেমিকোলন দিয়ে শেষ করে দিলাম।

লক্ষ্যনীয়, এখানে যা যা করা হল এতক্ষণ, সবকিছু কেবল একটা ডাটা টাইপ ডিফাইন করল যার নাম student। এখনও কোন ভেরিয়েবল তৈরি করা হয়নি, অতএব কোন মেমরি বরাদ্দ করা হয়নি, এবং তাই এখনও এটি নিয়ে কোন কাজ করা যাবে না। এখন আমাদেরকে এরকম 100 জন ছাত্রের তথ্য রাখতে হবে। তাই আমরা এই student টাইপের 100 এলিমেন্ট বিশিষ্ট একটা অ্যারে ডিক্লেয়ার করব।

[code language=”cpp”]
struct student stdnt[100];
[/code]

এখানে আমরা একটা অ্যারে ডিক্লেয়ার করলাম। এটা অন্যান্য যেকোন অ্যারে এর মতই। পার্থক্য হল এটা একটা স্ট্রাকচারের অ্যারে। আর তাই এখানে শুরুতে ডাটা টাইপ বলার সময় পুরো কথাটা লিখতে হয়, struct student । এই ভেরিয়েবল ডিক্লেয়ারের কাজটা আলাদা করে না করে স্ট্রাকচার ডিফাইন করার সাথে সাথেই করা যায়।

[code language=”cpp”]
struct student {
char name[30];
int roll;
int age;
} stdnt[100];
[/code]

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

এখন যেহেতু স্ট্রাকচারের ভেরিয়েবল ডিক্লেয়ার করা হয়ে গেল, তাই ভেরিয়েবলগুলো নিয়ে এখন কাজ করা যাবে। ভেরিয়েবলগুলোর প্রতিটির মধ্যে তিনটি করে মেম্বার আছে, name, roll এবং age । এই মেম্বারগুলো ব্যবহার করার জন্য, অর্থাৎ এতে ইনপুট নিতে বা এর থেকে আউটপুট পেতে বা এই মানগুলো ব্যবহার করার জন্য বিশেষ সিন্ট্যাক্স রয়েছে।

এখানে আমাদের একেকটি ভেরিয়েবল হল stdnt[0], stdnt[1], … , stdnt[99] এগুলো। স্ট্রাকচারের অন্তর্ভুক্ত কোন একটি মেম্বার এক্সেস করতে হলে . (ডট) ব্যবহার করতে হয়। যেমন,

[code language=”cpp”]stdnt[0].roll = 10;[/code]

অতএব 100 জন ছাত্রের নাম, রোল, বয়স ইনপুট নেওয়া যাবে এভাবে।

[code language=”cpp”]
int i;
for( i = 0; i < 100; i++ ) {
scanf( "%s %d %d", stdnt[i].name, &stdnt[i].roll, &stdnt[i].age );
}
[/code]

এখান থেকে খুব সহজেই বোঝা যাচ্ছে যে স্ট্রাকচারের প্রতিটা মেম্বার একেকটা ভেরিয়েবল হিসেবে কাজ করে, এবং সেইজন্য কেবল স্ট্রাকচার ভেরিয়েবলটার নাম দিয়ে সাথে ডট দিয়ে মেম্বারটি লিখে দিলেই হয়ে যায়। এভাবে করে অন্য যেকোন ভেরিয়েবল নিয়ে যেভাবে কাজ করা যায় সেভাবেই কাজ করা যাবে।

আমরা চাইলে এই স্ট্রাকচারের একটি ভেরিয়েবল ডিক্লেয়ার করার সময় অন্যান্য ভেরিয়েবলের মত সেটিকে initialize করতে পারি। int i = 0; লিখলে i নামক int ডিক্লেয়ার হওয়ার সাথে সাথে এতে 0 এসাইন করা হয়ে যায়। একই কাজ স্ট্রাকচারে করা যায়। ধরা যাক একটি স্ট্রাকচার টাইপ ডিফাইন করব আমরা যেটি হবে একটি box এর স্ট্রাকচার। এতে তিনটা মেম্বার থাকবে যারা একটি box এর দৈর্ঘ্য, প্রস্থ এবং উচ্চতা হবে।

[code language=”cpp”]
struct box {
int length;
int width;
int height;
};
[/code]

এখন এই box টাইপের একটি ভেরিয়েবল b ডিক্লেয়ার করব আমরা যেটির length, width এবং height তিনটি ভেরিয়েবলকেই আমরা 0 দিয়ে initialize করব।

[code language=”cpp”]
struct box b = { 0, 0, 0 };
[/code]

এভাবে যেকোন স্ট্রাকচার ভেরিয়েবলকে initialize করা যায়। সেজন্য যতগুলো মেম্বার আছে তার প্রতিটার মান ক্রমানুসারে { } এর মধ্যে কমা দিয়ে আলাদা করে লিখে দিলেই হয়ে যাচ্ছে।

স্ট্রাকচারের একটা বড় সুবিধা হচ্ছে একটা স্ট্রাকচারকে আরেকটা স্ট্রাকচারে এসাইন করা যায় যদি দুইটা একই টাইপের হয়। অর্থাৎ ধরা যাক আমাদের box টাইপের দুটি স্ট্রাকচার ভেরিয়েবল আছে, b1 এবং b2 । তাহলে আমরা b1 = b2 এভাবে করে b2 স্ট্রাকচারকে b1 এ এসাইন করতে পারি। এতে যা হবে তা হল, b2 এর সবগুলা মেম্বারে যা যা মান আছে সেগুলো b1 এর অনুরূপ মেম্বারগুলোতে এসাইন হয়ে যাবে।

আরেকটা ব্যাপার হল যেকোন ডাটা টাইপের যেমন পয়েন্টার ডিক্লেয়ার করা যায় তেমনি স্ট্রাকচারেরও পয়েন্টার ডিক্লেয়ার করা যায়। যেমন box টাইপের একটা পয়েন্টার ডিক্লেয়ার করা যায় এভাবে,

[code language=”cpp”]
struct box *b;
[/code]

এভাবে আমরা b নামক একটা পয়েন্টার ডিক্লেয়ার করলাম। কিন্তু যেহেতু মেমরি এলোকেট করা হয়নি তাই এটি নিয়ে এখনও কাজ করা যাবে না। এই ব্যাপারে বুঝতে হলে পয়েন্টার সম্পর্কে ভালো ধারণা থাকা প্রয়োজন। এই লেখাটি পড়লে আশা করি অনেকখানি পরিষ্কার হয়ে যাবে। এখন আমরা মেমরি এলোকেট করে নিব, তারপর কাজ করব।

[code language=”cpp”]
b = ( struct box * ) malloc( sizeof( struct box ) );
[/code]

এই b যেহেতু ভেরিয়েবল না, পয়েন্টার, তাই এর মাধ্যমে মেম্বার এক্সেস করতে হলে . (ডট) এর পরিবর্তে -> (তীর চিহ্ন) ব্যবহার করতে হয়। যেমন,

[code language=”cpp”]
b->length = 10;
scanf( "%d", &b->width );
printf( "%d", b->height );
[/code]

মূলত এগুলোই হল স্ট্রাকচারের বেসিক। বিভিন্ন ডাটা স্ট্রাকচারে এই স্ট্রাকচার ব্যবহার করা হয়। যেমন স্ট্যাক, কিউ, ট্রি এইসব ডাটা স্ট্রাকচারের মূল ভিত্তি হল স্ট্রাকচার। তাই স্ট্রাকচার কিভাবে ব্যবহার করতে হয়, এবং কিভাবে এটি কাজ করে তা জানা থাকা গুরুত্বপূর্ণ।

লিংকড লিস্ট (Linked List)

Standard

বিভিন্ন তথ্য রাখার জন্য বিভিন্ন ধরনের লিস্ট তৈরি করা হয়। লিস্টের একটি পরিচিত রূপ হল অ্যারে। অতগুলো তথ্য রাখতে হবে সেই অনুযায়ী একটা অ্যারে ডিক্লেয়ার করে সেখানে তথ্যকে লিস্ট আকারে রাখা যায় এবং পরবর্তিতে বিভিন্ন কাজ করা যায়। কিন্তু অ্যারেতে বেশ কিছু সমস্যা রয়েছে। যখন অ্যারে ডিক্লেয়ার করা হয় তখন সেখানে বলে দিতে হয় কতগুলো এলিমেন্ট থাকবে। যেহেতু অধিকাংশ ক্ষেত্রেই আমরা বলতে পারি না কতগুলো এলিমেন্ট লিস্টে থাকতে পারে তাই আনুমানিক আকারের একটা লিস্ট তৈরি করতে হয়। এতে করে অনেকগুলো এলিমেন্ট ডিক্লেয়ার করার ফলে অনেক মেমরি অপচয় হয়।

এছাড়া আরও একটি সমস্যা রয়েছে। অ্যারে মেমরিতে অবিচ্ছিন্নভাবে থাকে। অর্থাৎ একটার পর একটা এলিমেন্ট একের পর এক মেমরি ব্লকে থাকে। এর মাঝে আর কিছু থাকে না। আমাদের কাছে অনেকগুলো এলিমেন্টের একটা লিস্ট আছে। এখন যদি নতুন একটা এলিমেন্টকে সেই লিস্টের মাঝে কোথাও অন্তর্ভুক্ত করতে হয় তাহলে যে অবস্থানে এলিমেন্টটিকে রাখতে হবে সেখান থেকে তারপরের সবগুলো এলিমেন্টকে একটা একটা করে সরিয়ে পিছে নিতে হবে এবং মাঝে একটা জায়গা ফাঁকা করতে হবে। তারপর নতুন এলিমেন্ট রাখতে হবে। এটা অনেক সময়সাপেক্ষ একটা প্রক্রিয়া। এছাড়া যদি এমন হয় যে আবার কোন একটা এলিমেন্ট ডিলিট করতে হবে, তাহলে আবার পিছের সব এলিমেন্টকে এক এক করে সামনে নিয়ে আসতে হবে। আর এই নতুন এলিমেন্ট যোগ করা বা কোন এলিমেন্ট ডিলিট করার প্রক্রিয়া যদি বারবার করতে হয় তাহলে প্রোগ্রামটি অনেক সময় নিবে কাজগুলো করতে।

এইসব সমস্যা দূর করার জন্যই লিংক লিস্ট। এই লিস্টের বৈশিষ্ট্য হল এখানে এলিমেন্টগুলো মেমরিতে অবিচ্ছিন্নভাবে থাকে না। বরং বিচ্ছিন্নভাবে বিভিন্ন জায়গায় ছড়িয়ে ছিটিয়ে থাকে। অ্যারেতে যেহেতু একের পর এক থাকে তাই সেখানে প্রথম এলিমেন্টের এড্রেস জানলে সেই এড্রেসের সাথে এক করে যোগ করে এগিয়ে যেতে থাকলেই পরের এলিমেন্টগুলো পাওয়া যেত। কিন্তু এখানে তো একের পর এক থাকবে না। তাহলে আমরা প্রথম এলিমেন্টের পরের এলিমেন্ট পাব কিভাবে? সেইজন্য আমাদেরকে প্রতিটা এলিমেন্টের সাথে পরবর্তি এলিমেন্টটি যেখানে আছে তার এড্রেস রাখতে হয়। ফলে আমরা যখন একটা এলিমেন্টে যাব তখন এর পরবর্তি এলিমেন্টের এড্রেস তার সাথে থাকায় সেটা দিয়ে আমরা পরের এলিমেন্টে চলে যেতে পারি। আবার সেখানে তার পরের এলিমেন্টের এড্রেস থাকায় তার পরের এলিমেন্টে চলে যেতে পারি। এভাবে একটা চেইন তৈরি হয়। আর এই চেইনটাই হল লিংক লিস্ট।

তাহলে এটুকু বোঝা গেল যে অ্যারেতে যেমন লিস্টের একেকটা এলিমেন্টে শুধু ঐ এলিমেন্টটাই থাকে, লিংক লিস্টে তেমনটি থাকে না। লিংক লিস্টে প্রতিটা এলিমেন্টের সাথে সাথে তার পরবর্তি এলিমেন্টের এড্রেস থাকে। তাহলে লিস্টের শুরু কোথায় সেটা জানবো কিভাবে? সেটা জানার জন্য আমরা যখন লিস্ট তৈরি করবো তখন প্রথম এলিমেন্টের এড্রেসটা আমাদের কাছে একটা পয়েন্টারে রেখে দিব। এটা অনেক গুরুত্বপূর্ণ ব্যাপার। প্রথম এলিমেন্টের এড্রেস না থাকলে কিন্তু লিস্টের কিছুই পাওয়া যাবে না। হারিয়ে যাবে। কারণ অন্য সব এলিমেন্টের এড্রেস তার আগের এলিমেন্টের সাথে আছে। আর লিস্ট শেষ হল কোথায় সেটা জানবো/বুঝবো কিভাবে? শেষ যে এলিমেন্ট থাকবে তার সাথে তার পরবর্তি এলিমেন্টের এড্রেস যে থাকার কথা, সেই এড্রেসটা হবে NULL। অতএব লিস্টের একেকটা এলিমেন্ট ধরে এগোতে থাকলে যখনই আমরা NULL পয়েন্টার পেয়ে যাব তখনই বুঝবো যে লিস্ট এখানেই শেষ। লিংক লিস্ট ভালো মত বুঝতে হলে পয়েন্টার সম্পর্কে অনেক ভালো ধারণা থাকা দরকার। তাই আগে এই লেখাটি পড়লে বুঝতে সুবিধা হবে।

এখন আমরা তাহলে লিংক লিস্টের মূল বৈশিষ্ট্য জানলাম। আসল ব্যাপারটাই এখনও পরিষ্কার করা হয় নি। অ্যারেতে যে সমস্যা ছিল যে কোন একটা এলিমেন্ট যদি এর মাঝে বসাতে হয় তাহলে পরের সবগুলাকে সরাতে হয়, লিংক লিস্টে সেটি না করেই কিভাবে বসানো যায়? সহজ উত্তর। যে দুইটা এলিমেন্টের মাঝে আমাকে নতুন এলিমেন্ট বসাতে হবে সেই দুইটা এলিমেন্ট আমরা খুঁজে বের করবো। তারপর আগের এলিমেন্টের যে স্থানে পরবর্তি এলিমেন্টের এড্রেস ছিল সেখানে নতুন এলিমেন্টের এড্রেস দিয়ে দিব। আর নতুন এলিমেন্টের সাথে পরবর্তি এলিমেন্টের এড্রেস রাখার যে ব্যবস্থা আছে সেখানে পরের যে এলিমেন্টটা আছে তার এড্রেস বসিয়ে দিব। অর্থাৎ যদি এমন হয় আমাকে x এবং y এর মাঝে a কে বসাতে হয় তাহলে x এর সাথে a এর এড্রেস রাখবো এবং a এর সাথে y এর এড্রেস রাখবো। তাহলে যখন আমরা x এ যাব তখন এরপরের এলিমেন্টে গেলে a পাব। কারণ a এর এড্রেস রাখা আছে। আবার a থেকে আমরা পরের এলিমেন্ট y এ যাব। কারণ a এর সাথে y এর এড্রেস আছে। তাহলে বস্তুত পক্ষে যেটা হল তা হল, এলিমেন্টগুলো মেমরিতে যেখানে যেই এড্রেসে আছে সেখানেই রয়ে যাচ্ছে। আমরা কেবল পরের এলিমেন্ট কোনটা হবে সেটার এড্রেস পালটিয়ে দিলেই লিংক লিস্টের ক্রম পালটিয়ে যাচ্ছে। ফলে অনেকগুলো এলিমেন্টকে এক এক করে কোথাও সরাতে হচ্ছে না।

এখন পর্যন্ত যা আলোচনা হল তাতে এটা বোঝা গেল যে এখানে একটা এলিমেন্ট একা থাকবে না। তার সাথে পরের এলিমেন্টের এড্রেস থাকবে। কিন্তু একসাথে একটা ডাটা টাইপে আমরা দুই ধরনের ডাটা রাখতে পারি না। সেটা করতে হলে আমাদের লাগবে স্ট্রাকচার। অতএব লিংক লিস্ট বুঝতে হলে আমাদেরকে স্ট্রাকচার সম্পর্কেও ভালো ধারনা রাখতে হবে। আমরা একেকটা এলিমেন্টকে বলব নোড। কারণ লিস্টে যে কেবল একটা করে এলিমেন্ট নিয়েই তৈরি হবে তা না। এমন যদি হয় ১০০ জন ছাত্রের তথ্য আছে। ছাত্রের নাম, রোল, ঠিকানা। যদি ১০০ জন ছাত্রের লিস্ট তৈরি করা হয় তবে সেখানে প্রতিটা নোড হবে একেকজন ছাত্রের জন্য আর সেখানে ৪ টা করে তথ্য থাকবে। নামের জন্য একটা স্ট্রিং, রোলের জন্য একটা int, ঠিকানার জন্য আরেকটা স্ট্রিং এবং পরবর্তি ছাত্রের তথ্য কোন এড্রেসে আছে সেটার পয়েন্টার। অতএব একটা লিস্ট তৈরি হয় কতগুলো নোড দিয়ে। নোডগুলো কিরকম হবে তা আমরা স্ট্রাকচার ডিক্লেয়ার করে বলে দিব। বোঝার সুবিধার জন্য এখানে কেবল কতগুলো সংখ্যার লিস্টের ব্যাপারে আলোচনা করা হল। এতে একটা নোডে একটা int থাকবে, যেই সংখ্যা আমরা রাখতে চাই। আর থাকবে পরের নোডের এড্রেস। তাহলে একটা স্ট্রাকচার ডিফাইন করা যাক।

[code language=”cpp”]
code:
struct aNode {
int nodeValue; // the value of the node
struct aNode *nextNode; // the address of the next node
};
[/code]

এখানে আমরা aNode নামে একটা নতুন ডাটা টাইপ তৈরি করলাম। প্রতিটা নোড এই টাইপের হবে। আগেই বলা হয়েছে আমরা এই লিস্ট ব্যবহার করতে চাইলে তার প্রথম নোডের এড্রেস আমাদের রাখতে হবে। সেই জন্য একটা পয়েন্টার ডিক্লেয়ার করব।

[code language=”cpp”]
code:
typedef struct aNode Node;
Node *head;
[/code]

প্রথমে আমরা struct aNode ডাটা টাইপের একটা প্রতিশব্দ তৈরি করলাম typedef ব্যবহার করে। কারণ আমাদেরকে অনেক বড় একটা কোডের মধ্যে বারবার struct aNode লিখতে হবে। সেখানে এই পুরো অংশটার জন্য একটা প্রতিশব্দ তৈরি করলাম Node নামে। ফলে যেখানেই Node লেখা থাকবে আমরা বুঝবো যে আসলে সেটা struct aNode লেখা। ব্যাপারটা বুঝতে অসুবিধা হলে এভাবে চিন্তা কর যে এরপর থেকে কোডের যেখানেই দেখবে Node লেখা, সেখানে তুমি struct aNode বসিয়ে নিবে। ভালোমত ধারনা পেতে চাইলে এই লেখাটা পড়তে পারো।

এরপর আমরা head নামে একটা পয়েন্টার ডিক্লেয়ার করলাম যার মধ্যে লিস্টের প্রথম নোডের এড্রেস থাকবে। এই head কে আমরা গ্লোবাল ভেরিয়েবল হিসেবে ডিক্লেয়ার করলাম কারণ বিভিন্ন ফাংশন একে ব্যবহার করবে। যখন আমরা main ফাংশনে কাজ শুরু করব তখন প্রথমেই head = NULL। এটা বোঝাবে যে এখনও লিস্ট তৈরি হয়নি। আমরা যখন head এ প্রবেশ করতে চাব তখন NULL থাকার কারণে কোথাও প্রবেশ করা যাবে না। ফলে আমরা বুঝতে পারব যে এখনও লিস্ট তৈরি হয়নি।

লিস্ট তৈরি এবং ব্যবহারে সাধারণভাবে তিনধরনের কাজ করা হয় –
১) নতুন নোড যোগ করা হয় (insert node)
২) কোন নোড ডিলিট করা হয় (delete node)
৩) সমগ্র লিস্ট প্রিন্ট করা হয় (print list)

এই তিনটা কাজের জন্য আমরা তিনটা ফাংশন লিখবো –
১) void insertNode( int insertValue )
২) void deleteNode( int deleteValue )
৩) void printList( )

প্রথমেই দেখা যাক insertNode() কিভাবে কাজ করে।

[code language=”cpp”]
code:
void insertNode( int insertValue )
{
Node *temp;
Node *current;

temp = ( Node * ) malloc( sizeof( Node ) );
temp->nodeValue = insertValue;
temp->nextNode = NULL;

if( !head ) {
head = temp;
}
else if( insertValue <= head->nodeValue ) {
temp->nextNode = head;
head = temp;
}
else {
for( current = head; current->nextNode;
current = current->nextNode ) {
if( insertValue > current->nodeValue &&
insertValue <= current->nextNode->nodeValue )
break;
}

if( !( current->nextNode ) )
current->nextNode = temp;

else {
temp->nextNode = current->nextNode;
current->nextNode = temp;
}
}

printf( “Value has been inserted successfully!\n” );
printList();

return;
}
[/code]

৪ ও ৫ নাম্বার লাইনে দুইটা পয়েন্টার ডিক্লেয়ার করা হয়েছে। পয়েন্টার দুইটা Node এর এড্রেস রাখতে পারবে। এরপর আমরা malloc( ) ব্যবহার করে Node এর জন্য মেমরি এলোকেট করলাম। এটা আমরা যে নাম্বারটা লিস্টে রাখতে চাই তার জন্য তৈরি করা নতুন নোড। এখানে আমরা নাম্বারটাকে রাখব। এই যে মেমরি এলোকেট করলাম, যেখানে মেমরি এলোকেট করা হয়েছে সেই এড্রেসটা আমরা temp এর মধ্যে রাখলাম। এরপর আমরা যে লিখলাম temp->nodeValue = insertValue তার মানে হল, temp এর মধ্যে যে এড্রেস আছে, অর্থাৎ আমাদের নতুন তৈরিকৃত নোডের এড্রেস, সেই এড্রেসে যে স্ট্রাকচার আছে (একটু আগেই আমরা মেমরি এলোকেট করে তৈরি করেছি) তার nodeValue ভেরিয়েবলের মধ্যে আমরা আমাদের insertValue কে রাখলাম। আর ঐ একই স্ট্রাকচারের মধ্যে পরবর্তি নোডের এড্রেস রাখার যে পয়েন্টার তার মধ্যে NULL রাখলাম। কারণ এখনও আমরা জানি না এর পরে কি আর কিছু থাকবে নাকি।

এরপর যেটা করা হল তা হল, if( !head ) বা যদি head এর ভেলু NULL হয় তাহলে আমরা জানি যে এখনও লিস্ট তৈরি হয়নি। অর্থাৎ এটাই লিস্টের প্রথম এবং শেষ, একমাত্র নোড হবে। তাই আমরা head এর মধ্যে temp এ থাকা এড্রেস, অর্থাৎ আমাদের নতুন নোডের এড্রেস রেখে দিলাম।

এখন যদি head এর ভেলু NULL না হত, মানে যদি লিস্টে আগে থেকেই কিছু না কিছু থাকত, তাহলে আমরা প্রথমে দেখতাম যে যেই নাম্বারটা লিস্টে রাখতে হবে সেটা কি head এ থাকা এড্রেসে থাকা স্ট্রাকচারে থাকা (এরপর থেকে এভাবে বলব “head এ থাকা”) নাম্বারের সমান কিংবা ছোট কিনা। কারণ আমরা এখানে একটা ordered লিংক লিস্ট তৈরি করছি। এখানে প্রতিটা তথ্য যখন ইনপুট দেওয়া হবে তখন তা সর্ট হয়ে লিস্টে জায়গা মত বসবে। তাই যদি head এর ভেলু থেকে ছোট বা সমান হয় তাহলে সেই নাম্বারকে আমরা head এর আগে বসাবো। আর এটাই হবে আমাদের নতুন head।

এই জন্য আমরা প্রথমে বর্তমান head এ যে এড্রেস আছে তাকে নতুন নোডের পরবর্তি নোডের এড্রেসের মধ্যে রেখে দিলাম। তারপর head পয়েন্টারের মধ্যে আমরা আমাদের নতুন সৃষ্ট নোডের এড্রেস রেখে দিলাম। তাহলেই নতুন নোডটা শুরুতে যোগ হয়ে গেল এবং সেখানে গেলে আমরা পরের নোডের এড্রেসে গেলে আগে যেটি head ছিল (এখন যেটি দ্বিতীয়) সেখানে যেতে পারব।

যদি ভেলুটি head এর ভেলু থেকে বড় হয়, তাহলে আমরা একটা লুপ চালাবো, head থেকে লুপ শুরু করবো এবং তা চলতে থাকবে যতক্ষণ না পর্যন্ত আমরা ভেলুটিকে বসানোর উপযুক্ত স্থান পাচ্ছি। অর্থাৎ যখন আমরা দেখবো যে লিস্টের এমন অবস্থানে চলে এসেছি যেখানে পূর্ববর্তি ভেলুটি নতুন ভেলুর চেয়ে ছোট এবং পরবর্তি ভেলুটি এর সমান বা বড় তখন আমরা সেখানে নতুন নোডটিকে বসাবো। যদি এমন অবস্থান না পাই, নতুন ভেলুটি যদি লিস্টের সব ভেলুর চেয়ে বড় হয় তাহলে তাকে লিস্টের একেবারে শেষে নিয়ে বসাবো। এখন দেখা যাক লুপটা কিভাবে কাজ করে।

লুপ চালানোর জন্য এখানে current নামক পয়েন্টারটা ব্যবহার করা হয়েছে। অ্যারেতে লুপ চালানোর সময় আমরা যেমন int i ব্যবহার করি (সাধারনত) তেমনি এখানে যেহেতু এড্রেস ধরে লুপ চালানো হবে তাই পয়েন্টার ব্যবহার করা হচ্ছে। শুরুতে আমরা current এর মধ্যে head এ থাকা এড্রেস রাখলাম। head থেকে লুপ শুরু হবে। এরপর আমরা দেখলাম current এর মধ্যে যে পরবর্তি নোডের এড্রেস আছে সেটা NULL কি না। যদি NULL হয় তার মানে আমরা লিস্টের শেষে চলে এসেছি। যদি তা না হয় তাহলে আমরা লুপের ভিতরে ঢুকলাম। এখন আমরা current এ থাকা ভেলু এবং current এর পরবর্তি নোডে থাকা ভেলুর সাথে নতুন ভেলু তুলনা করে দেখবো যে সেটা কি এদের মাঝে বসবে কি না। যদি বসে তার মানে আমরা অবস্থান পেয়ে গিয়েছি, তাই লুপ ব্রেক করব। আর যদি না বসে, তাহলে আমরা current এর মধ্যে বর্তমান current এ থাকা নোডের পরবর্তি নোডের এড্রেস দিয়ে দিব। এভাবে একটা একটা করে নোড আগাতে থাকবে এবং লুপ চলতে থাকবে। সেই সাথে অবস্থান চেক করা হবে।

যখন লুপ ব্রেক হয়ে আসলো তখন আমরা দেখবো সেটি কিভাবে ব্রেক হয়েছে। আমরা কি কোন অবস্থান পেয়েছি নাকি লিস্টের শেষে চলে এসেছি। যদি লিস্টের শেষে চলে আসি তাহলে current->nextNode এর মধ্যে থাকা এড্রেস NULL হবে। যদি সেটি হয় তাহলে আমরা সেখানে আমাদের নতুন নোডের এড্রেস দিয়ে দিব। যেহেতু শুরুতেই আমরা আমাদের নতুন নোডের nextNode NULL করে নিয়েছিলাম তাই শেষ নোডের পরবর্তি নোডের এড্রেস NULL-ই থাকবে এখানে।

আর যদি current->nextNode এর মধ্যে থাকা এড্রেস NULL না হয় তার মানে লুপ কোন একটা অবস্থান খুঁজে পেয়ে ব্রেক হয়েছে, লুপের শেষ পর্যন্ত যেতে হয়নি। এখন আমাদেরকে এই নতুন অবস্থানে নোডটিকে সংযোগ দিতে হবে। যেহেতু আমরা লুপের শেষে যাই নি, আমরা একটা অবস্থান পেয়েছি যেই অবস্থানটা হল current এবং current->nextNode এই দুই এড্রেসে থাকা নোড দুইটার মাঝখানে। এদের মাঝে নতুন নোডকে সংযোগ দেওয়ার জন্য যেটা করতে হবে হল নতুন নোডের এড্রেসকে current->nextNode এর মধ্যে রাখতে হবে, আর current->nextNode এ থাকা এড্রেসকে নতুন নোডের nextNode এর মধ্যে রাখতে হবে। সেটিই কোডে করা হয়েছে। এখানে একটা বিষয় খেয়াল করতে হবে, কেন আগে temp->nextNode = current->nextNode করা হয়েছে। আগে current->nextNode = temp করলে কি হত? যেটা হত তা হল আমরা current->nextNode এ থাকা এড্রেসটা হারিয়ে ফেলতাম। ফলে লিংক লিস্টের সংযোগ মাঝখান থেকে ভেঙ্গে যেত। এই জন্য আগে সেই এড্রেসকে temp->nextNode এর মধ্যে রাখা হয়েছে, তারপরে সেখানে temp এ থাকা এড্রেস রাখা হয়েছে। ফলে নতুন নোডটি লিংক লিস্টে তার জায়গা মত সংযুক্ত হয়ে গেল। এরপরে একটা আউটপুট দিয়ে বলা হয়েছে যে কাজটি সম্পন্ন হয়েছে এবং তারপর সম্পূর্ণ লিস্টটি প্রিন্ট করা হয়েছে printList( ) ফাংশন দিয়ে।

লিংক লিস্টে কিভাবে লুপ চালিয়ে এগিয়ে যেতে হয়, কিভাবে সেখানে নতুন নোড সংযোগ করা হয় ইত্যাদি মূল ঝামেলার অংশগুলোই এখানে আলোচনা করা হল। এই ব্যাপারগুলো ব্যবহার করেই ডিলিট এবং প্রিন্ট ফাংশন লিখতে হবে। সেই ফাংশন দুইটা কিভাবে কাজ করে তা বোঝার দায়িত্ব তোমার। সব বলে দিলে নিজের চিন্তা করে শেখাটা হবে না। আর এই ফাংশনটা বুঝলে ঐদুইটা আরও সহজ, নিজে থেকে বোঝার চেষ্টা কর।

ডিলিট কিভাবে করতে হবে তার আইডিয়াটা একটু বলে দেই। যদি যেই ভেলু ডিলিট করতে হবে সেটি head এ থাকে তাহলে তো কেবল পরের নোডটার এড্রেস head এ দিয়ে দিলেই হয়ে যাবে। লিস্টে আগের হেডটা আর থাকল না। আর যদি মাঝ থেকে কোন নোড ডিলিট করতে হয় তাহলে আগের মতই সেই নোড খুঁজে বের করতে হবে। তারপর সেই নোডের মধ্যে যেহেতু পরবর্তি নোডের এড্রেস আছে তাই সেই এড্রেসটাকে আগের নোডের nextNode এর মধ্যে রেখে দিব। ফলে মাঝের নোডটি লিংক লিস্ট থেকে বিচ্ছিন্ন হয়ে গেল। বাকিটুকু নিজে চিন্তা কর।

[code language=”cpp”]
code:
void deleteNode( int deleteValue )
{
if( !head ) {
printf( “No node available yet.\n” );
return;
}

Node *current, *temp;

if( deleteValue == head->nodeValue ) {
temp = head;
head = head->nextNode;
free( temp );
}

else {
for( current = head; current->nextNode;
current = current->nextNode ) {
if( current->nextNode->nodeValue == deleteValue )
break;
}

if( !current->nextNode ) {
printf( “The value was not found in the list.\n” );
return;
}

temp = current->nextNode;
current->nextNode = current->nextNode->nextNode;
free( temp );
}

printf( “The node has been deleted!\n” );
printList();

return;
}

void printList()
{
Node *current;

if( !head ) {
printf( “No node available.\n” );
return;
}

printf( “Here is the updated list:\n” );

for( current = head; current; current = current->nextNode )
printf( “%d –> “, current->nodeValue );
printf( “NULL\n” );

return;
}
[/code]

লিংক লিস্টের সমগ্র প্রোগ্রামটির কোড কমেন্টসহ পাওয়া যাবে এখানে

পয়েন্টার (Pointer)

Standard

প্রায় সব নবীন প্রোগ্রামারদের জন্যই সম্ভবত পয়েন্টার একটা ভয়ংকর অভিজ্ঞতা! এটি না হওয়ারও আসলে কোন কারণ নেই। পয়েন্টার অনেক বেশি মাত্রায় কনফিউশন তৈরি করে। এটি কি এবং কিভাবে কাজ করে সেই সম্পর্কে স্পষ্ট ধারণা না থাকলে এটি ব্যবহার করাও বেশ দুরূহ হয়ে দাঁড়ায়।

পয়েন্টার হচ্ছে একটা ভেরিয়েবল। হ্যাঁ, int, char, double এগুলো যেমন ভেরিয়েবল, পয়েন্টারও তেমনি একটি ভেরিয়েবল। int এ থাকে ১৬/৩২ বিট পূর্ণ সংখ্যা, double এ থাকে দশমিক সংখ্যা, আর পয়েন্টারে থাকে একটা এড্রেস। শুরু হয়ে গেল কনফিউশন! 😛

কনফিউশন দূরীকরণ শুরু করি। ব্যাপারটাকে ভিজুয়ালাইজ করতে পারলে হয়তো একটু সহজ হবে বুঝতে পারা, তাই বাস্তব কিছুর সাথে সাদৃশ্য খোঁজার চেষ্টা করা যাক। ধরা যাক, ভেরিয়েবল হচ্ছে একটি বাক্স। বাক্সে কিছু জিনিস রাখা যায়। অর্থাৎ বাক্সের মেমরি থাকে। ভেরিয়েবলেরও কিছু মেমরি থাকে। ৮ বিট, ১৬ বিট, ৩২ বিট ইত্যাদি বিভিন্ন পরিমাণের মেমরি থাকে। অতএব ভেরিয়েবল হল একটি বাক্স যার নির্দিষ্ট মেমরি থাকে। ভেরিয়েবলের যে নাম, সেটি হল বাক্সের নাম। আমার কাছে অসংখ্য বাক্স আছে। চেনার জন্য প্রতিটা বাক্সের নাম থাকবে।

[code language=”cpp”]int i;[/code]

আমি i নামে একটা int টাইপের ভেরিয়েবল ডিক্লেয়ার করলাম। অর্থাৎ, অসংখ্য মেমরি ব্লক বা বাক্স থেকে একটা বাক্স আমার জন্য বরাদ্দ হয়ে গেল। বাক্সটার ধারণ ক্ষমতা ১৬/৩২ বিট (সিস্টেমভেদে) এবং এর নাম দিলাম i । আমি এখানে যদি 5 রাখতে চাই তাহলে লিখব i = 5; সহজ ব্যাপার। বাক্সের মধ্যে 5 ঢুকিয়ে দিলাম। যেখানেই এটা ব্যবহার করতে হবে আমি i লিখলেই হয়ে যাবে।

এখন আমার কাছে তো অনেক মেমরি আছে, অনেক বাক্স আছে। ধরলাম বাক্সগুলো একটা বিশাল গোডাউনের মধ্যে রাখা আছে। বাক্সগুলো সুন্দর করে একটার পর একটা সাজিয়ে রাখা আছে যাতে সহজেই কোন একটা বাক্স কোথায় আছে জানা থাকলেই সেটা খুঁজে পাওয়া যায়। এই যে বললাম ‘কোথায় আছে’ এর মানেই হল প্রতিটার বাক্সের একটা ঠিকানা/এড্রেস আছে। গোডাউনের কোথায় বাক্সটা আছে, কত নাম্বার ব্লকে, কত নাম্বার লাইনে আছে এরকম কোন একটা ঠিকানা আছে। এভাবে পুরো গোডাউনে যত বাক্স আছে সবগুলোর জন্যই একটা করে ঠিকানা আছে। আমি যদি বাক্সটার নাম নাও জানি, আমি বাক্সটার ঠিকানা জানলে সেখানে গেলেই বাক্সটাকে পেয়ে যাব।

অর্থাৎ আমরা যে ভেরিয়েবল ডিক্লেয়ার করি, তখন যে মেমরি আমাদের জন্য বরাদ্দ হয়ে যায়, তার একটা এড্রেস আছে। আমরা চাইলে ভেরিয়েবলের নাম দিয়েও সেটা ব্যবহার করতে পারি, আবার চাইলে এড্রেস দিয়েও ব্যবহার করতে পারি কারণ এড্রেসে গেলে ঐ ভেরিয়েবলটাই পাওয়া যাবে।

এখন এড্রেস তো একটা তথ্য। মেমরিতে প্রতিটা মেমরি ব্লকের জন্য এড্রেস নির্দিষ্ট। এই এড্রেস যদি আমি ব্যবহার করতে চাই তাহলে তাকে একটা ভেরিয়েবলের মধ্যে রাখতে হয়। সেই ভেরিয়েবলটাই হল পয়েন্টার। অন্য সব ভেরিয়েবলের যেমন সাইজ নির্দিষ্ট তেমনি পয়েন্টারেরও সাইজ নির্দিষ্ট। আমরা যে অপারেটিং সিস্টেম ব্যবহার করি, ধরা যাক উইন্ডোজ, এটায় যে বলা থাকে ৩২ বিট বা ৬৪ বিট সেটা আসলে পয়েন্টারের সাইজ। অর্থাৎ একেকটা এড্রেসকে ভেরিয়েবলে স্টোর করে রাখতে অত বিট মেমরি লাগে।

আমরা পয়েন্টার ডিক্লেয়ার যখন করি তখন যে ভেরিয়েবলের মধ্যে এড্রেস রাখবো তার সামনে একটা * দিয়ে দেই। *ptr মানে হল “ptr” নামে একটা ভেরিয়েবল তৈরি হয়েছে যেখানে কোন একটা এড্রেস স্টোর করে রাখা যাবে। এটা কিন্তু বেশ গুরুত্বপূর্ণ। ptr কোন এড্রেস না, এটা একটা ভেরিয়েবল যেখানে একটা এড্রেস আছে! এড্রেসকে হেক্সাডেসিমেলে প্রকাশ করা হয়। printf( ) দিয়ে এড্রেস আউটপুট দিলে দেখা যাবে একটা হেক্সাডেসিমেল নাম্বার দেখাচ্ছে। কিভাবে করা যায় সেটায় পরে আসি।

তাহলে যা বুঝলাম তা হল যে *ptr লিখলে ptr নামে একটা ভেরিয়েবল তৈরি হয়, যেমন তৈরি হয়েছিল i নামে একটা ভেরিয়েবল। এই ptr ভেরিয়েবলের মধ্যে একটা এড্রেস থাকবে। এখন কিসের এড্রেস এখানে থাকবে সেটা কম্পিউটারকে বলে দিতে হয়। নাহলে সে কাজ করতে পারবে না। তাই যদি আমি একটা ইন্টিজারের পয়েন্টার ডিক্লেয়ার করি তাহলে লিখব, int *ptr; তাহলে যেটি হবে তা হল ptr নামে একটা পয়েন্টার (মানে এড্রেসের ভেরিয়েবল) তৈরি হবে যেটায় শুধুমাত্র একটা int টাইপ ভেরিয়েবলের এড্রেস থাকতে পারবে। এখানে আরেকটা গুরুত্বপূর্ণ বিষয় খেয়াল করা লাগবে। যখন আমি ptr ডিক্লেয়ার করলাম, তখন কিন্তু কোন ইন্টিজারের জন্য মেমরি বরাদ্দ হয়নি যেমনটি হয় যদি আমি লেখি int i;

int i; লিখলে i নামের একটা int ভেরিয়েবল তৈরি হয়। সেখানে ৩২ বিট মেমরি বরাদ্দ হয়েছে। কিন্তু যখন ডিক্লেয়ার করলাম তখন কি সেই মেমরি এর ভিতরে কিছু আছে? নেই। কারণ কিছু রাখিনি আমরা। (বাস্তবে গার্বেজ ভেলু থাকে, সেইটা আলোচনার বিষয় না। ধরা যাক কিছু নেই।) যেটি হয়েছে তা হল আমি কম্পিউটারকে বললাম যে আমার জন্য i নামের একটা ভেরিয়েবল তৈরি কর যেখানে আমি int টাইপের (৩২ বিটের) কিছু রাখতে পারব।

একইভাবে যখন আমি লিখলাম int *ptr; তখন ptr নামের একটা ভেরিয়েবল তৈরি হল। সেখানে ৩২/৬৪ বিট মেমরি বরাদ্দ হয়েছে। কিন্তু সেই মেমরি এর ভিতরে কিছু নেই। যেমনটি ছিল না int i এর ক্ষেত্রে। এই যে মেমরি বরাদ্দ হয়েছে, যেখানে এখনও কিছু নেই, এটা কিন্তু কোন ইন্টিজারের মেমরি না! এটা এড্রেসের জন্য বরাদ্দ হয়েছে। অনেকেই ভাবে আমি তো ptr ডিক্লেয়ার করলাম। তাহলে কেন আমি এখানে একটা ইন্টিজার রাখতে পারব না। তার কারণ হল যখন আমি ডিক্লেয়ার করলাম তখন আমি একটা এড্রেসের জন্য মেমরি বরাদ্দ করেছি। এটা কোন ইন্টিজারের মেমরি না। এখানে একটা এড্রেস থাকবে যেই এড্রেস কোন একটা int এর এড্রেস। কিন্তু এখনও এখানে কোন এড্রেস নেই। ঠিক আগের int i এর মত। এই অংশটুকু বুঝতে অসুবিধা হলে বারবার পড়ে দেখ। এই ব্যাপারটা ক্লিয়ার হওয়া অনেক জরুরী। অন্যথায় পয়েন্টার কি সেটাই বোঝা হবে না। প্রয়োগ তো পরের কথা!

ধরে নিচ্ছি এখন আমরা পয়েন্টার কি এতটুকু অন্তত জানি। 🙂 এখন দেখা যাক এটা নিয়ে কিভাবে কাজ করা হয়। একেবারে বেসিক জিনিসপত্র। নিচের অংশটুকু দেখা যাক।

[code language=”cpp”]int *ptr1, *ptr2;
int value1, value2;
ptr1 = &value1;
printf( "%p ", ptr1 );
ptr2 = &value2;
printf( "%p\n", ptr2 );
ptr1 = &value2;
printf( "%p ", ptr1 );
ptr2 = &value1;
printf( "%p", ptr2 );[/code]

প্রথম লাইনে ptr1 এবং ptr2 নামে দুটি পয়েন্টার ভেরিয়েবল ডিক্লেয়ার করলাম যারা কোন int এর এড্রেস রাখতে পারবে। এরপরের লাইনে দুইটা int ভেরিয়েবল ডিক্লেয়ার করলাম value1, value2। এরপরের লাইনে & অপারেটর ব্যবহার করা হয়েছে। এই অপারেটর দিয়ে কোন একটা ভেরিয়েবলের এড্রেস পাওয়া যায়। value1 হল একটা int ভেরিয়েবলের নাম। সেটার এড্রেস আমরা জানি না। আর আগেই বলেছি ptr1 কিন্তু কোন এড্রেস না। এটা এড্রেস রাখার একটা ভেরিয়েবল। এখানে আমরা value1 এর এড্রেস রাখতে চাই। value1 এর এড্রেস পাওয়ার জন্য আমরা লিখলাম &value1 । শুধু value1 একটা ভেরিয়েবল, যখনই তার সামনে & বসালাম তখনই সেটা হয়ে গেল value1 এর এড্রেস। তাহলে এখন ptr1 এর মধ্যে value1 এর এড্রেস রাখলাম। কেউ যদি এটা লিখে ptr1 = value1 তাহলেই ওয়ার্নিং দেখাবে, কারণ value1 হল ভেরিয়েবলের না, এখানে রাখতে হবে এড্রেস।

এরপরের লাইনে ptr1 এর মধ্যে যে এড্রেস আছে সেটিকে আউটপুট দেওয়া হল। এড্রেসকে হেক্সাডেসিমেলে প্রকাশ করা হয় এবং ইন্টিজার আউটপুট দিতে যেমন %d ব্যবহার করা হয় তেমনি এড্রেস আউটপুট দিতে %p ব্যবহার করা হয়। আউটপুটে একটা হেক্সাডেসিমেল নাম্বার দেখা যাবে যেটিই আসলে value1 এর এড্রেস। এর পরেরলাইনে একইভাবে ptr2 এর মধ্যে value2 এর এড্রেস রাখা হচ্ছে এবং তার পরের লাইনে সেটিকে আউটপুট দেওয়া হচ্ছে।

এরপরের চার লাইনে শুধু উলটিয়ে দেওয়া হয়েছে। ptr1 এ রাখা হয়েছে value2 এর এড্রেস এবং ptr2 এ রাখা হয়েছে value1 এর এড্রেস। আমার কম্পিউটারে এই কোডের আউটপুট এরকমঃ

0022FF14 0022FF10
0022FF10 0022FF14

এই কোডটার উদ্দেশ্য এটা বোঝানো যে যখন আমি একটা int ভেরিয়েবল ডিক্লেয়ার করি তখন কোন একটা জায়গায় একটা মেমরি বরাদ্দ হয়ে যায় এবং যেখানে মেমরি বরাদ্দ হয়েছে সেই এড্রেসটা নির্দিষ্ট। দুইটি ভেরিয়েবল দুই জায়গায় বরাদ্দ হয়েছে। দুইটির আলাদা আলাদা নির্দিষ্ট এড্রেস আছে। আমি কখন কোন পয়েন্টারে কোন এড্রেস রাখবো এবং কিভাবে সেটা ব্যবহার করব তা সম্পূর্ণ আমার ব্যাপার। উপরের কোডে আমি একবার ptr1 এ value1 এর এড্রেস রাখলাম আরেকবার ptr2 এ রাখলাম। এড্রেস কিন্তু একই আছে। আউটপুটেই তা দেখা যাচ্ছে। শুধু দুইবার দুই ভেরিয়েবলে এড্রেসটা রাখলাম। আশা করি ব্যাপারটা পরিষ্কার করতে পেরেছি।

একটা পয়েন্টারে যে এড্রেস আছে আমরা যদি সেই এড্রেসের ভেরিয়েবলটা ব্যবহার করতে চাই তাহলে পয়েন্টারের আগে একটা * লেখি। যেমন উপরের ক্ষেত্রে আমরা ptr এর মধ্যে যখন value1 এর এড্রেস রাখলাম তখন যদি আমরা value1 কে ptr1 এর মাধ্যমে ব্যবহার করতে চাই তাহলে লিখবো *ptr1 । যেমন যদি value2 এর মধ্যে value1 রাখতে চাই, তাহলে লিখবো, value2 = *ptr1; যদি value1 এর মধ্যে value2 রাখতে চাই এড্রেসের মাধ্যমে তাহলে লিখবো, *ptr1 = value2; অর্থাৎ &value1 লিখলে যেমন value1 এর এড্রেস পাওয়া যায় তেমন *ptr1 লিখলে ptr1 এ যে এড্রেস আছে সেই এড্রেসে অবস্থিত ভেরিয়েবল পাওয়া যায়।

আরেকটা বিষয় জানা থাকা দরকার। ptr = 0 বা ptr = NULL যদি লেখা হয় তাহলে কি হয়? কোন এড্রেস 0 বা NULL মানে হচ্ছে সেটি আসলে কোন কিছুকেই নির্দেষ করছে না। 0 কোন এড্রেস মেমরিতে নেই। তাই যখনই প্রোগ্রাম এড্রেস 0 পাবে তখন সে বুঝবে যে এটা আসলে কোন কিছুকেই নির্দেশ করছে না, বা এখানে আসলে কোন এড্রেস নেই। অনেক ফাংশন NULL পয়েন্টার রিটার্ন করে। অর্থাৎ তার হয়তো কিছু একটা করে সেটার জন্য একটা এড্রেস রিটার্ন করার কথা ছিল, কিন্তু সে সেই এড্রেসটা পায়নি বা অন্য কোন সমস্যা হওয়ায় সে NULL রিটার্ন করেছে।

আরেকটা ব্যাপার দেখা যাক।

[code language=”cpp”]int *ptr;
scanf( "%d", ptr );[/code]

এই কোড রান করলে প্রোগ্রাম ক্র্যাশ করবে। সমস্যা কি এই কোডের? এতক্ষণ যা আলোচনা করা হল তাতে বুঝতে পারার কথা। এখানে ptr নামে একটা পয়েন্টার ভেরিয়েবল ডিক্লেয়ার করা হয়েছে। কিন্তু ptr এর মধ্যে কোন int ভেরিয়েবলের এড্রেস নেই। তাই যখন আমি ptr এ ইনপুট নিতে চাচ্ছি তখন সে invalid মেমরি এক্সেস করতে যায়। ফলে প্রোগ্রাম ক্র্যাশ করে। এইজন্য যদি আমি পয়েন্টার ডিক্লেয়ার করে সেটা নিয়ে কাজ করতে চাই তাহলে তার জন্য মেমরি এলোকেট করে নিতে হয় malloc( ) দিয়ে।

[code language=”cpp”]int *ptr;
ptr = ( int * ) malloc( sizeof( int ) );
scanf( "%d", ptr );[/code]

দেখা যাক কি হচ্ছে। প্রথমেই আমরা একটা পয়েন্টার ডিক্লেয়ার করলাম ptr যেখানে কোন একটা মেমরি এড্রেস থাকবে যেই মেমরিতে শুধু মাত্র int থাকতে পারবে। এখানে আমরা কেবল এড্রেস রাখার একটা ভেরিয়েবল তৈরি করেছি। এখনও কিন্তু কোন মেমরি তৈরি হয়নি যেখান কোন একটা int আমরা রাখতে পারব। একারণেই আগের প্রোগ্রামটা ক্র্যাশ করেছিল। এবার আমরা দ্বিতীয় লাইনে ইন্টিজারের জন্য মেমরি এলোকেট করলাম। malloc( ) stdlib.h হেডার ফাইলের অন্তর্গত একটা ফাংশন। এটা কিভাবে কাজ করে দেখা যাক।

malloc( ) এর আর্গুমেন্ট হিসেবে কতখানি মেমরি আমাদের লাগবে সেটা লিখে দিতে হয়। এটা বাইটে হিসাব হয়। যদি আমরা লেখি malloc( 5 ) তাহলে ৫ বাইট মেমরি অর্থাৎ ৪০ বিট মেমরি এলোকেট হবে আমাদের জন্য। যেহেতু আমরা int রাখতে চাই তাই int এর সাইজ দিব। একেক কম্পিউটারে int এর সাইজ একেকরকম বলে আমরা sizeof( ) (এটা কোন ফাংশন না, এটা অপারেটর। এটুকু আপাতত জানলেই হবে যে এটা কিছু একটার সাইজ বলে দেয়।) ব্যবহার করব। ফলে যেই কম্পিউটারে চালানো হবে সেখানে int এর যে সাইজ সেই সাইজের মেমরি বরাদ্দ হবে।

এই যে মেমরি বরাদ্দ হল এটার তো কোন নাম আমরা দেইনি। তাহলে আমরা এই মেমরি ব্যবহার করব কিভাবে? এটা ব্যবহার করার উপায় হল মেমরি এর এড্রেস ব্যবহার করা। malloc( ) ফাংশন মেমরি বরাদ্দ করে সেই মেমরি এর শুরু যেখান থেকে সেই ব্লকের এড্রেস রিটার্ন করে। তাহলে আমরা যদি একটা int পয়েন্টারে সেই এড্রেসটা রাখি তাহলে সেই পয়েন্টার ব্যবহার করলেই হয়ে যাবে। যদি আমরা ptr = ( int * ) malloc( sizeof( int ) ) না লিখে লিখতাম, malloc( sizeof( int ) ) তাহলেও কিন্তু প্রোগ্রাম কাজ করবে এবং মেমরি বরাদ্দ হবে। কিন্তু আমাদের কাছে সেই মেমরি এর এড্রেস থাকবে না। ফলে আমরা মেমরি বরাদ্দ করব ঠিকই কিন্তু ব্যবহার করতে পারব না।

আরেকটা জিনিস এখানে দেখা যাচ্ছে, ( int * ) । ইতোমধ্যে জানা থাকার কথা এটা হল টাইপকাস্টিং। এক টাইপের ডাটা অন্য টাইপে রূপান্তর করা হচ্ছে। এর কারণ কি? malloc( ) যে পয়েন্টার রিটার্ন করে সেটার টাইপ থাকে void * । অর্থাৎ আমরা যেমন বললাম int *, char * ইত্যাদি বিভিন্ন পয়েন্টার থাকতে পারে, তেমনি একটা পয়েন্টার হল void * । এর মানে হল এটা কোন নির্দিষ্ট টাইপের পয়েন্টার না। কম্পিউটার জানে না এটায় যে এড্রেস থাকবে সেটি কিসের এড্রেস, অর্থাৎ সেখানে কি রাখা যাবে। malloc( ) void * রিটার্ন করে কারণ সে জানে না আমরা এই এড্রেসে কি রাখব। void * কে ব্যবহার করার জন্য টাইপকাস্ট করে নিতে হয়। যেই টাইপের ডাটা আমরা রাখতে চাই সেই টাইপে কাস্ট করব। আমরা যেহেতু একটা int * এর মধ্যে সেই এড্রেসটাকে রাখছি কারণ আমরা এই মেমরি ব্যবহার করব int রাখার জন্য, তাই এটাকে int * এ রূপান্তর করে নিচ্ছি ( int * ) এই টাইপকাস্টিং এর মাধ্যমে।

অর্থাৎ, উপরের কোডে যেটা হয় তা হল, প্রথমে ptr নামে একটা ভেরিয়েবল তৈরি হল যার মধ্যে একটা int এর জন্য বরাদ্দকৃত মেমরির এড্রেস থাকতে পারবে। এরপর আমরা malloc( ) দিয়ে int এর সাইজের মেমরি বরাদ্দ করলাম এবং যেখানে মেমরি বরাদ্দ করা হল সেই এড্রেসটা, খেয়াল কর, বরাদ্দকৃত মেমরির এড্রেসকে আমরা ptr এর মধ্যে রাখলাম। আবারও বলি, ptr কিন্তু সেই বরাদ্দকৃত মেমরির এড্রেস না। ptr একটা ভেরিয়েবল যার মধ্যে বরাদ্দকৃত মেমরির এড্রেস আমরা রাখলাম। এরপর সেই এড্রেসে আমরা scanf() দিয়ে ইনপুট নিলাম। malloc( ) যখন মেমরি এলোকেট করতে যায়, সে যদি যতখানি মেমরি এলোকেট করার কথা তা করতে না পারে, যেকোন কারণেই হোক না সেটা, যেমন যদি মেমরি না থাকে, অন্য কোন প্রোগ্রামের কারণে বা যেকোন কারণেই মেমরি ফুল হয়ে যাওয়ায় আর মেমরি না পেলে NULL রিটার্ন করে।

আরেকটা ফাংশন আছে free( )। এই ফাংশন দিয়ে আমরা এলোকেট করা মেমরি ফ্রি করে দিতে পারি। আমরা যখন একটা প্রোগ্রাম চালানোর সময় মেমরি এলোকেট করি তখন সেই মেমরি আর কোন প্রোগ্রাম ব্যবহার করতে পারে না। তাই আমাদের প্রোগ্রাম চালানোর সময় যখন মেমরি ব্যবহার করা শেষ হয়ে যাবে তখন আমরা সেই মেমরি ফ্রি করে দিব। এইজন্য আমরা যেই মেমরি ফ্রি করতে চাই সেই মেমরির এড্রেস আর্গুমেন্ট হিসেবে পাঠালেই তা ফ্রি হয়ে যাবে। যেমন, free( ptr ) বা free( &value ) ।

এই হল মূলত পয়েন্টার নিয়ে আলোচনা। আরেকটা ব্যাপার আছে, অ্যারের সাথে পয়েন্টারের সম্পর্ক। অল্প কথায় বলে ফেলি। যখন আমরা ডিক্লেয়ার করি, int ara[100]; তখন একটা ইন্টিজার অ্যারে তৈরি হয়। এই অ্যারের একেকটা এলিমেন্ট হল ara[0], ara[1], ara[2], … , ara[99] । আর “ara” হল একটা পয়েন্টার ভেরিয়েবল। এই অ্যারেটা মেমরিতে যেখান থেকে শুরু হয়েছে সেখানের এড্রেস আছে “ara” এর মধ্যে। খেয়াল করার বিষয় এইটা যে এই অ্যারেটির প্রথম ভেরিয়েবল হল ara[0] আর প্রথম ভেরিয়েবলের এড্রেস আছে ara এর মধ্যে। যদি আমরা ara+1 লেখি তাহলে যে এড্রেস পাব তা হল ara[1] এর এড্রেস। অতএব ara[1] হল দ্বিতীয় ভেরিয়েবল আর তার এড্রেস হল ara+1 । এড্রেস দিয়ে দ্বিতীয় ভেরিয়েবলটিকে ব্যবহার করতে চাইলে তাই লিখব *( ara + 1 ) । অর্থাৎ এই ara[index] এবং *( ara + index ) দুটি আসলে একই জিনিস, একই ভেরিয়েবলকে নির্দেশ করে। আবার &ara[index] এবং ( ara + index ) এই দুইটাও একই জিনিস, একই ভেরিয়েবলের এড্রেস নির্দেশ করে।

পয়েন্টার অনেক কনফিউশন সৃষ্টিকারী একটা বিষয়। তবে এটার প্রয়োজনীয়তাও আছে। যথাযথভাবে ব্যবহার করতে পারলে প্রোগ্রামের efficiency অনেকাংশে বাড়ানো যায়। পয়েন্টার আমাদেরকে রানটাইমে মেমরি এলোকেট করে নিয়ে ব্যবহার করে আবার সেই মেমরি ডিলিট করে দেওয়ার সুবিধা দেয়। ফলে মেমরির ব্যবহারও প্রয়োজনের বেশি করতে হয় না। তাই পয়েন্টারের গুরুত্ব অনেক বেশি। এটা বুঝে ব্যবহার করতে পারলে অনেক কাজে আসবে।

char এবং int

Standard

char এবং int নিয়ে বেশ কিছু কনফিউশন কাজ করতে দেখেছি। তাই ব্যাপারটা পরিষ্কার করা দরকার। char এবং int দুটিই হল ডাটা টাইপ। ডাটা টাইপ দিয়ে আমরা কোন একটি ভেরিয়েবলে কি থাকবে সেটি নির্দিষ্ট করে দেই। যেমন যখন আমরা একটা ভেরিয়েবল ডিক্লেয়ার করি, int x; তখন সহজ কথায় যেটি হয় তা হল, x নামের একটি ভেরিয়েবল তৈরি হয় যেখানে একটি ইন্টিজার রাখা যাবে। আবার যখন ডিক্লেয়ার করা হয় char ch; তখনও ch নামের একটি ভেরিয়েবল তৈরি হয় যেখানে একটি ক্যারেক্টার রাখা যাবে। এখানেই আসলে কনফিউশন!

যখন int দিয়ে ডিক্লেয়ার করা হয় তখন সেখানে ইন্টিজার রাখা যাবে আর যখন char দিয়ে ডিক্লেয়ার করা হয় তখন সেখানে কেবল ক্যারেক্টারই রাখা যাবে এমনটি ভুল ধারণা। int এবং char দিয়ে আসলে যেটি হয় তা হল এরা ভেরিয়েবলের জন্য নির্দিষ্ট মেমরি বরাদ্দ করে দেয়। দেখা যাক আসলে কি হয়।

কম্পিউটারকে যাই বলা হোক না কেন, ইনপুট ডিভাইস দিয়ে যাই ইনপুট দেওয়া হোক না কেন, সে সবকিছুকে নাম্বারের মাধ্যমে প্রকাশ করে। সকল প্রকার তথ্য সে নাম্বারের মাধ্যমে রাখে। এই নাম্বার হল বাইনারি নাম্বার। কারণ কম্পিউটার যেটা বুঝে তা হল হয় ভোল্টেজ আছে, নয়তো নাই। কারেন্ট প্রবাহ আছে, অথবা নাই। চৌম্বক ক্ষেত্র হলে সেখানে হয় উত্তর মেরু, নয় দক্ষিণ মেরু। অতএব যেভাবেই ডাটা রাখা হোক না কেন, দুইটি ঘটনা ঘটতে পারে। এদেরকে ০ এবং ১ দিয়ে প্রকাশ করা হয়। একারণেই কম্পিউটার শধু বাইনারি বুঝে।

যখন আমরা কোন তথ্য ইনপুট দেই তখন সেটিকে সে বাইনারিতে তার মেমরিতে রাখে। এই কারণে সে সকল তথ্যকে বাইনারি নাম্বারে কনভার্ট করে। আমরা মানুষেরা বোঝার সুবিধার কারণে এই নাম্বারগুলোকে ডেসিমেলে প্রকাশ করি। ধরা যাক যখন কোন একটি নাম্বার 5 ইনপুট দেওয়া হল। তাহলে কম্পিউটার যখন এটিকে মেমরিতে রাখবে তখন সে 5 এর বাইনারি হিসেবে একে রাখবে। সেটি হল 101। এই যে বাইনারিতে নাম্বারটিকে ইনপুট নিয়ে রাখতে হল এটার জন্য আগে থেকেই ভেরিয়েবল ডিক্লেয়ার করে মেমরি বরাদ্দ করে দেওয়া হয়। এখন যা ইচ্ছা তাই বরাদ্দ করলেই তো হবে না। কিছু স্ট্যান্ডার্ড আছে। সেই স্ট্যান্ডার্ডগুলোর মধ্যে দুইটি হল char এবং int।

যেহেতু সবকিছুকে বাইনারিতে রাখা হয় তাই মেমরির হিসাবও হয় কয়টি বাইনারি ডিজিট আছে তার উপর। ১টি ডিজিট হল ১ বিট। এরকম ৮টি ডিজিট নিয়ে হয় ১ বাইট। যখন char বা int দিয়ে ভেরিয়েবল ডিক্লেয়ার করা হয় তখন char যত বিট এবং int যত বিট হওয়ার কথা তত বিট মেমরি বরাদ্দ করা হয়। char এর সাইজ হচ্ছে ৮ বিট এবং int এর সাইজ কোথাও ১৬, কোথাও ৩২ বিট (বেশিরভাগ ক্ষেত্রেই ৩২ বিট)।

অর্থাৎ char টাইপের ভেরিয়েবল যখন আমি ডিক্লেয়ার করব তখন ৮ বিট মেমরি বরাদ্দ হয়ে যাবে। অতএব সেখানে আমি এমন কিছু ইনপুট নিয়ে রাখতে পারব যার জন্য ৮ বিট মেমরি লাগে, অর্থাৎ যাকে বাইনারিতে প্রকাশ করতে ৮টা ডিজিট লাগে। ধরা যাক আমি 0 রাখব। তাহলে 0 এর জন্য একটি বিটই যথেষ্ট। কারণ 0 কে বাইনারিতে প্রকাশ করলে 0-ই হবে। আর একেবারেই প্রাথমিক পর্যায়ের গণিতেই আমরা শিখেছি যে কোন নাম্বারের বামে যদি 0 থাকে তাহলে সেই 0 এর কোন অর্থ নেই। তাহলে 0 কে বাইনারিতে লিখলে হবে 0। আর যেহেতু আরও ৭ বিট বাকি আছে সেগুলোতে বাম পাশে ৭টি 0 বসে যাবে। অর্থাৎ হবে 0000 0000।

এখন একটা সহজ পার্মুটেশন করা যাক। এখানে ঘর আছে ৮টা। ৮টা ঘরে ২টা করে ডিজিট বসতে পারে। তাহলে মোট কত রকম পার্মুটেশন সম্ভব? 2^8 রকম, অর্থাৎ 256 রকম। অতএব ৮টি ডিজিটের সাহায্যে আমরা সর্বোচ্চ 256 টি নাম্বারকে প্রকাশ করতে পারি। যদি আমরা 0 থেকে শুরু করি, তাহলে 0-255 পর্যন্ত নাম্বার আমরা ৮ ডিজিটে প্রকাশ করতে পারব, অর্থাৎ ৮ বিট মেমরিতে রাখতে পারব।

এই যে আমরা char ডাটা টাইপ ডিক্লেয়ার করলাম, এখানে আসলে ৮ বিট মেমরির একটি ভেরিয়েবল তৈরি হল। যেহেতু আমরা দেখতে পাচ্ছি ৮ বিটে ০-২৫৫ পর্যন্ত রাখা যায়, অতএব আমরা char ডাটা টাইপের মধ্যে ০-২৫৫ এর মধ্যে যেকোন নাম্বার রাখতে পারব। এখানেই একটা কনফিউশন। এটা তো char ডাটা টাইপ, এখানে তো ক্যারেক্টার রাখা যায়, নাম্বার কিভাবে রাখে? এই কথাটার সহজ একটা উত্তর। ক্যারেক্টার ডাটা টাইপ বলে কিছু নাই, ডাটা টাইপ হল char। এর মানে ৮ বিট মেমরি। আর আগেই বলেছি, কম্পিউটার সবকিছুকে নাম্বার হিসেবে রাখে। যখন আমরা কোন ক্যারেক্টার ইনপুট নেই, তখন সে আসলে ক্যারেক্টারের জন্য একটা নাম্বার রাখে যাকে বলে ASCII কোড। প্রতিটা ক্যারেক্টারের জন্য এই কারণেই ASCII কোড দেওয়া হয়েছে। কম্পিউটার সংখ্যা বাদে কিছু রাখতে পারে না। তাকে ক্যারেক্টার ইনপুট দেওয়ার হলেও সে সেটার ASCII রাখে।

এখন int এর ব্যাপারে আসা যাক। এতক্ষণে এইটুকু বুঝে যাওয়ার কথা যে আসলে char মানে এই না যে সেখানে শুধু ক্যারেক্টার রাখা যাবে। কোথাও আসলে ক্যারেক্টার রাখা যায় না। যা রাখা যায় তা হল নাম্বার। সেই নাম্বার মেমরির মধ্যে জায়গা হল নাকি সেটি হল বিষয়। int এর মেমরি হল ৩২ বিট। অতএব এখানে 2^32 সংখ্যক নাম্বার রাখা যাবে। 0 থেকে শুরু করলে 2^32-1 পর্যন্ত নাম্বার রাখা যাবে। এখন প্রতিটা ক্যারেক্টারকে আমরা যেহেতু প্রকৃত অর্থে ASCII কোড হিসেবে রাখি, অতএব সেই ASCII কোড যদি char এ জায়গা হয় তাহলে নিঃসন্দেহে তা int এও জায়গা হবে। অতএব আমরা int এর মধ্যেও ক্যারেক্টার ইনপুট নিয়ে রাখতে পারব।

মাথায় মনে হয় তালগোল পাকিয়ে গেছে। উদাহরণ দিয়ে দেখা যাক।

[code language=”cpp”]
code:
int x;
char ch;
scanf( "%d %c", &x, &ch );
[/code]

এখানে আসলে কি হয়? প্রথমেই আমরা দুটি ভেরিয়েবল ডিক্লেয়ার করলাম, int টাইপের x এবং char টাইপের ch। অতএব দুটি ভেরিয়েবল তৈরি হল, x এ আমরা ৩২ বিটের সংখ্যা রাখতে পারব, ch এ রাখতে পারব ৮ বিট। এখন scanf এর ভিতরে আসা যাক। %d এবং %c এই দুইটি দিয়ে আসলে বোঝানো হয় যে কি ইনপুট আসছে। %d মানে সেখানে একটি ডেসিমেল ইন্টিজার ইনপুট আসছে। %c মানে হচ্ছে একটি ক্যারেক্টার ইনপুট আসছে। এখন আমরা যত ক্যারেক্টার ব্যবহার করি সেগুলোর প্রতিটির জন্য যে ASCII কোড দেওয়া হয়েছে সেগুলো ০ থেকে ১২৮ এর মধ্যে। অর্থাৎ তা ৮ বিটের মধ্যেই জায়গা হয়ে যায়। তাই ক্যারেক্টার ইনপুট নেওয়ার জন্য char টাইপ ভেরিয়েবল নেওয়া হয়। যখন আমরা A ইনপুট দিব তখন সে আসলে A এর ASCII নিয়ে ch এর মধ্যে রাখবে। কারণ তাকে %c এর মাধ্যমে বলে দেওয়া হয়েছে যে এখানে একটি ক্যারেক্টার ইনপুট দেওয়া হবে যেটির ASCII কোড নিয়ে রাখতে হবে। কিন্তু যদি %d এর জায়গায় আমি নাম্বার ইনপুট না দিয়ে A দেই তাহলে কাজ করবে না। কারণ সে একটি নাম্বার ইনপুট হিসেবে আশা করছে। তাকে বলে দেওয়া হয়নি যে ইনপুট যা দেওয়া হচ্ছে তার ASCII নিতে হবে। বলা হয়েছে একটি নাম্বার আসবে। যেহেতু A কোন নাম্বার না তাই এটি কাজ করবে না, কারণ সে মেমরিতে এটিকে রাখতে পারবে না।

এখন যদি আমি লেখি, scanf( "%c %d", &x, &ch ); তাহলে কাজ করবে? অবশ্যই কাজ করবে! কারণ x এবং ch আসলে মেমরিতে বরাদ্দকৃত কিছু জায়গা। %d এবং %c মানে কি ধরনের ডাটা আসবে। এখন আমি প্রথমে A ইনপুট দিলে %c যেহেতু আছে তাই A এর ASCII কোড ইনপুট হিসেবে যাবে। আর সকল ASCII যেহেতু ৮ বিটেই জায়গা হয়ে যায় অতএব ৩২ বিটে জায়গা না হওয়ার তো কোন মানেই হয় না! আর যখন এরপরের ইনপুট নিব তখন একটি নাম্বার ইনপুট দিব। কারণ এখানে বলা আছে %d, অর্থাৎ একটি ইন্টিজার ইনপুট হিসেবে আসবে। একটি গুরুত্বপূর্ণ বিষয় আবারও বলি, char ডাটা টাইপ মানে এই না যে সেখানে ক্যারেক্টার থাকবে। এর মানে হল এখানে ৮ বিটের কিছু থাকবে। অতএব আমরা char ডাটা টাইপের মধ্যে ৮ বিটে জায়গা হয় এমন যেকোন নাম্বার %d এর সাহায্যে ইনপুট নিতে পারব। আর যেহেতু ইতোমধ্যেই দেখেছি, ৮ বিটে ০ থেকে ২৫৫ পর্যন্ত নাম্বার থাকতে পারে, অতএব char এর মধ্যে আমরা 0 থেকে 255 পর্যন্ত যেকোন নাম্বার রাখতে পারব।

অতএব ডাটা টাইপ আসলে মেমরি নির্দিষ্ট করে। এটি দিয়ে কিছু বিট নির্দিষ্ট হয় যেখানে আমি কোন নাম্বার রাখতে পারব। আর আমরা কি ধরনের ডাটা ইনপুট বা আউটপুট দিব সেটি আসলে %d, %c, %f ইত্যাদি দিয়ে বোঝানো হয়। ভিতরে সবকিছুই নাম্বার হিসেবে থাকে, আমরা কি হিসেবে ইনপুট এবং আউটপুট দিব সেটিই আসলে % দিয়ে বলা হয়।

আরেকটা উদাহরণ দেই। একটি char টাইপ ভেরিয়েবল আছে x ।

[code language=”cpp”]
code:
scanf( "%d", &x ); // একটি নাম্বার ইনপুট নিবে। ধরা যাক ইনপুট 65 ।
printf( "%d %c", x, x ); // এখানে আউটপুট হিসেবে দেখাবে 65 A ।
[/code]

কারণ কি? কি ঘটছে? যেটি ঘটছে তা হল ইনপুট নেওয়ার সময় কম্পিউটার দেখছে যে ইনপুট আসবে %d টাইপের, অর্থাৎ একটি ইন্টিজার। যেহেতু আমরা 65 ইনপুট দিচ্ছি যা 255 এর মধ্যে অতএব তা char টাইপের মধ্যে জায়গা না হওয়ার কিছু নেই। যখন আমরা আউটপুট দিচ্ছি তখন প্রথমে দেওয়া আছে %d। অতএব এখানে একটি ইন্টিজার আউটপুট হিসেবে যাবে। এরপর যখন দেওয়া আছে %c তখন কম্পিউটার যেটি বুঝে তা হল এখানে একটি ইন্টিজার নাম্বার আসবে, যেটি কোন একটি ক্যারেক্টারের ASCII কোড। সেই কোডটি যেই ক্যারেক্টারের সেই ক্যারেক্টারকে আউটপুটে দেখাতে হবে। যেহেতু A এর ASCII কোড 65 তাই এখানে A দেখানো হচ্ছে।

অর্থাৎ আমরা কি ইনপুট দিব বা আউটপুট দিব তা আসলে বোঝানো হয় % এর পরে কি আছে তা দিয়ে। ডাটা টাইপ দিয়ে কেবল মেমরি নির্দিষ্ট করা হয়। সেই মেমরিতে যেকোন কিছু থাকতে পারে। char মানে ৮ বিট, int মানে ৩২ বিট। যদি মানুষের বয়স ইনপুট নিয়ে রাখতে হয়, তাহলে char নিলেই হয়। কারণ বয়স 255 এর বেশি হবে না। আর সেটি ৮ বিটের মধ্যেই জায়গা হয়ে যায়। শুধু শুধু ৩২ বিট নিয়ে জায়গা নষ্ট করার কোনই মানে হয় না। গুরুত্বপূর্ণ কথাটা আবার বলি, char ডাটা টাইপ মানে এই না যে সেখানে ক্যারেক্টার থাকবে। এর মানে হল এখানে ৮ বিটের কিছু থাকবে। ক্যারেক্টার নাকি ইন্টিজার ইনপুট বা আউটপুট দিব সেটি ডাটা টাইপের উপর না, সেটি নির্ভর করে % এর পর কি আছে তার উপর।

আর unsigned এর ব্যাপারটা আসলে কিছুই না। একটি ভেরিয়েবলে যে নাম্বার থাকবে সেটি যদি signed হয়, অর্থাৎ সেখানে যদি মাইনাস কোন নাম্বার থাকে তাহলে সেটি বোঝানোর জন্য একটি বিট লাগে। তাই একটি বিট খরচ হয় সাইন নির্দিষ্ট করার জন্য। আর এতক্ষণে বুঝে যাওয়ার কথা unsigned char হওয়া অসম্ভব কিছু না। যেহেতু একটি নাম্বার এই মেমরিতে থাকবে সেটি signed বা unsigned দুটিই হতে পারে।

for লুপ

Standard

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

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

C তে তিন ধরণের লুপ আছে
১। for লুপ
২। while লুপ
৩। do-while লুপ

৩ নাম্বারটির কাজ প্রথম দুটি দিয়েই করা যায়। আসলে কোন একটি লুপ চালানোর জন্য প্রথম দুটির একটি ব্যবহার করলেই হয়। দুটি দিয়েই লুপের সব কাজ করা যায়। সিন্ট্যাক্স এর দিক থেকে দুটি ভিন্ন। অর্থাৎ দুটি লেখার ধরণ ভিন্ন। তবে কাজ একই। লেখার ধরণ ভিন্ন হওয়ায় যার যখন যেভাবে লিখতে সুবিধা সে তখন সেই অনুযায়ী লুপ ব্যবহার করে। বেশিরভাগ ক্ষেত্রেই সাধারণত for লুপ ব্যবহার করা হয়। এখানে সেটি নিয়েই আলোচনা করা হল।

প্রথমেই দেখা যাক for এর সিন্ট্যাক্স কিরকম।

for( initialization ; condition ; increment/decrement ) {
    /* যে কাজগুলো বারবার করতে হবে */
}

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

এখন দেখা যাক ব্র্যাকেটের মধ্যে কি আছে। ব্র্যাকেটের মধ্যের যে তিনটি অংশ তার প্রথমটি দেখা যাচ্ছে initialization । এই অংশটি লুপ শুরু হওয়ার সময় সংঘটিত হয়। এখানে সাধারণত একটি এসাইনমেন্ট স্টেটমেন্ট থাকে। কোন একটি ভেরিয়েবলে, যেই ভেরিয়েবল দিয়ে লুপটি চলবে, তাকে একটি মান দেওয়া হয়। এর পরের অংশ হল condition । এখানে কোন একটি শর্ত থাকবে। এরপরে increment/decrement এ কোন একটি ভেরিয়েবলের মান বাড়ানো হয় বা কমানো হয়। এতে করে প্রতিবার লুপ চলার সাথে সাথে লুপটি শর্তের দিকে আগাতে থাকে এবং এক সময় শর্ত মিথ্যা হয়ে লুপ থেমে যায়।

এবার দেখি কিভাবে এটি কাজ করে। একটি উদাহরণ দেখি। ধরা যাক আমাদেরকে একই লেখা বারবার প্রিন্ট করতে হবে। আমরা প্রিন্ট করতে চাই "This is a C program." । এই লেখাটা দশবার প্রিন্ট করতে চাই। তাহলে আমরা printf() এর মধ্যে দশবার এটি লিখতে পারি। আর যদি কষ্ট কম করতে চাই তাহলে একটা লুপ ব্যবহার করে দশবার প্রিন্ট করে দিতে পারি। লুপের মাধ্যমে লিখলে প্রোগ্রামটি হবে এরকমঃ

int i;
for( i = 1; i <= 10; i++ ) {
    printf( "This is a C program.\n" );
}

এখানে প্রথমে i একটি ভেরিয়েবল ডিক্লেয়ার করা হয়েছে। এটি দিয়ে লুপ চালানো হবে। অর্থাৎ কতবার একটি কাজ হবে সেটি নির্দিষ্ট করে দিব। প্রথমে initialization অংশে প্রোগ্রাম যাবে। সেখানে যে স্টেটমেন্ট থাকবে সেটি সম্পন্ন হবে। এখানে আছে i = 1 । অর্থাৎ i এর মান 1 হয়ে গিয়েছে। এই কাজটি একবারই হবে। একবার হয়ে যাওয়ার পর যতবার লুপ চলবে এটি আর হবে না। অর্থাৎ initialization / প্রথম সেমিকোলনের আগের অংশে যা থাকবে সেটি পুরো লুপ চলার শুরুতে সম্পন্ন হবে এবং আর কখনই হবে না।

এরপরের যে অংশটি আছে সেটি হল শর্ত। Initialization হওয়ার ঠিক পরপরই এখানে এসে প্রোগ্রাম দেখবে যে শর্ত দেওয়া আছে সেটি সত্য কিনা। শর্ত বিভিন্নভাবে দেওয়া যেতে পারে। এটার সাথে initialization এর সম্পর্ক থাকতেও পারে, নাও থাকতে পারে। শর্ত আছে, সেটি সত্য নাকি মিথ্যা এটিই দেখার বিষয়। যদি শর্ত সত্য না হয় তাহলে লুপের ভিতরে প্রবেশ করবে না, যেমনটি if এর ক্ষেত্রে হয়। সরাসরি লুপের পরে যে স্টেটমেন্ট আছে সেখানে চলে যাবে। আর যদি শর্ত সত্য হয় তাহলে তখনই লুপের ভিতরে প্রবেশ করবে। ভিতরে যে স্টেটমেন্টগুলো আছে সেগুলো সম্পাদিত হবে। এখানে আছে i <= 10 । যেহেতু প্রথমেই i এর মান 1 করে নেওয়া হয়েছে অতএব এখন শর্তটি সত্য। তাহলে লুপের ভিতরে প্রবেশ করবে এবং printf() সম্পাদিত হবে।

স্টেটমেন্টগুলো সম্পাদিত হয়ে গেলে প্রোগ্রাম increment/decrement অংশে আসবে। এখানে যে স্টেটমেন্ট লেখা থাকবে সেটি সংঘটিত হবে। অর্থাৎ উপর্যুক্ত উদাহরণ অনুযায়ী i++ হবে। i এর মান এক বাড়বে। এরপর প্রোগ্রাম আবার শর্ত পরীক্ষা করে দেখবে। শর্ত সত্য হলে আগেরমতই লুপের ভিতর প্রবেশ করবে। আরেকবার printf() হবে। আবার increment/decrement অংশে যাবে। i এর মান এক বাড়বে। এরপর শর্ত পরীক্ষা করবে। printf() হবে। এভাবে চলতে থাকবে। এখানে খেয়াল রাখা উচিৎ, initialization অংশটি আর আসবে না। একেবারে শুরুতে একবার হবে, তখনই শেষ।

এভাবে লুপটি চলতে থাকবে এবং i এর মান এক এক করে বাড়বে এবং প্রিন্ট হবে। i এর মান যখন 10 হবে তখন শর্ত i <= 10 সত্য হয়ে প্রিন্ট হয়ে i++ হয়ে i এর মান 11 হবে। এবার যখন শর্ত পরীক্ষা করতে যাবে তখন শর্ত মিথ্যা হবে। ফলে লুপ থেকে প্রোগ্রাম বের হয়ে আসবে। এভাবে দশবার একই কাজ সম্পাদিত হল। এখানে লক্ষনীয়, যখন লুপের শর্ত মিথ্যা হল তখন i এর মান 11। এই ধারণাটি বিভিন্ন ক্ষেত্রে প্রয়োজন হয়। এটি বুঝতে অনেকেরই সমস্যা হয়। একটু ভালো মত খেয়াল করলেই বুঝা যাবে। কোন কাজের পর কোনটি হচ্ছে সেটি খেয়াল করতে হবে। i++ হয়ে, অর্থাৎ i এর মান এক বেড়ে যাওয়ার পর শর্ত মিথ্যা হয়েছে, অতএব ইতোমধ্যে i এর মান 11 হয়েছে।

এগুলো একেবারেই প্রাথমিক পর্যায়ের কথাবার্তা। আরেকটু আগানো যাক। for লুপের ব্র্যাকেটের ভিতরের যে তিনটি অংশ আছে সেগুলোতে যে কিছু লেখা থাকতে হবে এমন কোন কথা নেই। তবে লেখা না থাকলেও সেমিকোলন দুইটা থাকতেই হবে। ধরা যাক, আমার initialization করার দরকার নেই। অথবা লুপের আগেই আমি সেটা করে ফেলেছি। তাহলে প্রথম অংশ খালি রাখলেই চলবে। যেমন,

i = 1;
for( ; i <= 10; i++ ) {
    printf( "This is a C program.\n" );
}

এক্ষেত্রে প্রথম অংশটি খালি রাখা হয়েছে। কারণ আগেই i এর মান 1 করে নেওয়া হয়েছে। আবার একইভাবে শর্তের অংশটিও খালি রাখা যায় যদি লুপটি অন্য কোনভাবে (যেমন break ব্যবহার করে) থামানোর ব্যবস্থা করা থাকে। একইভাবে increment/decrement অংশটিও খালি রেখে দেওয়া যায়। যেমন, যদি for এর ভিতরেই printf() এর পরে, একেবারে শেষে i++ করা হয়, তাহলে increment/decrement অংশে কিছু না লেখলেও হচ্ছে। এমনকি চাইলে প্রতিটি অংশই ফাঁকা রেখে দেওয়া সম্ভব! তবে এক্ষেত্রে লুপ থামানোর কোন ব্যবস্থা না করলে তা ইনফিনিট লুপে পরিণত হবে, অর্থাৎ লুপ চলতেই থাকবে। কখনই থামবে না।

একটু লক্ষ্য করলে দেখা যাবে প্রথম এবং শেষ অংশে যা লেখা হয়েছে সেগুলো মূলত একেকটি স্টেটমেন্ট। অর্থাৎ এখানে যেকোন ধরণের স্টেটমেন্ট লেখা যাবে এবং যেটি লেখা হবে প্রোগ্রাম ঐখানে গেলে সেটি সম্পাদিত হবে। অতএব উপরের উদাহরণটিকে চাইলে এভাবেও লেখা যায়ঃ

int i;
for( i = 1; i <= 10; printf( "This is a C program.\n" ) ) {
    i++;
}

কোনটির পর কোনটি হচ্ছে সেটি উপরের লেখা অনুযায়ী মিলিয়ে দেখলেই খুব সহজেই বোঝা যাবে কেন এটি একইরকম আউটপুট দিচ্ছে। সাধারণত এভাবে লেখা হয় না, এটা লেখলাম এইটুকু বোঝানোর জন্য যে প্রথম/শেষ অংশের স্থানে কোন একটি স্টেটমেন্ট বসবে, সেটি যেকোন ধরণের হতে পারে।

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

এবার লুপের প্রয়োগের সাধারণ একটা উদাহরণ দেখা যাক। লুপের মাধ্যমে নিচের প্রবলেমটি সমাধান করতে হবে। একটা নাম্বার ইনপুট দেওয়া হবে। যত নাম্বার ইনপুট দেওয়া হবে আউটপুটে ততগুলা লাইন হবে এবং প্রতি লাইনে ঐ লাইন নাম্বারের সমান সংখ্যক * থাকবে।

input:              input:
7                   5
output:             output:
*                   *
**                  **
***                 ***
****                ****
*****               *****
******
*******

এটার কোড কিরকম হবে? আগে নিজে চিন্তা করে দেখ সমাধান করতে পার নাকি। সময় নাও। নিচে আমার সমাধানের কোড দিলাম।

[code language=”cpp”]
code:
int n, i, j;
scanf( "%d", &n );
for( i = 1; i <= n; i++ ) {
for( j = 1; j <= i; j++ ) {
printf( "*" );
}
printf( "\n" );
}
[/code]

কিভাবে কি হচ্ছে দেখ। প্রথমে সমস্যাটার সমাধানের জন্য কি করতে হবে তা চিন্তা করতে হবে। এখানে বলা হয়েছে কতগুলো লাইন হবে তা ইনপুট দেওয়া হবে এবং ততগুলো লাইন প্রিন্ট করতে হবে। তার মানে প্রতিটা লাইন প্রিন্ট করার জন্য একটা লুপ চালাতে হবে। সেই জন্য আমরা প্রথম লুপটা লিখলাম। সেখানে i মানে লাইন নাম্বার, 1 থেকে শুরু হয়ে n পর্যন্ত লাইন হবে। প্রতি লাইনে আবার লাইনের নাম্বারের সমান সংখ্যক * প্রিন্ট করতে হবে। অর্থাৎ প্রতি লাইনের জন্য আবার একটা করে লুপ চলবে। এটা হল Nested Loop। মানে লুপের ভিতরে লুপ। এখানে আমরা আরেকটি ভেরিয়েবল j নিলাম যার মধ্যে * এর সংখ্যা থাকবে। এটা 1 থেকে শুরু করে লাইন যত তত নাম্বার পর্যন্ত অর্থাৎ i পর্যন্ত যাবে। এই লুপ দিয়ে একটা লাইনে যতগুলা * প্রিন্ট করতে হবে সেটি হবে। এরপর এক লাইনে সবগুলা * প্রিন্ট করা হয়ে গেলে আমরা একটা ‘\n’ এর মাধ্যমে এর পরের লাইনে চলে যাব। এভাবে করে প্রোগ্রামটি চলতে থাকবে।

আবার যদি এর উল্টোটা করতে বলা হত?

input:              input:
7                   5
output:             output:
*******             *****
******              ****
*****               ***
****                **
***                 *
**
*

আবারও একই কথা। আগে নিজে চিন্তা কর। সময় নাও। তারপর না পারলে নিচের কোড দেখ।

[code language=”cpp”]
int n, i, j;
scanf( "%d", &n );
for( i = 1; i <= n; i++ ) {
for( j = n; j >= i; j– ) {
printf( "*" );
}
printf( "\n" );
}
[/code]

এছাড়াও বিভিন্ন ক্ষেত্রে বিভিন্নভাবে for লুপ ব্যবহার করা হয়। এগুলোর কোন ধরা বাধা নিয়ম নেই। যখন যেরকমভাবে প্রয়োজন প্রোগ্রামার সেরকমভাবে এটি ব্যবহার করে। বেসিক জিনিসগুলো মাথায় থাকলেই হল। for এর প্রথম অংশে এক/একাধিক স্টেটমেন্ট থাকবে যেগুলো সমগ্র লুপ শুরুর আগেই সম্পাদিত হবে। এরপর একটি শর্ত থাকবে। তারপরের অংশে আবারও এক/একাধিক স্টেটমেন্ট থাকবে যেগুলো প্রতিবার লুপটি চলার শেষে সম্পাদিত হবে। যদিও প্রথম অংশে বা শেষ অংশে যেকোন স্টেটমেন্ট লেখা যায়, কনভেনশন অনুযায়ী সাধারণত এই অংশগুলোতে এমন স্টেটমেন্টগুলোই কেবল লেখা হয় যেগুলোর উপর নির্ভর করেই মূলত লুপটি চলছে। বাকি স্টেটমেন্টগুলো লুপের বাইরে বা ভিতরে প্রয়োজন মত জায়গায় বসানো হয়।

এগুলোই একেবারে বেসিক। লুপ জিনিসটা কিভাবে কাজ করে এবং এর সিন্ট্যাক্স কিরকম সেগুলো বুঝতে পারলে বাকি কাজ হল প্র্যাকটিস করা। বিভিন্ন সমস্যা সমাধান করতে গেলে নতুন নতুন পরিস্থিতির সম্মুখীন হতে হয় এবং তখন নতুন আঙ্গিকে চিন্তা করা যায়। এছাড়াও একই জিনিস বিভিন্নভাবে লিখে এক্সপেরিমেন্ট চালিয়েও অনেক কিছু খুব সহজে বোঝা সম্ভব যেগুলো হয়তো লিখে বোঝান সম্ভব না। যত যাই হোক, অভিজ্ঞতার বিকল্প নেই! আর এই অভিজ্ঞতা অর্জনের জন্য এখানে কিছু প্রবলেম দিলাম। চিন্তা করে সমাধানের চেষ্টা কর। কোন সমস্যা হলে প্রশ্ন কর।

১) 1 <= x <= 50 সীমার মধ্যে সকল জোড় সংখ্যা প্রিন্ট কর।
২) 1 <= x <= 50 সীমার মধ্যে সকল বিজোড় সংখ্যা প্রিন্ট কর।
৩) 10 <= x <= 70 সীমার মধ্যে 3 দ্বারা নিঃশেষে বিভাজ্য সকল সংখ্যা প্রিন্ট কর।
৪) 44 <= x <= 89 সীমার মধ্যে 3 দ্বারা নিঃশেষে বিভাজ্য সংখ্যা বাদে বাকি সকল সংখ্যা প্রিন্ট কর।
৫) 10 <= x <= 101 সীমার মধ্যে সকল মৌলিক সংখ্যা প্রিন্ট কর।
৬) উপরের সবগুলো প্রবলেম উলটো দিকে (সীমার শেষ থেক শুরুর দিকে) প্রিন্ট কর।
৭) N ইনপুট দেওয়া হবে। এরপর N সংখ্যক নাম্বার ইনপুট দেওয়া হবে। এদের যোগফল প্রিন্ট কর।
৮) N ইনপুট দেওয়া হবে। এরপর N সংখ্যক নাম্বার ইনপুট দেওয়া হবে। এদের মধ্যে বিজোড় সংখ্যাগুলোর যোগফল প্রিন্ট কর।
৯) N ইনপুট দেওয়া হবে। এরপর N সংখ্যক নাম্বার ইনপুট দেওয়া হবে। এদের average প্রিন্ট কর।
10) 1 <= x <= 10 সীমার মধ্যে প্রতিটা সংখ্যার ফ্যাক্টরিয়াল (x!) প্রিন্ট কর।

End of File (EOF)

Standard

অনেকের মধ্যেই দেখেছি এই End of File বা EOF নিয়ে এক ধরনের আতংক কাজ করে! তবে এত আতংকিত হওয়ার কিছু নেই। এটা তেমন কিছুই না। সোজা বাংলা কথায় বলতে গেলে EOF হল ইনপুট নেওয়ার শেষ নির্দেশক কোন একটা সংকেত যেটি পাওয়ার পর প্রোগ্রাম আর কোন ইনপুট নিবে না।

শুরুতে প্রবলেমে এটা কিভাবে উল্লেখ করা থাকে দেখা যাক। জনপ্রিয় অনলাইন জাজ UVa এর ১০০৫৫ নাম্বার প্রবলেম “Hashmat the brave warrior” কথা যদি বলি তাহলে সেখানে দেখা যাবে বলা আছে, “Input is terminated by End of File”। কথাটার মানে হল, প্রবলেমটির জন্য যে ইনপুট সেট আছে সেখানে অনেকগুলো ইনপুট দেওয়া আছে। সেই ইনপুটগুলোর কোন এক জায়গায় EOF এর ইন্সট্রাকশন দেওয়া আছে। প্রোগ্রাম যতক্ষণ পর্যন্ত EOF না পাবে ততক্ষণ পর্যন্ত ইনপুট নিতে থাকবে। EOF পেয়ে গেলে প্রোগ্রাম শেষ হবে। বিভিন্ন অপারেটিং সিস্টেমে EOF কে বিভিন্নভাবে নির্দেশ করা হয়। উইন্ডোজে ctrl + z চাপ দিয়ে EOF এর কমান্ড দেওয়া হয়।

উপরের উদাহরণের প্রবলেমটিতে বলে দেওয়া আছে EOF আসলে প্রোগ্রাম শেষ হবে। কিছু প্রবলেমে বলা থাকে শুন্য আসলে ইনপুট নেওয়া শেষ করতে হবে। কোথাও বলা থাকে ঋণাত্মক সংখ্যা আসলে ইনপুট নেওয়া বন্ধ হবে। প্রবলেমভেদে কিভাবে ইনপুট নেওয়া শেষ হবে সেটা ভিন্ন হয়। তবে এমন কিছু প্রবলেম আছে যেখানে বলাই থাকে না যে কখন ইনপুট নেওয়া শেষ হবে। এরকম যদি হয় যে কখন ইনপুট নেওয়া শেষ করতে হবে, অর্থাৎ প্রোগ্রামটি কখন রান করা শেষ হবে সে বিষয়ে কিছুই বলা নেই তখন ধরে নিতে হয় যে EOF দিয়েই প্রোগ্রাম রান করা শেষ হবে।

এখন একটু গভীরতর কথায় যাওয়া যাক। ভয় পাওয়ার কিছু নাই, খুব বেশি গভীর না, হালকা গভীর, হাটু পর্যন্ত! 😛 EOF আমরা কিভাবে ব্যবহার করব? তবে তার আগে জানতে হবে আমরা একটি প্রোগ্রাম বারবার ইনপুট নিয়ে কিভাবে চালাব? সহজ উত্তর, লুপ দিয়ে। আমরা জানি একই ধরনের কাজ বার বার করার জন্য লুপ নামে সুন্দর একটি জিনিস আছে। Hashmat এর প্রবলেমটার কথাই ধরা যাক। প্রবলেমে বলা আছে প্রতিবার ২ টা করে সংখ্যা ইনপুট নিতে হবে এবং সেই ইনপুটের জন্য কাজ করতে হবে। কাজ হলে আউটপুট দেখাবে। এভাবে একবার কাজটি হবে। এরপর আবার ২ টা সংখ্যা ইনপুট নিতে হবে, এবং সমগ্র কাজের পুনরাবৃত্তি করতে হবে। এই বার বার ইনপুট নেওয়ার বিষয়টি আমরা যেভাবে লিখব সেটা নিচের চিত্রে দেখানো হল।

[code language=”cpp”]
code:

while( scanf( "%llu %llu", &a, &b ) ) {

/* বাকি নির্দেশনা */

}

[/code]

যেহেতু আমরা একই ধরনের কাজ বারবার করব, সেটার জন্য আমরা while লুপ ব্যবহার করেছি। তবে এখানে একটা অদ্ভুত জিনিস দেখা যাচ্ছে। সেটা হল, while এর পরে ব্র্যাকেটের মধ্যে যেখানে আমাদের শর্ত লেখার কথা ছিল, সেখানে আমরা scanf( ) ফাংশন ব্যবহার করেছি! এখন আসা যাক, কিভাবে এটা কাজ করে সেই প্রসঙ্গে।

আমরা জানি, কম্পিউটার সবকিছুকেই 1 এবং 0 তে রূপান্তর করে নিয়ে কাজ করে। যেকোন ধরনের শর্তকে যখন আমরা লিখি, সেই শর্তটি থেকে দুটি ঘটনা ঘটতে পারে, ১। শর্তটি সত্য, ২। শর্তটি মিথ্যা। শর্তটি যতই জটিল হোক না কেন, সেটি নিয়ে সকল প্রকার হিসাব নিকাশ করে শেষ পর্যন্ত সত্য বা মিথ্যা, এই দুইটা সিদ্ধান্তের যেকোন একটাতেই এসে উপনীত হয় কম্পিউটার। এখন এই সত্য বা মিথ্যাকেও সংখ্যায় প্রকাশ করতে হবে যাকে পরবর্তিতে কম্পিউটার বাইনারিতে রূপান্তর করবে। কারণ সে বাইনারি ছাড়া কিছু বুঝে না। তাই কম্পিউটার যেটা করে সেটা হল সে সকল প্রকার মিথ্যাকে 0 দিয়ে প্রকাশ করে এবং সকল সত্য শর্তকে 0 ব্যতীত অন্য যেকোন সংখ্যা দিয়ে প্রকাশ করে। অতএব মিথ্যা হলে মেমরিতে সকল বিট শুন্য হয়ে যায়। যদি কোন একটি বিটেও শুন্য না থাকে তাহলে সে সেটাকে সত্য ধরে নেয়। অতএব উপরে প্রোগ্রামের যে অংশটুকু দেওয়া আছে সেটি দেখে এটুকু ইতোমধ্যে বুঝে যাওয়ার কথা যে প্রোগ্রামটি চলতে হলে শর্তটি সত্য হতে হবে, অর্থাৎ শর্তের স্থানে কোন একটি অশুন্য নাম্বার থাকতে হবে। সেই অশুন্য নাম্বারটি আসবে scanf( ) ফাংশন থেকে। এখন সেটি দেখার জন্য আরও একটু গভীরে যাওয়া যাক, বেশি না, কোমর পর্যন্ত গভীর! 😛

আমরা জানি ফাংশন দুই ধরনের হতে পারে, এক ধরনের ফাংশন কোন একটি মান return করে, আরেকধরনের ফাংশন সেটি করে না। scanf( ) ফাংশনটি C এর একটি স্ট্যান্ডার্ড ফাংশন। এটি একটি মান return করে। এখন কথা হল, সেই মানটি কত। scanf( ) ফাংশনের সাহায্যে যতগুলো ডাটা ইনপুট নেওয়ার কথা, সে যদি ততগুলো ডাটা সঠিকভাবে ইনপুট পায় তাহলে তত return করবে। অর্থাৎ উপরের প্রোগ্রামটিতে দেখা যাচ্ছে, scanf( "%llu %llu", &a, &b ) ফাংশনটির দুইটি ডাটা ইনপুট নেওয়ার কথা। সে যদি এই দুইটি ডাটা সঠিকভাবে ইনপুট পায় তাহলে 2 return করবে। এক্ষেত্রে তাকে সঠিক ডাটা টাইপের ডাটা ইনপুট পেতে হবে। সঠিক বলতে, যদি %d লেখা থাকে, তার মানে তাকে একটা নাম্বার ইনপুট দিতে হবে। যদি আমি a/b/c/d/?/!/* ইত্যাদি ইনপুট দেই তাহলে হবে না। অর্থাৎ যে ধরনের ডাটা যতগুলো ইনপুট নেওয়ার কথা ততগুলো যদি সঠিকভাবে ইনপুট পায় তাহলে তত return করবে।

যদি সে সঠিকভাবে ততগুলো ইনপুট না পায় তাহলে কি হবে? তাহলে যদি দুইটি ইনপুট নেওয়ার কথা থাকে এবং সে একটি ঠিকমত পায়, আরেকটি না পায়, তাহলে 1 return করবে। এখানে বলে রাখি, যদি return ব্যাপারটি কি সেই বিষয়ে সমস্যা থাকে তাহলে আপাতত ধরে নিন যেখানে ফাংশনটি লেখা আছে সেখানে ফাংশনটির জায়গায় একটা মান বসে যাবে। যেমন scanf( "%llu %llu", &a, &b ) যখন 2 return করবে তখন এরকম হয়ে যাবে – while( 2 ) । বিষয়টি বাস্তবে এমন না, তবে আপাতত ধরে নিন। তাহলে কি দাঁড়ালো? ফাংশনটি তার কাজ করল, এরপর একটা মান return করল। যেহেতু ফাংশনটি while এর শর্তের স্থানে লেখা আছে তাই সেই মানটি শর্ত হিসেবে ব্যবহৃত হবে। আর যেহেতু মানটি 0 না, তাই শর্তটি সত্য বলে গণ্য হবে এবং ভিতরের স্টেটমেন্টগুলো সম্পাদিত হবে।

এখন আস্তে আস্তে আবার অগভীরে যাওয়া যাক। এই কথাগুলো থেকে বুঝে যাওয়ার কথা কিভাবে while এর শর্তের স্থানে scanf( ) বা অন্য কোন ফাংশন কাজ করবে। কিন্তু যেটি হল এখানে, তা হল প্রতিবার ইনপুট নিবে, শর্ত সত্য হবে, প্রোগ্রাম রান করবে, এভাবে চলতেই থাকবে। ফলে জাজ আমাদেরকে Time Limit Exceeded নামক একটি verdict দিবে। যেহেতু প্রোগ্রামটি তার নির্ধারিত সময়ের মধ্যে থামতে পারেনি তাই এমনটি হবে। তাহলে থামাবো কিভাবে? বলেই দেওয়া আছে, EOF আসলে থামাতে হবে। আবার সেই প্রশ্নে চলে আসি, EOF ব্যবহার করব কিভাবে?

EOF হল একটি ম্যাক্রো। এটার মান সাধারনভাবে -1 ঠিক করা থাকে। ম্যাক্রো কি জিনিস সেই আলোচনায় না যাই। ধরে নেয়া যাক, EOF হল প্রোগ্রাম থামানোর একটা কমান্ড। আগেই বলেছি scanf( ) ফাংশনটি যতগুলো ইনপুট পাওয়ার কথা ততগুলো যদি সঠিকভাবে পায় তাহলে তত return করে। উপরের উদাহরণ থেকে দেখা যাচ্ছে, scanf( ) ফাংশনটির ২টি সংখ্যা ইনপুট পাওয়ার কথা। EOF পাওয়া মানে, EOF ইনপুট দেওয়া হয়েছে। অর্থাৎ ফাংশনটি তখন ২টি সংখ্যা ঠিকমত ইনপুট পায়নি, সেখানে EOF ইনপুট পেয়েছে। EOF ইনপুট পেলে scanf( ) এর একটি বিশেষত্ব হল যে সে EOF এর মান -1 return করে। অর্থাৎ তার যা return করার কথা তার বদলে অন্য একটা মান EOF এর মান return করছে। এই কনসেপ্ট আমাদেরকে ব্যবহার করতে হবে। দেখা যাক এটা ব্যবহার করে কিভাবে প্রোগ্রামটি লেখা যায়।

[code language=”cpp”]
code:

/* ১ম উপায় */
while( scanf( "%llu %llu", &a, &b ) != EOF ) {

/* বাকি নির্দেশনা */

}

/* ২য় উপায় */
while( scanf( "%llu %llu", &a, &b ) == 2 ) {

/* বাকি নির্দেশনা */

}
[/code]

প্রথম উপায়টি দেখা যাক। যখন আমরা শর্তের ভিতরে লিখছি scanf( … ) != EOF তখন যেটা হয় তা হল, scanf( ) তার কাজ করবে। করে একটা মান return করবে। সেই মান কি EOF এর সমান নাকি তা দেখা হবে। যদি সমান না হয় তাহলে শর্ত সত্য বিবেচিত হবে। অতএব যতবার scanf( ) ঠিকমত ইনপুট নিবে ততবার তার একটা ধনাত্মক মান পাওয়া যাবে এবং সেটি EOF এর সমান না হওয়ার প্রোগ্রাম চলতে থাকবে। এখন যখনই EOF আসবে, তখন scanf( ) ফাংশন EOF এর মান return করবে, বেশিরভাগ ক্ষেত্রে যেটি থাকে -1 । এই মানটি EOF এর মান বলে তা অবশ্যই EOF এর সমান হবে, অর্থাৎ শর্ত মিথ্যা হয়ে যাবে। প্রোগ্রাম আর রান করবে না।

দ্বিতীয় উপায়টিতেও একই কথা, এখানে আমাদের ২টি সংখ্যা ইনপুট পাওয়ার কথা। সঠিকভাবে সেটি ইনপুট পেলে scanf( ) ফাংশন 2 return করবে। তাহলে 2 == 2 হবে এবং শর্ত সত্য হবে। যখনই সে EOF পাবে তখনই -1 return করবে। ফলে -1 == 2 শর্তটি মিথ্যা হয়ে যাবে। এতে করে প্রোগ্রাম আর রান করবে না।

আশা করি এই লেখা দিয়ে EOF এবং তার ব্যবহার সম্পর্কে মোটামুটি একটা ধারণা দিতে পেরেছি। EOF এর বেসিক ব্যবহার সম্পর্কে আশা করি আর কোন কনফিউশন থাকবে না। থাকলে নিঃসংকোচে প্রশ্ন করতে পারেন। 🙂