পয়েন্টার হলো এমন একটি ভ্যারিয়েবল বা কনস্ট্যান্ট, যার মান অন্য একটি ভ্যারিয়েবল বা কনস্ট্যান্টের অ্যাড্রেস। পয়েন্টার C-এর একটি শক্তিশালী, আবার সম্ভাব্য বিপজ্জনক বৈশিষ্ট্য। পয়েন্টার বোঝা চ্যালেঞ্জিং হতে পারে।
int i = 5;
আমরা যখন একটি ভ্যারিয়েবলে একটি মান অ্যাসাইন করি, তখন দুটি প্রশ্ন করতে পারি।
আপনি কে?
আমি i.
আপনার কাছে কী আছে?
আমার কাছে ইন্টিজার 5 আছে।
আরও একটি তৃতীয় প্রশ্নও করা যায়।
আপনি কোথায় আছেন?
আমি আছি 0x6dfed4-এ।
এক মিনিট, কী বললেন?
ভ্যারিয়েবল আর কনস্ট্যান্ট তাদের মান সংরক্ষণ করে একটি মেমোরি লোকেশনে। মেমোরির সেই লোকেশনটিকেই বলা হয় ওই ভ্যারিয়েবল বা কনস্ট্যান্টের অ্যাড্রেস।
একটি ভ্যারিয়েবলের অ্যাড্রেস পেতে আমরা অ্যাড্রেস অপারেটর ব্যবহার করি, যেটি হলো &.
int i = 5;
int *i_address = &i;
এক মিনিট, i_address-এর আগে ওই *-এর মানে কী? এটা কি গুণ বোঝায় না?
একটি টাইপ, তারপর একটি *, তারপর একটি আইডেন্টিফায়ার: এভাবেই ওই টাইপের একটি ভ্যারিয়েবলের পয়েন্টার ডিক্লেয়ার করা হয়। *-কে ইনডাইরেকশন অপারেটর বলা হয়, কারণ এটি পরোক্ষভাবে সেই মানকে বোঝায়, যার অ্যাড্রেস পয়েন্টারটির কাছে আছে।
আচ্ছা, কিন্তু শুধু অন্য একটি ভ্যারিয়েবলের অ্যাড্রেস ধরে রাখার জন্য আলাদা একটি ভ্যারিয়েবল বানাব কেন?
ভ্যারিয়েবলের অ্যাড্রেস নিয়ে আমাদের আদৌ এত মাথাব্যথা কেন?
আসলে, ভ্যারিয়েবলগুলো যখন একটি অ্যারেতে থাকে, তখন তারা কোথায় আছে সেটা আমাদের জানা দরকার।
আমরা যখন এভাবে একটি অ্যারে তৈরি করি
int my_ints[] = {1, 2};
my_ints-এর মান আসলে তার অ্যাড্রেস, যা তার প্রথম এলিমেন্টের অ্যাড্রেসেরও সমান। তার দ্বিতীয় এলিমেন্টের অ্যাড্রেস হবে প্রথম এলিমেন্টের অ্যাড্রেস আর সিস্টেমে একটি int-এর সাইজের সমষ্টি।
এটা আরও ভালোভাবে দেখতে, নিচের কোডটি চালিয়ে দেখুন
#include <stdio.h>
int main() {
// my_int's value is its address
int my_ints[] = {1, 2};
// since my_ints is already an address
// we don't need the address operator to make a pointer from it.
// a pointer to my_ints holds the address of my_ints
int *my_ptr = my_ints;
// the address of my_ints and the value of my_ptr are the same
printf("my_ints is %p. my_ptr is %p.\n", my_ints, my_ptr);
// the address of the first element is the same as the values in the line above
printf("&my_ints[0] is %p. my_ptr + 0 is %p.\n", &my_ints[0], my_ptr + 0);
// the address of the second element has increased by the size of an int
printf("&my_ints[1] is %p. my_ptr + 1 is %p.\n", &my_ints[1], my_ptr + 1); //
return 0;
}
আগের উদাহরণটি দেখায় কেন অ্যারে ইনডেক্সিং 1-এর বদলে 0 থেকে শুরু হয়। অ্যারের ভেতরে ইনডেক্স নির্ধারণ হয় এভাবে: ইনডেক্সটিকে টাইপের সাইজ দিয়ে গুণ করা হয়, আর তা অ্যারের শুরুর অ্যাড্রেসের সাথে যোগ করা হয়। যেহেতু প্রথম এলিমেন্টটি অ্যারের শুরুতেই বসে, তাই প্রথম এলিমেন্ট পেতে শুরুর অ্যাড্রেসের সাথে যোগ করার জন্য টাইপকে 0 দিয়ে গুণ করি। দ্বিতীয় এলিমেন্ট পেতে শুরুর অ্যাড্রেসের সাথে যোগ করার জন্য টাইপকে 1 দিয়ে গুণ করি। টাইপটির দৈর্ঘ্য যদি চার বাইট হয়, তবে দ্বিতীয় এলিমেন্টের অ্যাড্রেস প্রথম এলিমেন্টের চেয়ে চার বাইট বেশি হবে।
এ কারণেই পয়েন্টারকে একটি নির্দিষ্ট টাইপের হিসেবে ডিক্লেয়ার করা হয়। একটি পয়েন্টার যদি না জানে সে কোন টাইপের দিকে নির্দেশ করছে, তাহলে তার সাথে 1 যোগ করার সময় কত বাইট এগোতে হবে তাও জানবে না। পয়েন্টারের সাথে 1 যোগ করলেই যে অ্যাড্রেসে এক বাইট যোগ হচ্ছে, তা নয়। এর মানে হলো তার টাইপের দৈর্ঘ্য (বাইটে) 1 দিয়ে গুণ করে যোগ করা হচ্ছে।
কিন্তু আরও বাকি আছে! ভাবলেন না তো যে *-এর কাজ শেষ?
এখন যেহেতু বুঝলাম অ্যারে ইনডেক্সিংয়ে অ্যাড্রেস কীভাবে কাজ করে, কিন্তু কোনো অ্যাড্রেসে সংরক্ষিত মান আমরা না পেলে সেটা দিয়ে লাভ কী? এটা আমরা কীভাবে করি? কোনো অ্যাড্রেসে সংরক্ষিত মান পেতে আমরা ইনডাইরেকশন অপারেটর ব্যবহার করি, যেটি নিশ্চয়ই আপনার মনে আছে, *. এই ক্ষেত্রে ইনডাইরেকশন অপারেটরকে অনেক সময় ডিরেফারেন্স অপারেটরও বলা হয়, কারণ এটি সেই মানটিকে ডিরেফারেন্স করে, যার অ্যাড্রেস পয়েন্টারের কাছে আছে। ডিরেফারেন্স করা বোঝাতে এটি ব্যবহার করলে একে ডিরেফারেন্স অপারেটর বললে পয়েন্টার ডিফাইন করার ইনডাইরেকশন অপারেটর থেকে আলাদা করা যায়, তবে কারিগরি দিক থেকে দুই ব্যবহারেই *-এর নাম ইনডাইরেকশন অপারেটর।
আরে! মজা করছেন? *-এর আবার কত রকম ব্যবহার আছে?! গুণ, পয়েন্টার ডিক্লেয়ার, আর এখন ডিরেফারেন্সিং! এ তো গোলমেলে! আর ডিরেফারেন্সিংটা আসলে কী?
একটি উদাহরণ দেখায় ডিরেফারেন্স করার জন্য ইনডাইরেকশন অপারেটর কীভাবে ব্যবহার করতে হয়
#include <stdio.h>
int main() {
// define the variable
int my_ints[] = {1, 2};
// define the pointer from the address of the variable
int *my_ptr = my_ints;
// prints: my_ints[0] is 1. *(my_ptr + 0) is 1.
printf("my_ints[0] is %u. *(my_ptr + 0) is %u.\n", my_ints[0], *(my_ptr + 0));
// prints: my_ints[1] is 2. *(my_ptr + 1) is 2.
printf("my_ints[1] is %u. *(my_ptr + 1) is %u.\n", my_ints[1], *(my_ptr + 1));
return 0;
}
পয়েন্টার অ্যারিথমেটিক ব্যবহারের সময় বন্ধনী দিলে উদ্দেশ্যের অস্পষ্টতা কাটাতে সাহায্য হয়। নিচের উদাহরণটি প্রোগ্রামার যা চেয়েছিলেন, তা নাও প্রিন্ট করতে পারে
#include <stdio.h>
int main() {
int my_ints[] = {1, 42};
int *my_ptr = my_ints;
// prints: my_ints[0] is 1. *++my_ptr is 42.
printf("my_ints[0] is %u. *++my_ptr is %u.\n", my_ints[0], *++my_ptr);
// prints: my_ints[1] is 43. ++*my_ptr++ is 43.
printf("my_ints[1] is %u. ++*my_ptr++ is %u.\n", my_ints[1], ++*my_ptr);
return 0;
}
* আর ++ দুটিরই প্রিসিডেন্স একই, তবে এরা ডান থেকে বামে ইভ্যালুয়েট হয়। প্রথম লাইনে my_ptr প্রথমে ইনক্রিমেন্ট হয়ে দ্বিতীয় এলিমেন্টে যায়, তারপর সেটি ডিরেফারেন্স হয়। দ্বিতীয় লাইনে my_ptr, যেটি প্রথম লাইনে দ্বিতীয় এলিমেন্টে ইনক্রিমেন্ট হয়েছিল, প্রথমে ডিরেফারেন্স হয়, আর তারপর ডিরেফারেন্স করা মানটি ইনক্রিমেন্ট হয়। উপরের উদাহরণে এই লাইনটি যোগ করলে
// prints: my_ints[1] is 43. *++my_ptr is (some garbage value).
printf("my_ints[1] is %u. *++my_ptr is %u.\n", my_ints[1], *++my_ptr);
ওই লাইনে আমরা পয়েন্টারটিকে অ্যারের বাইরে পর্যন্ত ইনক্রিমেন্ট করে ফেলেছি। আমরা অ্যারে ছাপিয়ে গেছি এবং মেমোরিতে পরের চার বাইটের দিকে নির্দেশ করছি, যাকে পয়েন্টার একটি int হিসেবে ব্যাখ্যা করে, কিন্তু বাস্তবে ওই বাইটগুলো অন্য কোনো ভ্যারিয়েবল বা ভ্যারিয়েবলগুলোর হতে পারে। ওই চার বাইট একটি double-এর অংশ হতে পারে, আবার চারটি char-ও হতে পারে। সমস্যা হলো, আমাদের পয়েন্টার দিয়ে ওগুলোর দিকে তাকানোই উচিত নয়।
আমরা এটাও ব্যবহার করতে পারতাম
// prints: my_ints[2] is (some garbage value).
printf("my_ints[2] is %u", my_ints[2]);
যেহেতু অ্যারে ইনডেক্সিং একটি পয়েন্টার দিয়েই ইমপ্লিমেন্ট করা হয়, তাই অ্যারে ছাপিয়ে যাওয়ার একই বিপদ এখানেও আছে।
আর যেহেতু একটি স্ট্রিং আসলে char-এর একটি অ্যারে, তাই স্ট্রিংয়ের সাথেও পয়েন্টার ব্যবহার করা যায়। স্ট্রিং প্রায়ই পয়েন্টার সিনট্যাক্স দিয়ে ডিক্লেয়ার করা হয়, এভাবে
char *my_string = "This is my string";
// instead of
// char my_string[] = "This is my string";
তবে একটি প্রশ্ন। আমরা তারা চিহ্নটি আইডেন্টিফায়ারের পাশে রাখি কেন? টাইপের পাশেই বা রাখি না কেন, যেহেতু এটি ওই টাইপেরই পয়েন্টার? কারণ এটি
int* my_ptr1, my_ptr2_that_is_really_an_int;
একটি int-এর দুটি পয়েন্টার ডিক্লেয়ার করে না, বরং একটি int-এর একটি পয়েন্টার আর একটি int ডিক্লেয়ার করে। একটি int-এর দুটি পয়েন্টার ডিক্লেয়ার করতে হলে এভাবে লিখতে হবে
int *my_ptr1, *my_ptr2;
তাই অস্পষ্টতা কমাতে * আইডেন্টিফায়ারের কাছে রাখাই ভালো। কিংবা প্রতি লাইনে কেবল একটি আইডেন্টিফায়ার ডিক্লেয়ার করুন। আর *-এর প্রতি যদি এখনো আপনার তৃষ্ণা না মিটে থাকে, স্বাগতম...
স্ট্রিংয়ের অ্যারে এভাবে উপস্থাপন করতে প্রায়ই দেখা যায়
#include <stdio.h>
void print_strings(char **passed_in_strings) {
printf("%s\n", *passed_in_strings);
printf("%s\n", *(passed_in_strings + 1));
}
int main() {
char *my_strings[] = {"One string", "Two string"};
print_strings(my_strings);
return 0;
}
বোঝা নাও যেতে পারে, কিন্তু স্ট্রিংয়ের অ্যারেটি আসলে একাধিক char অ্যারে নিয়ে গঠিত একটি অ্যারের পয়েন্টার হিসেবে ডিক্লেয়ার ও ইনিশিয়ালাইজ করা হয়েছে। যখন char অ্যারে দিয়ে গঠিত অ্যারেটি print_strings()-এ পাস করা হয়, তখন সিগনেচার এটিকে char-এর প্রতি পয়েন্টার বা একাধিক পয়েন্টারের প্রতি পয়েন্টার হিসেবে বর্ণনা করে।
তাহলে আমরা কীভাবে জানব যে char ** আসলে একাধিক char অ্যারে-এর প্রতি পয়েন্টার নিয়ে গঠিত একটি অ্যারের পয়েন্টার, আর একটি মাত্র char-এর পয়েন্টারের প্রতি পয়েন্টার নয়?
জানি না।
এমন আরও অনেক কিছু আছে যা পয়েন্টার ব্যবহারকে গোলমেলে করে তুলতে পারে। নিচের কোডটি হয়তো কম্পাইল হবে
#include <stdio.h>
void print_strings(char **passed_in_strings) {
// prints: A (followed by gibberish)
printf("%s\n", *passed_in_strings);
// prints gibberish
printf("%s\n", *(passed_in_strings + 1));
}
int main() {
char my_char = 'A';
char *my_ptr = &my_char;
char **my_ptr_to_ptr = &my_ptr;
print_strings(my_ptr_to_ptr);
return 0;
}
কিন্তু print_strings()-কে যা বোঝানো হয়েছিল সেভাবে ব্যবহার করা হচ্ছে না। আলাদা সিগনেচার ব্যবহার করলেও লাভ নেই। সিগনেচার এভাবে বদলালেও
void print_strings(char *passed_in_strings[]) {
// code snipped
}
তবুও কম্পাইল হতে পারে আর তবুও এলোমেলো আউটপুট প্রিন্ট করতে পারে। কিন্তু একটি মাত্র char-এর পয়েন্টারের প্রতি পয়েন্টার পাস করে আসলে কোনো লাভ নেই। সম্ভবত আপনি যখন char ** দেখবেন, এটি একাধিক char অ্যারে-এর প্রতি পয়েন্টার নিয়ে গঠিত একটি অ্যারের পয়েন্টার বোঝায়।
আমরা একটি ফাংশনে পয়েন্টার পাস করতে দেখেছি। কিন্তু কেন ফাংশনে পয়েন্টার পাস করবেন?
C-তে আর্গুমেন্ট ফাংশনে পাস করা হয় ভ্যালু দিয়ে। এর মানে আর্গুমেন্ট থেকে মানটি কপি করা হয়, আর সেই কপিটি ফাংশনের ভেতরেই সীমাবদ্ধ থাকে। ফাংশন শেষ হলে কপি করা মানটিও শেষ হয়ে যায়।
ফাংশনের ভেতরে মান বদলানোর চেষ্টা করলে কী হয়, দেখুন
#include <stdio.h>
void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
//prints: In 'swap', a is 4 and b is 3.
printf("In 'swap', a is %d and b is %d.\n", a, b);
}
int main() {
int a = 3, b = 4;
swap(a, b);
// prints: After 'swap', a is 3 and b is 4
printf("After 'swap', a is %d and b is %d.\n", a, b);
return 0;
}
এর কারণ, ভ্যারিয়েবলগুলোর নাম এক হলেও swap()-এর a আর b আর main()-এর a আর b এক নয়। এগুলো a আর b-এর মানের কপি। আমরা কি swap-কে বদলে যা চাই সেভাবে কাজ করাতে পারি?
হ্যাঁ, পারি! আমরা a আর b-এর অ্যাড্রেস পয়েন্টার হিসেবে পাস করতে পারি। কিন্তু ফাংশন তো পয়েন্টারগুলো কপি করে, তাহলে তাতে লাভ কী? লাভ আছে, কারণ কপি করা অ্যাড্রেসও একই অ্যাড্রেস থাকে। অ্যাড্রেসটি ডিরেফারেন্স করে আমরা main()-এ যে মূল মানটি ডিফাইন করা হয়েছিল সেটিতে পৌঁছাতে পারি।
#include <stdio.h>
// pass in pointers to a and b
void swap(int *a, int *b) {
// set temp to value at a's dereferenced address
int temp = *a;
// set a's value to value at b's dereferenced address
*a = *b;
// set b's value to temp
*b = temp;
// prints: In 'swap', a is 4 and b is 3.
printf("In 'swap', a is %d and b is %d.\n", *a, *b);
}
int main() {
int a = 3, b = 4;
swap(&a, &b);
// prints: After 'swap', a is 4 and b is 3.
printf("After 'swap', a is %d and b is %d.\n", a, b);
return 0;
}
এখন swap-এ অনেক * হয়ে গেল, কিন্তু এখন এটি ঠিকঠাক কাজ করে।
ইনডাইরেকশন অপারেটরটিকে পড়ার একটি উপায় হলো "যার মান"। তাহলে *a মানে হলো a-তে থাকা মান। স্ট্রিংয়ের ক্ষেত্রে এটি কতটা ভালো কাজ করে?
আমরা জানি একটি অ্যারে ভ্যারিয়েবলের মান হলো সেই অ্যাড্রেস, যেখান থেকে অ্যারেটি শুরু হয়। আর আমরা জানি একটি স্ট্রিংকে আমরা হয় char অ্যারে char my_string[] হিসেবে, নয়তো char-এর পয়েন্টার char * হিসেবে ডিফাইন করতে পারি। এদের মধ্যে পার্থক্য কী?
এই কোডে আমরা describe_me()-এ একটি char * আর্গুমেন্ট পাস করি। adjective আর্গুমেন্টটি একটি char অ্যারে হিসেবে ডিফাইন করা, আর ফাংশনটি এটিকে char-এর পয়েন্টার হিসেবে গ্রহণ করে।
#include <stdio.h>
#include <string.h>
// takes pointer to char
void describe_me(char *adjective){
strcpy(adjective, "evil");
// prints: I am evil.
printf("I am %s.\n", adjective);
}
int main() {
// defined as a char array
char adjective[] = "good";
describe_me(adjective);
// prints: I am really evil.
printf("I am really %s.\n", adjective);
return 0;
}
আমরা adjective-এ একটি স্থায়ী পরিবর্তন আনতে পারছি, যা প্রমাণ করে যে ডিফাইন করা char অ্যারেটিকে char-এর পয়েন্টার হিসেবে গ্রহণ করা যায়।
এখন চলুন adjective-কে সরাসরি char-এর পয়েন্টার হিসেবে ডিফাইন করার চেষ্টা করি।
#include <stdio.h>
#include <string.h>
void describe_me(char *adjective){
// segmentation fault on this line when program is executed
strcpy(adjective, "evil");
printf("I am %s.\n", adjective);
}
int main() {
char *adjective = "good";
describe_me(adjective);
printf("I am really %s.\n", adjective);
return 0;
}
মনে আছে, পয়েন্টার কত শক্তিশালী, আবার কতটা সম্ভাব্য বিপজ্জনক? কিন্তু এটি কাজ করে না কেন? অ্যারে তো একটি পয়েন্টার, আর পয়েন্টার তো char অ্যারের সাথে কাজ করছিলই! কারণ হলো, আগে adjective ডিফাইন করা হয়েছিল একটি char অ্যারে হিসেবে, যেটি main()-এর অধীন একটি ভ্যারিয়েবল। এখন adjective ডিফাইন করা হয়েছে একটি স্ট্রিং লিটারাল হিসেবে, অর্থাৎ charsগুলো লেখা হয় রিড-অনলি স্ট্যাটিক মেমোরিতে, যেটি গোটা প্রোগ্রামের। দুটোই পয়েন্টার, কিন্তু তারা ভিন্ন জিনিসের দিকে নির্দেশ করে। একটি লিটারাল স্ট্রিং মান বদলানোর চেষ্টা করা মানে 5-এর লিটারাল মান বদলানোর চেষ্টা করা। এটা অনুমোদিত নয়। তাহলে আসলে সমস্যাটা এটি পয়েন্টার হওয়া নয়। সমস্যা হলো এটি কীসের দিকে নির্দেশ করছে, সেটা।