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

pointer — ঠিকানা দিয়ে কাজ

Pointers — Working with Addresses

Taught in Bangla, this lesson explains pointers in modern C++: addresses and dereferencing, nullptr, the arrow operator, when a pointer beats a reference, pointer arithmetic, const with pointers, a glimpse of function pointers and this, and the rule that raw pointers observe while smart pointers own.

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

  • মেমরির ছবিতে pointer — ভ্যারিয়েবল, ঠিকানা, pointer আর dereference-এর সম্পর্ক
  • nullptr কেন এল, আর null হতে পারে এমন pointer নিরাপদে ব্যবহারের অভ্যাস
  • struct-এর pointer আর ->, আর pointer বনাম reference — কখন কোনটি
  • Array-তে pointer arithmetic, আর কেন আধুনিক C++ এর বদলে iterator আর std::span পছন্দ করে
  • const-এর সাথে pointer, pointer-এর pointer, function pointer, আর AI-এর লেখা pointer-কোড যাচাই

১ · Pointer — ঠিকানা রাখা একটি ভ্যারিয়েবল

পর্ব ১০-এ আপনি reference দেখেছেন — একটি object-এর দ্বিতীয় নাম। সেখানে একটি কথা বারবার এসেছে: ভেতরে compiler প্রায়ই একটি ঠিকানা পাঠায়। এই পর্বে সেই ঠিকানাটিকে সরাসরি হাতে নেবেন। যে ভ্যারিয়েবল একটি ঠিকানা রাখে, তার নাম pointer.

আগে মেমরির ছবিটা মনে করুন। কম্পিউটারের মেমরি হলো লম্বা এক সারি বাইট, আর প্রতিটি বাইটের একটি ক্রমিক নম্বর আছে — একে বলে memory address বা ঠিকানা। আপনি যখন int marks = 70; লেখেন, compiler মেমরির কোথাও ৪ বাইটের একটি বাক্স ঠিক করে, তাতে 70 রাখে, আর নাম দেয় marks. বাক্সটির একটি ঠিকানাও আছে — ধরুন 1000.

ঢাকার একটি কুরিয়ার অফিসের কথা ভাবুন। গুদামের তাকে পার্সেলগুলো রাখা — এগুলো হলো আসল জিনিস (object). ডেলিভারি-কর্মীর হাতে থাকে একটি স্লিপ, তাতে লেখা “তাক নম্বর ৪২”। স্লিপটি নিজে পার্সেল নয়, শুধু বলে পার্সেল কোথায় আছে। স্লিপ হাতে থাকলে কর্মী তাকে গিয়ে পার্সেল খুলে দেখতে পারেন, বদলাতেও পারেন। স্লিপে নতুন তাক নম্বর লিখে দিলে তিনি অন্য পার্সেলের দিকে যাবেন। আর স্লিপ ফাঁকা থাকলে কোথাও যাওয়ার নেই। Pointer হলো এই স্লিপ। আর একটি জিনিস খেয়াল করুন: স্লিপ ছিঁড়ে ফেললে পার্সেল হারায় না — স্লিপ পার্সেলের মালিক নয়। এই শেষ কথাটিই আধুনিক C++-এর pointer-নীতির মূল (অংশ ৮)।

দুটি চিহ্ন দিয়েই সব কাজ। নামের আগে & হলো address-of operator — “এর ঠিকানা দাও”। আর pointer-এর আগে * হলো dereference — “এই ঠিকানায় গিয়ে আসল জিনিসটা ধরো”।

int marks = 70;
int* p = &marks;   // p একটি "int-এর pointer"; ভেতরে marks-এর ঠিকানা
*p = 85;           // p-র ঠিকানায় গিয়ে লেখো — marks এখন 85

int* পড়ুন “pointer to int” — অর্থাৎ “এমন একটি ভ্যারিয়েবল, যা একটি int বাক্সের ঠিকানা রাখে।” টাইপটি গুরুত্বপূর্ণ: compiler জানে *p করলে ঠিকানা থেকে কত বাইট পড়তে হবে আর সেগুলোকে কী হিসেবে বুঝতে হবে। তাই double-এর ঠিকানা int*-এ রাখা যায় না — compile error.

চিহ্ন দুটির দ্বৈত অর্থে নতুনরা প্রায়ই গুলিয়ে ফেলেন, তাই একবার পরিষ্কার করে নিই। ঘোষণায় (টাইপের পাশে) * মানে “এটি একটি pointer” আর & মানে “এটি একটি reference”. Expression-এ (একটি নামের আগে) *p মানে “p-র ঠিকানার জিনিস” আর &x মানে “x-এর ঠিকানা”। এবার মেমরির ভেতরে ধাপে ধাপে দেখুন — প্রতিটি ধাপে আগে নিজে অনুমান করুন:

1int marks = 70;2int* p = &marks;3*p = 85;4int bonus = 5;5p = &bonus;6*p += 1;
মেমরির বাক্স (ঠিকানা উদাহরণমাত্র)
marks @1000
bonus @1004
p @2000
*p
  1. একটি সাধারণ বাক্স। marks নামে একটি int তৈরি হলো, ধরুন ঠিকানা 1000-এ, মান 70. এখনো কোনো pointer নেই।
  2. Pointer নিজেও একটি বাক্স। int* p = &marks; মেমরিতে নতুন একটি বাক্স বানায় (ঠিকানা 2000; 64-bit সিস্টেমে সাধারণত ৮ বাইট)। তার ভেতরে কোনো নম্বর নয়, রাখা হলো marks-এর ঠিকানা: 1000. Reference-এর সাথে এটাই প্রথম পার্থক্য — reference সাধারণত আলাদা বাক্স নয়, pointer সবসময় আলাদা বাক্স।
  3. Dereference দিয়ে লেখা। *p = 85; মানে “p-র ভেতরে যে ঠিকানা আছে, সেখানে যাও, আর 85 লেখো।” ফলে marks হলো 85. p-এর নিজের ভেতরের লেখা (1000) একটুও বদলায়নি।
  4. আরেকটি বাক্স। bonus তৈরি হলো, ঠিকানা 1004, মান 5. p এখনো marks-কেই দেখাচ্ছে।
  5. Pointer সরানো — reference যা পারে না। p = &bonus; — এবার * ছাড়া লেখা, তাই বদলায় p নিজে: তার ভেতরে এখন 1004. marks-এর কিছুই হয়নি। পর্ব ১০-এ দেখেছেন, r = y reference-কে সরায় না, মান কপি করে। Pointer-এ p = &y সত্যিই অন্য object-এর দিকে সরায় (একে বলে re-pointing বা reseat).
  6. নতুন ঠিকানায় কাজ। *p += 1; এখন যায় ঠিকানা 1004-এ — bonus হলো 6. একই লেখা *p, কিন্তু কোন বাক্সে কাজ হবে তা নির্ভর করে p-র ভেতরে সেই মুহূর্তে কোন ঠিকানা আছে তার ওপর। মনে রাখুন দুটি প্রশ্ন: p বদলাচ্ছি, না *p?
মূল ধারণা

Pointer-এর দুটি আলাদা জিনিস আছে, আর দুটিই আলাদাভাবে বদলানো যায়: pointer নিজে (তার ভেতরের ঠিকানা — p = ...) আর যে object-কে দেখায় (*p = ...)। এই পর্বের প্রায় সব কিছু — nullptr, const, dangling — এই দুই স্তরের কোনো একটির প্রশ্ন।

ঠিকানা প্রিন্ট করবেন না, সম্পর্ক প্রিন্ট করুন। std::cout << p দিলে একটি hex নম্বর আসে, যা প্রতিবার চালালে আলাদা হতে পারে (অপারেটিং সিস্টেম নিরাপত্তার জন্য প্রোগ্রামের মেমরির জায়গা এলোমেলো করে)। শেখার সময় বরং প্রশ্ন করুন: p == &marks? — “একই object কি না” এর উত্তর সবসময় একই আসে। আরেকটি সতর্কতা: int* a, b;-এ শুধু a pointer, b সাধারণ int — তাই এক লাইনে একটি ঘোষণাই ভালো।

২ · nullptr — “কাউকে দেখাচ্ছে না”

Reference সবসময় কারও নাম। Pointer-এর একটি বাড়তি অবস্থা আছে: সে কাউকেই না দেখাতে পারে। এমন pointer-কে বলে null pointer, আর আধুনিক C++-এ লেখা হয় nullptr দিয়ে:

Rider* rider = nullptr;     // অর্ডারের এখনো কোনো রাইডার নেই
int* q;                     // বিপদ! ভেতরে আবর্জনা ঠিকানা (wild pointer)

দ্বিতীয় লাইনটি খেয়াল করুন। মান না দেওয়া local pointer-এর ভেতরে থাকে মেমরিতে আগে থেকে পড়ে থাকা যেকোনো সংখ্যা — একে বলে wild pointer. সেটি হয়তো আপনার প্রোগ্রামেরই অন্য কোনো ভ্যারিয়েবলের ঠিকানা! তাই প্রথম অভ্যাস: pointer ঘোষণার সময়েই হয় বৈধ ঠিকানা, নয়তো nullptr দিন।

ফুড ডেলিভারির অর্ডার — রাইডার থাকতেও পারে, না-ও

একটি ফুড ডেলিভারি অ্যাপের কথা ভাবুন। গ্রাহক অর্ডার দিলেন, কিন্তু রাইডার ঠিক হয়নি। কয়েক মিনিট পর একজন রাইডার — জামাল ভাই — অর্ডারটি নিলেন। অর্ডারের সাথে রাইডারের সম্পর্কটি তাই ঐচ্ছিক: শুরুতে কেউ নেই, পরে কেউ আসেন, হয়তো পরে বদলেও যান। Reference দিয়ে এটা বোঝানো যায় না (reference খালি থাকে না, সরেও না), কিন্তু pointer দিয়ে স্বাভাবিকভাবে যায়। শর্ত একটাই: ব্যবহারের আগে পরীক্ষা। ধাপে ধাপে দেখুন:

Rider* rider = nullptr;অর্ডার তৈরি, রাইডার নেই
order #101
  rider = nullptr   <- কোনো ঠিকানা নেই, কাউকে দেখায় না
std::println("{}", rider->name);পরীক্ষা ছাড়া ব্যবহার
rider = nullptr
rider->name  ->  null dereference: undefined behavior
সাধারণ ফল: Segmentation fault (core dumped)
if (rider != nullptr) { ... } else { ... }আগে পরীক্ষা
rider != nullptr  ?  false
-> else শাখা: "rider not assigned yet"
প্রোগ্রাম নিরাপদে চলতে থাকে
rider = &jamal;রাইডার পাওয়া গেল
jamal  @3000  { name = "Jamal", ... }
rider = 3000   -> jamal
rider != nullptr  ?  true
-> "rider: Jamal"
jamal-এর object মুছে গেল, rider আগের মতোইসীমাবদ্ধতা
jamal  @3000  (ধ্বংস হয়ে গেছে)
rider = 3000   -> মৃত object
rider != nullptr  ?  true   <- পরীক্ষা পাস, তবু ভুল!
  1. শুরুতেই nullptr. অর্ডার তৈরি হলো, রাইডার নেই। pointer-টিকে স্পষ্টভাবে nullptr দেওয়া হলো, যাতে কেউ পরে পরীক্ষা করে বুঝতে পারেন “এখনো কেউ নেই”। আবর্জনা ঠিকানার সাথে এর পার্থক্য: nullptr পরীক্ষা করা যায়, আবর্জনা যায় না।
  2. পরীক্ষা ছাড়া ব্যবহার — undefined behavior. rider->name মানে “rider-এর ঠিকানায় গিয়ে name পড়ো” — কিন্তু ঠিকানাই নেই। একে বলে null dereference. বেশিরভাগ অপারেটিং সিস্টেমে প্রোগ্রাম তখন segmentation fault দিয়ে থেমে যায়। তবে standard-এর চোখে এটা undefined behavior — crash হওয়াও নিশ্চিত নয়।
  3. আগে পরীক্ষা, তারপর ব্যবহার। if (rider != nullptr) (বা সংক্ষেপে if (rider)) — null হলে অন্য শাখা চলে, অ্যাপ বলে “রাইডার খোঁজা হচ্ছে”। এটাই null হতে পারে এমন প্রতিটি pointer-এর নিয়ম।
  4. রাইডার পাওয়া গেল। rider = &jamal; — এবার pointer-এ বৈধ ঠিকানা, পরীক্ষা পাস, আর rider->name নিরাপদে "Jamal" দেয়। পরে অন্য রাইডার এলে শুধু rider = &sumon; — re-pointing.
  5. Null পরীক্ষার সীমা। এখন ধরুন jamal-এর object কোনো কারণে ধ্বংস হয়ে গেল, কিন্তু কেউ rider-কে nullptr করেনি। pointer-এ পুরোনো ঠিকানা রয়ে গেছে — একে বলে dangling pointer. rider != nullptr পরীক্ষা পাস করবে, অথচ ব্যবহার করলে undefined behavior. শিক্ষা: null পরীক্ষা শুধু “কেউ নেই” ধরে, “মরে গেছে” ধরে না। সেটি এড়ানোর উপায় মালিকানা আর আয়ু বোঝা — পর্ব ১৭ আর ১৮।

কেন NULL বা 0 নয়?

C++11-এর আগে null pointer লেখা হতো 0 বা NULL দিয়ে। C++-এ NULL একটি macro, যা সাধারণত পূর্ণসংখ্যা 0 (বা compiler-এর একটি বিশেষ শূন্য)। সমস্যা হলো, এটা আসলে একটি সংখ্যা। পর্ব ০৯-এর function overloading মনে করুন:

void find(int roll);            // রোল নম্বর দিয়ে খোঁজা
void find(const char* name);    // নাম দিয়ে খোঁজা

find(NULL);      // উদ্দেশ্য: "কোনো নাম নেই" — কিন্তু ডাকা হতে পারে find(int)!
find(nullptr);   // নিশ্চিতভাবে find(const char*)

find(NULL) লিখে আপনি চেয়েছিলেন pointer-টি, কিন্তু compiler দেখে একটি পূর্ণসংখ্যা — তাই হয় find(int) বেছে নেয়, নয়তো দ্বিধায় পড়ে ambiguous error দেয় (compiler-ভেদে)। nullptr-এর নিজস্ব টাইপ আছে — std::nullptr_t — যা যেকোনো pointer টাইপে বদলায়, কিন্তু int-এ বদলায় না। তাই কোনো দ্বিধা থাকে না। Template কোডে (পর্ব ২৮) এই পার্থক্য আরও গুরুত্বপূর্ণ, কারণ সেখানে 0 পাঠালে টাইপ হিসেবে ধরা হয় int, pointer নয়। ইতিহাস ভাবনার প্রশ্ন ০৩-এ।

৩ · struct-এর pointer আর ->

বাস্তব প্রোগ্রামে pointer প্রায়ই int-কে নয়, একটি struct বা class object-কে দেখায়। ধরুন একটি ক্লাস-ম্যানেজমেন্ট অ্যাপে ছাত্রদের তালিকা আছে, আর স্ক্রিনে শিক্ষক একজনকে “নির্বাচিত” করেছেন — তাঁর নম্বর বদলানো হবে। নির্বাচন বদলায়, আর শুরুতে কেউ নির্বাচিত না-ও থাকতে পারেন — দুটোই pointer-এর স্বাভাবিক কাজ:

Student* selected = nullptr;   // শুরুতে কেউ নির্বাচিত নন
selected = &list[1];           // শিক্ষক দ্বিতীয় জনকে চাপলেন
(*selected).marks += 4;        // কাজ করে, কিন্তু লিখতে ঝামেলা
selected->marks += 4;          // একই কথা, সহজ লেখা

*selected.marks লিখলে কাজ হয় না, কারণ .-এর অগ্রাধিকার *-এর চেয়ে বেশি — compiler পড়ে *(selected.marks). তাই বন্ধনী লাগে, আর এত বন্ধনী এড়াতে C থেকেই এসেছে arrow operator ->. নিয়মটি সহজ: object হাতে থাকলে ., object-এর pointer হাতে থাকলে ->. Reference হাতে থাকলেও ., কারণ reference object-এরই নাম।

এবার দুটি বাস্তব উদাহরণ একসাথে চালিয়ে দেখুন — নির্বাচিত ছাত্র, আর রাইডার থাকতে পারে বা না-ও থাকতে পারে এমন অর্ডার। (Order-এ রাইডারের নাম শুধু দেখার জন্য রাখা, তাই const std::string*.)

#include <print>
#include <string>
#include <vector>
struct Student { std::string name; int marks; };
struct Order { int id; const std::string* rider; };   // rider na-o thakte pare

void showRider(const Order& o) {
    if (o.rider != nullptr) std::println("order {}: rider {}", o.id, *o.rider);
    else                    std::println("order {}: no rider yet", o.id);
}
int main() {
    std::vector<Student> list{{"Rahim", 70}, {"Sumi", 91}, {"Karim", 58}};
    Student* selected = &list[1];           // Sumi selected
    selected->marks += 4;                   // asol Sumi bodlay
    std::println("{} {} same object? {}", list[1].name, list[1].marks, selected == &list[1]);
    selected = &list[2];                    // onno jon-ke select
    std::println("now selected: {}", selected->name);
    std::string jamal = "Jamal";
    Order a{101, nullptr}, b{102, &jamal};
    showRider(a); showRider(b);
}

আউটপুট: Sumi 95 same object? true, তারপর now selected: Karim, order 101: no rider yet আর order 102: rider Jamal. selected->marks += 4 তালিকার ভেতরের আসল Sumi-কে বদলেছে — কোনো কপি নয়, তাই selected == &list[1] সত্য। পরের লাইনে একই pointer Karim-এর দিকে সরল, যা reference দিয়ে সম্ভব হতো না। showRider ব্যবহারের আগে nullptr পরীক্ষা করে, তাই রাইডার-বিহীন অর্ডারেও প্রোগ্রাম নিরাপদ। একটি সতর্কতা: list-এ push_back করলে vector নতুন মেমরিতে সরে যেতে পারে, আর selected তখন dangling হয়ে যায় (পর্ব ১৩ আর ১৮)। দীর্ঘ সময় ধরে “নির্বাচিত” মনে রাখতে হলে pointer-এর বদলে index রাখা প্রায়ই নিরাপদ।

৪ · Pointer বনাম reference — কখন pointer সত্যিই দরকার

পর্ব ১০-এর নিয়মটি মনে করুন: যখন পারেন reference, যখন দরকার pointer. Reference সবসময় কারও নাম, কখনো সরে না, আর ব্যবহারে বাড়তি চিহ্ন লাগে না — তাই ভুলের সুযোগ কম। তাহলে pointer কখন “দরকার”? চারটি পরিস্থিতি মনে রাখুন:

১. ঐচ্ছিক সম্পর্ক

“থাকতেও পারে, না-ও” — অর্ডারের রাইডার, ছাত্রের মেন্টর, তালিকায় খোঁজার ফল। nullptr মানে “নেই”। শুধু একটি মান ফেরত দিতে হলে std::optional (পর্ব ৩৮)-ও ভাবুন।

nullable

২. পরে অন্য object-এর দিকে সরানো

“নির্বাচিত ছাত্র” বদলায়, লিংকড লিস্টের “বর্তমান ঘর” এগোয়। Reference একবার bind হলে আর সরে না; pointer সরে।

re-pointing

৩. Array আর মেমরির সারি

Built-in array-র নাম নিজেই প্রথম উপাদানের pointer হয়ে যায়; নিচু স্তরের কোডে pointer দিয়ে সারির ওপর হাঁটা হয় (অংশ ৫)।

array

৪. C লাইব্রেরি আর অপারেটিং সিস্টেম

C-তে reference নেই। fopen, strlen, SQLite বা Windows-এর ফাংশন — C API pointer নেয় আর ফেরত দেয়।

C interop

আরও একটি জায়গা আছে: container-এ রাখা। std::vector<Student&> হয় না, কিন্তু std::vector<Student*> হয় — যেমন “আজ যারা অনুপস্থিত” তালিকা, যা মূল তালিকার ছাত্রদেরই দেখায়। ফাংশনের প্যারামিটারে সিদ্ধান্তটি এভাবে নিন:

ফাংশনের প্রয়োজনস্বাক্ষরকেন
Student অবশ্যই আছে, শুধু পড়বেconst Student&null হওয়ার সুযোগ নেই, কপিও নেই
Student অবশ্যই আছে, বদলাবেStudent&একই, তবে লেখা যায়
Student না-ও থাকতে পারেconst Student* বা Student*nullptr মানে “নেই” — ভেতরে পরীক্ষা বাধ্যতামূলক
অনেকগুলো পাশাপাশি উপাদানstd::span<const int>শুরুর ঠিকানা আর আকার একসাথে (অংশ ৫)
মালিকানা হাতবদলstd::unique_ptr<Student>raw pointer মালিকানা বোঝায় না (পর্ব ১৭)
C জানলে

&, *, ->, pointer arithmetic আর array-র decay — সব C-র মতোই কাজ করে। পার্থক্য তিনটি: null-এর জন্য NULL-এর বদলে nullptr; C-তে void* থেকে যেকোনো pointer-এ নিজে থেকে বদলায়, C++-এ cast লাগে (malloc-এর ফল সরাসরি int*-এ রাখা যায় না); আর বেশিরভাগ জায়গায় pointer-এর বদলে reference, std::span বা smart pointer ব্যবহার হয়। মেমরির ছবি আর segfault-এর বিস্তারিত গল্প পড়তে C কোর্সের pointer-এর মূল কথা দেখুন।

৫ · Array, pointer arithmetic — আর কেন আধুনিক C++ এড়িয়ে চলে

পর্ব ১৩-এ দেখেছেন, array-র উপাদানগুলো মেমরিতে পাশাপাশি থাকে। তাই একটি উপাদানের ঠিকানা জানলে পরেরটির ঠিকানাও হিসাব করা যায়। Pointer-এর সাথে একটি পূর্ণসংখ্যা যোগ করাকে বলে pointer arithmetic. এর মূল নিয়ম: p + 1 মানে এক বাইট পরে নয়, পরের একটি উপাদান। int ৪ বাইট হলে ঠিকানা বাড়ে ৪, double হলে ৮ — compiler টাইপ দেখে নিজেই গুণ করে নেয়।

আরেকটি পুরোনো নিয়ম: বেশিরভাগ জায়গায় একটি built-in array-র নাম নিজে থেকেই তার প্রথম উপাদানের pointer-এ বদলে যায় — একে বলে array-to-pointer decay. তাই int* p = marks; আর int* p = &marks[0]; একই। এমনকি marks[i] আসলে *(marks + i)-এর সংক্ষিপ্ত রূপ। ধাপে ধাপে দেখুন:

1int marks[4] = {70, 58, 91, 66};2const int* p = marks;3++p;4p += 2;5++p;          // marks + 46int x = *p;   // ভুল!
marks — ঠিকানা 1000 থেকে, প্রতিটি ৪ বাইট
[0] @1000
[1] @1004
[2] @1008
[3] @1012
@1016 (শেষের পরে)
  1. পাশাপাশি চারটি বাক্স। marks-এর ৪টি int মেমরিতে গায়ে গায়ে, ঠিকানা 1000, 1004, 1008, 1012.
  2. Decay. const int* p = marks; — array-র নাম নিজে থেকে &marks[0] হয়ে গেল। p এখন 1000, *p হলো 70. (const কেন, অংশ ৬-এ দেখবেন — এখানে শুধু পড়া হবে।)
  3. এক ঘর এগোনো। ++p — ঠিকানা 1001 নয়, 1004. compiler জানে p একটি int-এর pointer, তাই এক ধাপ মানে sizeof(int) বাইট। *p এখন 58.
  4. লাফ দেওয়া। p += 2 — দুই উপাদান = ৮ বাইট, p হলো 1012, *p 66. এখানে marks[3] আর *(marks + 3) একই জিনিস।
  5. শেষের পরের ঘর — একটি বিশেষ ঠিকানা। আরেক ধাপে p হলো 1016 = marks + 4. C++ এই “শেষের ঠিক পরের” ঠিকানাটি বানানো আর তুলনা করার অনুমতি দেয় — লুপ থামাতে লাগে (p != marks + 4). কিন্তু এর চেয়ে আরও বাইরে pointer বানানোই undefined behavior.
  6. পড়লেই বিপদ। *p এখন array-র বাইরের মেমরি পড়ে — হয়তো অন্য ভ্যারিয়েবল, হয়তো আবর্জনা। undefined behavior. Compiler কোনো সতর্কতা না-ও দিতে পারে, কারণ সে আগে থেকে জানে না p কোথায় যাবে। কয়েক দশকের নিরাপত্তা-ত্রুটির (buffer overflow) একটি বড় অংশ ঠিক এই রকম ভুল।

সমস্যাটা খেয়াল করেছেন? একটি pointer জানে শুধু শুরুর ঠিকানা, কিন্তু সারিটি কতদূর পর্যন্ত — জানে না। ফাংশনে array পাঠালে decay-এর পর আকারের তথ্য হারিয়ে যায়; তাই C-তে প্রায় সব ফাংশন একটি pointer আর একটি আলাদা আকার নেয়, আর দুটি না মিললেই বিপদ। আধুনিক C++ এর তিনটি সমাধান দিয়েছে:

  • Range-for (পর্ব ০৮) — সীমা নিয়ে ভাবতেই হয় না।
  • Iterator (পর্ব ৩২) — pointer-এর মতোই ++ আর * চলে, কিন্তু যেকোনো container-এ; বেশিরভাগ STL algorithm iterator নেয়।
  • std::span (C++20) — শুরুর ঠিকানা আর আকার একটি জিনিসে বাঁধা। Array, std::array বা std::vector — যেকোনোটি পাঠানো যায়, আর span কিছুর মালিক হয় না।
#include <print>
#include <span>

int total(std::span<const int> marks) {      // shuru + akar ekshathe
    int sum = 0;
    for (int m : marks) sum += m;
    return sum;
}

int main() {
    int marks[5] = {70, 58, 91, 66, 80};
    const int* p = marks;                      // decay: &marks[0]
    std::println("*p = {}, *(p + 2) = {}", *p, *(p + 2));
    std::println("p + 2 == &marks[2]? {}", p + 2 == &marks[2]);

    int sum = 0;
    for (const int* q = marks; q != marks + 5; ++q) sum += *q;   // purono C style
    std::println("pointer loop sum = {}", sum);
    std::println("span sum = {}, end - begin = {}", total(marks), (marks + 5) - p);
}

আউটপুট: *p = 70, *(p + 2) = 91, p + 2 == &marks[2]? true, pointer loop sum = 365 আর span sum = 365, end - begin = 5. দুটি pointer বিয়োগ করলে পাওয়া যায় মাঝে কতগুলো উপাদান — বাইট নয়। Pointer-লুপে আপনাকে নিজে marks + 5 লিখতে হয়েছে; ভুল করে 6 লিখলে কোড compile হতো, আর পড়ত array-র বাইরে। total ফাংশন std::span নিয়েছে, তাই ডাকার সময় শুধু total(marks) — আকার span নিজেই জেনে নেয়। পরে std::vector পাঠালেও একই ফাংশন চলবে।

৬ · const আর pointer — ডান থেকে বামে পড়ুন

অংশ ১-এর মূল ধারণা মনে আছে? Pointer-এর দুটি স্তর — pointer নিজে, আর যে ডেটা দেখায়। const এই দুটির যেকোনোটিতে, বা দুটিতেই তালা দিতে পারে। কোনটিতে তালা, তা বোঝার সহজ কৌশল: ভ্যারিয়েবলের নাম থেকে শুরু করে ডান থেকে বামে পড়ুন, আর *-কে পড়ুন “pointer to”. ধাপে ধাপে চারটি রূপ দেখুন:

int* p = &x;p is a pointer to int
পড়া:  p  ->  *  ->  int
      "p হলো একটি pointer, int-এর দিকে"

*p = 5;    চলে   (ডেটা খোলা)
p = &y;    চলে   (pointer খোলা)
const int* p = &x;p is a pointer to int const
পড়া:  p  ->  *  ->  int  ->  const
      "p হলো একটি pointer, এমন int-এর দিকে যা const"

*p = 5;    error  (ডেটায় তালা)
p = &y;    চলে   (pointer খোলা)
int const* p — একই অর্থ, শুধু লেখার রীতি আলাদা
int* const p = &x;p is a const pointer to int
পড়া:  p  ->  const  ->  *  ->  int
      "p হলো একটি const pointer, int-এর দিকে"

*p = 5;    চলে   (ডেটা খোলা)
p = &y;    error  (pointer-এ তালা)
বানানোর সময়েই ঠিকানা দিতে হয়
const int* const p = &x;p is a const pointer to int const
পড়া:  p  ->  const  ->  *  ->  int  ->  const
      "p হলো একটি const pointer, const int-এর দিকে"

*p = 5;    error  (ডেটায় তালা)
p = &y;    error  (pointer-এ তালা)
void print(const Student* s);বাস্তবে সবচেয়ে প্রচলিত
ফাংশন কথা দিচ্ছে:  "আমি আপনার Student বদলাব না"
s->marks = 0;   error
s = nullptr;    চলে, কিন্তু শুধু ফাংশনের নিজের কপি বদলায়

তালা = শুধু এই পথ দিয়ে লেখা নিষেধ; x নিজে const নয়
  1. কোনো তালা নেই। int* p — নাম p, বামে * (pointer to), তারপর int. ডেটা বদলানো যায়, pointer-ও অন্য দিকে সরানো যায়।
  2. ডেটায় তালা — pointer to const. const int* p ডান থেকে বামে: “p is a pointer to int const”. const বসেছে *-এর বামে, তাই তালা ডেটায়। p দিয়ে x বদলানো যাবে না, কিন্তু p-কে y-এর দিকে সরানো যাবে। int const* p হুবহু একই — *-এর কোন পাশে const, সেটাই আসল।
  3. Pointer-এ তালা — const pointer. int* const p — নামের ঠিক বামে const: “p is a const pointer to int”. ডেটা বদলানো যায়, কিন্তু p সারাজীবন x-কেই দেখাবে। আচরণে এটা অনেকটা reference-এর মতো, শুধু null হতে পারে আর * লাগে।
  4. দুটিতেই তালা। const int* const p — দুটি const, দুই স্তরে। কিছুই বদলানো যায় না, শুধু পড়া যায়।
  5. বাস্তবে কোনটি বেশি? দ্বিতীয়টি — const T*. ফাংশনের প্যারামিটারে এটা একটি চুক্তি: “আমি দেখব, বদলাব না।” পর্ব ১০-এর const T&-এর মতোই, শুধু null হতে পারে। মনে রাখুন, তালাটি শুধু এই pointer-এর পথ বন্ধ করে; x নিজে const না হলে অন্য পথে সে বদলাতে পারে।

Read-only ডেটার দিকে const-হীন pointer রাখা যায় না: const int limit = 100; int* p = &limit; — compile error, কারণ নইলে *p = 0 দিয়ে তালা ভাঙা যেত। উল্টোটি স্বাভাবিক: সাধারণ int-এর ঠিকানা const int*-এ রাখা যায় — অনুমতি কমানো সবসময় নিরাপদ। const, volatile আর জটিল ঘোষণা পড়ার আরও গভীর আলোচনা C কোর্সের উন্নত পয়েন্টার পর্বে আছে।

৭ · এক ঝলক — pointer-এর pointer, function pointer আর this

তিনটি জিনিস আপনি অন্যের কোডে দেখবেন, তাই চিনে রাখা দরকার। আধুনিক C++-এ নিজে লিখবেন কম।

Pointer-এর pointer — int**

Pointer নিজেও একটি বাক্স, তাই তারও ঠিকানা আছে। সেই ঠিকানা রাখে pointer to pointer: int** pp = &p;. *pp হলো p, আর **pp হলো p যাকে দেখায়। কোথায় দেখবেন? main(int argc, char** argv)-এ (কমান্ড লাইনের লেখাগুলোর তালিকা), আর C লাইব্রেরিতে, যেখানে একটি ফাংশন আপনার pointer-টিকেই বদলে দেয় — যেমন SQLite-এর sqlite3_open(filename, &db), যা db-তে নতুন connection-এর ঠিকানা লিখে দেয়। C++-এ একই কাজ সাধারণত T*& (pointer-এর reference) বা ফেরত মান দিয়ে করা হয়। পুরো ছবি C কোর্সের pointer-এর মূল কথা-য়।

Function pointer — ফাংশনের ঠিকানা

ফাংশনের কোডও মেমরিতে থাকে, তাই তারও ঠিকানা আছে। function pointer সেই ঠিকানা রাখে, যাতে চলার সময় ঠিক করা যায় কোন ফাংশন ডাকা হবে:

int byMarks(const Student& s) { return s.marks; }
int (*key)(const Student&) = byMarks;   // পুরোনো লেখা — পড়তে কঠিন
using KeyFn = int (*)(const Student&);  // using দিয়ে সহজ নাম
KeyFn k = byMarks;
int m = k(list[0]);                     // ফাংশনের মতোই ডাকা যায়

C-র qsort বা পুরোনো GUI লাইব্রেরির “ক্লিক হলে এই ফাংশন ডেকো” (callback) — এগুলো function pointer-এর ওপর দাঁড়ানো। কিন্তু function pointer কোনো বাড়তি তথ্য (যেমন “কত নম্বরের বেশি”) সাথে বহন করতে পারে না। আধুনিক C++-এ তাই এর জায়গা নিয়েছে lambda আর std::function, যা পর্ব ৩৪-এ দেখবেন। C-তে callback-এর পুরো গল্প C কোর্সের function pointer পর্বে।

this — “আমি নিজে” pointer

পর্ব ২০-এ class লিখতে গিয়ে দেখবেন, প্রতিটি member function-এর ভেতরে একটি লুকানো pointer থাকে — this — যা সেই object-কে দেখায়, যার ওপর ফাংশনটি ডাকা হয়েছে। rahim.addBonus(5) ডাকলে ভেতরে this হলো &rahim. this কেন reference নয়, pointer? কারণ C with Classes-এ this এসেছিল reference আসার আগেই — Stroustrup নিজেই তাঁর FAQ-তে এ কথা বলেছেন।

৮ · আধুনিক নিয়ম — raw pointer দেখে, smart pointer মালিক

এ পর্যন্ত যত pointer দেখেছেন, সবগুলো এমন object-কে দেখিয়েছে, যার মালিক অন্য কেউ — vector-এর ভেতরের ছাত্র, main-এর একটি string. Pointer শুধু “দেখেছে” বা ব্যবহার করেছে। একে বলে observer pointer বা non-owning pointer. অংশ ১-এর কুরিয়ার-স্লিপের কথা মনে করুন: স্লিপ ছিঁড়লে পার্সেল হারায় না।

কিন্তু পর্ব ১৬-এ দেখবেন, new দিয়ে তৈরি object-এর কোনো স্বাভাবিক মালিক নেই — কাউকে না কাউকে শেষে delete করতেই হবে। পুরোনো C++-এ এই দায়িত্ব দেওয়া হতো একটি সাধারণ pointer-কে — owning pointer. সমস্যা হলো, Student* দেখে বোঝার উপায় নেই এটা মালিক না দর্শক। কেউ ভাবেন “আমি মুছব”, কেউ ভাবেন “অন্য কেউ মুছবে” — ফল হয় মেমরি লিক, নয়তো দুবার মোছা। আধুনিক C++ তাই কাজটা ভাগ করে দিয়েছে:

ভূমিকাযা ব্যবহার করবেনকোথায় শিখবেন
Object-এর একমাত্র মালিকstd::unique_ptr<T>, বা সরাসরি value / containerপর্ব ১৭
ভাগাভাগি করা মালিকানাstd::shared_ptr<T>পর্ব ১৭
দেখা, অবশ্যই আছেT& বা const T&পর্ব ১০
দেখা, না-ও থাকতে পারে বা সরবেT* বা const T* — raw pointerএই পর্ব
পাশাপাশি অনেকগুলো দেখাstd::span<T>, iteratorঅংশ ৫, পর্ব ৩২

এখান থেকে নিয়মটি দাঁড়ায়: আধুনিক C++ কোডে একটি raw pointer (T*) কখনো মালিক নয়। সে delete করে না, আর তার জীবনকাল সবসময় যে object দেখায় তার চেয়ে ছোট হওয়া উচিত। মালিকানা থাকে smart pointer-এ বা container-এ, যা কাজ শেষে নিজেই মেমরি ফেরত দেয় — এটাই RAII, পর্ব ১৭-এর বিষয়।

দুটি undefined behavior মনে রাখুন। (১) Null dereference — nullptr-এর ভেতরে * বা ->. (২) Dangling pointer — মরে যাওয়া object-এর ঠিকানা দিয়ে পড়া-লেখা। দ্বিতীয়টির সবচেয়ে প্রচলিত রূপ হলো local ভ্যারিয়েবলের ঠিকানা ফেরত দেওয়া: int* best() { int m = 95; return &m; } — ফাংশন শেষে m-এর stack frame মুছে যায়, ঠিকানাটি থাকে বাসিন্দাহীন। GCC এখানে -Wreturn-local-addr সতর্কতা দেয়। Undefined behavior মানে কোড “কাজ করছে” দেখালেও ভুল; compiler এমনকি আপনার পরের null-পরীক্ষাও মুছে দিতে পারে। পুরো আলোচনা পর্ব ১৮ (lifetime) আর পর্ব ৪৩ (Undefined Behavior ও নিরাপদ C++)-এ।

১৯৬৫ সালে ALGOL W ভাষা নকশা করতে গিয়ে Tony Hoare null reference যোগ করেছিলেন, কারণ “বানানো খুব সহজ ছিল।” ২০০৯ সালে লন্ডনের QCon সম্মেলনে তিনি একে বলেন তাঁর “billion-dollar mistake” — কয়েক দশকে এর কারণে যত crash আর নিরাপত্তা-ত্রুটি হয়েছে, তার মূল্য অন্তত শত কোটি ডলার। C++ null pointer রেখেছে C-র সাথে মিল আর নিচু স্তরের কাজের জন্য, কিন্তু C++11-এ এসেছে nullptr, আর আধুনিক নির্দেশিকা বলে — যেখানে null-এর কোনো অর্থ নেই, সেখানে reference ব্যবহার করুন।

পরীক্ষা ও ইন্টারভিউতে
  • const int* p আর int* const p-এর পার্থক্য? — প্রথমটিতে ডেটা বদলানো যায় না, pointer সরে; দ্বিতীয়টিতে pointer সরে না, ডেটা বদলায়। ডান থেকে বামে পড়ুন।
  • NULL-এর বদলে nullptr কেন? — NULL আসলে একটি পূর্ণসংখ্যা, তাই overload-এ f(int) বেছে নিতে পারে; nullptr-এর টাইপ std::nullptr_t, যা শুধু pointer-এ বদলায়।
  • Pointer আর reference-এর তিনটি পার্থক্য? — pointer null হতে পারে, অন্য object-এ সরানো যায়, আর তাতে arithmetic চলে; reference-এ কোনোটিই নয়।
  • int a[5]; int* p = a; হলে p + 1 কোথায়? — a[1]-এ; ঠিকানা বাড়ে sizeof(int) বাইট, এক বাইট নয়।
  • Dangling pointer কী? — যে pointer এমন object-এর ঠিকানা রাখে যা আর বেঁচে নেই; সেটি ব্যবহার undefined behavior, আর null-পরীক্ষা তা ধরতে পারে না।

৯ · AI-এর যুগে — AI-এর pointer-কোড যাচাই

Pointer-এর ভুলগুলো সবচেয়ে বিপজ্জনক, কারণ সেগুলো প্রায়ই ছোট পরীক্ষায় ধরা পড়ে না। AI টুল অনলাইনের বিপুল পরিমাণ পুরোনো C আর C++98 কোড দেখে শিখেছে, তাই তার উত্তরে পুরোনো অভ্যাস প্রায়ই ফিরে আসে। সবচেয়ে বেশি দেখা ভুলগুলো:

AI যা লেখেসমস্যাযা হওয়া উচিত
Student* s = new Student{...}; আর পরে কোথাও deleteraw owning pointer — exception বা আগেভাগে return হলে লিক; কে মুছবে অস্পষ্টসরাসরি value, std::vector, বা std::make_unique (পর্ব ১৭)
if (p == NULL), p = 0;পুরোনো রীতি; overload আর template-এ ভুল টাইপnullptr
খোঁজার ফাংশন Student* find(...), ফল সরাসরি ->না পেলে nullptr ফেরে — পরীক্ষা ছাড়া null dereferenceফল পরীক্ষা করুন, বা std::optional / reference ফেরত
int* makeArray() { int a[10]; ...; return a; }local-এর ঠিকানা ফেরত — danglingstd::vector<int> by value ফেরত
void sum(int* a, int n)আকার আলাদা — ভুল n দিলে array-র বাইরেstd::span<const int> বা const std::vector<int>&
অবশ্যই-থাকা প্যারামিটারে Student* sঅকারণে null হওয়ার সুযোগ, ভেতরে পরীক্ষা নেইStudent& বা const Student&

AI-কে কোড লিখতে বলার সময় মালিকানা আর null-এর নিয়ম আগেই বলে দিন, আর প্রতিটি pointer-এর জন্য ব্যাখ্যা চেয়ে নিন:

আমি C++23 শিখছি (g++, -Wall -Wextra). একটি ছোট ফুড ডেলিভারি প্রোগ্রাম লেখো: struct Rider { std::string name; } আর struct Order { int id; const Rider* rider; }. রাইডারদের একটি std::vector<Rider> থাকবে, আর কিছু অর্ডারে রাইডার থাকবে না। নিয়ম: ১. কোনো new/delete বা raw owning pointer ব্যবহার করবে না; মালিক হবে vector. ২. null বোঝাতে শুধু nullptr, কখনো NULL বা 0 নয়। ৩. যে pointer null হতে পারে, প্রতিটি ব্যবহারের আগে পরীক্ষা করবে। ৪. যে প্যারামিটার অবশ্যই থাকে, সেটি reference হবে, pointer নয়। ৫. কোনো ফাংশন local ভ্যারিয়েবলের ঠিকানা ফেরত দেবে না। ৬. ঠিকানা প্রিন্ট করবে না; একই object কি না দেখাতে তুলনা (==) প্রিন্ট করবে। কোডের পরে প্রতিটি pointer-এর জন্য বলো: এটি কার object দেখায়, সেই object কতক্ষণ বাঁচে, আর এটি null হতে পারে কি না। vector-এ নতুন রাইডার যোগ করলে কোন pointer বিপদে পড়বে, সেটাও বলো।

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

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

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১Reference, smart pointer, span — এত নিরাপদ বিকল্প থাকার পরেও C++ raw pointer বাদ দেয়নি কেন?

প্রথম কারণ ঐতিহাসিক, কিন্তু আজও বাস্তব: C-র সাথে সামঞ্জস্য। C++-এর জন্মই হয়েছিল “C-র সব ক্ষমতা রেখে তার ওপরে বিমূর্ততা” নীতিতে। পৃথিবীর অপারেটিং সিস্টেম, ডেটাবেস, নেটওয়ার্ক আর গ্রাফিক্সের লাইব্রেরির বড় অংশ C-তে লেখা, আর সেগুলোর সাথে কথা বলার ভাষা pointer. Linux-এর system call, Windows API, SQLite, OpenSSL — সবই pointer নেয় ও ফেরত দেয়। raw pointer বাদ দিলে C++ রাতারাতি এই বিশাল জগত থেকে বিচ্ছিন্ন হয়ে যেত।

দ্বিতীয় কারণ — নিরাপদ বিকল্পগুলো নিজেরাই pointer দিয়ে বানানো। std::vector-এর ভেতরে থাকে তিনটি pointer (শুরু, শেষ, ধারণক্ষমতার শেষ)। std::unique_ptr হলো একটি raw pointer, যার destructor delete ডাকে। std::span একটি pointer আর একটি আকার। Standard library C++-এই লেখা, তাই ভাষার ভেতরে এমন একটি নিচু স্তরের যন্ত্র থাকতেই হবে, যা দিয়ে এই নিরাপদ জিনিসগুলো গড়া যায়। এই কারণে Stroustrup প্রায়ই বলেন, C++-এর লক্ষ্য হলো উঁচু স্তরের সুবিধা দেওয়া নিচু স্তরের নিয়ন্ত্রণ না হারিয়ে।

তৃতীয় কারণ — শূন্য বাড়তি খরচ আর হার্ডওয়্যারের কাছে থাকা। ক্ষুদ্র microcontroller-এ একটি নির্দিষ্ট ঠিকানায় একটি হার্ডওয়্যার রেজিস্টার থাকে; LED জ্বালাতে সেই ঠিকানায় লিখতে হয়। Game engine বা trading system-এ লাখ লাখ object-এর মধ্যে সম্পর্ক রাখতে pointer-এর চেয়ে সস্তা কিছু নেই — ৮ বাইট, কোনো reference count নেই। Tree, graph, লিংকড লিস্টে “parent” বা “পরের ঘর” লিংক স্বাভাবিকভাবেই non-owning pointer.

তাহলে বিপদ কমানো হলো কীভাবে? বাদ না দিয়ে ভূমিকা সংকুচিত করে। আধুনিক নির্দেশিকায় raw pointer-এর কাজ শুধু “দেখা” — মালিকানা, array-র আকার, আর “অবশ্যই আছে” — এই কাজগুলো অন্য টাইপকে দেওয়া হয়েছে। ফলে কোড রিভিউতে কেউ T* দেখলে জানেন এর অর্থ কী, আর new/delete দেখলে সন্দেহ করেন।

মূল উপলব্ধি: একটি ভাষা থেকে বিপজ্জনক যন্ত্র সরিয়ে দেওয়া সবসময় সমাধান নয়, বিশেষত যখন সেটি ভিত্তি। C++ বেছে নিয়েছে “যন্ত্র রাখো, কিন্তু প্রতিদিনের কাজে নিরাপদ মোড়ক ব্যবহার করো” — আর raw pointer রেখে দিয়েছে সেই জায়গায়, যেখানে আর কিছু চলে না।

প্র ০২Tony Hoare null reference-কে কেন “billion-dollar mistake” বললেন? Null কি সত্যিই এত খারাপ, আর এর বিকল্প কী?

১৯৬৫ সালে Tony Hoare ALGOL W ভাষার জন্য একটি টাইপ-ব্যবস্থা নকশা করছিলেন, যার লক্ষ্য ছিল প্রতিটি reference-এর ব্যবহার compiler-এর পরীক্ষায় নিরাপদ রাখা। তার মধ্যে তিনি null reference যোগ করেন — তাঁর নিজের ভাষায়, কারণ এটা বানানো খুব সহজ ছিল। ২০০৯ সালে লন্ডনের QCon সম্মেলনে “Null References: The Billion Dollar Mistake” শিরোনামের বক্তৃতায় তিনি এর জন্য দুঃখ প্রকাশ করেন: এই একটি সিদ্ধান্ত পরের চল্লিশ বছরে অগণিত ত্রুটি, দুর্বলতা আর crash ঘটিয়েছে, যার খরচ সম্ভবত শত কোটি ডলারের বেশি। (Hoare আরও বিখ্যাত Quicksort আবিষ্কারের জন্য, আর তিনি ১৯৮০ সালে Turing Award পেয়েছিলেন।)

সমস্যাটা “কিছু নেই” অবস্থা থাকায় নয় — বাস্তবে সত্যিই অনেক কিছু না-ও থাকে, যেমন রাইডার-বিহীন অর্ডার। সমস্যা হলো, টাইপ-ব্যবস্থা প্রতিটি reference-কে null হওয়ার অনুমতি দিয়েছিল, আর compiler বলত না কোনটি পরীক্ষা করতে হবে। ফলে Student* দেখে বোঝার উপায় নেই এটা “অবশ্যই আছে” নাকি “না-ও থাকতে পারে”। প্রোগ্রামাররা হয় সব জায়গায় পরীক্ষা লেখেন (কোড ভারী হয়), নয়তো কোথাও ভুলে যান (crash).

C আর C++-এ ব্যাপারটা আরও গুরুতর, কারণ null dereference শুধু একটি exception নয় — undefined behavior. ২০০৯ সালে Linux kernel-এর একটি driver-এ এমন কোড পাওয়া গিয়েছিল, যেখানে pointer আগে dereference করা হয়েছিল, পরে null-পরীক্ষা। Compiler যুক্তি দিল, “আগেই dereference হয়েছে, তাই এটা null হতে পারে না” — আর পরীক্ষাটি মুছে দিল। ফল হলো একটি নিরাপত্তা-ত্রুটি।

বিকল্প হলো টাইপেই আলাদা করা: “অবশ্যই আছে” আর “না-ও থাকতে পারে” — দুটি আলাদা টাইপ। C++-এ “অবশ্যই আছে” বোঝাতে reference আর gsl-এর not_null, ঐচ্ছিক মান বোঝাতে std::optional (পর্ব ৩৮)। Rust-এ সাধারণ reference কখনো null হয় না, আর ঐচ্ছিক হলে Option<&T> — compiler আপনাকে দুটি অবস্থাই সামলাতে বাধ্য করে। Kotlin আর Swift-এ String আর String? আলাদা টাইপ।

মূল উপলব্ধি: “null আছে” ভুল নয়; ভুল হলো “সবকিছুই null হতে পারে” আর compiler নীরব থাকা। তাই আপনার নিজের কোডে যেখানে null-এর কোনো অর্থ নেই, সেখানে reference ব্যবহার করে compiler-কে সেই নিশ্চয়তা দিয়ে দিন।

প্র ০৩C++ তো বহু বছর 0 আর NULL দিয়ে চলছিল। C++11-এ আলাদা একটি keyword nullptr আনার এত দরকার পড়ল কেন?

C-তে NULL প্রায়ই সংজ্ঞায়িত হয় ((void*)0) হিসেবে — একটি pointer. কিন্তু C++-এ void* নিজে থেকে অন্য pointer-এ বদলায় না, তাই সেই সংজ্ঞা চলত না। ফলে C++-এ NULL হয়ে গেল একটি পূর্ণসংখ্যা শূন্য — 0 বা 0L, অথবা GCC-র মতো compiler-এর বিশেষ __null. Stroustrup নিজেও বহু বছর পরামর্শ দিতেন সরাসরি 0 লিখতে, কারণ NULL লিখলে মনে হয় pointer, অথচ আসলে int.

এর ফলে overloading-এ বিভ্রান্তি। void find(int) আর void find(const char*) থাকলে find(NULL) লিখে প্রোগ্রামার চান pointer-টি, কিন্তু 0 সবার আগে একটি int — তাই find(int) ডাকা হয়, বা 0L হলে ambiguous error হয়। আরও খারাপ অবস্থা template-এ। ধরুন একটি template ফাংশন তার argument অন্য ফাংশনে পাঠিয়ে দেয় (forwarding). আপনি 0 পাঠালেন — template টাইপ ধরে নেয় int. ভেতরে সেই int আর null pointer constant নয়, শুধু একটি সাধারণ int, যা pointer-এ বদলায় না — compile error. অর্থাৎ “null” বোঝানোর তথ্যটি পথে হারিয়ে যায়।

Herb Sutter আর Bjarne Stroustrup ২০০০-এর দশকের মাঝামাঝি standard committee-তে একটি প্রস্তাব দেন: null pointer-এর জন্য আলাদা একটি keyword, যার নিজস্ব টাইপ থাকবে। সেটি C++11-এ গৃহীত হয় — nullptr, টাইপ std::nullptr_t. এই টাইপ যেকোনো pointer-এ (আর member pointer-এ) বদলায়, bool-এ শুধু সরাসরি পরীক্ষায় (if (p)) — কিন্তু int-এ বদলায় না। তাই overload সবসময় pointer-টি বাছে, আর template দিয়ে পাঠালেও টাইপটি std::nullptr_t থাকে, তথ্য হারায় না।

লক্ষ্য করুন, C++ NULL বা 0 নিষিদ্ধ করেনি — পুরোনো কোটি লাইন কোড ভাঙত। শুধু ভালো বিকল্প দিয়েছে, আর clang-tidy-র modernize-use-nullptr-এর মতো টুল পুরোনো কোড স্বয়ংক্রিয়ভাবে বদলে দেয়। মজার ব্যাপার, C ভাষাও পরে একই সিদ্ধান্তে পৌঁছেছে: C23-এ nullptr যোগ হয়েছে।

মূল উপলব্ধি: একটি ধারণার (null pointer) নিজস্ব টাইপ না থাকলে তা অন্য ধারণার (শূন্য সংখ্যা) সাথে গুলিয়ে যায়। ভালো নকশা প্রতিটি আলাদা অর্থকে আলাদা টাইপ দেয়, যাতে compiler পার্থক্যটা বুঝতে ও পাহারা দিতে পারে।

প্র ০৪Pointer “মালিক” না “দর্শক” — compiler তো এই পার্থক্য জানে না। তাহলে C++ Core Guidelines-এর gsl::owner আর gsl::not_null কী কাজে লাগে?

সমস্যাটা আগে পরিষ্কার করি। Student* s — compiler-এর কাছে এটা শুধু একটি ঠিকানা। কিন্তু মানুষের কাছে তিনটি প্রশ্ন থেকে যায়: (১) শেষে আমাকে কি delete করতে হবে? (২) এটা কি null হতে পারে? (৩) এটা কি একটি object দেখায়, নাকি একটি array-র শুরু? টাইপ এর কোনোটির উত্তর দেয় না। পুরোনো কোডবেসে এই উত্তরগুলো থাকত মন্তব্যে বা প্রোগ্রামারদের স্মৃতিতে — আর সেখান থেকেই লিক, দুবার মোছা আর null crash.

২০১৫ সালে CppCon-এ Bjarne Stroustrup আর Herb Sutter C++ Core Guidelines ঘোষণা করেন — আধুনিক C++ লেখার একটি উন্মুক্ত নির্দেশিকা। এর মূল নীতির একটি: একটি raw pointer (T*) মালিক নয় (নিয়ম R.3). মালিকানা প্রকাশ করা হবে unique_ptr, shared_ptr বা container দিয়ে। আর যেখানে পুরোনো কোড বা C API-র কারণে raw pointer-কেই মালিক হতে হয়, সেখানে লিখুন gsl::owner<T*>.

gsl::owner আসলে কিছুই করে না — এটা শুধু একটি type alias, T*-এরই আরেক নাম। প্রোগ্রাম চলার সময় কোনো পার্থক্য নেই। তাহলে লাভ? এটা একটি লেবেল, যা মানুষ আর static analysis টুল দুজনেই পড়তে পারে। টুল তখন পরীক্ষা করতে পারে: প্রতিটি owner কি কোথাও delete হচ্ছে? কেউ কি সাধারণ T*-কে delete করছে (যা নিয়ম ভাঙে)? অর্থাৎ ভাষা না বদলেই উদ্দেশ্যটি দৃশ্যমান হলো।

gsl::not_null<T*> দ্বিতীয় প্রশ্নের উত্তর দেয়। এটা একটি ছোট মোড়ক, যা nullptr দিয়ে বানাতে গেলে আটকায় — সরাসরি nullptr লিখলে compile error, আর চলার সময় null এলে Microsoft-এর GSL implementation পরীক্ষা করে প্রোগ্রাম থামিয়ে দেয়। ফাংশন not_null<Student*> নিলে ভেতরে আর null-পরীক্ষা লাগে না, আর ডাকার জায়গাতেই ভুল ধরা পড়ে। তৃতীয় প্রশ্নের উত্তর — “এটা কি array?” — দিয়েছে gsl::span, যা পরে C++20-এ std::span হিসেবে standard-এ ঢুকেছে। এমন একটি “দর্শক” pointer-কে টাইপে আলাদা করার জন্য observer_ptr নামে একটি প্রস্তাবও Library Fundamentals TS-এ এসেছিল, তবে মূল standard-এ জায়গা পায়নি — অনেকে যুক্তি দেন, নিয়ম মানলে সাধারণ T*-ই তো দর্শক।

মূল উপলব্ধি: টাইপ শুধু compiler-এর জন্য নয়, পাঠকের জন্যও। একটি ঠিকানার পেছনে “মালিকানা”, “null হতে পারে কি না”, “কতগুলো উপাদান” — এই তথ্যগুলো টাইপে লিখে দিলে ভুল কমে, কারণ তখন টুল আর রিভিউয়ার দুজনেই সেগুলো দেখতে পান।

প্র ০৫Java-র reference আর Rust-এর borrow pointer-এর এই সমস্যাগুলো কীভাবে সামলায়? C++-এর পথের সাথে তুলনা কী?

তিনটি ভাষা একই প্রশ্নের তিনটি ভিন্ন উত্তর দিয়েছে: “কেউ একটি object দেখছে — object-টি কি তখনো বেঁচে আছে, আর কে তাকে মুছবে?”

Java: Java-র প্রতিটি object ভ্যারিয়েবল আসলে একটি reference — C++-এর ভাষায় প্রায় একটি pointer, তবে arithmetic নেই। মোছার দায়িত্ব কারও নয়: garbage collector দেখে কোনো reference আর object-টিকে দেখাচ্ছে কি না, আর না দেখালে মুছে দেয়। তাই Java-তে dangling pointer প্রায় অসম্ভব — যতক্ষণ কেউ দেখছে, object বাঁচে। কিন্তু null রয়ে গেছে: যেকোনো reference null হতে পারে, আর ভুল করলে NullPointerException — বছরের পর বছর Java-র সবচেয়ে পরিচিত ত্রুটিগুলোর একটি। দাম: GC-র জন্য বাড়তি মেমরি ও সময়, আর কখন মেমরি ফেরত যাবে তার নিশ্চয়তা নেই। Java 8-এ Optional এসেছে, তবে পুরোনো API-র null থেকে মুক্তি দেয়নি।

Rust: Rust GC ছাড়াই dangling আর null দুটোই compile-এর সময় আটকায়। সাধারণ reference (&T, &mut T) কখনো null নয়; ঐচ্ছিক হলে Option<&T>, আর compiler দুটি অবস্থা সামলাতে বাধ্য করে। (মজার তথ্য: Option<&T> মেমরিতে একটি pointer-এর সমান জায়গাই নেয় — ভেতরে “None” মানে শূন্য ঠিকানা, কিন্তু প্রোগ্রামার তাতে ভুল করে ঢুকতে পারেন না।) আর borrow checker প্রমাণ করে যে কোনো reference তার object-এর চেয়ে বেশি দিন বাঁচবে না, আর একই সময়ে একটি object বদলানোর অধিকার একজনেরই। অর্থাৎ অংশ ৩-এর “vector-এ push_back করলে selected dangling” — এই ভুল Rust-এ compile-ই হয় না। দাম: শেখা কঠিন, আর কিছু বৈধ নকশাও compiler-কে বোঝাতে ঝামেলা। নিচু স্তরের কাজের জন্য Rust-এও raw pointer আছে — শুধু unsafe block-এর ভেতরে।

C++: C++ এর মাঝামাঝি পথে — GC নেই (খরচ ও নিয়ন্ত্রণের কারণে), আর borrow checker-ও নেই (পুরোনো কোড ভাঙবে বলে)। এর বদলে আছে নিয়ম আর টাইপ: মালিকানা smart pointer-এ, “অবশ্যই আছে” reference-এ, “না-ও থাকতে পারে” pointer বা optional-এ, সারি span-এ। আর আছে টুল: sanitizer, static analyzer, compiler warning. নিরাপত্তা আসে শৃঙ্খলা থেকে, compiler-এর প্রমাণ থেকে নয়। এই কারণেই সাম্প্রতিক বছরগুলোতে বিভিন্ন সরকারি সংস্থা ও বড় প্রতিষ্ঠান memory-safe ভাষার দিকে ঝোঁকার কথা বলেছে, আর C++ committee “profiles”-এর মতো প্রস্তাব নিয়ে কাজ করছে।

মূল উপলব্ধি: দর্শক-সমস্যার সমাধান হয় চলার সময়ে (GC), compile-এর সময়ে (borrow checker), অথবা নিয়ম ও টুলে (আধুনিক C++). প্রতিটির দাম আলাদা। C++-এ আপনি নিজেই borrow checker — তাই প্রতিটি pointer দেখে জিজ্ঞেস করুন: “এর object কে বাঁচিয়ে রাখছে?”

অনুশীলন

বেশিরভাগ কাজে কোড লিখতে হবে না — আগে অনুমান করুন, তারপর উত্তর মিলিয়ে নিন।

  1. আউটপুট অনুমান: int a = 10, b = 20; int* p = &a; *p += 5; p = &b; *p = *p * 2; int* q = p; *q -= 1; — শেষে a, b, *p কত, আর p == q কি সত্য?

    a = 15, b = 39, *p = 39, আর p == q সত্য। প্রথমে p দিয়ে a হলো 15. তারপর p সরল b-তে, b হলো 40. q = p pointer কপি করে — এখন দুটি pointer একই b-কে দেখায়, তাই *q -= 1 b-কে 39 করে, আর *p-ও 39 দেখায়। pointer কপি করলে object কপি হয় না।

  2. const পড়ুন: প্রতিটি ঘোষণায় *p = 1; আর p = &y; চলবে কি না বলুন — (ক) int const* p = &x; (খ) int* const p = &x; (গ) const int* const p = &x; (ঘ) const int x = 5; int* p = &x;

    (ক) pointer to const int: *p = 1 error, p = &y চলে। (খ) const pointer: *p = 1 চলে, p = &y error. (গ) দুটোই error. (ঘ) ঘোষণাটিই compile error — const int-এর ঠিকানা const-হীন int*-এ রাখা যায় না, নইলে তালা ভাঙা যেত; লিখতে হবে const int* p.

  3. বাগ শিকার: একটি AI এই কোড দিয়েছে — Student* findTopper(std::vector<Student> list) { Student* best = NULL; for (auto& s : list) if (!best || s.marks > best->marks) best = &s; return best; } আর ডাকার জায়গায় std::println("{}", findTopper(students)->name);. কয়টি সমস্যা খুঁজে পান?

    তিনটি। (১) সবচেয়ে বড়: list by value — ফাংশনের নিজস্ব কপি। best সেই কপির ভেতরের উপাদান দেখায়, আর ফাংশন শেষে কপি মরে যায় — ফেরত দেওয়া pointer dangling. সমাধান: const std::vector<Student>& list আর ফেরত const Student*. (২) NULL-এর বদলে nullptr. (৩) তালিকা খালি হলে nullptr ফেরে, অথচ ডাকার জায়গায় পরীক্ষা ছাড়াই ->name — null dereference. আগে ফল একটি ভ্যারিয়েবলে রেখে পরীক্ষা করুন।

  4. নিজে লিখুন (কোড): উপরের walk_array.cpp-এ একটি ফাংশন যোগ করুন, যা একটি std::span<const int> নিয়ে সবচেয়ে বেশি নম্বরটির pointer ফেরত দেবে, আর span খালি হলে nullptr. main-এ ফলটি পরীক্ষা করে প্রিন্ট করুন, আর দেখান যে pointer-টি &marks[2]-এর সমান।

    একটি সমাধান: const int* highest(std::span<const int> v) { if (v.empty()) return nullptr; const int* best = &v[0]; for (const int& m : v) if (m > *best) best = &m; return best; }. লুপে const int& নেওয়া জরুরি — int m নিলে &m হতো লুপের local কপির ঠিকানা, যা পরের ধাপেই মরে যায়। main-এ: if (const int* h = highest(marks)) std::println("max = {}, at index 2? {}", *h, h == &marks[2]); — আউটপুট max = 91, at index 2? true. span মালিক নয়, তাই ফেরত দেওয়া pointer নিরাপদ যতক্ষণ marks বেঁচে আছে। (STL-এ এই কাজের তৈরি সমাধান std::ranges::max_element — পর্ব ৩৩।)

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

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

ক্রমঅর্ডারের রাইডারের মতো ঐচ্ছিক একটি সম্পর্ক pointer দিয়ে নিরাপদে সামলানোর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Pointer ঘোষণা করে শুরুতেই nullptr দিন
  2. রাইডার পাওয়া গেলে তাঁর object-এর ঠিকানা (&rider) pointer-এ রাখুন
  3. ব্যবহারের আগে if (p != nullptr) দিয়ে পরীক্ষা করুন
  4. পরীক্ষা পাস হলে p->name দিয়ে তথ্য পড়ুন
  5. রাইডারের object মুছে যাওয়ার আগে pointer-টিকে আবার nullptr করুন

C++-এ 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
  • Compile error

C++-এ const int* p = &x; ঘোষণার পর কোনটি বৈধ?

  • *p = 10;
  • p = &y;
  • *p = 10; আর p = &y; দুটোই
  • কোনোটিই নয়

আধুনিক C++-এ null pointer বোঝাতে NULL-এর বদলে nullptr ব্যবহারের প্রধান কারণ কী?

  • nullptr দ্রুত চলে
  • nullptr dereference করলে crash হয় না
  • C++11 থেকে NULL লিখলে compile error হয়
  • nullptr-এর নিজস্ব টাইপ আছে, তাই overload ও template-এ int-এর সাথে গুলিয়ে যায় না

C++-এ int arr[4] = {5, 6, 7, 8}; int* p = arr; p += 2; — এখন *p কত?

  • 7
  • 6
  • 8
  • আবর্জনা — ২ বাইট এগিয়ে int-এর মাঝখানে পড়ে

একটি C++ ফাংশনের প্যারামিটার সবসময় একটি বৈধ Student পাবে, আর ফাংশনটি শুধু তা পড়বে। সবচেয়ে উপযুক্ত টাইপ কোনটি?

  • Student*
  • const Student*
  • const Student&
  • std::unique_ptr<Student>

C++-এ int* best() { int m = 95; return &m; } ফাংশনের ফেরত দেওয়া pointer দিয়ে *p পড়লে কী হয়?

  • সবসময় 95 পাওয়া যায়
  • Undefined behavior — pointer একটি মরে যাওয়া ভ্যারিয়েবলের ঠিকানা রাখে
  • সবসময় compile error হয়
  • Pointer নিজে থেকে nullptr হয়ে যায়

শব্দকোষ

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

মনে রাখুন

  1. Pointer = ঠিকানা রাখা একটি আলাদা ভ্যারিয়েবল। &x ঠিকানা দেয়, *p সেখানকার object-এ পৌঁছায়; p = ... pointer সরায়, *p = ... object বদলায়।
  2. Null-এর জন্য শুধু nullptr, আর null হতে পারে এমন pointer প্রতিবার ব্যবহারের আগে পরীক্ষা। struct-এর pointer-এ member পেতে ->.
  3. যখন পারেন reference, যখন দরকার pointer — ঐচ্ছিক সম্পর্ক, re-pointing, array আর C API-তে।
  4. const ডান থেকে বামে পড়ুন: const int* — ডেটায় তালা, int* const — pointer-এ তালা। Array-তে pointer arithmetic-এর বদলে range-for, iterator বা std::span.
  5. Raw pointer দেখে, smart pointer মালিক। Null বা dangling pointer দিয়ে পড়া-লেখা undefined behavior; local-এর ঠিকানা কখনো ফেরত নয়।

পরের পর্বে: এ পর্যন্ত pointer শুধু অন্যের object দেখেছে। কিন্তু প্রোগ্রাম চলার সময় যদি নতুন object বানাতে হয়, যার আকার আগে জানা নেই? new আর delete দিয়ে heap মেমরি নেওয়া — আর কেন আধুনিক C++ এগুলো নিজে লিখতে নিষেধ করে।

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

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