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

সি (C) পয়েন্টার অ্যারিথমেটিক এবং অ্যারে (C Pointer Arithmetic & Arrays)

সি (C) প্রোগ্রামিংয়ে অ্যারে (Arrays) এবং পয়েন্টারগুলো (pointers) মূলত একে অপরের সবথেকে ভালো বন্ধু (best friends) — অর্থাৎ তারা প্রায় একই কাজ করে, তবে শুধু দেখতে একটু আলাদা

দুই বন্ধু, এক ঠিকানা (Two Friends, One Address)

সি (C)-এর সবচেয়ে বেশি মাথা ঘুরিয়ে দেওয়া বা মাইন্ড-বেন্ডিং (mind-bending) ঘটনাগুলোর মধ্যে একটি হলো: যেকোনো অ্যারের নাম (array name) মূলত গোপনে তার নিজের প্রথম উপাদানটির একটি পয়েন্টার (pointer) হিসেবে কাজ করে। যখন আপনি int arr[5]; লেখেন, তখন এই arr নামটির মানে মূলত এই &arr[0]-ই বোঝায় — অর্থাৎ এটি এর 0 নম্বর লকারের (locker 0) ঠিকানাটিকে বা অ্যাড্রেসটিকে (address) নির্দেশ করে。

এর মানে হলো আপনি চাইলে অ্যারেগুলোতে পয়েন্টারের সিনট্যাক্স (pointer syntax) এবং পয়েন্টারগুলোতে অ্যারের সিনট্যাক্সটি (array syntax) ব্যবহার করতে পারেন। এগুলো মূলত বেশিরভাগ ক্ষেত্রেই একে অপরের ইন্টারচেঞ্জেবল বা পরিবর্তনযোগ্য (interchangeable)। এই arr[i] এক্সপ্রেশনটিকে (expression) কম্পাইলার বা compiler মূলত *(arr + i) হিসেবেই পুনরায় লিখে (rewritten) নেয়。

তবে এগুলো কিন্তু একে অপরের হুবহু এক বা আইডেন্টিকাল (identical) নয়। কারণ যেকোনো অ্যারের নাম (array name) হলো মূলত একটি কনস্ট্যান্ট বা ধ্রুবক (constant) — তাই আপনি চাইলে এটিকে কখনো অন্য কোথাও পয়েন্ট (point) করাতে পারবেন না। কিন্তু যেকোনো পয়েন্টার ভ্যারিয়েবলকে (pointer variable) চাইলে সহজেই রিঅ্যাসাইন বা পুনরায় সেট (reassigned) করে নেওয়া যায়। ব্যাপারটিকে ঠিক এরকমভাবে ভাবতে পারেন যে, একটি অ্যারের নাম (array name) হলো এর 0 নম্বর লকারের (locker 0) গায়ে লাগানো একটি পার্মানেন্ট বা স্থায়ী সাইনবোর্ড (permanent sign), অন্যদিকে একটি পয়েন্টার (pointer) হলো একটি মুভেবল বা সরানো যায় এমন একটি স্টিকি নোট (sticky note)।

পয়েন্টার হিসেবে অ্যারের নাম (Array Name as Pointer)

#include <stdio.h>
int main() {
int arr[] = {10, 20, 30, 40, 50};
int *ptr = arr; // &arr[0]-এর মতোই (same as &arr[0])
printf("arr[0] = %d\n", arr[0]);
printf("*ptr = %d\n", *ptr);
printf("*(arr+2) = %d\n", *(arr + 2));
printf("ptr[2] = %d\n", ptr[2]);
// এরা মূলত একই জায়গাকে (same place) নির্দেশ বা পয়েন্ট (point) করে থাকে
printf("\narr = %p\n", (void*)arr);
printf("ptr = %p\n", (void*)ptr);
printf("&arr[0] = %p\n", (void*)&arr[0]);
return 0;
}
Output
arr[0] = 10
*ptr   = 10
*(arr+2) = 30
ptr[2]   = 30

arr  = 0x7ffd3a2b1040
ptr  = 0x7ffd3a2b1040
&arr[0] = 0x7ffd3a2b1040

পয়েন্টার অ্যারিথমেটিক (Pointer Arithmetic) কীভাবে কাজ করে

যখন আপনি কোনো একটি পয়েন্টারের (pointer) সাথে ১ (1) যোগ (add) করেন, তখন সেটি মূলত সামনের দিকে ১ বাইট (byte) এগিয়ে যায় না। বরং এটি মূলত এর পয়েন্ট করা টাইপটির সাইজ (size of the type) অনুযায়ী সামনের দিকে এগিয়ে যায়। যেমন ধরুন একটি int* মূলত ৪ বাইট (4 bytes) এগিয়ে যায়, একটি double* মূলত ৮ বাইট (8 bytes) এগিয়ে যায় এবং একটি char* মূলত ১ বাইট (1 byte) করে এগিয়ে যায়。

আর মূলত এই জিনিসটিই পয়েন্টার অ্যারিথমেটিককে (pointer arithmetic) এত সুন্দর (elegant) করে তোলে — এখানে আপনি সরাসরি এর উপাদান বা এলিমেন্টগুলোতে (elements) চিন্তা করতে বা ভাবতে পারেন, কোনো নির্দিষ্ট বাইটে (bytes) নয়। ptr + 3-এর মানে হলো "৩টি উপাদান (3 elements) এগিয়ে যাব," আর এর আসল বাইটের সংখ্যা (bytes) যাই হোক না কেন, তাতে এর কিছু যায় বা আসে না。

আপনি চাইলে যেকোনো একই টাইপের (same type) দুটি পয়েন্টারকে বিয়োগও (subtract) করতে পারবেন। এর ফলাফলটি মূলত বাইটের (bytes) পরিবর্তে, ঠিক ওই দুটি পয়েন্টারের মাঝখানে থাকা উপাদানগুলোর (elements) মোট সংখ্যাটিকে বুঝিয়ে থাকে。

টাইপ সাইজ অনুযায়ী পয়েন্টার অ্যারিথমেটিকের ধাপগুলো (Pointer Arithmetic Steps by Type Size)

#include <stdio.h>
int main() {
int iArr[] = {10, 20, 30};
char cArr[] = {'A', 'B', 'C'};
double dArr[] = {1.1, 2.2, 3.3};
int *ip = iArr;
char *cp = cArr;
double *dp = dArr;
printf("=== int* (প্রতি ধাপে বা step-এ ৪ বা 4 বাইট করে) ===\n");
printf("ip = %p -> %d\n", (void*)ip, *ip);
printf("ip + 1 = %p -> %d\n", (void*)(ip+1), *(ip+1));
printf("ip + 2 = %p -> %d\n", (void*)(ip+2), *(ip+2));
printf("\n=== char* (প্রতি ধাপে বা step-এ ১ বা 1 বাইট করে) ===\n");
printf("cp = %p -> %c\n", (void*)cp, *cp);
printf("cp + 1 = %p -> %c\n", (void*)(cp+1), *(cp+1));
printf("\n=== double* (প্রতি ধাপে বা step-এ ৮ বা 8 বাইট করে) ===\n");
printf("dp = %p -> %.1f\n", (void*)dp, *dp);
printf("dp + 1 = %p -> %.1f\n", (void*)(dp+1), *(dp+1));
// পয়েন্টার সাবট্রাকশন বা পয়েন্টার বিয়োগ (Pointer subtraction)
printf("\nElements between ip+2 and ip: %ld\n",
(ip + 2) - ip);
return 0;
}
Output
=== int* (4 bytes per step) ===
ip     = 0x7ffd1a2b0040  -> 10
ip + 1 = 0x7ffd1a2b0044  -> 20
ip + 2 = 0x7ffd1a2b0048  -> 30

=== char* (1 byte per step) ===
cp     = 0x7ffd1a2b0030  -> A
cp + 1 = 0x7ffd1a2b0031  -> B

=== double* (8 bytes per step) ===
dp     = 0x7ffd1a2b0050  -> 1.1
dp + 1 = 0x7ffd1a2b0058  -> 2.2

Elements between ip+2 and ip: 2
Note: পয়েন্টার অ্যারিথমেটিকের (Pointer arithmetic) প্রতিটি ধাপ বা স্টেপগুলো (steps) মূলত এর sizeof(type) অনুযায়ী হয়ে থাকে, কখনোই শুধু ১ (1) বাইট (byte) করে নয়। আপনি যখন int *p; p + 1; করেন, তখন এর অ্যাড্রেসটি বা ঠিকানাটি মূলত ১-এর বদলে ঠিক ৪ (একটি int-এর সাইজ) করে বেড়ে (increases) যায়। আর এটিই এখানকার সবথেকে বড় এবং সাধারণ একটি ভুল ধারণা (misconception)। যদি আপনি এটিকে char*-এ কাস্ট বা cast করে নেন, এবং তার সাথে ১ (1) যোগ (add) করেন, তাহলে (then) এটি মূলত ঠিক ১ বাইট (1 byte) করে এগিয়ে যাবে — কারণ এই sizeof(char)-টি হলো 1।

পয়েন্টার (Pointer) দিয়ে একটি অ্যারেতে (Array) ট্রাভার্স (Traversing) করা

কোনো ইনডেক্স ভ্যারিয়েবল (index variable) ব্যবহার করার বদলে, আপনি চাইলে সরাসরি একটি পয়েন্টার (pointer) ব্যবহার করে যেকোনো অ্যারের (array) ভেতর দিয়ে খুব সহজেই হেঁটে (walk) ট্রাভার্স (Traversing) করে যেতে পারেন। একেবারে শুরু (beginning) থেকে কাজ শুরু করুন, এই ptr++-এর মাধ্যমে সেটিকে ইনক্রিমেন্ট (increment) বা সামনের দিকে এগিয়ে নিয়ে যান এবং একেবারে শেষে (end) পৌঁছে গেলে সেটিকে থামিয়ে দিন। সি-এর (C) বেশিরভাগ স্টান্ডার্ড লাইব্রেরি ফাংশনগুলো (standard library functions) মূলত ভেতরের দিকে (internally) ঠিক এভাবেই কাজ করে থাকে।

পয়েন্টার দিয়ে অ্যারেতে ট্রাভার্স করা (Traversing an Array with a Pointer)

#include <stdio.h>
void printArray(int *arr, int size) {
int *end = arr + size; // একদম শেষ উপাদানের (last element) ঠিক পরেরটি
printf("[ ");
for (int *p = arr; p < end; p++) {
printf("%d ", *p);
}
printf("]\n");
}
int sumArray(int *arr, int size) {
int total = 0;
for (int *p = arr; p < arr + size; p++) {
total += *p;
}
return total;
}
int main() {
int data[] = {4, 8, 15, 16, 23, 42};
int len = sizeof(data) / sizeof(data[0]);
printArray(data, len);
printf("Sum = %d\n", sumArray(data, len));
// আপনি চাইলে এর একটি স্লাইসও (slice) পাস (pass) করে দেখতে পারেন!
printf("Middle 3: ");
printArray(data + 1, 3);
return 0;
}
Output
[ 4 8 15 16 23 42 ]
Sum = 108
Middle 3: [ 8 15 16 ]

সমতা বা ইক্যুইভ্যালেন্স (The Equivalence): arr[i] == *(arr + i)

এটিই হলো সি (C) প্রোগ্রামিংয়ের অ্যারে (arrays) এবং পয়েন্টারগুলোর (pointers) অন্যতম ফান্ডামেন্টাল আইডেন্টিটি বা মৌলিক পরিচয় (fundamental identity)। এখানকার কম্পাইলারটি (compiler) মূলত প্রতিটি arr[i]-কে *(arr + i) হিসেবে অনুবাদ (translates) করে থাকে। আর যেহেতু যেকোনো যোগফলই (addition) মূলত কমিউটেটিভ বা পরিবর্তনযোগ্য (commutative) হয়ে থাকে, তাই এর মানে হলো আপনি এই arr[i]-কে *(i + arr) হিসেবেও লিখতে পারেন, যার অর্থ হলো — আপনি বিশ্বাস করুন আর নাই করুন — সি (C)-তে এখানকার এই i[arr]-ও একটি সম্পূর্ণ ভ্যালিড বা বৈধ (valid) কোড। (তবে দয়া করে কখনোই এর আসল বা রিয়েল (real) কোডের মধ্যে এমন কিছু লিখবেন না।)

স্ট্রিংয়ের অ্যারে (Array of Strings)

সি-তে (C) অন্যতম একটি কমন প্যাটার্ন (common pattern) হলো ক্যারেক্টার পয়েন্টারের একটি অ্যারে (array of pointers to characters) — অর্থাৎ যা মূলত এক কথায় স্ট্রিংয়ের একটি অ্যারে (array of strings)। এখানকার প্রতিটি এলিমেন্ট বা উপাদানই (element) হলো মূলত একটি char* যেটি যেকোনো স্ট্রিং লিটারেলকে (string literal) পয়েন্ট (pointing) করে থাকে। int main(int argc, char *argv[])-এর ভেতরের এই argv-টিও মূলত ঠিক এই একই জিনিসে তৈরি।

স্ট্রিংয়ের অ্যারে বা Array of Strings (char*[])

#include <stdio.h>
int main() {
// স্ট্রিং লিটারেলগুলোর (string literals) জন্য পয়েন্টারগুলোর অ্যারে (Array of pointers)
const char *colors[] = {
"red",
"green",
"blue",
"yellow"
};
int count = sizeof(colors) / sizeof(colors[0]);
for (int i = 0; i < count; i++) {
printf("Color %d: %s\n", i, colors[i]);
}
// এর প্রতিটি উপাদান বা element-ই হলো একেকটি পয়েন্টার (pointer)
printf("\nFirst char of colors[1]: %c\n",
colors[1][0]); // 'g'
printf("Same thing with pointers: %c\n",
*(colors[1])); // একই জিনিস পয়েন্টার দিয়েও (Same thing with pointers): g
// ফান ফ্যাক্ট বা মজার তথ্য (Fun fact): arr[i] == *(arr+i) == *(i+arr) == i[arr]
printf("\n2[colors] = %s (please don't do this)\n",
2[colors]);
return 0;
}
Output
Color 0: red
Color 1: green
Color 2: blue
Color 3: yellow

First char of colors[1]: g
Same thing with pointers: g

2[colors] = blue (please don't do this)

পয়েন্টার টু অ্যারে (Pointer to Array) বনাম অ্যারে অফ পয়েন্টারস (Array of Pointers)

এখানকার সিনট্যাক্সটি (syntax) হয়তো আপনাকে একটু কনফিউজ (confusing) করে দিতে পারে:

  • int *arr[5] — int-এর জন্য ৫টি (5) পয়েন্টারের একটি অ্যারে বা array of 5 pointers (৫টি বা ৫ স্টিকি নোটস বা sticky notes)
  • int (*arr)[5] — ৫টি int-এর একটি অ্যারেতে পয়েন্টার বা pointer to an array (১টি বা 1 স্টিকি নোট যা ৫টি লকারের বা 5 lockers সারির দিকে পয়েন্ট করে থাকে)

এক্ষেত্রে এখানকার এই প্যারেনথেসিস (parentheses) বা ব্র্যাকেটগুলোই মূলত এর সবথেকে বড় পার্থক্যগুলো (all the difference) তৈরি করে। তাই যেকোনো সন্দেহের (doubt) ক্ষেত্রে, সবসময় এর ভ্যারিয়েবলের নামটির (variable name) দিক থেকে বাইরের দিকে এর ডিক্লেয়ারেশনগুলো (declarations) পড়বেন: arr হলো এমন একটি *(পয়েন্টার বা pointer) যা মূলত [5](৫টির বা 5-এর একটি অ্যারে) int(পূর্ণসংখ্যা বা integers)-এর দিকে পয়েন্ট করে আছে।

চ্যালেঞ্জ

ছোট কুইজ

যদি int *p মূলত কোনো 0x1000 অ্যাড্রেসটিকে (address) পয়েন্ট (points) করে থাকে, তবে এর এই p + 3-এর অ্যাড্রেসটি (address) আসলে কত হবে?
PointersDynamic Memory