সরাসরি পর্বে যান
পর্ব ১৭ · ৫৬-এর মধ্যে · মডিউল ৩

পয়েন্টার — মান নয়, মানের ঠিকানা নিয়ে কাজ

Pointers, Part 1 — Addresses, & and *, NULL, and the Swap That Finally Works

Taught in Bangla, this first pointer lesson explains memory addresses, the & and * operators, why pointers have types, printing addresses with %p, fixing swap with pointers, NULL and wild pointers, segmentation faults, and a first look at pointer-to-pointer.

এই পর্বে যা শিখবেন

  • মেমরির ঠিকানা আর pointer-এর সম্পর্ক — একটি বাড়ি আর তার ঠিকানা লেখা চিরকুটের মতো করে
  • & ও * অপারেটর জোরে পড়ার নিয়ম, আর int *p; ঘোষণার আসল অর্থ
  • %p দিয়ে ঠিকানা ছাপানো, আর ঠিকানা প্রতিবার কেন বদলায়
  • NULL, wild pointer আর Segmentation fault — প্রোগ্রাম কেন ও কীভাবে crash করে
  • Pointer দিয়ে কাজ করা swap, scanf-এর রহস্য, আর AI-এর লেখা pointer-কোড যাচাইয়ের চেকলিস্ট

১ · প্রতিটি ভ্যারিয়েবলের একটি ঠিকানা আছে

শুরুতেই একটি কথা বলে রাখি। Pointer নিয়ে অনেক ভয়ের গল্প শুনেছেন হয়তো — “C-এর সবচেয়ে কঠিন জিনিস”, “এখানেই সবাই আটকে যায়”। আসলে pointer-এর পেছনের ধারণাটি খুবই সরল। আপনি এর বেশিরভাগ অংশ আগের পর্বগুলোতেই শিখে ফেলেছেন। এই পর্বে আমরা শুধু টুকরোগুলো জোড়া দেব — ধীরে, ছবি দেখে দেখে।

পর্ব ০৬-এ দেখেছিলেন, প্রতিটি ভ্যারিয়েবল মেমরিতে একটি “বাক্স”। পর্ব ১৬-এ দেখেছেন, মেমরি আসলে byte-এর এক বিশাল লম্বা সারি, আর প্রতিটি byte-এর একটি নম্বর আছে। এই নম্বরটিই memory address — মেমরির ঠিকানা। আপনি যখন লেখেন int x = 42;, তখন compiler মেমরিতে ৪ byte জায়গা ঠিক করে, সেখানে 42 রাখে, আর মনে রাখে “x মানে ওই জায়গাটি”।

মেমরি — নম্বর দেওয়া ঘরের লম্বা সারি

Every variable has a value and an address

  1. x42ঠিকানা 0x1000
  2. y7ঠিকানা 0x1004
  3. marks85ঠিকানা 0x1008
ছবি ১৭.১ — প্রতিটি বাক্সের দুটি জিনিস: ভেতরে মান, আর বাক্সের গায়ে লেখা ঠিকানা. (বোঝার সুবিধার জন্য ঠিকানাগুলো ছোট করে লেখা; আসল ঠিকানা অনেক লম্বা।)

এতদিন আমরা শুধু বাক্সের ভেতরের মান নিয়ে কাজ করেছি। এই পর্বের নতুন কথা একটাই: ঠিকানাটিও একটি সংখ্যা, তাই ঠিকানাকেও একটি ভ্যারিয়েবলে রাখা যায়। যে ভ্যারিয়েবল অন্য কোনো ভ্যারিয়েবলের ঠিকানা রাখে, তাকেই বলে pointer.

ধরুন মিরপুরে আপনার একটি বাড়ি আছে। বাড়িটি হলো আসল জিনিস — ভেতরে মানুষ, আসবাব, সব। এবার একটি ছোট চিরকুটে লিখলেন: “বাড়ি ১২, রোড ৫, মিরপুর ১০”. এই চিরকুটটি হলো pointer. চিরকুটটি বাড়ি নয়, শুধু বাড়ির ঠিকানা। তিনটি জিনিস খেয়াল করুন:

  • চিরকুটের ফটোকপি করলে বাড়ি দুটি হয়ে যায় না — শুধু ঠিকানাটি দুই জায়গায় লেখা থাকে।
  • চিরকুট দেখে কেউ সেই ঠিকানায় গিয়ে দেয়ালে রং করলে আসল বাড়িটিই বদলায়।
  • চিরকুটে ভুল ঠিকানা লেখা থাকলে লোকটি অন্য কারও বাড়িতে গিয়ে রং করে আসবে — অথবা এমন ঠিকানায় যাবে যেখানে কোনো বাড়িই নেই।

এই তিনটি বাক্য মনে রাখুন। পুরো পর্বটি আসলে এই তিনটি বাক্যেরই বিস্তারিত রূপ।

লাইব্রেরির উদাহরণও একই কথা বলে। ক্যাটালগ কার্ডে লেখা থাকে “তাক ৩, সারি ২”. কার্ডটি বই নয়, বইটি কোথায় আছে তার নির্দেশ। কার্ড কপি করলে বই দুটি হয় না, কিন্তু কার্ড দেখে তাকে গিয়ে বইয়ে দাগ দিলে আসল বইটিতেই দাগ পড়ে।

মূল ধারণা

সাধারণ ভ্যারিয়েবল = বাক্স, যার ভেতরে একটি মান. Pointer = আরেকটি বাক্স, যার ভেতরে অন্য একটি বাক্সের ঠিকানা. Pointer-ও নিজে একটি ভ্যারিয়েবল — তারও নিজের একটি ঠিকানা আছে, আর তার ভেতরের মানও বদলানো যায়।

২ · দুটি অপারেটর — & আর *

Pointer-এর পুরো কাজ চলে মাত্র দুটি অপারেটরে। এগুলো জোরে পড়ার একটি নিয়ম শিখে নিলে বেশিরভাগ বিভ্রান্তি দূর হয়ে যায়:

লেখাজোরে পড়ুনকাজচিরকুটের ভাষায়
&x“x-এর ঠিকানা”Address-of operator — x কোথায় থাকে সেটি দেয়বাড়ির ঠিকানা চিরকুটে লেখা
*p“p-এর ঠিকানায় যা আছে”Dereference — ঠিকানায় গিয়ে আসল বাক্সটি ধরাচিরকুট দেখে বাড়িতে পৌঁছানো

আর pointer ঘোষণা করা হয় এভাবে:

int x = 42;      // x একটি int বাক্স, ভেতরে 42
int *p = &x;     // p একটি pointer বাক্স, ভেতরে x-এর ঠিকানা
*p = 99;         // p-এর ঠিকানায় যা আছে, সেখানে 99 রাখো — অর্থাৎ x = 99

int *p; লাইনটি পড়ুন ডান থেকে বাঁয়ে: “p হলো একটি pointer (*), যা একটি int-কে দেখায়।” সংক্ষেপে বলা হয় “p হলো int-এর pointer” বা “int pointer”. এবার নিচের ডায়াগ্রামে তিনটি লাইন এক এক করে চলতে দেখুন। ডান দিকের টেবিলে x আর p-এর মান, আর p কোন দিকে দেখাচ্ছে সেটি খেয়াল করুন।

1int main(void) {2    int x = 42;3    int *p = &x;4    printf("%d\n", *p);5    *p = 99;6    printf("%d\n", x);7    return 0;8}
মেমরি (ঠিকানা ছোট করে লেখা)
x · ঠিকানা 0x1000
p · ঠিকানা 0x1008
*p (p যেখানে দেখায়)
  1. লাইন ২ চলল — x তৈরি। মেমরির 0x1000 ঠিকানায় একটি int বাক্স, ভেতরে 42. p-এর জন্যও জায়গা নেওয়া আছে, কিন্তু তাতে এখনো কিছু রাখা হয়নি — আছে আগের কোনো garbage value. এই অবস্থায় p কোথায় দেখায়, কেউ জানে না।
  2. লাইন ৩ চলল — p পেল x-এর ঠিকানা। &x মানে “x-এর ঠিকানা”, অর্থাৎ 0x1000. সেটিই রাখা হলো p-এর বাক্সে। এখন p “x-কে দেখায়” — চিরকুটে x-এর বাড়ির ঠিকানা লেখা হলো। খেয়াল করুন, x-এর মান 42 কোথাও কপি হয়নি।
  3. লাইন ৪ — *p পড়া। CPU প্রথমে p-এর বাক্স খুলে পেল 0x1000. তারপর 0x1000 ঠিকানায় গিয়ে সেখান থেকে একটি int পড়ল — 42. স্ক্রিনে ছাপা হলো 42. দুই ধাপের এই যাত্রাকেই বলে dereference.
  4. লাইন ৫ — *p = 99. আবার একই যাত্রা: p থেকে ঠিকানা 0x1000, সেখানে গিয়ে এবার লেখা হলো 99. ফল — x নিজেই 99 হয়ে গেছে, অথচ এই লাইনে x-এর নাম একবারও লেখা নেই! p-এর মান কিন্তু একটুও বদলায়নি — এখনো 0x1000.
  5. লাইন ৬ — x ছাপা। স্ক্রিনে এলো 99. প্রমাণ হলো: *p হলো x-এর আরেকটি নাম, যতক্ষণ p-তে x-এর ঠিকানা আছে।
  6. সারসংক্ষেপ। লাইন ৩-এ & দিয়ে ঠিকানা নেওয়া হলো, লাইন ৫-এ * দিয়ে সেই ঠিকানায় যাওয়া হলো। Pointer-এর সব কাজ এই দুই চালের নানা রকম মিশ্রণ। চিরকুটের ভাষায়: ঠিকানা লেখা, তারপর ঠিকানায় গিয়ে রং করা।
& = “এর ঠিকানা” · * = “এই ঠিকানায় যা আছে”
*(&x) আর x একই জিনিস

& আর * একে অপরের উল্টো কাজ করে। &x আপনাকে বাক্স থেকে ঠিকানায় নিয়ে যায়, * ঠিকানা থেকে আবার বাক্সে ফিরিয়ে আনে। তাই *&x লিখলে ঘুরে আবার x-এই ফেরেন। এটি কাজে লাগানোর জিনিস নয়, শুধু বোঝার জন্য।

৩ · Pointer-এর টাইপ কেন লাগে, আর *-এর তিন রূপ

একটি যৌক্তিক প্রশ্ন: ঠিকানা তো শুধু একটি সংখ্যা। তাহলে int *, char *, double * — আলাদা আলাদা টাইপ কেন? সব ঠিকানার জন্য একটিই টাইপ হলে কী ক্ষতি ছিল?

উত্তর লুকিয়ে আছে পর্ব ০৫-এ। সেখানে দেখেছিলেন, মেমরিতে শুধু বিট থাকে, আর সেই বিটের অর্থ নির্ভর করে আপনি সেগুলোকে কী হিসেবে পড়ছেন তার ওপর। একই চারটি byte কে int হিসেবে পড়লে এক সংখ্যা, float হিসেবে পড়লে সম্পূর্ণ আরেক সংখ্যা। ঠিকানা শুধু বলে কোথা থেকে পড়া শুরু করতে হবে। কিন্তু দুটি প্রশ্নের উত্তর ঠিকানায় নেই:

  • কয়টি byte পড়ব? char হলে ১টি, int হলে সাধারণত ৪টি, double হলে ৮টি।
  • পড়া byte-গুলোকে কী হিসেবে বুঝব? পূর্ণসংখ্যা, দশমিক সংখ্যা, নাকি অক্ষর।

Pointer-এর টাইপ এই দুটি প্রশ্নের উত্তর দেয়। int *p মানে “p-এর ঠিকানায় গেলে সেখান থেকে একটি int-এর সমান byte পড়বে, আর সেগুলোকে int হিসেবে বুঝবে।” Data type ছাড়া ঠিকানা অনেকটা “তাক ৩” লেখা কার্ডের মতো, যাতে লেখা নেই সেখানে একটি বই আছে, না পুরো বিশ খণ্ডের অভিধান।

পোস্ট অফিসের পার্সেল

ডেলিভারি স্লিপে শুধু ঠিকানা লেখা থাকলে চলে না — “ছোট খাম” না “বড় কার্টন”, সেটিও লেখা থাকে। ঠিকানা = কোথায়, টাইপ = কত বড় আর কী জিনিস।

ঠিকানা + টাইপ = পূর্ণ তথ্য

Pointer নিজে কত বড়?

যে টাইপকেই দেখাক, pointer নিজে সাধারণত একই মাপের — ৬৪ বিট কম্পিউটারে ৮ byte. চিরকুটের মাপ বাড়ির মাপের ওপর নির্ভর করে না।

sizeof(int *) = sizeof(double *)

একই * চিহ্নের তিনটি আলাদা কাজ

নতুনদের সবচেয়ে বড় বিভ্রান্তি এখানেই। C-তে * চিহ্নটি তিন জায়গায় তিন অর্থে ব্যবহার হয়। কোন অর্থ, সেটি বোঝা যায় চিহ্নটি কোথায় বসেছে তা দেখে:

কোথায়উদাহরণঅর্থ
দুটি মানের মাঝখানেa * bগুণ
Declaration-এ, টাইপের পরেint *p;“p একটি pointer” — শুধু টাইপের অংশ, কোথাও যাওয়া হচ্ছে না
Expression-এ, একটি pointer-এর আগে*p = 99;Dereference — “p-এর ঠিকানায় যা আছে”

সবচেয়ে বিপজ্জনক গোলমাল হয় এই লাইনে: int *p = &x;. দেখে মনে হয় “*p-তে &x রাখা হচ্ছে”। আসলে তা নয়। এখানে * declaration-এর অংশ, তাই লাইনটির অর্থ: “p নামের একটি int pointer বানাও, আর p-তে রাখো x-এর ঠিকানা।” এটি নিচের দুই লাইনের সমান:

int *p;      // ঘোষণা: p একটি int pointer
p = &x;      // p-তে ঠিকানা রাখা — এখানে * নেই!

আরেকটি ফাঁদ: int* a, b; লিখলে মনে হয় a আর b দুটিই pointer. আসলে শুধু a pointer, b একটি সাধারণ int! কারণ C-তে * টাইপের সাথে নয়, প্রতিটি নামের সাথে আলাদাভাবে যুক্ত হয়। দুটিকেই pointer বানাতে লিখতে হয় int *a, *b;. এই কারণেই অনেক পেশাদার *-কে নামের গায়ে লাগিয়ে লেখেন, আর এক লাইনে একটির বেশি pointer ঘোষণা করেন না।

৪ · নিজের চোখে ঠিকানা দেখুন — %p

এবার তত্ত্ব থেকে বাস্তবে আসি। পর্ব ১০-এ format specifier-এর টেবিলে %p দেখেছিলেন — ঠিকানা ছাপার জন্য। নিচের প্রোগ্রামটি চালান। আপনার কম্পিউটারের মেমরিতে x আসলে কোথায় থাকে, দেখে নিন:

#include <stdio.h>

int main(void) {
    int x = 42;
    int *p = &x;

    printf("x         = %d\n", x);
    printf("&x        = %p\n", (void *)&x);
    printf("p         = %p\n", (void *)p);
    printf("*p        = %d\n", *p);

    *p = 99;
    printf("x now     = %d\n", x);
    printf("sizeof(p) = %zu bytes\n", sizeof p);
    return 0;
}

দ্বিতীয় আর তৃতীয় লাইনে একই ঠিকানা ছাপা হবে — কারণ p-তে x-এর ঠিকানাই রাখা আছে। *p দেখাবে 42, আর *p = 99-এর পরে x নিজেই 99. শেষ লাইনে pointer-এর নিজের মাপ — ৬৪ বিট সিস্টেমে সাধারণত 8 bytes. %p-এর সাথে (void *) লেখা হয়েছে কারণ C-এর নিয়ম অনুযায়ী %p একটি void pointer আশা করে — যে pointer শুধু ঠিকানা বহন করে, টাইপ বলে না।

ছাপা ঠিকানাটি দেখতে সম্ভবত এমন: 0x7ffd5c3a9e2c. এর মানে কী?

  • 0x দিয়ে শুরু: সংখ্যাটি hexadecimal-এ লেখা (পর্ব ০৫). ঠিকানা অনেক বড় সংখ্যা, hex-এ লিখলে ছোট দেখায়।
  • 7ff… দিয়ে শুরু: Linux-এ local ভ্যারিয়েবল থাকে stack-এ, আর stack সাধারণত প্রোগ্রামের ঠিকানা-জগতের উঁচু দিকে বসানো হয়। তাই এমন বড় সংখ্যা। Windows-এ চেহারা কিছুটা আলাদা হবে।
  • প্রতিবার আলাদা: প্রোগ্রামটি দুইবার চালান — ঠিকানা বদলে যাবে! এটি ভুল নয়। নিরাপত্তার জন্য অপারেটিং সিস্টেম প্রতিবার প্রোগ্রামের মেমরি এলোমেলো জায়গায় বসায়। এই কৌশলের নাম ASLR. হ্যাকার আগে থেকে জানতে পারে না কোন জিনিস কোন ঠিকানায় থাকবে, তাই আক্রমণ অনেক কঠিন হয়।
মূল ধারণা

ঠিকানার আসল সংখ্যাটি নিয়ে মাথা ঘামানোর দরকার নেই — সেটি প্রতিবার বদলায়, আর আপনার প্রোগ্রামের ঠিকানাটি “জানা” দরকারও হয় না। দরকার শুধু সম্পর্কটি: p-তে যে সংখ্যা আছে, সেটি x-এর ঠিকানার সমান। এজন্যই ডায়াগ্রামে আমরা সংখ্যার বদলে তীর আঁকি: p → x.

৫ · পর্ব ১৩-এর ভাঙা swap — এবার ঠিক হলো

পর্ব ১৩-এর সেই হতাশার কথা মনে আছে? swap(int a, int b) ফাংশনের ভেতরে অদলবদল নিখুঁতভাবে হয়েছিল, কিন্তু main-এর x আর y একটুও বদলায়নি। কারণ C-তে pass by value — ফাংশন পায় মানের ফটোকপি, আর কাজ শেষে ফটোকপিসহ পুরো stack frame মুছে যায়।

সমাধান এখন আপনার হাতে। ফাংশনকে মান না দিয়ে ঠিকানা দিন। ফাংশন সেই ঠিকানায় গিয়ে আসল বাক্সে লিখবে। চিরকুটের ভাষায়: মিস্ত্রিকে বাড়ির ছবি না দিয়ে ঠিকানা দিলেন — তিনি গিয়ে আসল বাড়িতেই কাজ করবেন।

1#include <stdio.h>23void swap(int *a, int *b) {4    int t = *a;5    *a = *b;6    *b = t;7}89int main(void) {10    int x = 5, y = 9;11    swap(&x, &y);12    printf("x=%d y=%d\n", x, y);13    return 0;14}
দুটি frame — main আর swap
main: x · 0x1000
main: y · 0x1004
swap: a
swap: b
swap: t
  1. main-এ দুটি ভ্যারিয়েবল। x = 5 থাকে 0x1000 ঠিকানায়, y = 9 থাকে 0x1004 ঠিকানায়। লক্ষ্য আগের মতোই: x = 9, y = 5.
  2. swap ডাকা হলো — এবার কপি যায় ঠিকানার। &x আর &y পাঠানো হয়েছে। নতুন frame-এ a পেল 0x1000 (x-এর ঠিকানা), b পেল 0x1004. খেয়াল করুন, এখনো কপিই গেছে — তবে মানের নয়, ঠিকানার কপি। ফটোকপি করা চিরকুটে একই বাড়ির ঠিকানা।
  3. লাইন ৪: t = *a. a-এর ঠিকানায় (0x1000) গিয়ে পড়া হলো 5, রাখা হলো t-তে।
  4. লাইন ৫: *a = *b. b-এর ঠিকানা থেকে পড়া হলো 9, লেখা হলো a-এর ঠিকানায় — অর্থাৎ main-এর x-এ! সবুজ দাগ দেখুন: main-এর x বদলে 9 হয়েছে, অথচ swap ফাংশন “x” নামটিই জানে না।
  5. লাইন ৬: *b = t. t-এর 5 লেখা হলো b-এর ঠিকানায় — main-এর y এখন 5.
  6. swap শেষ — frame মুছে গেল। a, b, t হারিয়ে গেল, ঠিক পর্ব ১৩-এর মতোই। কিন্তু এবার কাজটি করা হয়েছিল আসল বাক্সে, তাই ফলাফল রয়ে গেল। চিরকুট ফেলে দিলেও বাড়ির নতুন রং থেকে যায়।
  7. ফল: x=9 y=5. অদলবদল সফল! পর্ব ১৩-এর সাথে পার্থক্য মাত্র তিন জায়গায়: parameter-এ int *, ফাংশনের ভেতরে *a, *b, আর ডাকার সময় &x, &y.

নিজে চালিয়ে নিশ্চিত হোন:

#include <stdio.h>

void swap(int *a, int *b) {
    int t = *a;
    *a = *b;
    *b = t;
}

int main(void) {
    int x = 5, y = 9;
    printf("before: x=%d y=%d\n", x, y);
    swap(&x, &y);
    printf("after:  x=%d y=%d\n", x, y);
    return 0;
}

প্রথম লাইনে x=5 y=9, দ্বিতীয় লাইনে x=9 y=5. এখন একটি পরীক্ষা করুন: swap(&x, &y)-কে বদলে swap(x, y) লিখে চালান। Compiler সাথে সাথে আপত্তি জানাবে — “int থেকে pointer বানানো হচ্ছে” ধরনের মেসেজ। পুরোনো GCC-তে এটি warning, আর GCC ১৪ থেকে সরাসরি error. কারণ ফাংশন ঠিকানা চাইছে, আর আপনি দিচ্ছেন সাধারণ সংখ্যা। চালালে প্রোগ্রাম 5 আর 9-কে ঠিকানা ভেবে সেখানে যেতে চাইবে — প্রায় নিশ্চিত crash.

নাম নিয়ে একটি সতর্কতা: এই কৌশলকে অনেকে বলেন “pass by reference”. কিন্তু খুঁটিয়ে দেখলে C-তে সবকিছুই এখনো pass by value — শুধু এবার যে value কপি হচ্ছে, সেটি একটি ঠিকানা। তাই সঠিক বর্ণনা হলো: “C-তে pass by reference-এর কাজটি করা হয় pointer-কে by value পাঠিয়ে।” C++-এর মতো কিছু ভাষায় সত্যিকারের reference আছে, যেখানে & না লিখেই এই কাজ হয়। C-তে সব কিছু খোলাখুলি — ডাকার জায়গায় & দেখলেই বুঝবেন “এই ফাংশন আমার ভ্যারিয়েবল বদলাতে পারে।”

এবার বোঝা গেল — scanf-এ & কেন

পর্ব ১০-এ একটি নিয়ম মুখস্থ করতে বলেছিলাম: scanf("%d", &n) — ভ্যারিয়েবলের আগে &. সেদিন দারাজের ডেলিভারিম্যানের উদাহরণ দিয়েছিলাম। এবার পুরো কারণটি আপনি নিজেই বলতে পারবেন: scanf-কে আপনার ভ্যারিয়েবলে লিখতে হবে, আর C-তে কোনো ফাংশন অন্যের ভ্যারিয়েবলে লিখতে পারে কেবল তার ঠিকানা পেলে — ঠিক swap-এর মতো। নিচে ধাপে ধাপে দেখুন:

  1. কীবোর্ড"42"শুধু দুটি অক্ষর
  2. scanf পেল&n = 0x1000লেখার ঠিকানা
  3. রূপান্তর'4','2' → 42লেখা থেকে সংখ্যা
  4. n বদলাল*(0x1000) = 42main-এর আসল বাক্স
scanf("%d", n);& ভুলে গেলে কী হয়
// n-এ এখনো আবর্জনা মান, ধরুন 1937
scanf("%d", n);   // scanf পেল 1937 — একে ঠিকানা ভেবে বসল!
// 1937 নম্বর ঠিকানায় 42 লেখার চেষ্টা →
Segmentation fault (অথবা আরও খারাপ: অন্য কারও মেমরি নষ্ট)

gcc -Wall আগেই সাবধান করে:
warning: format '%d' expects argument of type 'int *',
         but argument 2 has type 'int'
  1. ব্যবহারকারী লিখলেন 42. কীবোর্ড থেকে আসে শুধু দুটি অক্ষর — '4' আর '2'. এগুলো এখনো সংখ্যা নয়, লেখা। (পর্ব ১০-এর stream আর buffer মনে করুন।)
  2. scanf ঠিকানা হাতে পেল। আপনি &n দিয়েছেন, তাই scanf-এর কাছে এখন n-এর ঠিকানা 0x1000. scanf জানে না ভ্যারিয়েবলের নাম “n” — তার দরকারও নেই। ঠিকানাই যথেষ্ট।
  3. লেখা থেকে সংখ্যা। %d দেখে scanf বুঝল int চাই। '4' আর '2' অক্ষর দুটিকে সে হিসাব করে পূর্ণসংখ্যা 42 বানাল।
  4. ঠিকানায় লেখা। scanf ভেতরে আসলে swap-এর মতোই একটি কাজ করে: *ঠিকানা = 42. ফলে main-এর আসল n-এ 42 বসে গেল। scanf শেষ হলে তার frame মুছে যায়, কিন্তু n-এ লেখা মান থেকে যায়।
  5. যদি & ভুলে যান। তখন scanf পায় n-এর মান — হয়তো কোনো আবর্জনা সংখ্যা। scanf সেটিকেই ঠিকানা ভেবে সেখানে লিখতে যায়। ফল প্রায়ই crash, আর ভাগ্য খারাপ হলে নীরবে অন্য কোনো ডেটা নষ্ট। ভালো খবর: -Wall দিলে compiler এই ভুল আগেই ধরিয়ে দেয়।

৬ · NULL, wild pointer আর Segmentation fault

চিরকুটের তৃতীয় বাক্যটি মনে করুন: “চিরকুটে ভুল ঠিকানা থাকলে লোকটি ভুল জায়গায় গিয়ে রং করবে, অথবা এমন ঠিকানায় যাবে যেখানে কোনো বাড়িই নেই।” Pointer-এর সব বিপদ এই বাক্যেই লুকানো। দুই ধরনের ভুল ঠিকানা সবচেয়ে বেশি দেখা যায়।

⭕ NULL pointer

int *p = NULL; — ইচ্ছা করে রাখা “কোথাও না” চিহ্ন। চিরকুটে লেখা “এখনো কোনো ঠিকানা নেই”. এটি নিরাপদ, যদি ব্যবহারের আগে যাচাই করেন।

জানা খালি

🎲 Wild pointer

int *q; — কোনো মান না দিয়ে ঘোষণা। ভেতরে আগের কোনো আবর্জনা সংখ্যা, যা যেকোনো জায়গার ঠিকানা হতে পারে। চিরকুটে কারও হিজিবিজি লেখা।

অজানা বিপদ

NULL হলো একটি বিশেষ মান, যা কোনো বৈধ বস্তুর ঠিকানা হতে পারে না। যে pointer-এ NULL আছে, তাকে বলে null pointer. এটি <stdio.h>, <stdlib.h> সহ কয়েকটি header-এ সংজ্ঞায়িত। বাস্তবে প্রায় সব কম্পিউটারে এর মান ০ নম্বর ঠিকানা। আর যে pointer-কে কোনো মানই দেওয়া হয়নি, তাকে বলে wild pointer. দুটির কোনোটিতেই * চালানো চলবে না। চালালে কী ঘটে, ধাপে ধাপে দেখুন:

  1. p = NULL0x0“কোথাও না”
  2. *p = 7ঠিকানা 0-তে লেখোCPU চেষ্টা করল
  3. পাহারাদারMMUএই ঠিকানা কি এর?
  4. OSSIGSEGVনিয়ম ভাঙার শাস্তি
  5. CrashSegmentation faultপ্রোগ্রাম বন্ধ
null.cযে কোড চলছে
int *p = NULL;
printf("before\n");
*p = 7;              // এখানেই বিপদ
printf("after\n");   // কখনো ছাপা হবে না
TerminalLinux-এ যা দেখবেন
$ ./null
before
Segmentation fault (core dumped)
wild.cwild pointer — আরও খারাপ
int *q;              // মান দেওয়া হয়নি — ভেতরে আবর্জনা
*q = 7;              // আবর্জনা ঠিকানায় লেখা!
// ফল ১: ঠিকানাটি অবৈধ → crash (ভাগ্য ভালো)
// ফল ২: ঠিকানাটি আপনারই অন্য কোনো ভ্যারিয়েবলের →
//        নীরবে সেটি বদলে গেল, কোনো error নেই!

int *safe = NULL;    // সমাধান: সবসময় শুরুতেই মান দিন
if (safe != NULL) { *safe = 7; }
  1. p-তে NULL রাখা হলো। এটি পুরোপুরি বৈধ ও নিরাপদ। মানে শুধু “p এখনো কাউকে দেখায় না”. “before” ছাপা হলো।
  2. এবার *p = 7. CPU নির্দেশ পেল: “ঠিকানা 0-তে একটি int লেখো।” CPU নিজে ভালো-মন্দ বিচার করে না, সে চেষ্টা শুরু করল।
  3. পাহারাদার যাচাই করল। প্রতিটি মেমরি ব্যবহারের আগে CPU-র ভেতরের একটি অংশ (MMU) দেখে নেয় — এই ঠিকানা কি এই প্রোগ্রামকে দেওয়া হয়েছে? অপারেটিং সিস্টেম ইচ্ছা করেই ০ আর তার কাছাকাছি ঠিকানাগুলো কোনো প্রোগ্রামকে দেয় না। তাই উত্তর: না!
  4. OS-এর হাতে খবর গেল। CPU কাজ থামিয়ে নিয়ন্ত্রণ দিল অপারেটিং সিস্টেমকে। OS দেখল প্রোগ্রামটি নিজের সীমার বাইরে হাত দিয়েছে। Linux-এ এর জন্য প্রোগ্রামকে পাঠানো হয় একটি সংকেত — SIGSEGV (segmentation violation).
  5. প্রোগ্রাম বন্ধ। স্ক্রিনে এলো Segmentation fault. “after” কখনো ছাপা হলো না। Windows-এ একই ঘটনাকে বলে “Access violation”. শুনতে খারাপ লাগলেও এটি আসলে ভালো ফল — ভুলটি সাথে সাথে, জোরে ধরা পড়ল।
  6. Wild pointer আরও বিপজ্জনক। NULL অন্তত সবসময় একই জায়গায় দেখায়, যেখানে পাহারাদার নিশ্চিতভাবে আটকায়। কিন্তু মান না দেওয়া pointer-এর ভেতরের আবর্জনা সংখ্যাটি যেকোনো কিছু হতে পারে — এমনকি আপনারই অন্য কোনো ভ্যারিয়েবলের ঠিকানা। তখন কোনো crash হয় না, শুধু নীরবে অন্য ডেটা নষ্ট হয়, আর বাগটি প্রকাশ পায় অনেক পরে, অন্য কোথাও। সমাধান একটাই: pointer ঘোষণার সময়ই হয় একটি বৈধ ঠিকানা, নয়তো NULL দিন।

সবসময় crash হবে, এমন নিশ্চয়তা নেই। C-এর নিয়মে NULL বা অবৈধ pointer-এ * চালানো হলো undefined behavior — standard বলে না কী হবে। সাধারণ কম্পিউটারে প্রায় সবসময় Segmentation fault হয়। কিন্তু ছোট microcontroller-এ ০ নম্বর ঠিকানায় আসল মেমরি থাকতেও পারে — সেখানে লেখা কোনো আওয়াজ ছাড়াই চলে যেতে পারে। তাই “crash হচ্ছে না” মানে “ঠিক আছে” নয়।

নিরাপদ pointer-এর তিন অভ্যাস

  1. জন্মের সময়ই মান দিন: int *p = &x; অথবা int *p = NULL; — কখনো শুধু int *p; নয়। এটি পর্ব ০৬-এর initialization-এর নিয়মেরই বাড়তি রূপ।
  2. ব্যবহারের আগে যাচাই করুন: যে pointer NULL হতে পারে (যেমন কোনো ফাংশন থেকে পাওয়া), তাতে * চালানোর আগে if (p != NULL).
  3. কাজ শেষে আবার NULL: যে বস্তুকে pointer দেখাত, সেটি আর না থাকলে pointer-এ NULL রাখুন। (পর্ব ২০-এ ডায়নামিক মেমরির সময় এর গুরুত্ব আরও বুঝবেন।)

NULL-এর ধারণা অনেক পুরোনো। ১৯৬৫ সালে ব্রিটিশ কম্পিউটার বিজ্ঞানী Tony Hoare ALGOL W ভাষা নকশা করার সময় “null reference” যোগ করেন — কারণ বানানো খুব সহজ ছিল। ২০০৯ সালে লন্ডনের একটি সম্মেলনে তিনি একে বলেন তাঁর “billion-dollar mistake” — শত কোটি ডলারের ভুল। তাঁর যুক্তি: এই একটি সিদ্ধান্ত থেকে কয়েক দশকে অসংখ্য বাগ, crash আর নিরাপত্তা-ত্রুটি জন্মেছে। মজার ব্যাপার, Hoare-ই quicksort অ্যালগরিদমের আবিষ্কারক — একজন মহান বিজ্ঞানীও প্রকাশ্যে নিজের ভুল স্বীকার করেছেন। ভাবনার প্রশ্নে আমরা দেখব আধুনিক ভাষাগুলো এই ভুল কীভাবে এড়ায়।

৭ · Pointer-এর pointer — এক ঝলক

Pointer নিজেও একটি ভ্যারিয়েবল, তাই তারও একটি ঠিকানা আছে। সেই ঠিকানাও আরেকটি ভ্যারিয়েবলে রাখা যায়। একে বলে pointer to pointer:

int x = 42;
int *p = &x;      // p → x
int **pp = &p;    // pp → p → x

**pp = 7;         // pp থেকে p-তে, p থেকে x-এ — x এখন 7

ঠিকানার ঠিকানা

pp holds the address of p, which holds the address of x

  1. pp0x1008ঠিকানা 0x1010
  2. p0x1000ঠিকানা 0x1008
  3. x42ঠিকানা 0x1000
ছবি ১৭.২ — প্রতিটি * মানে তীর ধরে এক ধাপ এগোনো। *pp হলো p, **pp হলো x.

ধরুন আপনার বন্ধু বললেন, “আমার নতুন বাসার ঠিকানা মায়ের ডায়েরিতে লেখা আছে।” ডায়েরি কোথায় আছে আপনি জানেন। তাহলে আগে ডায়েরিতে যাবেন (প্রথম *), সেখান থেকে বাসার ঠিকানা পাবেন, তারপর বাসায় যাবেন (দ্বিতীয় *).

এটি কেন দরকার? দুটি জায়গায় আপনি এটি শিগগিরই দেখবেন:

  • int main(int argc, char **argv): প্রোগ্রাম চালানোর সময় terminal-এ দেওয়া শব্দগুলো (যেমন ./copy a.txt b.txt) main পায় এভাবে। প্রতিটি শব্দ একটি char *, আর সেই pointer-গুলোর তালিকার শুরুর ঠিকানা হলো char **. পর্ব ২৫-এ এটি পরিষ্কার হবে।
  • কোনো ফাংশনকে দিয়ে একটি pointer বদলাতে চাইলে: swap-এর যুক্তিই এখানেও খাটে। int বদলাতে লাগে int *; তাহলে একটি int * বদলাতে লাগবে int **. লিংকড লিস্টে (পর্ব ৩৩) এটি খুব কাজে লাগে।

এখন শুধু এটুকু বুঝলেই যথেষ্ট — নতুন কোনো নিয়ম নেই, একই & আর * একটির বেশি বার।

৮ · AI-এর যুগে pointer — যেখানে AI সবচেয়ে বেশি ভুল করে

AI-এর লেখা C কোডে সবচেয়ে বিপজ্জনক ভুলগুলো প্রায় সবসময় pointer-এ। কারণ এই ভুলগুলোর বেশিরভাগ compile হয়ে যায়, প্রথম কয়েকবার চালালে ঠিকঠাক ফলও দেয়, আর তারপর হঠাৎ কোনো এক ইনপুটে ভেঙে পড়ে। দেখতে ঠিক, ভেতরে ভাঙা — এমন কোড ধরার দায়িত্ব আপনার।

সবচেয়ে ক্লাসিক উদাহরণ — local ভ্যারিয়েবলের ঠিকানা ফেরত দেওয়া:

int *make_number(void) {
    int n = 42;          // n থাকে এই ফাংশনের frame-এ
    return &n;           // ফাংশন শেষ হলে frame মুছে যায়!
}
warning: function returns address of local variable

পর্ব ১৩-এ দেখেছেন, ফাংশন শেষ হলে তার stack frame মুছে যায় — local variable-এর lifetime সেখানেই শেষ। ফেরত দেওয়া ঠিকানাটি তখন এমন একটি বাড়ির, যা ভেঙে ফেলা হয়েছে, আর জায়গাটি পরের ফাংশনের কাছে ভাড়া চলে যাবে। এমন pointer-কে বলে dangling pointer. মাঝে মাঝে পুরোনো মান কিছুক্ষণ পড়ে থাকে বলে কোডটি “কাজ করছে” মনে হয় — এটাই সবচেয়ে বড় ফাঁদ।

AI সাধারণত যেসব pointer-ভুল করে

AI-এর ভুলকী হয়কীভাবে ধরবেন
Local ভ্যারিয়েবলের ঠিকানা returnDangling pointer — কখনো চলে, কখনো আবর্জনাGCC-র warning; প্রতিটি return &… খুঁজে দেখুন
কোনো ফাংশনের ফল NULL হতে পারে, কিন্তু যাচাই নেইবিশেষ পরিস্থিতিতে Segmentation faultপ্রতিটি pointer-ফেরত ফাংশনের পরে if (p == NULL) আছে কি না
int *p; — মান না দিয়ে ব্যবহারWild pointer — নীরবে ডেটা নষ্ট-Wall (অনেক ক্ষেত্রে “may be used uninitialized”); প্রতিটি ঘোষণায় মান আছে কি না
Declaration-এর * আর expression-এর * গুলিয়ে ফেলা*p = &x; — ঠিকানাকে int হিসেবে লেখাCompiler warning বা error; লাইনটি জোরে পড়ুন
int* a, b; — দুটিকেই pointer ভাবাb আসলে int, পরে টাইপের গোলমালএক লাইনে একটি pointer ঘোষণা
%p-এর বদলে %d দিয়ে ঠিকানা ছাপা৬৪ বিট ঠিকানার অর্ধেক কাটা পড়ে, ভুল সংখ্যা-Wall-এর format warning

AI-এর দেওয়া pointer-কোড হাতে পেলে নিচের প্রশ্নটি করুন। খেয়াল করুন, আমরা AI-কে কোড ঠিক করতে বলছি না — প্রতিটি pointer-এর “জীবনী” লিখতে বলছি। এতে AI নিজের ভুল প্রায়ই নিজেই ধরে ফেলে, আর আপনি মিলিয়ে দেখার মতো একটি তালিকা পান।

নিচের C কোডে যত pointer আছে, প্রতিটির জন্য একটি টেবিল বানাও: ১. pointer-টি কোথায় ঘোষণা হয়েছে, আর ঘোষণার সময় কী মান পেয়েছে (বৈধ ঠিকানা, NULL, নাকি কিছুই না)? ২. এটি কোন ভ্যারিয়েবলকে দেখায়, আর সেই ভ্যারিয়েবল কতক্ষণ বেঁচে থাকে (local, global, নাকি অন্য কিছু)? ৩. কোথাও কি এটি NULL হতে পারে? হলে * চালানোর আগে যাচাই আছে কি? ৪. কোনো ফাংশন কি local ভ্যারিয়েবলের ঠিকানা ফেরত দিচ্ছে? সহজ বাংলায় উত্তর দাও। যেখানে সমস্যা পাবে, শুধু সেই লাইন আর কারণ দেখাও — পুরো কোড আবার লিখো না। [এখানে কোড পেস্ট করুন]

Pointer-কোড যাচাইয়ের চেকলিস্ট

শেষ লাইনের -fsanitize=address হলো পর্ব ০৩-এর সেই “মেট্রো স্টেশনের সিকিউরিটি গেট” — sanitizer. Dangling pointer বা ভুল ঠিকানায় লেখার মতো অনেক ভুল এটি চলার সময় হাতেনাতে ধরে, কোন লাইনে হয়েছে তাও দেখায়। Linux আর Mac-এ GCC/Clang-এ এটি সহজেই চলে; Windows-এ MSYS2-র GCC-তে সবসময় পাওয়া যায় না।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। Pointer-কে শুধু ব্যবহার নয়, তার পেছনের নকশা বুঝতেই এই প্রশ্নগুলো।

প্র ০১Python, Java বা JavaScript-এ তো সরাসরি ঠিকানা দেখা যায় না। তাহলে C কেন মেমরির কাঁচা ঠিকানা প্রোগ্রামারের হাতে তুলে দেয়?

কারণটি C-এর জন্মকথায়। C তৈরি হয়েছিল Unix অপারেটিং সিস্টেম লেখার জন্য। একটি অপারেটিং সিস্টেমকে হার্ডওয়্যারের সাথে সরাসরি কথা বলতে হয় — নির্দিষ্ট ঠিকানায় থাকা ডিভাইসের রেজিস্টার পড়তে হয়, মেমরির কোন অংশ কোন প্রোগ্রাম পাবে সেই তালিকা বানাতে হয়, ডেটাকে নির্দিষ্ট byte-বিন্যাসে সাজাতে হয়। এসব কাজে “ঠিকানা” লুকিয়ে রাখার উপায় নেই; ঠিকানাই সেখানে মূল উপাদান।

দ্বিতীয় কারণ গতি ও স্বচ্ছতা। C-এর নকশার একটি নীতি হলো, প্রোগ্রামার যা লেখেন, মেশিনে প্রায় তা-ই ঘটে — লুকানো কোনো খরচ নেই। *p লিখলে CPU-তে প্রায় একটিমাত্র load নির্দেশ তৈরি হয়। কোনো লুকানো যাচাই নেই, কোনো লুকানো কপি নেই। বড় একটি ডেটা কাঠামো ফাংশনে পাঠাতে চাইলে পুরোটা কপি না করে শুধু ৮ byte-এর ঠিকানা পাঠানো যায়।

যে ভাষাগুলো ঠিকানা লুকায়, তারা কিন্তু ভেতরে pointer ব্যবহার করেই চলে। Java-র প্রতিটি object ভ্যারিয়েবল আসলে একটি ঠিকানা বহন করে; Python-এর প্রতিটি নাম একটি object-এর দিকে নির্দেশ করে। পার্থক্য হলো, সেই ভাষাগুলো আপনাকে ঠিকানার ওপর অঙ্ক করতে বা ইচ্ছামতো ঠিকানায় লিখতে দেয় না, আর মেমরি মুক্ত করার কাজটি garbage collection দিয়ে নিজেরাই করে। বিনিময়ে কিছু গতি আর নিয়ন্ত্রণ হারায়, পায় নিরাপত্তা।

মজার ব্যাপার হলো, এই নিরাপদ ভাষাগুলোর interpreter বা runtime-এর বড় অংশ নিজেই C বা C++-এ লেখা — CPython-এর কথা পর্ব ০১-এ বলেছিলাম। অর্থাৎ কাউকে না কাউকে কাঁচা ঠিকানা নিয়ে কাজ করতেই হয়; প্রশ্ন শুধু কোন স্তরে।

মূল উপলব্ধি: ঠিকানা লুকানো বা দেখানো — দুটিই নকশার সিদ্ধান্ত। C সিস্টেমের সবচেয়ে নিচের স্তরের ভাষা, তাই সে সত্যিটা খোলাখুলি দেখায়। এই সত্য একবার বুঝলে অন্য সব ভাষার ভেতরের কাজও আপনার কাছে স্বচ্ছ হয়ে যাবে।

প্র ০২Tony Hoare কেন null-কে “billion-dollar mistake” বললেন? আধুনিক ভাষাগুলো এই ভুল কীভাবে এড়ায়?

সমস্যাটি null-এর অস্তিত্বে নয়, বরং টাইপ সিস্টেমে তার অদৃশ্য থাকায়. C-তে int *p টাইপ দেখে বোঝার উপায় নেই p-তে একটি বৈধ ঠিকানা আছে, না NULL. দুটিরই টাইপ এক। ফলে compiler আপনাকে মনে করিয়ে দিতে পারে না “এখানে NULL যাচাই করোনি”. প্রতিটি pointer-এর প্রতিটি ব্যবহারে যাচাইয়ের দায় পুরোপুরি মানুষের স্মৃতির ওপর — আর লাখো লাইনের কোডে মানুষ কোথাও না কোথাও ভুলবেই। Java-র বিখ্যাত NullPointerException, C-এর Segmentation fault, JavaScript-এর “cannot read properties of null” — সব একই রোগের উপসর্গ।

Hoare-এর হিসাবটি আক্ষরিক নয়, বরং একটি অনুমান: কয়েক দশক ধরে কোটি কোটি প্রোগ্রামে এই একটি ধারণা থেকে যত crash, নিরাপত্তা-ত্রুটি আর ডিবাগিং-ঘণ্টা গেছে, তার মোট দাম।

আধুনিক ভাষাগুলোর সমাধান — “হয়তো-নেই”-কে আলাদা টাইপ বানানো:

  • Rust: সাধারণ reference কখনো null হতে পারে না। “থাকতেও পারে, না-ও পারে” বোঝাতে আছে Option<T> টাইপ। ভেতরের মান ব্যবহারের আগে compiler আপনাকে বাধ্য করে “নেই” অবস্থাটিও সামলাতে।
  • Kotlin, Swift: String আর String? আলাদা টাইপ। প্রশ্নবোধক চিহ্ন ছাড়া টাইপে null রাখাই যায় না।
  • TypeScript: strict মোডে null আলাদা করে লিখতে হয়, আর যাচাই ছাড়া ব্যবহার করলে compile-এর সময়ই error.

C নিজেও কিছুটা এগিয়েছে — C23 standard-এ এসেছে nullptr, যা পুরোনো NULL-এর চেয়ে টাইপের দিক থেকে স্পষ্ট। তবে মূল সমস্যা — টাইপ দেখে null-সম্ভাবনা বোঝা যায় না — C-তে রয়েই গেছে। তাই C-তে এই শৃঙ্খলা আসে অভ্যাস, ফাংশনের ডকুমেন্টেশন আর static analysis টুল থেকে।

মূল উপলব্ধি: যে নিয়ম মানুষকে মনে রাখতে হয়, সেটি একসময় ভাঙবেই; যে নিয়ম টাইপ সিস্টেম বা compiler জোর করে মানায়, সেটি টেকে। ভালো ভাষা-নকশা মানে ভুলকে অসম্ভব করে তোলা, শুধু নিষিদ্ধ নয়।

প্র ০৩NULL-এ হাত দিলে অপারেটিং সিস্টেম ঠিক কীভাবে টের পায়? প্রতিটি মেমরি ব্যবহার কি OS নিজে পাহারা দেয়?

না — প্রতিটি মেমরি ব্যবহার OS পাহারা দিলে কম্পিউটার অসহনীয় ধীর হতো। পাহারার কাজটি করে হার্ডওয়্যার, OS শুধু নিয়মটি লিখে দেয় আর বিপদ হলে ডাক পায়। এর পেছনে আছে virtual memory.

ধারণাটি এরকম: আপনার প্রোগ্রাম যে ঠিকানাগুলো দেখে (যেমন 0x7ffd…), সেগুলো আসল RAM-এর ঠিকানা নয় — প্রতিটি প্রোগ্রামের নিজস্ব “কল্পিত” ঠিকানা-জগৎ। এই জগৎ ছোট ছোট খণ্ডে (page, সাধারণত ৪ KB) ভাগ করা। OS প্রতিটি প্রোগ্রামের জন্য একটি তালিকা (page table) রাখে: “এই page আসল RAM-এর অমুক জায়গায়, পড়া যাবে, লেখা যাবে না” ইত্যাদি।

পাহারাদার: CPU-র ভেতরের MMU (Memory Management Unit) প্রতিটি মেমরি ব্যবহারের সময় এই তালিকা দেখে কল্পিত ঠিকানাকে আসল ঠিকানায় বদলায়। কাজটি হার্ডওয়্যারে হয়, আর সাম্প্রতিক অনুবাদগুলো একটি ছোট দ্রুত cache-এ রাখা থাকে, তাই প্রায় কোনো খরচ নেই।

NULL-এর ফাঁদ: OS ইচ্ছা করেই ০ ঠিকানার আশেপাশের page-গুলো কোনো প্রোগ্রামের তালিকায় রাখে না। ফলে *p যখন ০ ঠিকানা চায়, MMU তালিকায় কিছু খুঁজে পায় না এবং একটি “fault” তোলে। CPU তখন প্রোগ্রাম থামিয়ে kernel-এর একটি নির্দিষ্ট কোড চালায়। Kernel দেখে — এটি কি বৈধ কোনো ঠিকানা, যা শুধু এখনো RAM-এ আনা হয়নি? না, এটি কখনো দেওয়াই হয়নি। তখন প্রোগ্রামকে পাঠানো হয় SIGSEGV, আর প্রোগ্রাম বিশেষ ব্যবস্থা না নিলে সেখানেই শেষ।

এই একই ব্যবস্থা এক প্রোগ্রামকে আরেক প্রোগ্রামের মেমরি থেকেও আলাদা রাখে। আপনার ব্রাউজার crash করলে আপনার খোলা Word ফাইল নিরাপদ থাকে এই কারণেই। আর ASLR-ও সম্ভব হয় এই কারণে — কল্পিত ঠিকানা-জগতে কোন জিনিস কোথায় বসবে, OS যেমন খুশি ঠিক করতে পারে।

মূল উপলব্ধি: Segmentation fault শত্রু নয়, বরং হার্ডওয়্যার আর OS-এর যৌথ নিরাপত্তা-ব্যবস্থার সফল প্রতিক্রিয়া। Operating Systems কোর্সে virtual memory নিয়ে বিস্তারিত পড়লে এই ছবিটি পূর্ণ হবে।

প্র ০৪C++-এর reference আর C-এর pointer কি একই জিনিস? Java বা Python-এ “reference” বলতে কী বোঝায়?

সবগুলোর ভেতরে মূল ধারণা এক — “জিনিসটি নয়, জিনিসটির দিকে নির্দেশ।” পার্থক্য হলো কোন ভাষা প্রোগ্রামারকে কতটা স্বাধীনতা দেয়, আর কতটা বাধা।

  • C pointer: সবচেয়ে স্বাধীন। NULL হতে পারে, মান না দিয়ে রাখা যায়, পরে অন্য কাউকে দেখাতে পারে, ঠিকানার ওপর যোগ-বিয়োগও চলে (পর্ব ১৮). পুরো দায়িত্ব আপনার।
  • C++ reference (int &r = x;): তৈরির সময়ই কাউকে দেখাতে হয়, পরে আর অন্য কাউকে দেখানো যায় না, আর স্বাভাবিক নিয়মে null হয় না। ব্যবহারে * লিখতে হয় না — r = 5 লিখলেই x বদলায়। ভেতরে compiler সাধারণত এটিকে pointer দিয়েই বাস্তবায়ন করে। অর্থাৎ এটি “নিয়মে বাঁধা pointer”.
  • Java ও Python-এর reference: object ভ্যারিয়েবলে ঠিকানার মতো একটি নির্দেশ থাকে, কিন্তু সেটি দেখা বা তার ওপর অঙ্ক করা যায় না। ফাংশনে object পাঠালে নির্দেশটির কপি যায় — তাই ফাংশন object-এর ভেতরের জিনিস বদলাতে পারে, কিন্তু কলারের ভ্যারিয়েবলকে অন্য object দেখাতে পারে না। চেনা লাগছে? এটি হুবহু C-এর “pointer by value” — এই পর্বের swap-এর মতো। Java-তে তাই দুটি int-এর জন্য সাধারণ swap ফাংশন লেখা যায় না।
  • Rust-এর reference: null হয় না, আর compiler (borrow checker) প্রমাণ করে নেয় যে reference যাকে দেখায় সে reference-এর চেয়ে বেশি সময় বেঁচে থাকবে। ফলে AI-এর সেই “local-এর ঠিকানা ফেরত” ভুল Rust-এ compile-ই হয় না।

একটি বাস্তব শিক্ষা: যখন কেউ বলেন “Java-তে object pass by reference হয়”, তিনি আধা-সত্য বলছেন। আরও নিখুঁত বর্ণনা হলো “reference-এর মান কপি হয়” — আর এই পার্থক্য বোঝা যায় শুধু তখনই, যখন আপনি C-এর pointer বোঝেন।

মূল উপলব্ধি: প্রতিটি ভাষার “reference” আসলে pointer-এর ওপর কিছু নিয়ম চাপানো রূপ। নিয়ম যত কড়া, ভুলের সুযোগ তত কম, স্বাধীনতাও তত কম। C-এর pointer বুঝলে বাকি সবগুলো এক নজরে চিনতে পারবেন।

প্র ০৫একই pointer কীভাবে C-কে একসাথে এত দ্রুত আর এত বিপজ্জনক বানায়? এই দুটি কি আলাদা করা সম্ভব?

গতির উৎস:

  • কপি নয়, ঠিকানা: দশ লাখ ছাত্রের ফলাফলের তালিকা একটি ফাংশনে পাঠাতে পুরো তালিকা কপি না করে শুধু ৮ byte-এর ঠিকানা পাঠানো যায়।
  • কোনো লুকানো যাচাই নেই: *p লিখলে CPU সরাসরি সেই ঠিকানায় যায় — “p কি বৈধ?”, “p কি NULL?” — এসব যাচাইয়ে একটি নির্দেশও খরচ হয় না।
  • হার্ডওয়্যারের সাথে সরাসরি কথা: ডিভাইস ড্রাইভার, OS kernel, গেম ইঞ্জিন — যেখানে প্রতিটি byte-এর বিন্যাস নিজে ঠিক করতে হয়, সেখানে pointer অপরিহার্য।

বিপদের উৎসও ঠিক একই জায়গায়: যাচাই নেই মানে ভুল ঠিকানায় লিখলে কেউ থামাবে না। Buffer overflow, dangling pointer, use-after-free — বাস্তবের বহু গুরুতর নিরাপত্তা-ত্রুটি এই শ্রেণির। Microsoft আর Google-এর প্রকৌশলীরা প্রকাশ্যে জানিয়েছেন, তাঁদের বড় C/C++ প্রজেক্টে গুরুতর নিরাপত্তা-ত্রুটির প্রায় ৭০ শতাংশ memory safety-র ভুল।

আলাদা করা কি সম্ভব? আংশিকভাবে, হ্যাঁ — আর আধুনিক প্রকৌশলের বড় একটি অংশ এটাই খুঁজছে:

  • Compile-এর সময় প্রমাণ: Rust চালানোর সময়ের যাচাই যোগ না করে compile-এর সময় প্রমাণ করে নেয় যে কোনো reference ঝুলে থাকবে না। ফলে গতি প্রায় C-এর মতো, অনেক বিপদ ছাড়াই। দাম: ভাষাটি শেখা কঠিন, আর কিছু বৈধ কোডও compiler মানতে চায় না।
  • পরীক্ষার সময় যাচাই: AddressSanitizer-এর মতো টুল উন্নয়নের সময় সব যাচাই চালু রাখে, প্রকাশের সময় বন্ধ করে।
  • হার্ডওয়্যার-সহায়তা: কিছু নতুন CPU-তে pointer-এর সাথে বাড়তি “ট্যাগ” বা অনুমতির তথ্য রাখার গবেষণা ও প্রয়োগ চলছে, যাতে ভুল ব্যবহার হার্ডওয়্যারই ধরে।

মূল উপলব্ধি: গতি আর নিরাপত্তার দ্বন্দ্ব আসলে “যাচাই কখন হবে” — চালানোর সময়, compile-এর সময়, না কখনোই না — সেই প্রশ্ন। C “কখনোই না” বেছে নিয়েছে, আর সেই সিদ্ধান্তের দায় প্রোগ্রামারের কাঁধে দিয়েছে। আপনি C লিখলে সেই দায় আপনার — আর এজন্যই এই পর্বের অভ্যাসগুলো এত জরুরি।

অনুশীলন

খাতা-কলম নিয়ে বসুন। মেমরির বাক্স আর তীর আঁকতে আঁকতেই pointer হাতে চলে আসে।

  1. বাক্স আর তীর আঁকুন: নিচের কোড চলার পর a, b, p-এর মান কী? খাতায় বাক্স আর তীর এঁকে বের করুন, তারপর মিলিয়ে দেখুন। int a = 3, b = 8; int *p = &a; *p = *p + 10; p = &b; *p = *p * 2;

    শুরুতে p → a. *p = *p + 10 মানে a = 3 + 10 = 13. তারপর p = &b — এখানে * নেই, তাই শুধু তীরটি ঘুরল: এখন p → b, a আর বদলাবে না। *p = *p * 2 মানে b = 8 × 2 = 16. চূড়ান্ত: a = 13, b = 16, আর p-তে b-এর ঠিকানা। মূল শিক্ষা: p = … তীর বদলায়, *p = … বাক্সের মান বদলায়।

  2. ঠিকানার নাচ দেখুন: উপরের address.c প্রোগ্রামটি পরপর তিনবার চালান। প্রতিবার &x-এর মান লিখে রাখুন। কী লক্ষ করলেন? কোন অংশ বদলায়, কোন অংশ মোটামুটি একই থাকে?

    প্রতিবার ঠিকানা আলাদা আসবে — এটিই ASLR. সাধারণত শুরুর 0x7ff… অংশ মিলে যায় (stack মোটামুটি একই এলাকায় বসে), কিন্তু মাঝের অংশ এলোমেলো বদলায়। শেষ অঙ্কটি প্রায়ই একই রকম থাকে, কারণ int-এর ঠিকানা সাধারণত ৪-এর গুণিতকে বসে (alignment). শিক্ষা: প্রোগ্রামে কখনো ধরে নেবেন না যে কোনো ভ্যারিয়েবল নির্দিষ্ট ঠিকানায় থাকবে।

  3. Crash-কে বন্ধু বানান: একটি ছোট প্রোগ্রামে int *p = NULL; লিখে printf("%d\n", *p); চালান। কী মেসেজ এলো? এবার একই প্রোগ্রাম gcc -g -fsanitize=address দিয়ে (Linux/Mac বা অনলাইনে) চালান। দুটি রিপোর্টের পার্থক্য কী?

    সাধারণভাবে চালালে শুধু Segmentation fault — কোথায়, কেন, কিছুই বলে না। Sanitizer দিয়ে চালালে রিপোর্টে থাকে SEGV on unknown address 0x000000000000, সাথে কোন ফাইলের কোন লাইনে ঘটেছে তার পুরো তালিকা (stack trace). ঠিকানা 0 দেখেই বোঝা যায় এটি NULL dereference. শিক্ষা: crash হলে আগে sanitizer বা debugger দিয়ে চালান — অনুমান নয়, তথ্য দিয়ে ভুল খুঁজুন।

  4. দুটি ফল ফেরত দিন (কোড লিখুন): পর্ব ১৩-এ বলেছিলাম, return দিয়ে একটিমাত্র মান ফেরত দেওয়া যায়। এবার এমন একটি ফাংশন লিখুন — void min_max(int a, int b, int c, int *mn, int *mx) — যা তিনটি সংখ্যার ক্ষুদ্রতম আর বৃহত্তম, দুটিই main-কে জানায়।

    ফাংশনের ভেতরে প্রথমে *mn = a; *mx = a;. তারপর b আর c-এর প্রতিটির জন্য: ছোট হলে *mn-এ রাখুন, বড় হলে *mx-এ। main-এ int lo, hi; min_max(7, 2, 9, &lo, &hi); — এরপর lo = 2, hi = 9. এই কৌশলকে বলে “output parameter”. C-এর standard library-তে এটি সব জায়গায় আছে — scanf নিজেই এর সবচেয়ে পরিচিত উদাহরণ।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমPointer দিয়ে swap চলার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📦 main-এ x = 5, y = 9 তৈরি হলো
  2. 📨 swap(&x, &y) — দুটি ঠিকানার কপি গেল a আর b-তে
  3. 📥 t = *a — x-এর মান 5 রাখা হলো t-তে
  4. ✍️ *a = *b — main-এর x হলো 9
  5. ✍️ *b = t — main-এর y হলো 5
  6. 🧹 swap-এর frame মুছে গেল, কিন্তু x আর y বদলেই রইল

int x = 10; int *p = &x; — এই দুই লাইনের পর p-এর ভেতরে কী আছে?

  • 10
  • x-এর মেমরির ঠিকানা
  • p-এর নিজের ঠিকানা
  • NULL

int a = 1, b = 2; int *p = &a; p = &b; *p = 7; — শেষে a আর b কত?

  • a = 7, b = 2
  • a = 7, b = 7
  • a = 1, b = 7
  • a = 1, b = 2 — কিছুই বদলায় না

Pointer-এর টাইপ (int *, double *…) আলাদা কেন লাগে?

  • কারণ int pointer আর double pointer-এর নিজের মাপ আলাদা
  • কারণ int-এর ঠিকানা পূর্ণসংখ্যা, double-এর ঠিকানা দশমিক
  • শুধু প্রোগ্রামারের সুবিধার জন্য, মেশিনের কাছে অর্থহীন
  • ঠিকানায় গিয়ে কয়টি byte পড়তে হবে আর কীভাবে বুঝতে হবে, সেটি জানার জন্য

একটি প্রোগ্রাম Segmentation fault দিয়ে বন্ধ হলো। সবচেয়ে সম্ভাব্য কারণ কোনটি?

  • প্রোগ্রাম এমন ঠিকানায় হাত দিয়েছে, যা তাকে দেওয়া হয়নি — যেমন NULL dereference
  • কোডে একটি সেমিকোলন বাদ পড়েছে
  • Linker একটি ফাংশন খুঁজে পায়নি
  • printf-এর লেখায় বানান ভুল

scanf("%d", &n)-এ & কেন লাগে?

  • & বলে দেয় ইনপুটটি একটি সংখ্যা
  • scanf-কে n-এ লিখতে হবে, আর অন্যের ভ্যারিয়েবলে লিখতে হলে তার ঠিকানা লাগে
  • & না দিলে compile-ই হয় না, আর কোনো কারণ নেই
  • %d থাকলেই & দিতে হয়

AI একটি ফাংশন দিল: int *get(void) { int n = 5; return &n; }. এতে সমস্যা কী?

  • ফাংশন pointer ফেরত দিতে পারে না
  • &n লেখা ভুল, লিখতে হবে *n
  • ফাংশন শেষ হলেই n-এর lifetime শেষ — ফেরত দেওয়া ঠিকানা আর বৈধ নয়
  • কোনো সমস্যা নেই, চালালে 5-ই পাওয়া যাবে

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Pointer হলো এমন ভ্যারিয়েবল, যার ভেতরে থাকে আরেকটি ভ্যারিয়েবলের ঠিকানা — বাড়ি নয়, ঠিকানা লেখা চিরকুট।
  2. &x = “x-এর ঠিকানা”; *p = “p-এর ঠিকানায় যা আছে”. p = … তীর বদলায়, *p = … বাক্সের মান বদলায়।
  3. Pointer-এর টাইপ বলে ঠিকানায় গিয়ে কয়টি byte পড়তে হবে আর কীভাবে বুঝতে হবে।
  4. প্রতিটি pointer জন্মের সময়ই বৈধ ঠিকানা বা NULL পাবে; NULL হতে পারলে *-এর আগে যাচাই। NULL বা wild pointer-এ হাত দিলে সাধারণত Segmentation fault.
  5. ফাংশন দিয়ে ভ্যারিয়েবল বদলাতে ঠিকানা পাঠান — swap(&x, &y), scanf("%d", &n). আর কখনো local ভ্যারিয়েবলের ঠিকানা ফেরত দেবেন না।

পরের পর্বে: পর্ব ১৬-এ দেখেছিলেন, array-র ঘরগুলো পাশাপাশি বসে আর প্রতিটির ঠিকানা = শুরু + i × মাপ. এবার সেই সূত্রটিই pointer দিয়ে লিখব — p + 1 আসলে কত byte এগোয়, arr[i] আর *(arr + i) কেন একই, আর ফাংশনে array পাঠালে আসলে কী যায়।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান