এই পর্বে যা শিখবেন
- Pointer-এ যোগ-বিয়োগ করলে ঠিকানা কত বাইট সরে, আর কেন সেটা টাইপের ওপর নির্ভর করে
arr[i],*(arr + i)আর array-র নাম “pointer হয়ে যাওয়া” — কোথায় হয়, কোথায় হয় না- ফাংশনে array পাঠালে আসলে কী যায়, আর
sizeof-এর বিখ্যাত ফাঁদ - Output parameter দিয়ে এক ফাংশন থেকে একাধিক ফল পাওয়া, আর
constদিয়ে “বদলাব না” প্রতিশ্রুতি - Dangling pointer চেনা — আর AI-এর লেখা pointer-কোড যাচাইয়ের চেকলিস্ট
১ · ঠিকানায় ১ যোগ করলে কী হয়?
পর্ব ১৭-এ আপনি দেখেছেন, একটি pointer হলো এমন ভ্যারিয়েবল, যার ভেতরে থাকে আরেকটি ভ্যারিয়েবলের memory address. আর পর্ব ১৬-তে দেখেছেন, একটি array-র ঘরগুলো মেমরিতে গায়ে গায়ে লাগানো থাকে — i নম্বর ঘরের ঠিকানা = শুরুর ঠিকানা + i × এক ঘরের মাপ। আজ এই দুটি ধারণা জোড়া লাগবে। জোড়া লাগার পর বুঝবেন, C-তে array আর pointer আসলে আলাদা দুটি জিনিস নয় — একই মুদ্রার দুই পিঠ।
প্রথমে একটি ধাঁধা। ধরুন int *p-এর ভেতরে আছে ঠিকানা 1000. তাহলে p + 1 কত? গণিতের হিসাবে উত্তর 1001. কিন্তু C বলবে 1004! কেন?
কারণ C ধরে নেয়, আপনি “পরের বাইট” চান না — চান “পরের int”. একটি int সাধারণত ৪ বাইট জায়গা নেয়। তাই পরের int-টি শুরু হয় ৪ বাইট পরে। এই নিয়মের নাম pointer arithmetic: p + k মানে ঠিকানা সরবে k × sizeof(*p) বাইট। এখানে *p মানে “p যে টাইপের দিকে ইঙ্গিত করে”, আর sizeof বলে সেই টাইপের মাপ।
মিরপুরের একটি রাস্তায় পাশাপাশি দোকান। প্রতিটি দোকান ৪ মিটার চওড়া। কেউ যদি বলে “এই দোকান থেকে পরের দোকানে যান”, আপনি ১ মিটার হাঁটবেন না — ৪ মিটার হাঁটবেন, কারণ “পরের দোকান” মানে পরের দরজা। পাশের গলিতে যদি ১ মিটার চওড়া পানের দোকানের সারি থাকে, সেখানে “পরের দোকান” মানে ১ মিটার। Pointer ঠিক এভাবে ভাবে — int-এর গলিতে প্রতিটি দোকান ৪ বাইট, char-এর গলিতে ১ বাইট।
নিচের ডায়াগ্রামে দুটি array পাশাপাশি রাখা হয়েছে — একটি int-এর, একটি char-এর। দুটির ওপর একই কাজ করা হচ্ছে: pointer-এ ১ যোগ, তারপর ২ যোগ। “পরে” চেপে দেখুন, দুটি ঠিকানা কত করে লাফায়। (ঠিকানাগুলো বোঝার সুবিধার জন্য বানানো, আর দশমিকে লেখা। আসল ঠিকানা অনেক বড় আর সাধারণত hexadecimal-এ দেখানো হয়।)
1int marks[4] = {72, 85, 64, 90};2char grade[4] = {'A', 'B', 'C', 'D'};34int *p = marks; /* p = &marks[0] */5char *q = grade; /* q = &grade[0] */6p = p + 1; q = q + 1;7p = p + 2; q = q + 2;8printf("%td %td\n", p - marks, q - grade);
| marks শুরু | |
| p (int *) | |
| *p | |
| grade শুরু | |
| q (char *) | |
| *q |
- দুটি array তৈরি হলো।
marks-এর ৪টিintবসেছে ঠিকানা1000থেকে1015পর্যন্ত — মোট ১৬ বাইট।grade-এর ৪টিcharবসেছে2000থেকে2003পর্যন্ত — মোট ৪ বাইট। দুটিতেই ঘর ৪টি, কিন্তু জায়গা ভিন্ন। p = marks—pএখনmarks-এর প্রথম ঘরের ঠিকানা (1000) ধরে আছে।*pদিয়ে সেই ঘরে উঁকি দিলে পাই 72.q = grade—qধরে আছে2000, আর*qহলো 'A'. এখনো দুটি pointer-ই নিজের array-র শুরুতে।- দুটিতেই +1. খেয়াল করুন!
pহলো1004— লাফ ৪ বাইট।qহলো2001— লাফ ১ বাইট। কিন্তু দুটিই ঠিক পরের ঘরে পৌঁছেছে:*p= 85,*q= 'B'. Compiler টাইপ দেখে নিজেই গুণ করে নিয়েছে। - এবার +2.
p:1004 + 2 × 4 = 1012, অর্থাৎ ঘর ৩ (মান 90).q:2001 + 2 × 1 = 2003, অর্থাৎ ঘর ৩ (মান 'D'). বাইটের হিসাবে একটি ১২ সরেছে, অন্যটি ৩ — কিন্তু ঘরের হিসাবে দুটিই ৩ ঘর। - বিয়োগ করলে ঘর গোনা হয়।
p - marksবাইটের পার্থক্য ১২-কে ৪ দিয়ে ভাগ করে দেয় 3;q - gradeদেয় ৩/১ = 3. Pointer বিয়োগের ফল সবসময় “মাঝে কয়টি ঘর” — বাইট নয়। এর টাইপptrdiff_t, আর ছাপাতে লাগে%td.
একই নিয়ম সব টাইপে খাটে। নিচের টেবিলে শুরুর ঠিকানা 1000 ধরে হিসাব করা হয়েছে (মাপগুলো আজকের সাধারণ 64-bit কম্পিউটারের; char সবসময় ১ বাইট, বাকিগুলো যন্ত্রভেদে ভিন্ন হতে পারে):
| Pointer-এর টাইপ | sizeof(*p) | p + 1 | p + 3 |
|---|---|---|---|
char * | ১ বাইট | 1001 | 1003 |
int * | ৪ বাইট | 1004 | 1012 |
double * | ৮ বাইট | 1008 | 1024 |
Pointer শুধু একটি ঠিকানা নয় — ঠিকানা আর টাইপ। টাইপই ঠিক করে দুটি কথা: *p লিখলে কত বাইট পড়া হবে, আর p + 1 লিখলে কত বাইট এগোনো হবে। এজন্যই int * আর char *-এর ভেতরে একই সংখ্যা থাকলেও তারা আলাদা আচরণ করে।
২ · arr[i] আসলে *(arr + i) — array-র নাম যখন ঠিকানা হয়ে যায়
এবার একটি চমক। আপনি এতদিন marks[2] লিখে array-র ঘর পড়েছেন। C-এর মান (standard) অনুযায়ী এই লেখাটির সংজ্ঞাই হলো *(marks + 2). অর্থাৎ বন্ধনী [ ] আসলে একটি সংক্ষিপ্ত লেখা: “শুরুর ঠিকানা থেকে ২ ঘর এগোও, তারপর সেখানে কী আছে দেখো।”
কিন্তু marks তো একটি array, pointer নয়। তাহলে marks + 2 কীভাবে হয়? উত্তর হলো array decay: প্রায় সব expression-এ array-র নাম নিজে থেকেই তার প্রথম ঘরের ঠিকানায় (&marks[0]) বদলে যায়। “Decay” শব্দের অর্থ ক্ষয় — array-র নাম তার “আমি ৪টি ঘরের একটি সারি” পরিচয় হারিয়ে শুধু “প্রথম ঘরের ঠিকানা” হয়ে যায়।
| এভাবে লিখলে | আসলে মানে | বাংলায় |
|---|---|---|
marks | &marks[0] | প্রথম ঘরের ঠিকানা |
marks + i | &marks[i] | i নম্বর ঘরের ঠিকানা |
marks[i] | *(marks + i) | i নম্বর ঘরের মান |
p[i] (p একটি pointer) | *(p + i) | pointer-এর ওপরও [ ] চলে |
শেষ লাইনটি খেয়াল করুন: [ ] pointer-এর ওপরও চলে। int *p = marks; লিখলে p[3] আর marks[3] একই ঘর। এজন্যই ফাংশনের ভেতরে pointer পেয়েও আমরা স্বচ্ছন্দে a[i] লিখতে পারি।
যেখানে decay হয় না — তিনটি ব্যতিক্রম
sizeof marks
এখানে array পুরো array-ই থাকে। ফল ২০ (৫টি int × ৪), pointer-এর মাপ ৮ নয়। এজন্যই sizeof marks / sizeof marks[0] দিয়ে ঘর গোনা যায় — তবে শুধু যেখানে array ঘোষণা করা হয়েছে, সেখানে।
&marks
পুরো array-র ঠিকানা। সংখ্যাটি &marks[0]-এর সমান, কিন্তু টাইপ ভিন্ন: int (*)[5]. তাই &marks + 1 এক ঘর নয়, পুরো array পার হয়ে ২০ বাইট লাফ দেয়।
char s[] = "hi";
Array-তে প্রথম মান বসানোর সময় string literal কপি হয়ে array-র ভেতরে যায়, ঠিকানায় বদলায় না। স্ট্রিং নিয়ে বিস্তারিত পরের পর্বে।
পর্ব ১৯-এ আরওArray আর pointer “একই রকম আচরণ করে”, কিন্তু এক জিনিস নয়। int marks[5]; মানে ২০ বাইটের আসল জায়গা। int *p; মানে শুধু একটি ঠিকানা রাখার ৮ বাইটের ঘর — তার নিজের কোনো ঘরের সারি নেই। আরও একটি পার্থক্য: p++ বা p = other চলে, কিন্তু marks++ বা marks = other লিখলে compiler error দেবে। Array-র নাম একটি নির্দিষ্ট জায়গার নাম; তাকে সরিয়ে অন্য কোথাও বসানো যায় না।
এই অদ্ভুত নকশা এসেছে C-এর পূর্বপুরুষ B আর BCPL ভাষা থেকে। Dennis Ritchie তাঁর ১৯৯৩ সালের লেখা “The Development of the C Language”-এ ব্যাখ্যা করেছেন: B-তে array-র নাম আসলেই একটি আলাদা pointer ভ্যারিয়েবল ছিল, যা মেমরিতে জায়গা নিত। C-তে struct-এর ভেতরে array রাখার সুবিধার জন্য Ritchie সেই আলাদা pointer বাদ দেন, আর নিয়ম করেন — expression-এ array-র নাম নিজে থেকেই pointer-এ বদলে যাবে। এভাবে পুরোনো B-কোডের a[i] লেখার ধরনও টিকে গেল।
৩ · Pointer দিয়ে array-র ওপর হাঁটা — বিয়োগ, তুলনা আর “শেষের পরের ঘর”
এতদিন array পড়তে আপনি index ব্যবহার করেছেন: for (i = 0; i < n; i++). Pointer দিয়ে একই কাজ আরেকভাবে করা যায় — index না গুনে pointer-টিকেই এক ঘর এক ঘর করে সামনে সরানো:
int total = 0;
for (int *p = marks; p < marks + n; p++) {
total += *p; /* p যে ঘরে আছে, তার মান যোগ */
}লাইনটি ভেঙে পড়ি। p = marks — শুরুতে প্রথম ঘরে দাঁড়ানো। p < marks + n — যতক্ষণ শেষ ঘর পার হওয়া হয়নি। p++ — এক ঘর সামনে যাওয়া (int হলে ৪ বাইট)। আর *p — p এখন যে ঘরে দাঁড়িয়ে, সেখানকার মান। Index-এর লুপ আর এই লুপ একই কাজ করে; আধুনিক compiler দুটি থেকে প্রায় একই machine code বানায়। কোনটি লিখবেন, সেটা পড়ার সুবিধার প্রশ্ন — গতির নয়।
“শেষের পরের ঘর” — একটি গুরুত্বপূর্ণ নিয়ম
লুপের শর্তে marks + n হলো শেষ ঘরের পরের ঠিকানা। ৫ ঘরের array-তে ঘরগুলো ০ থেকে ৪; marks + 5 এমন একটি জায়গা, যেখানে array-র কোনো ঘর নেই। C এখানে একটি বিশেষ ছাড় দেয়: one-past-the-end ঠিকানা বানানো আর তুলনা করা বৈধ, কিন্তু সেখানে * দিয়ে পড়া বা লেখা নিষেধ। এটি ঠিক পর্ব ১২-এর half-open range চিন্তা — শুরু অন্তর্ভুক্ত, শেষ বাদ।
| কাজ | উদাহরণ | ফল |
|---|---|---|
| Pointer + পূর্ণসংখ্যা | p + 2 | ২ ঘর পরের ঠিকানা |
| Pointer − পূর্ণসংখ্যা | p - 1 | ১ ঘর আগের ঠিকানা |
| Pointer − pointer (একই array) | q - p | মাঝে কয়টি ঘর — টাইপ ptrdiff_t |
| তুলনা (একই array) | p < end | কে আগে, কে পরে |
| Pointer + pointer | p + q | অর্থহীন — compiler error |
| ভিন্ন array-র pointer তুলনা বা বিয়োগ | p - other | অনির্দিষ্ট আচরণ (UB) |
দুটি ঠিকানা যোগ করা কেন অর্থহীন? ভাবুন, “ধানমন্ডি ২৭ নম্বর বাড়ি + গুলশান ৫০ নম্বর বাড়ি” — যোগফল কোনো জায়গা বোঝায় না। কিন্তু “২৭ নম্বর থেকে ৩ বাড়ি পরে” বা “৫০ নম্বর আর ৪৪ নম্বরের মাঝে কয়টি বাড়ি” — দুটিই অর্থপূর্ণ। Pointer arithmetic ঠিক এই অর্থপূর্ণ প্রশ্নগুলোরই অনুমতি দেয়।
একটি ছোট কিন্তু বিপজ্জনক পার্থক্য: *p++ বনাম (*p)++
অভিজ্ঞ C প্রোগ্রামারদের কোডে প্রায়ই দেখবেন sum += *p++;. Operator-এর অগ্রাধিকার অনুযায়ী এর মানে *(p++): “p-এর বর্তমান ঘরের মান নাও, তারপর p-কে এক ঘর সামনে সরাও।” অন্যদিকে (*p)++ মানে “p যে ঘরে আছে, সেই ঘরের মান ১ বাড়াও” — pointer নড়বে না। একটি বন্ধনীর পার্থক্যে সম্পূর্ণ ভিন্ন কাজ। নিশ্চিত না হলে বন্ধনী দিয়ে লিখুন।
Pointer arithmetic-এ কোনো পাহারাদার নেই। marks + 10 লিখলে C আপত্তি করবে না — ঠিকানাটি হিসাব করে দেবে, যদিও সেখানে আপনার array নেই। সেখান থেকে পড়লে আসবে অন্য কোনো ভ্যারিয়েবলের মান, আর লিখলে হতে পারে buffer overflow. Array-র সীমার বাইরে (শেষের পরের ঘর ছাড়া) pointer বানানোই C-এর নিয়মে undefined behavior.
৪ · ফাংশনে array পাঠালে আসলে কী যায়
পর্ব ১৩-তে একটি রহস্য রেখে এসেছিলাম: C-তে সব argument যায় pass by value — কপি হিসেবে। তবু array পাঠালে ফাংশন মূল array বদলে দিতে পারে। কীভাবে?
উত্তর এখন আপনার হাতে। report(marks, n) লিখলে marks decay হয়ে হয় &marks[0] — একটি ঠিকানা। ফাংশন পায় সেই ঠিকানার কপি, পুরো array-র কপি নয়। কপি হলেও ঠিকানা তো একই জায়গা দেখায়! তাই ফাংশন সেই ঠিকানা ধরে মূল ঘরে লিখতে পারে। আর একটি মজার নিয়ম: ফাংশনের parameter-এ int marks[] লিখলেও compiler সেটিকে চুপচাপ int *marks বানিয়ে নেয়। বন্ধনী শুধু দেখার জন্য — ভেতরে ওটা pointer.
ধরুন আপনার কলেজের লাইব্রেরির সব বই বন্ধুকে দেখাতে চান। পুরো লাইব্রেরি ট্রাকে তুলে পাঠানো অসম্ভব রকম খরচের। তাই একটি চিরকুটে লাইব্রেরির ঠিকানা লিখে দিলেন। বন্ধু ঠিকানা ধরে গিয়ে বই পড়তে পারবে, চাইলে বইয়ে দাগও দিতে পারবে — কারণ সে আসল লাইব্রেরিতেই গেছে। কিন্তু চিরকুটে লেখা নেই লাইব্রেরিতে কয়টি তাক। আলাদা করে না বললে সে জানবেই না কোথায় থামতে হবে।
এই “কয়টি তাক জানে না” সমস্যাটিই C-এর সবচেয়ে বিখ্যাত ফাঁদগুলোর একটি। নিচের ডায়াগ্রামে main-এর array, ফাংশনের কাছে যাওয়া ঠিকানার কপি, আর sizeof-এর অবাক করা উত্তর ধাপে ধাপে দেখুন:
1void report(int marks[], int n) {2 printf("%zu\n", sizeof marks); /* ?? */3 marks[0] = 100;4}56int main(void) {7 int marks[5] = {72, 85, 64, 90, 58};8 int n = sizeof marks / sizeof marks[0];9 report(marks, n);10 printf("%d\n", marks[0]);11}
| main: marks শুরু | |
| main: marks[0] | |
| main: n | |
| report: marks | |
| report: n | |
| report-এ sizeof marks |
- main-এ array তৈরি। ৫টি
int, শুরু1000-এ, মোট ২০ বাইট — সবটাইmain-এর stack frame-এর ভেতরে। - ঘর গোনা। এখানে
marksসত্যিকারের array, তাইsizeof marks= 20, আর20 / 4 = 5. এই কৌশল শুধু এখানেই কাজ করে — যেখানে array ঘোষণা করা হয়েছে। report(marks, n)ডাকা হলো।marksdecay হয়ে হলো1000. নতুন frame-এreport-এরmarksপেল শুধু এই সংখ্যাটির কপি — একটি ৮ বাইটের pointer. ২০ বাইটের ডেটা কোথাও কপি হয়নি।- sizeof-এর ফাঁদ। ফাংশনের ভেতরে
sizeof marksউত্তর দিল 8 — 20 নয়! কারণ এখানেmarksএকটি pointer, আর 64-bit কম্পিউটারে pointer-এর মাপ ৮ বাইট। ফাংশনের কাছে array-র দৈর্ঘ্য জানার কোনো উপায় নেই — তাই আমরাnআলাদা করে পাঠিয়েছি। marks[0] = 100— মানে*(1000) = 100. ঠিকানাটিmain-এর array-র, তাই বদলালmain-এর আসল ঘর। ঠিকানার কপি দিয়েও আসল জায়গায় পৌঁছানো যায়।- ফাংশন শেষ, frame মুছে গেল।
report-এর pointer আরnহারিয়ে গেল, কিন্তুmain-এর array আছে — আর তার প্রথম ঘরে এখন 100. পর্ব ১৩-এর রহস্যের সমাধান: কপি হয়েছিল ঠিকই, তবে ঠিকানার।
এবার নিজে চালিয়ে দেখুন। খেয়াল করুন, compiler নিজেই একটি warning দিয়ে সাবধান করবে — এটাই এই কোডের শিক্ষা:
#include <stdio.h>
void report(int marks[], int n) {
printf("inside: sizeof marks = %zu (a pointer!)\n", sizeof marks);
printf("inside: n = %d (sent by caller)\n", n);
marks[0] = 100; /* changes main's array */
}
int main(void) {
int marks[5] = {72, 85, 64, 90, 58};
int n = sizeof marks / sizeof marks[0];
printf("main : sizeof marks = %zu, n = %d\n", sizeof marks, n);
report(marks, n);
printf("main : marks[0] is now %d\n", marks[0]);
return 0;
}main-এ sizeof marks দেয় 20, কিন্তু report-এর ভেতরে দেয় 8 (32-bit কম্পিউটারে 4). GCC সতর্ক করে: 'sizeof' on array function parameter 'marks' will return size of 'int *' — অর্থাৎ “parameter-এ বন্ধনী দেখে ভুলবেন না, এটা pointer.” তাই নিয়ম: array যেখানে ঘোষণা করা, শুধু সেখানেই sizeof দিয়ে দৈর্ঘ্য বের করুন, তারপর দৈর্ঘ্যটি আলাদা parameter হিসেবে পাঠান। শেষ লাইনে 100 দেখে নিশ্চিত হোন — ফাংশন সত্যিই মূল array বদলেছে।
C-তে array কখনো “পুরোটা” কোনো ফাংশনে যায় না। যায় দুটি আলাদা তথ্য, যা আপনাকে নিজ হাতে জোড়া রাখতে হয়: শুরুর ঠিকানা আর কয়টি ঘর। এজন্যই C-এর standard library-র অসংখ্য ফাংশনে দেখবেন একই জোড়া — যেমন fgets(buf, size, stdin), memcpy(dst, src, n).
৫ · const — “আপনার array আমি বদলাব না” প্রতিশ্রুতি
আগের অংশে দেখলেন, ঠিকানা পেলে ফাংশন মূল array বদলে দিতে পারে। এটা কখনো কাজের (যেমন array সাজানো), কখনো বিপদের। ধরুন একটি ফাংশন শুধু গড় নম্বর বের করবে। সে যদি ভুল করে কোনো নম্বর বদলে দেয়, পুরো ক্লাসের ফলাফল নষ্ট। কোড পড়েই কীভাবে বুঝবেন কোন ফাংশন array বদলায়, কোনটা বদলায় না?
উত্তর const. Parameter-এ const int *a লিখলে মানে: “এই ঠিকানা দিয়ে আমি শুধু পড়ব, লিখব না।” এর নাম const pointer parameter. ভেতরে ভুল করে a[0] = 0; লিখলে compiler সঙ্গে সঙ্গে error দেবে।
double average(const int *a, int n); /* শুধু পড়ে — বদলায় না */
void sort_marks(int *a, int n); /* const নেই — বদলাবে */শুধু এই দুটি লাইন — ফাংশনের prototype — পড়েই এখন বোঝা যায় কোনটি আপনার ডেটায় হাত দেবে। এটি একধরনের চুক্তি, যা compiler পাহারা দেয়। const নেই এমন int * থেকে const int *-এ পাঠানো সবসময় বৈধ (প্রতিশ্রুতি বাড়ানো যায়), কিন্তু উল্টোটা করলে compiler warning দেয় (প্রতিশ্রুতি চুপচাপ ভাঙা যায় না)।
const কোথায় বসছে, তার ওপর অর্থ বদলায়। const int *p মানে “p যে ঘর দেখায়, সেটি বদলানো যাবে না” — কিন্তু p-কে অন্য ঘরে সরানো যাবে (তাই p++ দিয়ে হাঁটা চলে)। আবার int *const p মানে উল্টো — p নিজে স্থির, কিন্তু ঘরের মান বদলানো যায়। এই সূক্ষ্ম পার্থক্য, সাথে restrict আর volatile, বিস্তারিত দেখবেন পর্ব ৪৫-এ। আপাতত একটি অভ্যাসই যথেষ্ট: যে array ফাংশন শুধু পড়ে, তার parameter-এ const দিন।
৬ · এক ফাংশন থেকে একাধিক ফল — output parameter
return দিয়ে একটিই মান ফেরত দেওয়া যায়। কিন্তু ধরুন আপনি ক্লাস টিচার, ৬০ জনের নম্বর থেকে সর্বনিম্ন আর সর্বোচ্চ — দুটিই চাই। দুটি আলাদা ফাংশন লিখলে array দুবার ঘুরতে হবে। এক ঘোরাতেই দুটি বের করা কি সম্ভব?
সম্ভব — pointer দিয়ে। কৌশলটি হলো: main দুটি খালি ভ্যারিয়েবল (lo, hi) বানিয়ে তাদের ঠিকানা ফাংশনকে দেবে। ফাংশন হিসাব করে সেই ঠিকানায় ফল লিখে দেবে। এমন parameter-কে বলে output parameter — তথ্য ভেতরে আনার জন্য নয়, ফল বাইরে পাঠানোর জন্য। পর্ব ১৭-এর swap ঠিক এই কৌশলেই কাজ করেছিল।
স্কুলের পরীক্ষার পর প্রধান শিক্ষক ক্লাস টিচারকে একটি ফর্ম দিলেন, তাতে দুটি খালি ঘর: “সর্বনিম্ন নম্বর ____” আর “সর্বোচ্চ নম্বর ____”. টিচার খাতাগুলো একবার দেখে দুটি ঘরই পূরণ করে ফর্ম ফেরত দিলেন। ফর্মের খালি ঘর দুটিই হলো output parameter — প্রধান শিক্ষক জায়গা দিয়েছেন, টিচার সেখানে লিখেছেন।
1void min_max(const int *a, int n, int *min, int *max) {2 *min = *max = a[0];3 for (const int *p = a + 1; p < a + n; p++) {4 if (*p < *min) *min = *p;5 if (*p > *max) *max = *p;6 }7}89int main(void) {10 int marks[6] = {72, 85, 64, 90, 58, 77};11 int lo, hi;12 min_max(marks, 6, &lo, &hi);13 printf("lowest = %d, highest = %d\n", lo, hi);14}
| main: lo | |
| main: hi | |
| min_max: min | |
| min_max: max | |
| p কোথায় | |
| *p |
- খালি ফর্ম পাঠানো হলো।
loআরhi-তে এখনো আবর্জনা মান (?).mainতাদের ঠিকানা&lo,&hiপাঠাল। ফাংশনেরminআরmaxএখনmain-এর ওই দুটি ঘরের দিকে ইঙ্গিত করছে। - শুরুর মান।
*min = *max = a[0]— প্রথম নম্বর 72 দিয়ে দুটি ঘরই পূরণ। খেয়াল করুন, বদলাচ্ছেmain-এরloআরhi— ফাংশনের ভেতর থেকেই। - p এক ঘর এগিয়ে
a + 1-এ: মান 85. 85 > 72, তাইhiহলো 85. - p গেল
a + 2-এ: মান 64. 64 < 72, তাইloহলো 64. a + 3: মান 90 — নতুন সর্বোচ্চ।hi= 90.a + 4: মান 58 — নতুন সর্বনিম্ন।lo= 58.a + 5: মান 77. এটি 58-এর চেয়ে বড়, 90-এর চেয়ে ছোট — কিছুই বদলাল না।- p পৌঁছাল
a + 6-এ — শেষের পরের ঘর।p < a + nমিথ্যা, লুপ শেষ। সেখানে*pকখনো পড়া হয়নি — নিয়ম মানা হয়েছে। ফাংশন শেষ হলো, কিন্তু ফল দুটি ইতিমধ্যেmain-এর ঘরে লেখা। একবার ঘুরেই দুটি উত্তর।
এবার পুরো প্রোগ্রামটি চালিয়ে দেখুন। এখানে একসাথে এই পর্বের তিনটি ধারণা আছে — pointer দিয়ে হাঁটা, const parameter, আর output parameter:
#include <stdio.h>
void min_max(const int *a, int n, int *min, int *max) {
*min = *max = a[0];
for (const int *p = a + 1; p < a + n; p++) {
if (*p < *min) *min = *p;
if (*p > *max) *max = *p;
}
}
int main(void) {
int marks[6] = {72, 85, 64, 90, 58, 77};
int n = sizeof marks / sizeof marks[0];
int lo, hi;
min_max(marks, n, &lo, &hi);
printf("lowest = %d, highest = %d, range = %d\n", lo, hi, hi - lo);
printf("90 is at index %td\n", (marks + 3) - marks);
return 0;
}ফলাফল: lowest = 58, highest = 90, range = 32. শেষ লাইনটি pointer বিয়োগের একটি ছোট প্রদর্শনী — (marks + 3) - marks দেয় 3, অর্থাৎ ঘরের সংখ্যা, বাইট নয় (%td হলো ptrdiff_t ছাপানোর ফরম্যাট)। একটি সতর্কতা: এই ফাংশন ধরে নেয় n অন্তত 1. খালি array (n = 0) দিলে a[0] পড়াটাই ভুল হবে। পেশাদার কোডে শুরুতে এই যাচাই রাখা হয়, আর ফাংশন সফল হলো কি না, সেটা return দিয়ে জানানো হয় — ফল যায় output parameter-এ।
৭ · বিপদ: local ভ্যারিয়েবলের ঠিকানা ফেরত দেওয়া
Pointer দিয়ে ফল ফেরত দেওয়ার কৌশল শিখে অনেকে আরেকটি পথ ভাবেন: “ফাংশনের ভেতরে একটি ভ্যারিয়েবলে ফল রাখি, তারপর তার ঠিকানা return করি।” শুনতে নিরীহ। কিন্তু এটি C-এর সবচেয়ে কুখ্যাত ভুলগুলোর একটি।
কারণ পর্ব ১৩ আর ১৫-তে দেখা call stack. ফাংশন ডাকা হলে তার local variable-গুলো stack frame-এ জায়গা পায়। ফাংশন শেষ হলে সেই frame সরিয়ে নেওয়া হয় — জায়গাটি পরের ফাংশন কলের জন্য খালি ঘোষণা করা হয়। মানে ভ্যারিয়েবলের lifetime শেষ। তার পুরোনো ঠিকানা হাতে রাখলে সেটি হয়ে যায় dangling pointer — এমন pointer, যা এমন জায়গা দেখায় যেটি আর বৈধ নয়।
কক্সবাজারের একটি হোটেলে দুই রাত থেকে চেক-আউট করলেন, কিন্তু রুমের চাবির একটি কপি রেখে দিলেন। পরদিন ফিরে চাবি ঘোরালেন। হয়তো রুমটা এখনো খালি, আপনার ফেলে যাওয়া তোয়ালেও আছে — সব “ঠিক আছে” মনে হবে। হয়তো নতুন অতিথি উঠে গেছেন, আর ঘরে তাঁর জিনিস। হয়তো হোটেল তালাই বদলে ফেলেছে। কোনটি ঘটবে, আপনি জানেন না — কারণ ঘরটি আর আপনার নয়।
নিচের ডায়াগ্রামে দেখুন কী ঘটতে পারে। মনে রাখবেন, এটি “যা ঘটতে পারে”, “যা অবশ্যই ঘটবে” নয় — ঠিক কী হবে, তা compiler, optimization আর ভাগ্যের ওপর নির্ভর করে।
1int *best_mark(void) {2 int best = 95;3 return &best; /* ভুল! */4}5void other(void) {6 int x = 7;7}89int main(void) {10 int *p = best_mark();11 printf("%d\n", *p);12 other();13 printf("%d\n", *p);14}
| ঘরটির মালিক | |
| ঘরে লেখা মান | |
| main: p |
- best_mark চলছে। তার frame-এ
bestবসেছে ঠিকানা3000-এ, মান 95. এ পর্যন্ত সব বৈধ। return &best— frame সরানো হলো।pপেল3000. কিন্তু ফাংশন শেষ, তাইbest-এর জীবনও শেষ। ঘরটি “খালি” ঘোষণা করা হয়েছে, যদিও 95 লেখাটি কেউ মুছে দেয়নি।pএখন একটি dangling pointer.- প্রথম
printf— বিভ্রান্তিকর সাফল্য।*pপড়ে হয়তো 95-ই পাবেন! অনেক শিক্ষার্থী এখানেই ভাবেন “কোড তো ঠিক চলছে।” আসলে পুরোনো মান শুধু এখনো মুছে যায়নি — যেমন চেক-আউটের পর ঘরে পড়ে থাকা তোয়ালে। other()ডাকা হলো। নতুন frame বসল stack-এর ঠিক সেই এলাকায়, যেখানে আগেbest_mark-এর frame ছিল। খুব সম্ভবxপেল সেই একই ঘর3000, আর লিখল 7. নতুন অতিথি উঠে গেছেন।- দ্বিতীয়
printf— মান বদলে গেছে।p-এর ভেতরের ঠিকানা একটুও বদলায়নি, অথচ*pএখন 7 (বা অন্য যেকোনো আবর্জনা মান)। আপনার কোডে কোথাও*p = 7লেখা নেই! এমন বাগ খুঁজতে ঘণ্টার পর ঘণ্টা লাগে, কারণ লক্ষণ দেখা দেয় ভুল লাইন থেকে অনেক দূরে। - আসল দোষ লাইন ৩-এ। C-এর নিয়মে frame মুছে যাওয়ার পর
*pপড়াই undefined behavior. ফল হতে পারে 95, 7, আবর্জনা মান, crash — বা optimization চালু করলে সম্পূর্ণ ভিন্ন কিছু। ভালো খবর: GCC আর Clang এই লাইনে নিজে থেকেই সতর্ক করে —function returns address of local variable. সেই warning কখনো উপেক্ষা করবেন না।
তাহলে নিরাপদ উপায় কী?
১ · যে ফাংশন ডাকে, সে জায়গা দিক
ফল রাখার ঘর main-এ বানিয়ে তার ঠিকানা পাঠান — অর্থাৎ output parameter. ঘরটি থাকে যে ফাংশন ডাকছে (caller) তার frame-এ, তাই ফাংশন শেষ হলেও টিকে থাকে।
২ · মান ফেরত দিন, ঠিকানা নয়
ফল যদি একটিমাত্র সংখ্যা হয়, return best; — মান কপি হয়ে যায়, ঠিকানার প্রশ্নই ওঠে না।
৩ · পাওয়া ঠিকানার ভেতরের ঠিকানা
পাঠানো array-র কোনো ঘরের ঠিকানা ফেরত দেওয়া নিরাপদ (যেমন “সর্বোচ্চ নম্বরটি কোন ঘরে”) — কারণ সেই array যে ফাংশন ডেকেছে তার, তার জীবন শেষ হয়নি।
যেমনreturn &a[i];
৪ · দীর্ঘজীবী মেমরি
static local ভ্যারিয়েবল সারা প্রোগ্রাম বেঁচে থাকে (তবে সবাই একটিই কপি ভাগ করে)। আর দরকারমতো আকারের জন্য malloc — পর্ব ২০-এ।
৮ · AI-এর যুগে — pointer-কোডে AI কোথায় ভুল করে
Pointer আর array-র কোড AI খুব আত্মবিশ্বাসের সাথে লেখে, এবং দেখতে প্রায়ই নিখুঁত লাগে। কিন্তু এই পর্বের প্রতিটি ফাঁদ AI-এর কোডেও নিয়মিত দেখা যায় — কারণ ইন্টারনেটের পুরোনো কোডে এই ভুলগুলো প্রচুর, আর AI সেখান থেকেই শিখেছে। ভালো খবর হলো, আপনি এখন এগুলো চিনতে পারবেন।
| AI-এর ভুল | দেখতে কেমন | ফল |
|---|---|---|
ফাংশনের ভেতরে sizeof দিয়ে দৈর্ঘ্য | int n = sizeof(arr) / sizeof(arr[0]); — যেখানে arr একটি parameter | 64-bit-এ int array হলে n সবসময় 2 — বেশিরভাগ ডেটা বাদ পড়ে |
| Local array-র ঠিকানা ফেরত | char buf[50]; … return buf; | Dangling pointer — কখনো চলে, কখনো আবর্জনা |
| সীমার বাইরে হাঁটা | p <= a + n বা i <= n | শেষের পরের ঘর পড়া — off-by-one, UB |
| দৈর্ঘ্য না পাঠানো | int sum(int a[]) — কোথায় থামবে জানে না | ভেতরে কোনো “ম্যাজিক” সংখ্যা বসানো থাকে |
const বাদ দেওয়া | শুধু পড়ে এমন ফাংশনে int *a | ভুলে লিখে ফেললে compiler ধরবে না |
| অগ্রাধিকার গুলিয়ে ফেলা | *p++ যেখানে দরকার ছিল (*p)++ | মান না বেড়ে pointer সরে যায় |
AI-কে pointer-কোড লিখতে বললে শুরুতেই শর্তগুলো বলে দিন। আর লেখা কোড পেলে AI-কেই তার নিজের কোডের “memory audit” করতে বলুন — শেষ অংশটি খেয়াল করুন:
AI-এর pointer-কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট
প্রতিটি pointer দেখে জিজ্ঞেস করুন: “এটি কোথায় দেখাচ্ছে, আর সেই জায়গা কতক্ষণ বাঁচবে?”
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১C কেন array-কে নিজে থেকেই pointer-এ বদলে দেয়? এটা কি ভুল নকশা, না সুচিন্তিত সিদ্ধান্ত?
উত্তর বুঝতে C-এর পূর্বপুরুষদের দিকে তাকাতে হয়। BCPL আর তার উত্তরসূরি B ছিল “typeless” ভাষা — সবকিছুই একটি machine word. B-তে একটি array ঘোষণা করলে দুটি জিনিস তৈরি হতো: ঘরের সারি, আর আলাদা একটি ভ্যারিয়েবল যার ভেতরে সেই সারির শুরুর ঠিকানা। a[i] মানে ছিল “a-এর ভেতরের ঠিকানা + i”.
Dennis Ritchie যখন C-তে struct যোগ করছিলেন, সমস্যায় পড়লেন। একটি struct-এর ভেতরে array থাকলে সেই লুকানো pointer-টি কোথায় থাকবে? struct-কে মেমরিতে হুবহু যেমন দেখায় তেমন রাখা যেত না, আর ডিস্ক বা হার্ডওয়্যার থেকে struct সরাসরি পড়ার সুবিধাও নষ্ট হতো। তাঁর সমাধান: লুকানো pointer পুরোপুরি বাদ। Array শুধু ঘরের সারি। তবে expression-এ array-র নাম এলে compiler নিজেই তাকে প্রথম ঘরের ঠিকানায় বদলে নেবে। ফলে B-এর পুরোনো কোড (a[i], ঠিকানা পাঠানো) প্রায় অপরিবর্তিত থেকে গেল। Ritchie নিজে ১৯৯৩ সালের HOPL সম্মেলনের প্রবন্ধে এই ইতিহাস লিখেছেন।
লাভ কী হলো? ফাংশনে array পাঠানো সস্তা — যত বড়ই হোক, যায় শুধু একটি ঠিকানা। ১৯৭০-এর দশকের কয়েক কিলোবাইট মেমরির কম্পিউটারে এটি জরুরি ছিল। একই ফাংশন যেকোনো দৈর্ঘ্যের array নিয়ে কাজ করতে পারে।
দাম কী দিতে হলো? দৈর্ঘ্যের তথ্য হারিয়ে যায়। ফাংশন জানে না কোথায় থামতে হবে — এই একটি নকশা-সিদ্ধান্ত থেকে অসংখ্য buffer overflow জন্মেছে। D ভাষার স্রষ্টা Walter Bright ২০০৯ সালের একটি লেখায় একে বলেছেন “C's Biggest Mistake”, আর প্রস্তাব করেছেন array-র সাথে দৈর্ঘ্যও পাঠানোর।
তাই উত্তরটি দুটোই। তখনকার পরিস্থিতিতে সিদ্ধান্তটি সুচিন্তিত ছিল — দ্রুত, সহজ, পুরোনো কোডের সাথে মানানসই। কিন্তু যে দুনিয়ায় প্রোগ্রাম ইন্টারনেটের অচেনা ইনপুট নেয়, সেখানে এর দাম অনেক বেশি।
মূল উপলব্ধি: নকশার সিদ্ধান্তকে তার সময়ের প্রেক্ষাপটে বিচার করুন। আজকের “ভুল” প্রায়ই গতকালের বুদ্ধিমান trade-off — আর এর ফল কয়েক দশক ধরে টিকে থাকে, কারণ পুরোনো কোড ভাঙা যায় না।
প্র ০২Pointer-এ ১ যোগ করলে ১ বাইট না এগিয়ে টাইপের মাপ অনুযায়ী এগোয় কেন? বাইট ধরে হিসাব করাই কি সহজ হতো না?
বাইট ধরে হিসাব করলে compiler-এর কাজ সহজ হতো, কিন্তু আপনার কাজ কঠিন হতো। প্রতিবার লিখতে হতো p + i * sizeof(int). একবার ভুলে sizeof বাদ দিলেই pointer একটি int-এর মাঝখানে গিয়ে দাঁড়াত — অর্ধেক এক সংখ্যার, অর্ধেক আরেকটির বাইট পড়ে আবর্জনা মান। এমন বাগ খুঁজে পাওয়া খুব কঠিন।
টাইপ-অনুযায়ী লাফের (scaled arithmetic) কয়েকটি গভীর সুবিধা আছে:
a[i] = *(a + i)নিয়মটি সব টাইপে খাটে।char,int,double, এমনকি পরে শেখা struct — সব array-র জন্য একই সূত্র। একটি নিয়ম, ব্যতিক্রম নেই।- কোড বহনযোগ্য হয়। কোনো কম্পিউটারে
int২ বাইট, কোথাও ৪। আপনিp + 1লিখলে দুই জায়গাতেই সঠিক কাজ হয়; সংখ্যা বসাতে হয় না। - হার্ডওয়্যারের সাথে মেলে। x86-এর মতো প্রসেসরে ঠিকানা হিসাবের একটি সরাসরি পদ্ধতি আছে: base + index × scale, যেখানে scale হতে পারে ১, ২, ৪ বা ৮। অর্থাৎ
a[i]প্রায়ই একটিমাত্র machine instruction-এ পড়া যায় — আলাদা গুণ লাগে না।
তাহলে বাইট ধরে হাঁটতে চাইলে? সেই সুযোগও C রেখেছে — char * বা unsigned char * দিয়ে। sizeof(char) সংজ্ঞা অনুযায়ী সবসময় ১, তাই এই pointer ঠিক এক বাইট করে এগোয়। নেটওয়ার্ক প্যাকেট পড়া, ফাইল ফরম্যাট বোঝা বা মেমরির কাঁচা বাইট দেখার সময় প্রোগ্রামাররা ঠিক এটাই করেন।
মূল উপলব্ধি: ভালো abstraction সাধারণ কাজটিকে সহজ আর নিরাপদ করে, কিন্তু অসাধারণ কাজের দরজা বন্ধ করে না। Pointer-এর টাইপ সেই abstraction — দরকার হলে char * দিয়ে তার নিচে নামা যায়।
প্র ০৩a[3] লেখার বদলে 3[a] লিখলেও কি কাজ করে? করলে কেন, আর তাহলে সেভাবে লিখি না কেন?
হ্যাঁ, কাজ করে — আর কারণটি এই পর্বের মূল সূত্রেই লুকানো। C standard E1[E2]-কে সংজ্ঞায়িত করে *((E1) + (E2)) হিসেবে। তাহলে:
a[3]=*(a + 3)3[a]=*(3 + a)
যোগ বিনিময়যোগ্য — a + 3 আর 3 + a একই ঠিকানা। (Pointer + পূর্ণসংখ্যা যেকোনো ক্রমে লেখা বৈধ, আর scaling সবসময় pointer-এর টাইপ দেখেই হয়।) তাই দুটি লেখা হুবহু একই ঘর দেখায়। বিশ্বাস না হলে অংশ ৪-এর কোডে printf("%d\n", 2[marks]); লাইনটি যোগ করে চালিয়ে দেখুন — উত্তর আসবে 64.
তাহলে এর মূল্য কী? এটি একটি কৌতূহল, স্টাইল নয়। এর আসল শিক্ষা হলো: [ ] কোনো “array-র বিশেষ ক্ষমতা” নয়, শুধু ঠিকানার যোগ আর *-এর একটি সংক্ষিপ্ত লেখা। যিনি বোঝেন কেন 3[a] চলে, তিনি আসলে বোঝেন array-র ভেতরে কী ঘটে।
কেন এভাবে লিখবেন না? কোড লেখা হয় মানুষের পড়ার জন্য। 3[a] দেখে আপনার সহকর্মী থমকে যাবেন, code review-তে সময় নষ্ট হবে। এজন্যই এ ধরনের কৌশল বেশি দেখা যায় International Obfuscated C Code Contest-এর মতো “ইচ্ছা করে দুর্বোধ্য কোড” প্রতিযোগিতায়। C++-এ আবার নিজের বানানো টাইপের operator[] থাকলে এই অদলবদল সাধারণভাবে কাজ করে না — অর্থাৎ নিয়মটি শুধু built-in array আর pointer-এর।
AI-এর ক্ষেত্রেও একই শিক্ষা: কোড “কম্পাইল হয় আর চলে” মানেই ভালো কোড নয়। পড়ার সহজতা, প্রচলিত রীতি আর রক্ষণাবেক্ষণের সহজতা সমান গুরুত্বপূর্ণ।
মূল উপলব্ধি: ভাষার অদ্ভুত কোণগুলো জানা উপকারী — এগুলো ভেতরের নিয়ম প্রকাশ করে। কিন্তু যা বৈধ আর যা বুদ্ধিমানের, দুটি আলাদা প্রশ্ন।
প্র ০৪অন্য ভাষাগুলো array-র দৈর্ঘ্য হারিয়ে যাওয়ার সমস্যা কীভাবে সমাধান করেছে? C-তে কি এর কোনো উপায় আছে?
C-এর মূল সমস্যা — ঠিকানা আর দৈর্ঘ্য আলাদা হয়ে যায়। আধুনিক ভাষাগুলোর সমাধানও তাই একটাই: দুটিকে একসাথে বেঁধে রাখা। একে বলা হয় fat pointer — সাধারণ pointer-এর চেয়ে “মোটা”, কারণ এতে ঠিকানার পাশে দৈর্ঘ্যও থাকে।
- Go: একটি slice-এর ভেতরে তিনটি জিনিস — শুরুর pointer, দৈর্ঘ্য (len) আর ধারণক্ষমতা (cap). ফাংশনে slice পাঠালে তিনটিই যায়, আর সীমার বাইরে গেলে প্রোগ্রাম সঙ্গে সঙ্গে থেমে যায় — একে বলে panic.
- Rust:
&[i32]ধরনের slice আসলে pointer + দৈর্ঘ্য। Index করলে সীমা যাচাই হয়। - Java, C#, Python: array বা list একটি object, যার ভেতরে দৈর্ঘ্য রাখা থাকে। সীমার বাইরে গেলে exception.
- C++: C++20-এ যোগ হয়েছে
std::span— ঠিক এই pointer + দৈর্ঘ্যের জোড়া।
দাম কী? প্রতিটি index-এ একটি অতিরিক্ত তুলনা (সীমা যাচাই), আর pointer-এর মাপ দ্বিগুণ। তবে আধুনিক compiler অনেক যাচাই লুপের বাইরে সরিয়ে দেয় বা পুরো বাদ দেয়, আর CPU-র branch prediction বাকিটার খরচ খুব কমিয়ে আনে। বেশিরভাগ প্রোগ্রামে খরচটি প্রায় চোখেই পড়ে না।
C-তে কী করবেন? ভাষায় fat pointer নেই, কিন্তু নিজে বানাতে পারেন — দৈর্ঘ্য সবসময় ঠিকানার পাশে রাখার অভ্যাস করে। সবচেয়ে সাধারণ রূপ হলো এই পর্বের নিয়ম: প্রতিটি array parameter-এর পাশে একটি দৈর্ঘ্য parameter. পর্ব ২২-এ struct শিখলে ঠিকানা আর দৈর্ঘ্য একটি struct-এ বেঁধে নিজের “slice” বানাতে পারবেন। এছাড়া C99 থেকে void f(int n, int a[n]) ধরনের লেখা চলে, যা কোড পড়ুয়াকে দৈর্ঘ্যের সম্পর্ক স্পষ্ট দেখায় — যদিও ভেতরে a তখনো একটি pointer-ই থাকে।
মূল উপলব্ধি: যে তথ্য একসাথে অর্থবহ, তাকে একসাথে রাখুন। ভাষা সেটা না করলে, অভ্যাস আর নকশা দিয়ে আপনাকেই করতে হবে।
প্র ০৫Dangling pointer তো শুধু একটি বাগ — এটি কীভাবে গুরুতর নিরাপত্তা-ত্রুটি হয়ে ওঠে?
সাধারণ বাগে প্রোগ্রাম ভুল ফল দেয় বা থেমে যায়। Dangling pointer-এর বিপদ অন্য জায়গায়: যে মেমরি এখন অন্য কারও, সেটি আপনি পড়ছেন বা লিখছেন। আর সেই “অন্য কেউ”-কে যদি একজন আক্রমণকারী নিয়ন্ত্রণ করতে পারে?
ধাপগুলো এরকম: (১) প্রোগ্রাম একটি মেমরি ব্যবহার শেষে ছেড়ে দিল, কিন্তু কোথাও একটি pointer তখনো সেদিকে তাকিয়ে আছে। (২) আক্রমণকারী এমন ইনপুট পাঠাল, যাতে সেই খালি জায়গায় তার পছন্দের ডেটা বসে। (৩) প্রোগ্রাম পুরোনো pointer দিয়ে সেই জায়গা পড়ল — ভাবল এটি তার নিজের ডেটা, যেমন একটি ফাংশনের ঠিকানা। ফলে প্রোগ্রাম আক্রমণকারীর ইচ্ছামতো কোড চালাতে পারে।
Heap মেমরিতে এই সমস্যার নাম use-after-free (পর্ব ২০-এ malloc/free শিখলে আরও স্পষ্ট হবে)। আর আজকের stack-এর রূপটির জন্য নিরাপত্তা জগতে আলাদা শ্রেণিই আছে — CWE তালিকায় “Return of Stack Variable Address”. ব্রাউজারগুলোতে বছরের পর বছর গুরুতর ত্রুটির একটি বড় অংশ এসেছে use-after-free থেকে। Microsoft ও Google-এর Chromium দল প্রকাশ্যে জানিয়েছে, তাদের গুরুতর নিরাপত্তা-ত্রুটির প্রায় ৭০% মেমরি-নিরাপত্তা সংক্রান্ত।
কেন এত কঠিন ধরা? কারণ লক্ষণ দেখা দেয় ভুলের অনেক পরে, অন্য কোনো ফাংশনে — যেমন আপনি stepper-এ দেখলেন, other() ডাকার পরে মান বদলাল। পরীক্ষায় প্রোগ্রাম হাজারবার ঠিক চলতে পারে।
প্রতিরোধ কীভাবে? compiler warning (-Wall), AddressSanitizer (-fsanitize=address; stack-এর এই ভুল ধরতে সাধারণত তার একটি আলাদা option চালু করতে হয়), কোড রিভিউ, আর ভাষা-স্তরে সমাধান — memory safety. Rust-এর borrow checker compile করার সময়ই প্রমাণ চায় যে কোনো reference তার ডেটার চেয়ে বেশি দিন বাঁচবে না; এই পর্বের best_mark Rust-এ compile-ই হতো না।
মূল উপলব্ধি: নিরাপত্তা-ত্রুটি প্রায়ই সাধারণ বাগেরই আরেক রূপ — শুধু পার্থক্য হলো, কেউ ইচ্ছা করে সেটিকে কাজে লাগাচ্ছে। তাই “মাঝে মাঝে ভুল মান আসে” কথাটি কখনো হালকাভাবে নেবেন না।
অনুশীলন
বেশিরভাগ কাজে কাগজ-কলমই যথেষ্ট — হিসাব করুন, অনুমান করুন, তারপর উত্তর মিলিয়ে নিন।
-
ঠিকানার হিসাব:
double prices[10];শুরু হয়েছে ঠিকানা5000-এ, আরdouble *p = prices + 2;. বলুন: (ক)p-এর ঠিকানা (খ)p + 3-এর ঠিকানা (গ)(p + 3) - prices-এর মান। (double= ৮ বাইট ধরুন।)(ক)
5000 + 2 × 8 = 5016. (খ)5016 + 3 × 8 = 5040. (গ)p + 3হলোprices + 5, তাই বিয়োগফল 5 — ঘরের সংখ্যা। বাইটের পার্থক্য ৪০, কিন্তু pointer বিয়োগ সেটিকে ৮ দিয়ে ভাগ করে দেয়। যদি উত্তর ৪০ লিখে থাকেন, আবার পড়ুন অংশ ১-এর stepper-এর শেষ ধাপ। -
বাগ শিকার: AI এই ফাংশনটি দিয়েছে। ১০০ জনের নম্বর পাঠালে গড় ভুল আসছে। কেন? কীভাবে ঠিক করবেন?
double avg(int a[]) { int n = sizeof(a) / sizeof(a[0]); int s = 0; for (int i = 0; i < n; i++) s += a[i]; return (double)s / n; }Parameter
int a[]আসলেint *a. তাইsizeof(a)= ৮ (64-bit), আরn = 8 / 4 = 2. ফাংশন ১০০ জনের বদলে শুধু প্রথম ২ জনের গড় দিচ্ছে! GCC এখানে warning-ও দেয়। সমাধান: দৈর্ঘ্য বাইরে থেকে নিন, আর যেহেতু শুধু পড়া হচ্ছে,constদিন —double avg(const int *a, int n). ডাকার সময়avg(marks, 100), অথবা যেখানে array ঘোষিত সেখানেsizeof marks / sizeof marks[0]দিয়ে n বের করে পাঠান। চাইলেn == 0হলে শূন্য দিয়ে ভাগ এড়ানোর যাচাইও রাখুন। -
নিরাপদ নাকি বিপজ্জনক? নিচের চারটি ফাংশনের কোনগুলো ফেরত দেওয়া ঠিকানা নিরাপদ? (ক)
int *f(void) { int x = 5; return &x; }(খ)int *g(int *a, int n) { return &a[n - 1]; }(গ)int *h(void) { static int c = 0; c++; return &c; }(ঘ)char *k(void) { char name[20] = "Rafi"; return name; }(ক) বিপজ্জনক — local
x-এর ঠিকানা; frame মুছে গেলে dangling. (খ) নিরাপদ (যদি n ≥ 1) — ঠিকানাটি caller-এর array-র ভেতরের, সেটি এখনো বেঁচে আছে। (গ) নিরাপদ —staticlocal সারা প্রোগ্রাম বেঁচে থাকে; তবে সব কল একইcভাগ করে। (ঘ) বিপজ্জনক —nameএকটি local array,return namedecay হয়ে তার ঠিকানা ফেরত দেয়। এটি AI-এর খুব প্রচলিত ভুল, বিশেষ করে স্ট্রিং ফেরত দেওয়ার ফাংশনে। পরের পর্বে স্ট্রিং শিখলে এটি আবার দেখবেন। -
নিজে লিখুন (একটিমাত্র কোডের কাজ): একটি ফাংশন
void count_pass_fail(const int *a, int n, int *pass, int *fail)লিখুন, যা ৪০ বা তার বেশি নম্বরকে পাস ধরে পাস আর ফেলের সংখ্যা output parameter দিয়ে দেবে। লুপটি index দিয়ে নয়, pointer দিয়ে লিখুন।একটি সম্ভাব্য সমাধান:
void count_pass_fail(const int *a, int n, int *pass, int *fail) { *pass = 0; *fail = 0; for (const int *p = a; p < a + n; p++) { if (*p >= 40) (*pass)++; else (*fail)++; } }দুটি বিষয় খেয়াল করুন। প্রথমত,
*pass = 0দিয়ে শুরু —main-এর ভ্যারিয়েবলে আবর্জনা মান থাকতে পারে। দ্বিতীয়ত,(*pass)++-এ বন্ধনী জরুরি;*pass++লিখলে মান না বেড়ে pointer সরে যেত।main-এ ডাকবেনcount_pass_fail(marks, n, &p, &f);এভাবে।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমreport(marks, n) ডাকলে যা ঘটে, সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
marksdecay হয়ে প্রথম ঘরের ঠিকানা হয়- নতুন stack frame তৈরি হয়, ঠিকানা আর n-এর কপি সেখানে বসে
- ফাংশন
marks[i]=*(marks + i)দিয়ে মূল ঘর পড়ে বা লেখে - ফাংশন শেষ — frame মুছে যায়, কিন্তু main-এর array অক্ষত থাকে
int *p-এর ঠিকানা 1000 (int = ৪ বাইট)। p + 3-এর ঠিকানা কত?
- 1003
- 1004
- 1012
- 1024
marks[4] লেখাটির হুবহু সমান কোনটি?
- *marks + 4
- *(marks + 4)
- marks + 4
- &marks + 4
64-bit কম্পিউটারে void f(int a[]) { printf("%zu", sizeof a); }-কে ২০টি int-এর array দিয়ে ডাকলে কী ছাপা হবে?
- 80
- 20
- 4
- 8
একটি ফাংশন থেকে সর্বনিম্ন আর সর্বোচ্চ — দুটি মানই পেতে চান। সবচেয়ে প্রচলিত নিরাপদ উপায় কোনটি?
- দুটি ভ্যারিয়েবলের ঠিকানা output parameter হিসেবে পাঠানো
- return min, max; লেখা
- ফাংশনের ভেতরে local array বানিয়ে তার ঠিকানা return করা
- দুটি global ভ্যারিয়েবলে ফল রাখা
একটি ফাংশন local ভ্যারিয়েবলের ঠিকানা ফেরত দেয়, আর পরীক্ষায় ঠিক মানই আসছে। এ থেকে কী বোঝা যায়?
- কোডটি ঠিক আছে, চিন্তার কিছু নেই
- Compiler ভ্যারিয়েবলটিকে নিজে থেকে static বানিয়ে দিয়েছে
- ভাগ্যক্রমে পুরোনো মান এখনো মোছেনি — এটি লুকানো বাগ
- Stack frame প্রোগ্রাম শেষ হওয়া পর্যন্ত থাকে
double average(const int *a, int n) — এখানে const কী বোঝায়?
- array-র দৈর্ঘ্য স্থির, বদলানো যাবে না
- ফাংশনটি a দিয়ে array-র ঘরগুলো বদলাবে না
- pointer a-কে অন্য ঘরে সরানো যাবে না
- ফাংশন array-র একটি আলাদা কপি পাবে
৫ ঘরের int a[5]-এর জন্য কোন কাজটি C-এর নিয়মে বৈধ?
- *(a + 5) পড়া
- (a + 1) + (a + 2) হিসাব করা
- p = a + 7 বানিয়ে রাখা
- p < a + 5 শর্তে তুলনা করা
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
p + kঠিকানা সরায় k × sizeof(*p) বাইট; pointer বিয়োগ দেয় ঘরের সংখ্যা, যার টাইপptrdiff_t.a[i]≡*(a + i); array-র নাম প্রায় সব জায়গায় প্রথম ঘরের ঠিকানা হয়ে যায় — ব্যতিক্রমsizeof aআর&a.- ফাংশন পায় শুধু ঠিকানা — ভেতরে
sizeofদেয় pointer-এর মাপ; তাই দৈর্ঘ্য আলাদা করে পাঠান, আর শুধু পড়লেconstদিন। - একাধিক ফল চাইলে output parameter: যে ফাংশন ডাকে, সে ঘর বানায়, ঠিকানা দেয়, ফাংশন সেখানে লেখে।
- Local ভ্যারিয়েবলের ঠিকানা কখনো return নয় — frame মুছে গেলে সেটি dangling pointer, আর “ঠিক চলছে” দেখাটাই সবচেয়ে বড় ধোঁকা।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ১৯ · স্ট্রিং — অক্ষরের সারিপরবর্তী পর্ব
chararray,'\0'আর স্ট্রিং ফাংশনের ভেতরের pointer-কৌশল। - C প্রোগ্রামিং · কোর্স হোম৫৬টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- Data Structures & Algorithmsগভীরে যানArray আর pointer দিয়েই তৈরি হয় linked list, tree, hash table — সেগুলোর নকশা ও বিশ্লেষণ।
- Computer Architecture & Digital Logicপাশের ট্র্যাকCPU কীভাবে ঠিকানা হিসাব করে মেমরি পড়ে, আর cache কেন পাশাপাশি সাজানো ডেটা পছন্দ করে।