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

সি (C) পয়েন্টার (C Pointers)

একটি অ্যাড্রেস (address) লেখা ছোট্ট স্টিকি নোট (sticky note) — এটি মূলত আপনাকে বলে দেয় যে কোনো জিনিস ঠিক কোথায় (WHERE) আছে, জিনিসটি আসলে কী (WHAT) তা নয়

স্টিকি নোট অ্যানালজি বা স্টিকি নোটের উদাহরণ (The Sticky Note Analogy)

ধরুন, কোনো একটি ওয়ারহাউস (warehouse) বা গুদামের B7 (বি৭) নম্বর শেলফে বা তাকে আপনার একটি বাক্স (box) রাখা আছে। এখানকার একটি পয়েন্টার (pointer) হলো মূলত এমন একটি স্টিকি নোট (sticky note) যাতে শুধু "B7" লেখা আছে — এর ভেতরে কিন্তু আপনার ওই আসল বাক্সটি বা জিনিসটি নেই, এটি শুধু আপনাকে বলে দেয় যে জিনিসটিকে ঠিক কোথায় পাওয়া যাবে (where to find it)

সি (C)-তে, প্রতিটি ভ্যারিয়েবলই (variable) মূলত এর মেমোরির একটি নির্দিষ্ট অ্যাড্রেস (memory address) বা ঠিকানায় অবস্থান করে। আর পয়েন্টার (pointer) হলো এমন এক ধরনের ভ্যারিয়েবল (variable) যা মূলত ওই অ্যাড্রেসটিকে (address) সেভ বা ধারণ (stores) করে রাখে। এর ভেতরে কোনো নাম্বার (number) বা ক্যারেক্টার (character) সেভ করে রাখার বদলে, এটি মূলত অন্য আরেকটি ভ্যারিয়েবলের (variable) লোকেশনটিকে (location) সেভ করে রাখে।

দুটি প্রধান বা কি অপারেটর (Two Key Operators)

  • & (অ্যাড্রেস-অফ বা address-of) — "এই বাক্সের অ্যাড্রেসটি বা ঠিকানাটি কী (What's the address of this box)?" আমাকে ওই স্টিকি নোটটি (sticky note) দাও।
  • * (ডিরেফারেন্স বা dereference) — "এই ঠিকানায় বা অ্যাড্রেসে (address) যাও এবং এর ভেতরে কী আছে তা নিয়ে আসো (get what's inside)।" বাক্সটি খোঁজার জন্য স্টিকি নোটটিকে (sticky note) ফলো বা অনুসরণ (Follow) করো।

এই অপারেটরগুলো (operators) মূলত একে অপরের বিপরীত (inverses)। *(&x) মূলত সবার আগে x-এর অ্যাড্রেসটি (address) খুঁজে বের করে, এবং সাথে সাথেই এটিকে ডিরেফারেন্স বা অনুসরণ (follows it back) করে — যার ফলে আপনি আবার সেই x-টিকেই ফেরত পেয়ে যান।

সাধারণ বা বেসিক পয়েন্টার ব্যবহার (Basic Pointer Usage)

#include <stdio.h>
int main() {
int age = 25;
int *ptr = &age; // ptr মূলত age-এর অ্যাড্রেসটি বা ঠিকানাটি ধারণ করে
printf("Value of age: %d\n", age);
printf("Address of age: %p\n", (void*)&age);
printf("Value of ptr: %p\n", (void*)ptr);
printf("Dereferencing ptr: %d\n", *ptr);
// পয়েন্টারের (pointer) মাধ্যমে age-কে পরিবর্তন বা মডিফাই (Modify) করুন
*ptr = 30;
printf("\nAfter *ptr = 30:\n");
printf("age is now: %d\n", age);
return 0;
}
Output
Value of age:     25
Address of age:   0x7ffd5e8a3b4c
Value of ptr:     0x7ffd5e8a3b4c
Dereferencing ptr: 25

After *ptr = 30:
age is now: 30

পয়েন্টারের টাইপগুলোও অত্যন্ত গুরুত্বপূর্ণ (Pointer Types Matter)

যেকোনো পয়েন্টারেরই (pointer) নিজস্ব কিছু টাইপ (type) থাকে — যেমন int*, char*, float*। যেকোনো পয়েন্টারকে ডিরেফারেন্স (dereference) করার সময় এখানকার টাইপগুলোই মূলত কম্পাইলারকে (compiler) বলে দেয় যে তাকে ঠিক কতগুলো বাইটকে (bytes) পড়তে বা রিড (read) করতে হবে। যেমন একটি int* মূলত ৪ বাইট (4 bytes) পড়ে, একটি char* মূলত ১ বাইট (1 byte) পড়ে এবং একটি double* মূলত ৮ বাইট (8 bytes) পড়ে থাকে।

আপনি চাইলে ব্যাপারটিকে ঠিক এভাবেও চিন্তা করতে পারেন: অ্যাড্রেসগুলো (address) মূলত আপনাকে বলে দেয় যে ঠিক কোন লকারটিতে (which locker) যেতে হবে, এবং এখানকার টাইপটি (type) আপনাকে বলে দেয় যে সম্পূর্ণ ভ্যালুটি বা মানটি (full value) পাওয়ার জন্য আপনাকে ঠিক কতগুলো করে লকার (how many lockers) খুলতে (open) হবে।

NULL বা নাল — দ্য পয়েন্টার টু নোহোয়ার বা ঠিকানাবিহীন পয়েন্টার (NULL — The Pointer to Nowhere)

এই NULL হলো একটি বিশেষ মান (special value) যার মানে হলো "এই পয়েন্টারটি (pointer) মূলত কোনো জায়গাতেই বা কিছুকেই পয়েন্ট করে নেই (doesn't point to anything)।" যেকোনো পয়েন্টারকে ইচ্ছাকৃতভাবে (intentionally) খালি বা এম্পটি (empty) দেখানোর জন্যই এটি মূলত ব্যবহার করা হয়। আপনার কাছে যদি আগে থেকে কোনো পয়েন্টারের অ্যাড্রেস বা ঠিকানা (address) জানা না থাকে, তবে সবসময় সেগুলোকে এই NULL-এর মাধ্যমে ইনিশিয়ালাইজ (initialize) করা নেওয়া উচিত।

বিভিন্ন টাইপের (Different Types) অ্যাড্রেসগুলোকে প্রিন্ট করা (Printing Addresses of Different Types)

#include <stdio.h>
int main() {
int i = 42;
char c = 'A';
double d = 3.14;
int *ip = &i;
char *cp = &c;
double *dp = &d;
printf("int: value=%d, size=%lu bytes\n", *ip, sizeof(int));
printf("char: value=%c, size=%lu byte\n", *cp, sizeof(char));
printf("double: value=%.2f, size=%lu bytes\n", *dp, sizeof(double));
// নাল পয়েন্টার (NULL pointer)
int *nothing = NULL;
if (nothing == NULL) {
printf("\n'nothing' points to NULL — safe to check!\n");
}
return 0;
}
Output
int:    value=42,  size=4 bytes
char:   value=A,  size=1 byte
double: value=3.14, size=8 bytes

'nothing' points to NULL — safe to check!

পাস-বাই-রেফারেন্সকে (Pass-by-Reference) আনলক করে পয়েন্টারগুলো (Pointers Unlock Pass-by-Reference)

আপনার কি মনে আছে যে সি (C) প্রোগ্রামিং মূলত একটি পাস-বাই-ভ্যালু (pass-by-value) ভাষা? তাই এখানকার ফাংশনগুলো (Functions) মূলত শুধু আর্গুমেন্টের (arguments) একটি কপি (copy) পায়, যার ফলে তারা আর এখানকার ওই আসল বা আসল আর্গুমেন্টটিতে (originals) আর কোনো পরিবর্তন (change) আনতে পারে না। কিন্তু আপনি যদি ওই ভ্যারিয়েবলটির (variable) পয়েন্টারটিকে (pointer) এখানে পাস করেন, তবে ওই ফাংশনটি (function) মূলত এর অ্যাড্রেসটির (address) একটি কপি পেয়ে যায় — এবং এটি তখন খুব সহজেই ওই অ্যাড্রেসটিকে (address) ব্যবহার করে তার কলারের মেমোরিতে (caller's memory) পৌঁছে যেতে পারে এবং সেখানকার আসল ভ্যারিয়েবলটিকে (original) পরিবর্তন বা মডিফাই (modify) করতে পারে।

এর একটি ক্লাসিক বা সাধারণ উদাহরণ হলো সোয়াপ ফাংশন (swap function)। পয়েন্টার (pointers) ছাড়া, এটিকে সি (C)-তে বাস্তবায়ন করা একেবারেই অসম্ভব (impossible)। তবে পয়েন্টারগুলোর সাহায্যে, এটি করা অত্যন্ত সহজ এবং সুন্দর (elegant)।

সোয়াপ বা বদল করা (Swap) — পয়েন্টার ছাড়া এবং পয়েন্টারসহ (Without and With Pointers)

#include <stdio.h>
// ব্রোকেন বা নষ্ট (BROKEN): এটি মূলত কপিগুলোকে (copies) সোয়াপ বা অদলবদল (swaps) করে, এর আসল ভ্যারিয়েবলগুলোকে (originals) নয়
void brokenSwap(int a, int b) {
int temp = a;
a = b;
b = temp;
// এখানকার a এবং b হলো শুধুমাত্র লোকাল কপি (local copies) — এগুলোকে পরিবর্তন (changes) করলে তা মূলত এখানেই শেষ (die) হয়ে যায়
}
// কাজ করে (WORKS): এর আসল ভ্যারিয়েবলগুলোতে (originals) পৌঁছাতে পয়েন্টার (pointers) ব্যবহার করে
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int x = 10, y = 20;
printf("Before brokenSwap: x=%d, y=%d\n", x, y);
brokenSwap(x, y);
printf("After brokenSwap: x=%d, y=%d\n\n", x, y);
printf("Before swap: x=%d, y=%d\n", x, y);
swap(&x, &y); // অ্যাড্রেসগুলোকে বা address-গুলোকে পাস (pass) করে দিন!
printf("After swap: x=%d, y=%d\n", x, y);
return 0;
}
Output
Before brokenSwap: x=10, y=20
After brokenSwap:  x=10, y=20

Before swap: x=10, y=20
After swap:  x=20, y=10
Note: NULL অথবা আনইনিশিয়ালাইজড (uninitialized) পয়েন্টারগুলোকে ডিরেফারেন্স (Dereferencing) করার ফলে এটি মূলত সেগফল্ট (segfault বা segmentation fault) নামক একটি এররের তৈরি করে। এর ফলে সাথে সাথেই প্রোগ্রামটি ক্র্যাশ (crashes) হয়ে যেতে পারে। সবসময় পয়েন্টারগুলোকে (pointers) যেকোনো একটি লিগ্যাল বা ভ্যালিড অ্যাড্রেস (valid address) দিয়ে অথবা এই NULL-এর সাহায্যে ইনিশিয়ালাইজ (initialize) করে নেবেন — এবং ডিরেফারেন্স (dereferencing) করার আগে সবসময় চেক (check) করে দেখবেন যে তার বর্তমান অবস্থাটি NULL কি না। আনইনিশিয়ালাইজড পয়েন্টারগুলো (uninitialized pointer) মূলত মেমোরির যেকোনো অচেনা বা র‍্যান্ডম গারবেজ অ্যাড্রেসকে (random garbage address) ধরে রাখে, যা NULL-এর চেয়েও আরও অনেক বেশি খারাপ বা বিপজ্জনক হতে পারে (even worse) কারণ এটি হয়তো ক্র্যাশের (crashing) বদলে গোপনে আপনার সমস্ত ডেটাকে (data) করাপ্ট বা নষ্ট (silently corrupt) করে দিতে পারে।

পয়েন্টারের সাধারণ ভুলগুলো (Common Pointer Mistakes)

পয়েন্টারগুলো (Pointers) একই সাথে যেমন অনেক বেশি ক্ষমতাশালী (powerful) তেমনি সেগুলো কোনো ধরনের ভুলের জন্য কাউকে ক্ষমাও করে না (unforgiving)। নিচে এরকমই কিছু সাধারণ ভুল বা ফাঁদের (traps) কথা তুলে ধরা হলো:

  • & ব্যবহার করতে ভুলে যাওয়া (Forgetting &) — যখন কোনো ফাংশন একটি পয়েন্টার (pointer) আশা করছে, তখন সেখানে অ্যাড্রেসের (address) বদলে শুধু ভ্যালু বা মানটিকে (value) পাস (passing) করে দেওয়া
  • ড্যাংলিং পয়েন্টার (Dangling pointers) — এমন কোনো মেমোরিকে পয়েন্ট (pointing) করা যা হয়তো এর আগেই ফ্রি (freed) বা ছেড়ে দেওয়া হয়েছে বা ওই নির্দিষ্ট লোকাল ভ্যারিয়েবলটি (local variable) এখন আর স্কোপের (scope) ভেতরে এক্সিস্ট (exist) করে না
  • আনইনিশিয়ালাইজড পয়েন্টার (Uninitialized pointers) — পয়েন্টারগুলোতে কোনো অ্যাড্রেস অ্যাসাইন (assigning) করার আগেই সেগুলোকে ব্যবহার (using) করতে শুরু করে দেওয়া
  • টাইপের অমিল বা মিসম্যাচ (Type mismatch) — কোনো কিছু না বুঝেই একটি int*-কে ভুল করে একটি char*-এ অ্যাসাইন (assigning) করে দেওয়া

তবে সুসংবাদটি হলো: বারবার চর্চা বা প্র্যাকটিসের (practice) ফলে এই ধরনের ভুলগুলোকে (mistakes) খুব সহজেই ধরে ফেলা বা স্পট (spot) করা যায়। এখানকার এই কম্পাইলার ওয়ার্নিংগুলোও (compiler warnings) মূলত আপনার বন্ধুর মতোই কাজ করে — তাই এগুলোকে কখনোই ইগনোর বা এড়িয়ে (ignore) যাবেন না!

একটি বাস্তব উদাহরণ (Practical Example): একাধিক মান বা ভ্যালু রিটার্ন করা ফাংশন (Function Returning Multiple Values)

#include <stdio.h>
// সি (C) ফাংশনগুলো মূলত শুধুমাত্র একটিমাত্র (ONE) ভ্যালুকেই রিটার্ন (return) করতে পারে।
// তাই একসাথে অনেকগুলো মান বা multiple values-কে "রিটার্ন বা return" করানোর জন্য মূলত পয়েন্টারের (pointers) ব্যবহার করা হয়।
void minMax(int arr[], int size, int *min, int *max) {
*min = arr[0];
*max = arr[0];
for (int i = 1; i < size; i++) {
if (arr[i] < *min) *min = arr[i];
if (arr[i] > *max) *max = arr[i];
}
}
int main() {
int data[] = {34, 12, 78, 5, 91, 23};
int lo, hi;
minMax(data, 6, &lo, &hi);
printf("Min: %d\n", lo);
printf("Max: %d\n", hi);
return 0;
}
Output
Min: 5
Max: 91
চ্যালেঞ্জ

ছোট কুইজ

এই & অপারেটরটি (operator) মূলত কী কাজ করে?
FunctionsPointer Arithmetic & Arrays