রিকার্শন (Recursion)

Standard

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

Continue reading

ফাংশন (Function)

Standard

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

Continue reading

বিট-ওয়াইজ অপারেটর (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!) প্রিন্ট কর।

if-else

Standard

if-else সি প্রোগ্রামিং এর অত্যন্ত গুরুত্বপূর্ণ একটি বিষয়। if-else এর সাহায্যে একটি প্রোগ্রাম কোন একটি শর্তের ভিত্তিতে কি করবে সেটি নির্ধারণ করে দেওয়া হয়। ব্যাপারটি অনেকটা ইংরেজি ব্যকরণের if-else এর মতই। একটি উদাহরণের সাহায্যে বিষয়টি পরিষ্কার করা যাক।

ধরা যাক কোন একটি স্কুলের কোন একটি পরীক্ষার ফল দিয়েছে। সেখানে যারা ৮০ অথবা এর উপরে নাম্বার পেয়েছে তাদের গ্রেড হবে A। তাহলে এটিকে সহজ বাংলায় বলতে হলে যেভাবে বলা হবে তা হল, “যদি নাম্বার ৮০ এর সমান অথবা বেশি হয় তাহলে গ্রেড হবে A।” এই কথাটিকেই যদি ইংরেজিতে বলা হয় তাহলে বলা হবে, “If number is greater than or equal to 80 then the grade will be A.” খুবই সহজ সাধারণ একটি কথা। আমাদের C তেও কথাটা এতটাই সহজ এবং সাধারণ। তবে একটা বিষয় হল যে আমরা মানুষ বলে এখানে কথাটা বলে দিতে পারলাম। কিন্তু কম্পিউটার তো মানুষ না, তাই সে তো এভাবে কথা বলতে পারবে না। সে মনিটরে আউটপুট দেখাবে। সেটা দেখাবে printf ফাংশনের সাহায্যে। printf কিভাবে কাজ করে তা ইতোমধ্যে জানা থাকার কথা। যাই হোক, আমরা দেখলাম কিভাবে বাংলা এবং ইংরেজি ভাষায় ঘটনাটা প্রকাশ করা যায়। এখন দেখা যাক C তে কিভাবে প্রকাশ করা যায়। C তে আমরা লিখব এভাবে,

[code language=”cpp”]
code:
if( number >= 80 ) {
printf( "A" );
}
[/code]

এখানে C এর সিন্ট্যাক্স খেয়াল রাখা আবশ্যক। প্রথমে লিখব if, এরপর প্রথম ব্র্যাকেট শুরু, এর ভিতরে লিখব শর্ত, যেটি সত্য হলে কাজটি সম্পাদিত হবে। শর্তটি লেখার পর ব্র্যাকেট শেষ। এরপর কোন সেমিকোলন (;) হবে না। এটি খেয়াল রাখতে হবে। এরপর দ্বিতীয় ব্র্যাকেট শুরু, এর ভিতরে যে কাজটি সম্পাদন করতে হবে সেটি লেখা থাকবে এবং সব শেষে দ্বিতীয় ব্র্যাকেট শেষ হবে। C তে কোন একটি স্টেটমেন্ট (সাধারণ ভাষায়, কাজ) এর পর সেমিকোলন (;) হয়। এখানে যেহেতু একটাই স্টেটমেন্ট আছে printf( “A” ), তাই এটার পরে সেমিকোলন হয়েছে।

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

এখন যদি এমন হয় যে কোন একটি শর্ত সত্য হলে আমাকে একটি নয়, একাধিক কাজ করতে হবে, তখন কি হবে? সেটিও খুব সহজ। if( ) এর পরে যে দ্বিতীয় বন্ধনী থাকে { } এই দুইটির মধ্যে যতগুলো স্টেটমেন্ট লেখা হবে সবগুলোই সম্পাদিত হবে যদি if এর ভিতরের শর্তটি সত্য হয়। উদাহরণের মাধ্যমে দেখা যাক। আবারও ধরা যাক কোন একটি স্কুলের পরীক্ষার ফল দিয়েছে। যদি কেউ এখানে ৩৩ এর নিচে পেয়ে থাকে তবে সে অকৃতকার্য বলে বিবেচিত হবে। এইজন্য যে প্রোগ্রামটি লেখা হবে সেটি নাম্বার ৩৩ এর কম হলে দেখাবে “Failed” এবং আরও দেখাবে “You need to take the test again.” এক্ষেত্রে প্রোগ্রামটি নিম্নরূপ হবে,

[code language=”cpp”]
code:
if( number < 33 ) {
printf( "Failed\n" );
printf( "You need to take the test again." );
}
[/code]

এখানে স্পষ্টভাবে দেখা যাচ্ছে দুইটি printf আছে। এরা দুইটি স্টেটমেন্ট হিসেবে কাজ করছে। এদেরকে একটি ব্লকের ভিতরে রাখা হয়েছে। ব্লক হচ্ছে কতগুলো স্টেটমেন্টের সমষ্টি যেই স্টেটমেন্টগুলো { } এর মধ্যে রাখা থাকে। if এর শর্তটি সত্য হলে এর জন্য যে ব্লকটি থাকবে তার মধ্যের সবগুলো স্টেটমেন্ট সম্পন্ন হবে। এটি হল বেসিক সিন্ট্যাক্স। এখানে দুইটি printf দিয়ে বোঝানো হয়েছে। বাস্তবে কোন একটি সমস্যার সমাধান করতে হলে এরকম ক্ষেত্রে অনেককিছুই থাকতে পারে। কোন একটি শর্ত সত্য হলে অনেককিছুই সংঘটিত হতে পারে। তখন কি কি সংঘটিত হবে সেটি খেয়াল রেখে সুনির্দিষ্টভাবে সেগুলোকে ব্লকের মধ্যে রাখতে হবে।

অর্থাৎ সাধারণভাবে C এর সিন্ট্যাক্স হবে এরকম,

[code language=”cpp”]
code:
if( শর্ত ) {
স্টেটমেন্ট ১ ;
স্টেটমেন্ট ২ ;
. . . ;
. . . ;
স্টেটমেন্ট n ;
}
[/code]

আরেকটি বিষয় হল যে if এর শর্তের স্থানে একাধিক শর্ত থাকতে পারে। একাধিক শর্তকে লজিকাল অপারেটরের মাধ্যমে একসাথে if এর শর্তের স্থানে লেখা হয়। লজিকাল অপারেটরগুলোর মধ্যে আছে && এবং | | । এই দুইটিই বাইনারি অপারেটর, অর্থাৎ এদের দুই পাশে দুইটি এক্সপ্রেশন থাকে যাদেরকে নিয়ে এরা কাজ করে। && দিয়ে বোঝানো হয় যে এর দুইপাশের দুটি শর্তই সত্য হতে হবে। তাহলে সমগ্র শর্তটিকে সত্য হিসেবে গণ্য করা হবে। যেকোন একটিও যদি সত্য না হয় তবে সমগ্র শর্তটিই মিথ্যা বলে বিবেচিত হবে। অর্থাৎ তখন আর if এর ভিতরের কোন কাজ সম্পাদিত হবে না। যেমন ধরা যাক কোন পরীক্ষায় যদি ৭০ এর সমান বা বেশি এবং ৮০ এর কম নাম্বার কেউ পায় তবে তার গ্রেড হবে B। সহজ ইংরেজিতে এটি হবে, “If number is greater than or equal to 70 and less than 80, then show grade B.” C তে এটি কি হবে এতক্ষণে সকলের বুঝে ফেলার কথা, তাও দেখে নেয়া যাক,

[code language=”cpp”]
code:
if( number >= 70 && number < 80 ) {
printf( "B" );
}
[/code]

এখানে আসলে যেটি হচ্ছে তা হল কম্পাইলার যখন কন্ডিশন টেস্ট করতে যাচ্ছে তখন সে প্রথমে && এর বাম পাশের কন্ডিশনটি পরীক্ষা করছে। সেটি যখন সত্য হয় তখন সে পরের কন্ডিশনটি পরীক্ষা করছে। দুইটাই যখন সত্য হয় তখন সে ধরে নেয় যে সমগ্র কন্ডিশন সত্য হয়েছে, অর্থাৎ এখন সে ভিতরের স্টেটমেন্টগুলো এক্সিকিউট করতে পারবে। এভাবে কন্ডিশনের ভিতরে কেবল দুইটি নয়, দুইয়েরও অধিক শর্ত থাকতে পারে। if( number1 > 80 && number2 > 70 && number3 > 60) এরকম একটি if প্রোগ্রামে থাকা অস্বাভাবিক কিছু না। এক্ষেত্রে যা হবে তা হল কম্পাইলার একটা একটা করে শর্ত দেখবে। প্রথমে সে প্রথম && এর দুইপাশের শর্তদুটি পরীক্ষা করে দেখবে। তারা উভয়েই সত্য হলে সমগ্রটি মিলে সত্য হবে। এই সমগ্র অংশটুকু পরবর্তী && এর বামপাশের শর্ত হিসেবে কাজ করবে। যেহেতু বামপাশেরটি ইতোমধ্যে সত্য হয়েছে অতএব এখন যদি শুধু ডানপাশেরটি সত্য হয় তাহলেই সমগ্র শর্ত সত্য হয়ে যাবে। দুইটি && দিয়ে আলাদা করা যে তিনটি শর্ত এখানে রয়েছে এর কোন একটিও যদি মিথ্যা হয় তাহলেই if এর শর্ত সামগ্রিকভাবে মিথ্যা হয়ে যাবে।

এই তো গেল && এর ব্যবহার, এখন দেখা যাক | | কিভাবে কাজ করে। | | এর দুইপাশে যে দুইটি শর্ত থাকে সেগুলোর যেকোন একটিই যদি সত্য হয়ে যায় তাহলেই সমগ্র শর্তটি সত্য বলে বিবেচিত হবে। অর্থাৎ যদি এমন থাকে,

[code language=”cpp”]
code:
if( number1 < 80 || number2 < 50 ) {
printf( "B" );
}
[/code]

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

পরিশেষে একটি সম্পূর্ণ প্রোগ্রামের মাধ্যমে দেখা যাক কিভাবে if কাজ করে।

[code language=”cpp”]
code:
#include
int main()
{
int number;

scanf( "%d", &number );
if( number >= 50 ) {
printf( "Passed\n" );
printf( "Congratulation!\n" )
}
printf( "Program Complete.\n" );

return 0;
}
[/code]

উপোরক্ত প্রোগ্রামটি যখন এক্সিকিউট হবে তখন প্রথমে scanf এর মাধ্যমে একটি নাম্বার ইনপুট নিয়ে সেটিকে number নামক একটি ইন্টিজার ভেরিয়েবল এর মধ্যে রাখা হবে। এরপর যখনই if আসবে তখন এর শর্ত পরীক্ষা করা হবে। পরীক্ষা যদি সত্য হয় অর্থাৎ যদি number এর মান 50 এর বড় হয় তাহলে এর ভিতরের কাজগুলো সম্পন্ন হবে। অতএব Passed এবং Congratulations! লেখা আসবে। এরপর প্রোগ্রামটি ঐ ব্লক থেকে বের হয়ে যাবে এবং এর পরবর্তী কাজটি সম্পাদন করবে, অর্থাৎ লেখা দেখাবে Program Complete. এখন যদি এক্ষত্রে শর্তটি সত্য না হয় তাহলে if এর জন্য যে ব্লক রয়েছে তার ভিতরে প্রোগ্রাম প্রবেশ করবে না এবং তথাপি ভিতরের কোন স্টেটমেন্ট এক্সিকিউট হবে না। সরাসরি প্রোগ্রাম if এর ব্লক যেখানে শেষ হয়েছে সেখানে চলে যাবে এবং পরবর্তী স্টেটমেন্ট এক্সিকিউট করবে।

অতএব এই পর্যন্ত আলোচনার সার সংক্ষেপ যা দাঁড়ায় তা হল,
– কোন শর্তাধীন কাজ থাকলে সেটিকে if এর মাধ্যমে লিখতে হয়।
– শর্ত একটি বা একাধিক হতে পারে।
– if এর শর্তগুলো সামগ্রিকভাবে সত্য হলে তবেই কাজটি সম্পাদিত হয়।
– if( শর্ত ) এরপরে কোন সেমিকোলন (;) হবে না

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

if-else এর প্রথম পর্বে বলা হয়েছে if( ) কিভাবে কাজ করে। এই পর্বে দেখা যাক if( ) এর সাথে else এবং else if( ) কিভাবে কাজ করে। if এর কাজ যেমন একেবারেই সহজ এবং সাধারণ, তেমনিভাবে else-ও খুব একটা জটিল কিছু না, বরং খুবই সহজ।

ঘুরেফিরে যেহেতু আমরা একটা ভাষা শিখছি, তাই আবারও একে আমাদের বাংলা এবং ইংরেজির সাথে তুলনা করে দেখা যাক কিরকম দাঁড়ায়। ধরা যাক, কোন একটি পরীক্ষায় যারা ৫০ বা তার উপরে নাম্বার পাবে তারা পাস করবে এবং যারা ৫০ এর কম পাবে তারা ফেল করবে। তাহলে এটাকে আমরা বাংলায় বলব, “যদি নাম্বার ৫০ বা এর চেয়ে বেশি হয় তাহলে দেখাবে ‘Passed’, অন্যথায় দেখাবে ‘Failed’।” এখন এই কথাটা যদি আমরা ইংরেজিতে বলি তাহলে বলব, “If number is greater than or equal to 50, show ‘Passed’, else show ‘Failed’.” একেবারেই সরল একটা কথা। এখন দেখা যাক এই কথাটাকেই যদি C তে লেখা হয় তাহলে সেটা কেমন হবে।

[code language=”cpp”]
code:
if( number >= 50 ) {
printf( "Passed\n" );
}
else {
printf( "Failed\n" );
}
[/code]

প্রথমে সিন্ট্যাক্স খেয়াল করা যাক। if( ) এর সিন্ট্যাক্স ইতোমধ্যে জেনে যাওয়ার কথা, না জানলে ১ম পর্ব পড়লেই জানা যাবে। যখন if এর সাথে else ব্যবহার করা হবে তখনও if এর অংশটুকুর সিন্ট্যাক্স যেমন থাকার তেমনই থাকবে। সাথে শুধু else এর অংশটুকু যুক্ত হবে। প্রথমে else থাকবে, এরপর দ্বিতীয় ব্র্যাকেট শুরু, তারপর else এর জন্য যে স্টেটমেন্টগুলো থাকার সেগুলো একটার পর একটা থাকবে, এবং সবশেষে দ্বিতীয় ব্র্যাকেট শেষ হবে।

ইতোমধ্যেই বুঝে যাওয়ার কথা else দিয়ে কি বোঝানো হচ্ছে। if এর জন্য একটা শর্ত দেওয়া থাকবে। সেই শর্তটি যদি সত্য হয় তাহলে if এর শর্তের পর { } এর মধ্যে যে স্টেটমেন্টগুলো আছে, সেগুলো সম্পাদিত হবে। আর যদি সেটি সত্য না হয় তাহলে else এর ভিতরে যে স্টেটমেন্টগুলো আছে সেগুলো সম্পাদিত হবে। ব্যাপারটি খুবই সহজ, বাংলা কথার সাথে মিলিয়ে দেখলেই বোঝা যাবে। ৫০ বা এর উপরে নাম্বার পেলে প্রিন্ট করবে Passed। যদি ৫০ বা এর উপরে নাম্বার হয়, তাহলে তো শুধুই Passed প্রিন্ট করা হবে, Failed প্রিন্ট করার প্রশ্নই আসে না। অতএব একবার শর্ত সত্য হয়ে গেলে else এর ভিতরে কি আছে সেটির আর কোন তাৎপর্য নেই। কিন্তু যদি if এর শর্ত সত্য না হত, তাহলেই এক্ষেত্রে Failed প্রিন্ট করা হত। অতএব, শর্ত সত্য না হলে তখনি কেবলমাত্র else এর ভিতরে যেটি আছে সেটি সংঘটিত হবে।

এখানে একটি কথা গুরত্বপূর্ণ, সেটি হল যে if এর শর্ত যদি পূরণ হয়ে যায় তাহলে প্রোগ্রামটি else এর ব্লকের ভিতরেই আর প্রবেশ করবে না। আর যদি if এর শর্ত সত্য না হয়, তাহলে প্রোগ্রামটি if এর ব্লকটিতে ঢুকবে না, সে সরারসরি এরপরে যদি else থাকে তাহলে সেখানে চলে যাবে, না থাকলে অন্য যে স্টেটমেন্ট থাকবে সেটি এক্সিকিউট করা শুরু করবে।

এখন এই কোডটাকে একটু ভিন্নভাবে লিখে দেখা যাক কি হতে পারে। নিচের কোড দুটি দেখা যাক।

[code language=”cpp”]
code:
int main() | int main()
{ | {
int number; | int number;
|
scanf( "%d", &number ); | scanf( "%d", &number );
if( number >= 50 ) { | if( number >= 50 ) {
printf( "Passed\n" ); | printf( "Passed\n" );
} | }
else { |
printf( "Failed\n" ); | printf( "Failed\n" );
} |
|
return 0; | return 0;
} | }
|
/**** Fig: A ****/ | /**** Fig: B ****/
[/code]

Fig: A এবং Fig: B এর মধ্যে তফাৎ কোথায়? প্রথমটিতে উপর্যুক্ত উদাহরণটি দেখানো হয়েছে, দ্বিতীয়টিতে else ব্যবহার না করে printf ব্যবহার করা হয়েছে। দুইটি ক্ষেত্রেই কি হবে তা বোঝার জন্য একটু যৌক্তিক চিন্তাই যথেষ্ট। প্রথমটিতে যা হবে তা এই লেখার প্রথমদিকেই বলা হয়েছে। যদি নাম্বার ৫০ বা এর বেশি হয় তাহলে ‘Passed’ প্রিন্ট করবে, যদি ৫০ এর কম হয় শুধু মাত্র তখনই ‘Passed’ প্রিন্ট না করে ‘Failed’ প্রিন্ট করবে। আর দ্বিতীয় প্রোগ্রামটির ক্ষেত্রে যেটি হবে তা হল, প্রথমে টেস্ট করা হবে number ভেরিয়েবলটি কি ৫০ এর সমান বা বড় কিনা। যদি সমান বা বড় হয়, অর্থাৎ যদি শর্ত সত্য হয় তাহল ‘Passed’ প্রিন্ট করবে এবং তারপর এর পরবর্তীতে যে স্টেটমেন্ট আছে সেটি এক্সিকিউট করবে অর্থাৎ ‘Failed’ও প্রিন্ট করবে! এর কারণ কি? এর কারণ হল এখানে কোন else ব্যবহার করা হয়নি। else এর কাজই হল এই ফারাক তৈরি করে দেওয়া। যদি একটি হয়ে যায় তাহলে যেন পরেরটি না হয়, আবার যদি প্রথমটি না হয়, তাহলে যেন পরেরটি হয়। অর্থাৎ একটি শর্ত সত্য নাকি মিথ্যা তার সাপেক্ষে যখন দুটি ভিন্ন কাজ সংঘটিত হতে হবে তখন if এবং সেই সাথে else ব্যবহার করতে হয়। else ব্যবহার না করলে কি হবে তা তো দেখাই গেল। অতএব প্রয়োজন অনুযায়ী কখন কিভাবে if এবং else ব্যবহার করতে হবে তা যুক্তিসংগতভাবে চিন্তা করে ব্যবহার করতে হবে। নতুবা প্রোগ্রামে ঠিকমত কাজ করবে না।

যে উদাহরণগুলো দেখানো হল সেগুলোতে একটি শর্ত সত্য হলে কি হবে, এবং সত্য না হলে কি হবে তা দেখানো হয়েছে। C তে অনেকগুলো শর্তসাপেক্ষে যদি অনেকগুলো কাজ থাকে তাহলে if এবং else এর মাঝে else if( ) ব্যবহারের সুযোগ আছে। এটির ব্যবহার বলতে গেলে if এর মতই, কেবল পার্থক্য এখানেই যে এটির আগে একটি if থাকতে হয়, যেমনটি থাকতে হয় else এর আগে। একটি উদাহরণের সাহায্যে বিষয়টি দেখা যাক। আবারও পরীক্ষার ফল নিয়ে আসি 😛 । কোন একটি পরীক্ষার নাম্বার যদি ৮০ বা এর বেশি হয় তাহলে প্রিন্ট করবে ‘A’। যদি সেটি না হয় তাহলে যদি নাম্বার যদি ৫০ বা এর বেশি হয় তাহলে প্রিন্ট করবে ‘Passed without A’। আর সেটিও যদি না হয় তাহলে প্রিন্ট করবে ‘Failed’। এটির কোড নিচে দেওয়া হল,

[code language=”cpp”]
code:
if( number >= 80 ) {
printf( "A\n" );
}
else if( number >= 50 ) {
printf( "Passed without A\n" );
}
else {
printf( "Failed\n" );
}
[/code]

এবার দেখা যাক এখানে কি হয়। প্রথমে if এর শর্তটি টেস্ট করা হয়। সেটি যদি সত্য হয়ে যায় তাহলে প্রিন্ট হবে ‘A’। এখন যৌক্তিকভাবেই এটা বোঝা যাচ্ছে যে নাম্বার যদি ৮০ বা তার উপরে হয় তাহলে শুধু A ই প্রিন্ট হবে, ‘Passed without A’ বা ‘Failed’ প্রিন্ট হওয়ার প্রশ্নই আসে না। তাই এরপরে অবস্থিত যতগুলো else if বা else রয়েছে তার কোনটিই আর টেস্ট করা হবে না। সরাসরি তার পরবর্তী স্টেটমেন্টে চলে যাবে। যদি শর্তটি সত্য না হয় অর্থাৎ নাম্বার ৮০ বা এর বেশি না হয় তাহলে – ১) নাম্বার ৫০ বা তার বেশি হতে পারে, সেক্ষেত্রে প্রিন্ট করা হবে ‘Passed without A’ এবং ২) নাম্বার ৫০ এর কম হতে পারে, সেক্ষেত্রে প্রিন্ট করা হবে ‘Failed’। নাম্বার যদি ৫০ এর সমান বা এর বেশি হয়, তাহলে ‘Passed without A’ প্রিন্ট হবে। সেটি বলার জন্যই else if ব্যবহার করা হয়েছে। অতএব else if তখনই ব্যবহার করা হয় যখন একটি শর্ত সত্য না হলে আরেকটি শর্ত সত্য হল কিনা দেখা হয়। আর যদি দ্বিতীয়টিও সত্য না হত তাহলে প্রিন্ট করা হত ‘Failed’।

সামগ্রিকভাবে প্রোগ্রামটি কিভাবে কাজ করে তা আরেকবার দেখা যাক। প্রোগ্রামে যদি কোন শর্তাধীন কাজ থাকে তাহলে সেটিকে if এর মাধ্যমে লেখা হয়। if এর শর্তটি সত্য হলে কাজটি সম্পাদিত হবে এবং এর পর যতগুলো else if বা else আছে তার কোনটিতেই আর প্রবেশ করা হবে না, শর্ত সত্য না হলে যদি আবারও কোন শর্তাধীন কাজ থাকে যেটি কেবলমাত্র পূর্ববর্তী শর্তগুলো সত্য না হলে এবং আরও কোন একটি শর্ত সত্য হলে কাজ করবে তখন else if ব্যবহার করা হয়। এই শর্তটি সত্য হয়ে গেলে এর পরবর্তী আর কোন else if বা else এ প্রোগ্রাম প্রবেশ করে না। এভাবে একটির পর একটি চলতে থাকবে। অতএব যখন কোন একটি প্রোগ্রাম লেখা হবে তখন কোন কাজটি কখন কোন শর্তের অধীনে সংঘটিত হবে তা যুক্তিসংগতভাবে চিন্তা করে নিতে হবে। এই শর্ত হলে এটি হবে, সত্য না হয়ে যদি আবার এই শর্তটি সত্য হয় তাহলে এটি হবে, সেটিও যদি সত্য না হয় তাহলে আবার এই শর্ত সত্য হলে এটি হবে, …, এর কোনটিই যদি না হয় তাহলে এটি হবে – এভাবে পর্যায়ক্রমিকভাবে চিন্তা করে সাজিয়ে নিতে হবে।

সর্বশেষ একটি উদাহরণের মাধ্যমে পর্যায়ক্রমিকভাবে কিভাবে if, else if এবং else ব্যবহার করে প্রোগ্রাম লেখা হয় তা দেখা যাক। আবারও পরীক্ষার ফল প্রসঙ্গ 😛 । একটি পরীক্ষায় যদি কেউ ৮০ বা তার উপরে নাম্বার পায় তাহলে প্রিন্ট করবে ‘A’, যদি ৭০ এর সমান বা বেশি কিন্তু ৮০ এর কম হয় তাহলে ‘B’, যদি ৬০ এর সমান বা বেশি কিন্তু ৭০ এর কম হয় তাহলে ‘C’, যদি ৫০ এর সমান বা বেশি কিন্তু ৬০ এর কম হয় তাহলে ‘D’ এবং যদি এর কোনটিই না হয় অর্থাৎ ৫০ এর কম হয় তাহলে প্রিন্ট করবে ‘F’। প্রোগ্রামটি হবে নিম্নরূপ,

[code language=”cpp”]
code:
#include

int main()
{
int number;

scanf( "%d", &number );
if( number >= 80 ) {
printf( "A\n" );
}
else if( number >= 70 ) {
printf( "B\n" );
}
else if( number >= 60 ) {
printf( "C\n" );
}
else if( number >= 50 ) {
printf( "D\n" );
}
else {
printf( "F\n" );
}

return 0;
}
[/code]

সবারই এখন বুঝে যাওয়ার কথা কিভাবে এটি কাজ করবে। একটি একটি শর্ত পরীক্ষা হবে এবং সেটি সত্য না হলে তার পরেরটি পরীক্ষা হবে। কোনটিই যদি সত্য না হয় তাহলে শেষে অবস্থিত else এর ভিতর যা আছে সেটি সম্পাদিত হবে। এখানে নাম্বার যদি ৭০ এর সমান বা বেশি হয় এবং ৮০ এর কম হয় তাহলে ‘B’ প্রিন্ট করার কথা। অতএব শর্তটি হওয়ার কথা else if( number >= 70 && number < 80 ) । কিন্তু শর্তের মধ্যে লেখা আছে else if( number >= 70 ) । এর কারণ একটু চিন্তা করলেই বোঝা সম্ভব। প্রথমেই বলা আছে if( number >= 80 ) । তাই যদি নাম্বার ৮০ এর সমান বা বড় হয়ে যায় তাহলে তো প্রথমের শর্তটিই সত্য হয়ে গেল। তখন তো আর পরবর্তী শর্তগুলো পরীক্ষাই হবে না। পরবর্তী শর্তগুলোতে কেবলমাত্র তখনই আসবে যখন আগেরগুলো মিথ্যা হবে। যেহেতু আগের শর্তটি মিথ্যা হয়ে এসেছে, তাই এটা নিশ্চিতভাবেই বলা যায় যে নাম্বার ৮০ এর কম হবেই। অন্যথায় প্রোগ্রাম এখানে আসতই না! তাই শুধু শুধু বাড়তি করে number < 80 লেখার কোনই প্রয়োজন নেই। পরবর্তী প্রতিটি শর্তের ক্ষেত্রে একই ঘটনা ঘটবে।

এখানে একটি বিষয় উল্লেখযোগ্য, সেটি হল, যখন কোন প্রোগ্রামের কোডে কোথাও else ব্যবহার করা হয়, তখন সেই else টি তার একই ব্লকে অবস্থিত ঠিক পূর্ববর্তী if এর জন্য কাজ করে। অর্থাৎ else টি যেই ব্লকে রয়েছে সেই একই ব্লকে তার ঠিক আগে যে if রয়েছে সেটির শর্ত যদি সত্য না হয় তাহলে ঐ else কাজ করবে। if যেই ব্লকের ভিতরে রয়েছে, else যদি সেই একই ব্লকের ভিতরে না থেকে অন্য কোন ব্লকে থাকে তাহলে প্রোগ্রামটি ঠিকমত কাজ করবে না। else if এর ক্ষেত্রেও একই কথা প্রযোজ্য। আসলে ব্যাপারটি বুঝতে সমস্যা হতে পারে, এভাবে লিখে বোঝানোটা একটু মুশকিল। তাই আমি বলব এটি নিয়ে যদি কারও কোন কনফিউশন থাকে তাহলে তার উচিৎ হবে একটি কোড লিখে সেখানে যতভাবে if এবং else ব্যবহার করা যায় (নিজের মন মত 😛 ) ততভাবে লিখে লিখে রান করে দেখা যে কখন কি হয়। Practically প্রতিটা জিনিস নিজের হাতে করে দেখার চেয়ে ভাল উপায় আসলে নেই।