এই পর্বে যা শিখবেন
- Reference আসলে কী — মেমরির ছবিতে নাম আর বাক্সের সম্পর্ক
- Pass by value আর pass by reference — কখন কোনটি, আর কেন
const T&এত প্রচলিত - Range-for লুপে
auto&আরconst auto&— কখন আসল উপাদান বদলায়, কখন কপি - Reference ফেরত দেওয়া, dangling reference-এর বিপদ আর temporary-র আয়ু বাড়ার নিয়ম
- Reference বনাম pointer, আর AI-এর লেখা কোডে reference-এর ভুল ধরা
১ · Reference — একই বাক্সের দ্বিতীয় নাম
আগের পর্বে আপনি ফাংশন লিখেছেন, আর দেখেছেন ফাংশনকে মান পাঠানো যায়। কিন্তু একটি প্রশ্ন তখন উত্তর পায়নি: ফাংশন কি আপনার পাঠানো আসল ভ্যারিয়েবলটি বদলাতে পারে? আর এক লাখ ছাত্রের তালিকা পাঠালে কি পুরো তালিকাটা কপি হয়? এই দুই প্রশ্নের উত্তর এই পর্বের বিষয় — reference.
আপনার এক বন্ধুর ভালো নাম “মাহমুদুল হাসান”, কিন্তু বাসায় সবাই ডাকে “রাজু”। মা যদি বলেন “রাজুকে ভাত খেতে ডাকো”, তাহলে যে মানুষটি খেতে আসেন, তিনিই মাহমুদুল হাসান। দুই নাম, একজন মানুষ। রাজুর জ্বর হলে মাহমুদুল হাসানেরও জ্বর — কারণ তাঁরা আলাদা কেউ নন। Reference হলো এমন একটি ডাকনাম। এটি নতুন কোনো মানুষ (নতুন বাক্স) তৈরি করে না, শুধু আগে থেকে থাকা একজনকে আরেক নামে ডাকার সুযোগ দেয়।
C++-এ লেখা হয় এভাবে — টাইপের পরে একটি & চিহ্ন:
int marks = 70; // একটি বাক্স, নাম marks
int& r = marks; // r হলো marks-এরই আরেক নাম — নতুন বাক্স নয়
r = 85; // marks এখন 85int& পড়ুন “reference to int” বা “int-এর reference” — অর্থাৎ “একটি int বাক্সের আরেক নাম।” এমন ডাকনামকে ইংরেজিতে বলে alias. খেয়াল করুন, এখানে & চিহ্নটি টাইপের অংশ হিসেবে বসেছে। একই চিহ্ন অন্য জায়গায় অন্য কাজ করে — &marks (নামের আগে) মানে marks-এর মেমরি ঠিকানা, যা পর্ব ১৫-এ pointer-এর সাথে দেখবেন। চিহ্ন এক, অর্থ জায়গা অনুযায়ী আলাদা।
মেমরির ভেতরে আসলে কী ঘটে, ধাপে ধাপে দেখুন। প্রতিটি ধাপে নিজে আগে অনুমান করুন, তারপর “পরে” চাপুন:
1int marks = 70;2int& r = marks;3r = 85;4marks += 5;5int copy = marks;6copy = 0;
| marks @1000 | |
| r | |
| copy @1004 |
- একটি বাক্স।
marksনামে একটিintবাক্স তৈরি হলো, ধরুন ঠিকানা 1000-এ, মান 70. - দ্বিতীয় নাম — কোনো নতুন বাক্স নয়।
int& r = marks;মেমরিতে নতুন কোনো int বানায় না।rশুধু ঠিকানা 1000-এর আরেকটি লেবেল।&rআর&marksপ্রিন্ট করলে হুবহু একই ঠিকানা আসবে। - Reference দিয়ে লেখা।
r = 85;— এটাr-কে নতুন কিছু বানায় না, সরাসরি ঠিকানা 1000-এর বাক্সে 85 লেখে। তাইmarks-এর মানও এখন 85. দুটি নাম, একটিই মান। - আসল নাম দিয়ে লেখা। উল্টো দিক থেকেও একই কথা:
marks += 5;করলেrদিয়ে পড়লেও 90 পাবেন। কোনটি “আসল” আর কোনটি “ডাকনাম”, বানানোর পর আর কোনো পার্থক্য থাকে না। - এবার একটি সত্যিকারের কপি।
&ছাড়াint copy = marks;লিখলে মেমরিতে নতুন বাক্স তৈরি হয় (ঠিকানা 1004), আর তাতে মানটি কপি হয়: 90. দুটি বাক্স এখন আলাদা জীবন যাপন করবে। - কপি বদলালে আসলটি বদলায় না।
copy = 0;শুধু 1004 বদলায়;marksআরrদুটোই 90-তেই থাকে। এক লাইনের পার্থক্য — একটি&— ঠিক করে দেয় আপনি একই জিনিসে কাজ করছেন, না তার ফটোকপিতে।
Reference নিজে কোনো মান রাখে না — এটি অন্য একটি object-এর (মেমরিতে থাকা একটি বাক্সের) নাম। Reference-এর ওপর যা করবেন — পড়া, লেখা, ঠিকানা নেওয়া — সব আসলে ঘটে সেই object-এর ওপর। তাই প্রশ্ন সবসময় একটাই: এই reference কোন বাক্সের নাম, আর সেই বাক্সটি কি এখনো বেঁচে আছে? শেষ প্রশ্নটির গুরুত্ব বুঝবেন অংশ ৬-এ।
২ · Reference-এর তিনটি কঠিন নিয়ম
ডাকনামের সাথে মিল রেখেই reference-এর নিয়মগুলো তৈরি। নিয়ম তিনটি মুখস্থ নয়, বুঝে রাখুন — কারণ pointer-এর সাথে পার্থক্য এখান থেকেই শুরু।
১. জন্মের সময়েই কার নাম
int& r; — compile error. “কারও না” এমন ডাকনাম হয় না; বানানোর মুহূর্তেই বলে দিতে হয় কোন বাক্সের নাম। একে বলে reference-কে bind করা।
২. একবার bind, চিরকাল bind
একবার marks-এর নাম হলে r সারাজীবন marks-এরই নাম। পরে অন্য ভ্যারিয়েবলের দিকে সরানো (reseat) যায় না।
৩. null reference নেই
Pointer-এর মতো “কিছুই দেখাচ্ছে না” অবস্থা reference-এর নেই। বৈধ C++-এ প্রতিটি reference কোনো না কোনো object-এর নাম।
সবসময় কারও নামদ্বিতীয় নিয়মের ফাঁদ — r = y কী করে?
নতুনরা প্রায়ই ভাবেন, r = y; লিখলে r এবার y-এর নাম হয়ে যাবে। হয় না। কারণ reference-এর ওপর যা লেখেন, সব যায় আসল বাক্সে। তাই r = y; মানে “y-এর মানটি r যে বাক্সের নাম, সেখানে কপি করো।”
int rahim = 70, karim = 58;
int& r = rahim; // r এখন rahim-এর নাম
r = karim; // reseat নয়! rahim = 58 হলো
karim = 99; // r এখনো rahim-এর নাম, তাই r == 58শেষে rahim = 58, r = 58, karim = 99. রহিমের নম্বর বদলে করিমের নম্বর বসে গেল — আর আপনি হয়তো ভেবেছিলেন শুধু “নাম সরাচ্ছেন”। বাস্তব প্রোগ্রামে এমন ভুল ধরা কঠিন, কারণ কোড দিব্যি compile হয়।
তৃতীয় নিয়ম — “null reference” কেন হয় না
কেউ যদি জোর করে এমন লেখেন — একটি খালি pointer থেকে reference বানানো (পর্ব ১৫-এ pointer দেখবেন) — compiler হয়তো আটকাবে না, কিন্তু সেই মুহূর্তেই প্রোগ্রাম undefined behavior-এ ঢুকে যায়। Standard-এর চোখে সেটা আর বৈধ C++ নয়। তাই ফাংশন যখন Student& নেয়, ভেতরে “এটা খালি কি না” পরীক্ষা করতে হয় না — খালি হওয়ার উপায়ই নেই। এই নিশ্চয়তাই reference-কে pointer-এর চেয়ে নিরাপদ করে।
লেখার রীতি: int& r, int &r আর int & r — compiler-এর কাছে তিনটিই এক। আধুনিক C++-এ বেশিরভাগ দল int& r লেখেন, কারণ & টাইপের অংশ। তবে এক লাইনে দুটি ঘোষণা সাবধানে: int& a = x, b = y;-এ শুধু a reference, b সাধারণ int! তাই এক লাইনে একটি ঘোষণাই ভালো।
৩ · ফাংশনে reference — আসলটি বদলানো
Reference-এর আসল কাজ শুরু হয় ফাংশনের সাথে। পর্ব ০৯-এ আপনি যেভাবে ফাংশনে মান পাঠিয়েছেন, তাকে বলে pass by value: ফাংশন পায় মানটির একটি কপি। ফাংশনের ভেতরে কপি যত খুশি বদলাক, বাইরের আসল ভ্যারিয়েবল ছোঁয়া পড়ে না। এটা প্রায়ই ভালো — ফাংশন ভুল করে আপনার ডেটা নষ্ট করতে পারে না।
কিন্তু কখনো কখনো আমরা চাই ফাংশন আসলটিই বদলাক। ধরুন শিক্ষক বোর্ড পরীক্ষার পর সবাইকে ৫ নম্বর গ্রেস দেবেন। addBonus(rahim) ডাকার পর রহিমের নম্বর সত্যিই বাড়া উচিত। এর জন্য প্যারামিটারকে reference বানান — একে বলে pass by reference. দুটি ভার্সন পাশাপাশি চালিয়ে দেখুন, প্রতিটি ফাংশন ডাকলে মেমরিতে একটি stack frame (ফাংশনের নিজস্ব কাজের জায়গা) তৈরি হয়:
[ main ]
rahim @1000 = 70
[ main ] rahim @1000 = 70 [ byValue ] m @2000 = 70 <- নতুন বাক্স, মান কপি
[ main ] rahim @1000 = 70 <- একটুও বদলায়নি [ byValue ] (শেষ, frame মুছে গেছে) m @2000 = 75 -> হারিয়ে গেল
[ main ]
rahim @1000 = 70
[ byRef ]
m -> @1000 <- নতুন int নেই, m হলো rahim-এর নাম
[ main ] rahim @1000 = 75 <- বদলে গেছে, থেকেও গেছে [ byRef ] (শেষ, শুধু নামটি মুছল)
- শুরু।
main-এ রহিমের নম্বর 70, ঠিকানা 1000-এ। - Pass by value — কপি তৈরি।
byValue(rahim)ডাকলেbyValue-এর নিজের frame-এmনামে একটি নতুন int তৈরি হয়, আর তাতে 70 কপি হয়। এখন মেমরিতে দুটি 70 — দুটি আলাদা বাক্সে। - কপি বদলাল, আসলটি নয়।
m += 5করে কপিটি 75 হলো। ফাংশন শেষ হতেই frame মুছে যায়, সাথে 75-ও। রহিম রইলেন 70-এই। অনেক নতুন প্রোগ্রামার এখানে অবাক হন: “ফাংশন তো ৫ যোগ করল!” — হ্যাঁ, করল, কিন্তু ফটোকপিতে। - Pass by reference — কপি নেই।
byRef(int& m)-এmনতুন বাক্স নয়; ডাকার মুহূর্তে এটিrahim-এর ডাকনাম হয়ে যায়। ফাংশনের ভেতরেmবলা মানেই বাইরেরrahimবলা। - আসল বাক্সে লেখা।
m += 5সরাসরি ঠিকানা 1000-এ লেখে। ফাংশন শেষ হলে শুধু নামটি (m) হারায়, বাক্স আর তার নতুন মান 75 থেকে যায়। ডাকার লাইন দুটি দেখতে হুবহু এক —byValue(rahim)আরbyRef(rahim)— পার্থক্য শুধু ফাংশনের ঘোষণায় একটি&.
শেষ ধাপের কথাটি গুরুত্বপূর্ণ: ডাকার জায়গা দেখে বোঝা যায় না ফাংশনটি আপনার ভ্যারিয়েবল বদলাবে কি না। জানতে হলে ফাংশনের ঘোষণা দেখতে হয়। এটি reference-এর একটি পরিচিত সমালোচনা, ভাবনার প্রশ্ন ০২-এ বিস্তারিত।
ক্লাসিক উদাহরণ — দুটি দাম অদলবদল
একটি দোকানের সফটওয়্যারে ভুল করে চাল আর ডালের দাম উল্টো বসে গেছে। দুটি অদলবদল (swap) করতে হবে। Pass by value দিয়ে এটা অসম্ভব — ফাংশন শুধু কপি অদলবদল করবে। Reference দিয়ে কাজটি স্বাভাবিক। নিচের কোডে দুটি ভার্সনের বোনাস আর একটি swap একসাথে চালিয়ে দেখুন:
#include <print>
void addBonusCopy(int marks) { marks += 5; std::println(" inside copy: {}", marks); }
void addBonusRef(int& marks) { marks += 5; } // marks = asol variable-er nam
void swapPrices(double& a, double& b) { double tmp = a; a = b; b = tmp; }
int main() {
int rahim = 70;
addBonusCopy(rahim);
std::println("after copy version : {}", rahim);
addBonusRef(rahim);
std::println("after ref version : {}", rahim);
double rice = 120.0, lentil = 75.5; // bhul kore ulto boshano
swapPrices(rice, lentil);
std::println("rice = {}, lentil = {}", rice, lentil);
}প্রথম ফাংশনের ভেতরে 75 দেখা যায়, কিন্তু ফেরার পর after copy version : 70 — কপিটি ফাংশনের সাথে মরে গেছে। দ্বিতীয় ফাংশনের পর 75, কারণ marks ছিল rahim-এরই নাম। শেষ লাইনে rice = 75.5, lentil = 120 — swapPrices-এর a আর b আসল দুটি ভ্যারিয়েবলের নাম, তাই অদলবদল বাইরেও টিকে গেছে। (বাস্তবে নিজে swap লিখতে হয় না — <utility>-এর std::swap(rice, lentil) ঠিক এই কাজই করে, আর ভেতরে সেটিও reference নেয়।)
C-তে reference নেই। একই কাজ করতে C-তে pointer লাগে: void swap(int *a, int *b) লিখে ভেতরে *a, *b ব্যবহার করতে হয়, আর ডাকতে হয় swap(&x, &y). C++-এর reference সেই * আর &-এর ঝামেলা লুকিয়ে ফেলে, আর null বা ভুল ঠিকানার সুযোগ কমায়; ভেতরে compiler সাধারণত ঠিকানাই পাঠায়। C কোর্সের pointer-এর মূল কথা পর্বটি পড়া থাকলে দেখবেন, reference আসলে “নিরাপদ, সবসময় ভরা, সরানো যায় না” এমন একটি pointer-এর মতো আচরণ করে।
৪ · const T& — কপি না করে শুধু পড়া
এবার উল্টো সমস্যা। ধরুন আপনার কাছে একটি জেলার সব SSC পরীক্ষার্থীর তালিকা — এক লাখ জনের নাম আর নম্বর, একটি std::vector-এ (পর্ব ১৩-এ বিস্তারিত)। একটি ফাংশন শুধু গড় নম্বর বের করবে, কিছু বদলাবে না। Pass by value দিলে কী হয়?
double average(std::vector<Student> list); // প্রতিবার ডাকলে পুরো তালিকা কপি!প্রতিবার average(students) ডাকলে এক লাখ Student কপি হয় — প্রতিটির নামের std::string-সহ, প্রায়ই নতুন মেমরি চেয়ে নিয়ে। শুধু একটি গড় বের করার জন্য এটা গুদাম থেকে পুরো মালপত্র ফটোকপি করে আরেক গুদামে তোলার মতো। আর ফাংশন শেষ হলে সেই কপি আবার ফেলেও দিতে হয়।
সাধারণ reference দিলে কপি বাঁচে, কিন্তু নতুন বিপদ: ফাংশনটি চাইলে (বা ভুল করে) তালিকা বদলে ফেলতে পারে। সমাধান দুটোকে মেলানো — const reference:
double average(const std::vector<Student>& list); // কপি নেই, বদলানোও নিষেধবাম থেকে ডানে পড়ুন: “Student-এর vector-এর একটি const reference” — অর্থাৎ আসল তালিকার একটি ডাকনাম, যার মাধ্যমে শুধু দেখা যায়, লেখা যায় না। ফাংশনের ভেতরে list[0].marks = 0; লিখলে compile error. দুই দিকের লাভ: কপির খরচ শূন্য, আর ডাকার মানুষ নিশ্চিত — তাঁর ডেটা অক্ষত থাকবে। আধুনিক C++ কোডে এটি সম্ভবত সবচেয়ে বেশি দেখা প্যারামিটার-রীতি।
কোন প্যারামিটার কখন — একটি সিদ্ধান্ত-তালিকা
| ফাংশন যা করবে | ছোট টাইপ (int, double, char, bool) | বড় টাইপ (string, vector, বড় struct) |
|---|---|---|
| শুধু পড়বে | int — by value (কপি সস্তা) | const std::string& |
| আসলটি বদলাবে | int& | std::vector<Student>& |
| নিজের একটি কপি রেখে দেবে | int — by value | by value (std::string name) — পর্ব ৩৬-এ move-এর সাথে কেন, দেখবেন |
| নতুন ফল তৈরি করবে | প্যারামিটারে নয় — return করে ফেরত দিন | |
দুটি ব্যাখ্যা দরকার। প্রথমত, int-এর মতো ছোট টাইপকে const int& দিয়ে পাঠানোর লাভ নেই। একটি int কপি করা আর একটি ঠিকানা পাঠানো প্রায় সমান খরচ, বরং reference-এর ভেতর দিয়ে পড়তে একটি বাড়তি ধাপ লাগে (ভাবনার প্রশ্ন ০৪)। দ্বিতীয়ত, ফাংশন যখন একটি নতুন মান হিসাব করে, তখন void calc(int& result)-এর মতো out parameter-এর চেয়ে int calc() লেখা পরিষ্কার। আধুনিক compiler ফেরত দেওয়া বড় object-ও সাধারণত কপি ছাড়াই পৌঁছে দেয়।
const T&-এর একটি বাড়তি ক্ষমতা
সাধারণ int& কেবল একটি নাম-ওয়ালা ভ্যারিয়েবলের সাথে bind হয়। addBonusRef(70) লিখলে compile error, কারণ 70-এর কোনো বাক্স নেই যাকে বদলানো যাবে। কিন্তু const reference সংখ্যা বা হিসাবের ফলের মতো অস্থায়ী মানও নিতে পারে: average(loadStudents()) বা greet("Rahim") — যেখানে greet নেয় const std::string&. তখন compiler একটি অস্থায়ী object (temporary) বানিয়ে reference-টিকে তার সাথে bind করে। এই কারণে const std::string& প্যারামিটার এত সুবিধাজনক — std::string, লেখা, বা হিসাবের ফল, সবই পাঠানো যায়। এর আয়ুর নিয়ম অংশ ৬-এ।
শুধু পড়ার জন্য লেখা পাঠাতে আরেকটি বিকল্প: C++17 থেকে আছে std::string_view, যা "Rahim"-এর মতো লেখা পেলেও কোনো std::string বানায় না। কখন const std::string& আর কখন string_view, তা পর্ব ১৪-এ দেখবেন। এখনকার জন্য const std::string&-ই নিরাপদ ডিফল্ট।
৫ · Range-for লুপে reference — আসল উপাদান, না কপি?
পর্ব ০৮-এ আপনি range-for লুপ দেখেছেন: for (int m : marks). এখানে লুকিয়ে আছে সেই একই প্রশ্ন। লুপের ভ্যারিয়েবল m প্রতিবার vector-এর একটি উপাদানের কপি। তাই m += 5 লিখলে কপি বদলায়, vector-এর আসল নম্বর বদলায় না — আর compiler কিছুই বলে না।
| লেখা | প্রতিবার কী পায় | কখন ব্যবহার করবেন |
|---|---|---|
for (auto s : v) | উপাদানের কপি | ছোট টাইপ শুধু পড়তে (int, double); বা যখন সত্যিই একটি আলাদা কপি দরকার |
for (auto& s : v) | আসল উপাদানের নাম | উপাদান বদলাতে — সবার নম্বরে বোনাস |
for (const auto& s : v) | আসল উপাদানের শুধু-পড়ার নাম | বড় উপাদান শুধু পড়তে — string, struct; সবচেয়ে প্রচলিত |
এবার সব একসাথে চালিয়ে দেখুন — একটি ছোট ক্লাসের তালিকা, সবাইকে বোনাস, একটি ভুল লুপ, আর একটি ফাংশন যা পুরো তালিকা কপি না করে মোট নম্বর গোনে। (struct এখানে শুধু নাম আর নম্বর একসাথে রাখার বাক্স; পর্ব ১২-এ বিস্তারিত।)
#include <print>
#include <string>
#include <vector>
struct Student { std::string name; int marks; };
int classTotal(const std::vector<Student>& list) { // copy hoy na, bodlano jay na
int total = 0;
for (const auto& s : list) total += s.marks;
return total;
}
int main() {
std::vector<Student> students{{"Rahim", 70}, {"Karim", 58}, {"Sumi", 91}};
for (auto& s : students) s.marks += 5; // asol student bodlay
for (auto s : students) s.marks += 100; // shudhu copy bodlay
for (const auto& s : students) std::println("{:6} {}", s.name, s.marks);
std::println("total = {}", classTotal(students));
}আউটপুটে আসবে Rahim 75, Karim 63, Sumi 96 আর total = 234. প্রথম লুপের auto& আসল তিনজনের নম্বরে ৫ যোগ করেছে। দ্বিতীয় লুপ প্রতিবার একজন ছাত্রের পুরো কপি (নামসহ) বানিয়েছে, তাতে ১০০ যোগ করেছে, তারপর ফেলে দিয়েছে — কারও নম্বর ১৭৫ হয়নি। এমন লুপ কোডে থাকলে সেটা প্রায় নিশ্চিত বাগ। প্রিন্টের লুপ আর classTotal দুটোই const auto&/const std::vector& নিয়েছে — কোনো কপি নেই, আর ভুল করে নম্বর বদলানোর উপায়ও নেই।
for (auto& s : students) লুপ চলার সময় students-এ নতুন ছাত্র যোগ (push_back) করবেন না। Vector বড় হলে সে নতুন মেমরিতে সরে যেতে পারে, আর পুরোনো উপাদানগুলোর reference তখন ফাঁকা জায়গার নাম হয়ে যায়। কেন এমন হয়, পর্ব ১৩ আর ১৮-এ দেখবেন।
৬ · Reference ফেরত দেওয়া — আর dangling-এর বিপদ
ফাংশন reference নিতে যেমন পারে, ফেরতও দিতে পারে। ফেরত দেওয়া reference দিয়ে ডাকার মানুষ সরাসরি আসল জিনিসটিতে কাজ করেন। আপনি এটা না জেনেই ব্যবহার করেছেন: students[0].marks = 80; — vector-এর [] একটি reference ফেরত দেয়, তাই ফলের ওপর লেখা যায়। একইভাবে std::cout << a << b চলে, কারণ প্রতিটি << একই cout-এর reference ফেরত দেয়, যাতে পরের << তার ওপর বসতে পারে।
// ঠিক আছে: তালিকাটি ডাকার মানুষের, ফাংশন শেষ হলেও বেঁচে থাকে
Student& topper(std::vector<Student>& list) {
std::size_t best = 0;
for (std::size_t i = 1; i < list.size(); ++i)
if (list[i].marks > list[best].marks) best = i;
return list[best]; // তালিকার ভেতরের আসল ছাত্র
}
topper(students).marks += 2; // আসল টপারের নম্বর বাড়লনিয়ম একটাই: যে object-এর reference ফেরত দিচ্ছেন, সেটি ফাংশন শেষ হওয়ার পরেও বেঁচে থাকতে হবে। প্যারামিটারে আসা তালিকা ডাকার মানুষের সম্পত্তি — তাই নিরাপদ। কিন্তু ফাংশনের ভেতরে বানানো local ভ্যারিয়েবল ফাংশনের stack frame-এ থাকে, আর frame মুছে গেলে সে-ও মরে যায়। তার reference ফেরত দিলে ডাকার মানুষ পান একটি মৃত বাক্সের নাম — dangling reference. নিচের কোডটি কখনো চালাবেন না; শুধু ধাপে ধাপে কী ঘটে দেখুন:
1const std::string& makeLabel(int roll) {2 std::string label = "Roll-" + std::to_string(roll);3 return label; // ভুল!4}5const std::string& s = makeLabel(7);6std::println("{}", s); // undefined behavior
| makeLabel frame | |
| label | |
| s |
- ফাংশন ডাকা হলো। লাইন ৫-এ
makeLabel(7)ডাকা হলো। stack-এ ফাংশনটির নিজস্ব frame তৈরি হলো — তার local ভ্যারিয়েবলগুলো এখানেই থাকবে। - Local object তৈরি।
labelনামে একটি std::string তৈরি হলো, মান"Roll-7". এটিmakeLabel-এর frame-এর বাসিন্দা। - ভুল লাইন — নামটি বাইরে পাঠানো।
return label;এখানে label-এর কপি পাঠায় না (ফেরতের টাইপ reference), পাঠায় label-এর নাম — ভেতরে আসলে তার ঠিকানা। GCC এখানে-Wreturn-local-addrসতর্কবার্তা দেয়। সতর্কবার্তাটিকে error-এর মতোই গুরুত্ব দিন। - Frame মুছে গেল। ফাংশন শেষ হতেই
label-এর আয়ু শেষ: তার destructor চলে, লেখার জন্য নেওয়া মেমরি ফেরত যায়, frame-এর জায়গা পরের ফাংশনের জন্য খালি হয়। - বাইরে একটি ভুতুড়ে নাম।
sএখনো সেই ঠিকানার নাম, কিন্তু সেখানে আর কোনো string নেই। ঠিকানা আছে, বাসিন্দা নেই — যেন এমন বাসার ঠিকানা, যে বাসা ভেঙে ফেলা হয়েছে। - পড়তে গেলেই undefined behavior.
sপড়া মানে মৃত object পড়া। ফল যা খুশি হতে পারে — আবর্জনা অক্ষর, crash, বা সবচেয়ে বিপজ্জনক ক্ষেত্রে ঠিকRoll-7, যাতে আপনি ভাবেন সব ঠিক আছে। কোড বদলালে বা optimization চালু করলে হঠাৎ ভাঙে। সমাধান সহজ: ফেরতের টাইপstd::stringকরুন (by value). আধুনিক C++-এ এটি কপি ছাড়াই পৌঁছায়।
Dangling reference শুধু return-এ নয়, আরও অনেক জায়গায় জন্মায় — vector বড় হয়ে সরে গেলে, একটি object মুছে ফেলার পর তার member-এর reference রেখে দিলে, lambda-য় local ভ্যারিয়েবল reference দিয়ে ধরে রাখলে। পুরো বিষয়টি — কোন object কতক্ষণ বাঁচে — পর্ব ১৮-এ “lifetime” নামে আলাদা করে দেখবেন। আজকের নিয়মটি মনে রাখুন: reference কখনো তার object-এর চেয়ে বেশি দিন বাঁচবে না।
Temporary-র আয়ু বাড়ে — কিন্তু সব সময় নয়
অংশ ৪-এ দেখেছেন, const reference একটি temporary-র সাথে bind হতে পারে। তাহলে সেই temporary কতক্ষণ বাঁচে? এখানে C++-এর একটি বিশেষ নিয়ম আছে, যার নাম lifetime extension: একটি temporary যখন সরাসরি একটি local const reference-এর সাথে bind হয়, তখন temporary-টি সেই reference-এর সমান দিন বাঁচে। কিন্তু মাঝখানে একটি ফাংশন থাকলে নিয়মটি কাজ করে না। সাবধানে ধাপে ধাপে দেখুন:
1int& a = 5; // error2const int& b = 5;3const std::string& city = std::string("Dhaka") + "-1205";4std::println("{} {}", b, city);5const int& big = std::max(3, 7);6std::println("{}", big); // undefined behavior
| a | |
| b | |
| city | |
| big |
- সাধারণ reference temporary নেয় না।
5একটি অস্থায়ী মান, এর কোনো স্থায়ী বাক্স নেই।int&দিয়ে একে বদলানোর সুযোগ দিলে অর্থহীন হতো, তাই compiler আটকায়। error-এর ভাষায় “lvalue reference” আর “rvalue” শব্দ দুটি খেয়াল করুন — অংশ ৮-এ এদের পরিচয়। - const reference নেয় — আর আয়ু বাড়ায়।
const int& b = 5;বৈধ। compiler একটি অস্থায়ী int বানায়, মান 5, আর যেহেতু এটি সরাসরিb-এর সাথে bind হলো, temporary-টিb-এর block শেষ হওয়া পর্যন্ত বাঁচবে। - হিসাবের ফলেও একই নিয়ম।
std::string("Dhaka") + "-1205"-এর ফল একটি temporary std::string. সাধারণত সেটি লাইনের শেষে (;-এ) মরে যেত, কিন্তু সরাসরিcity-র সাথে bind হওয়ায় এর আয়ুcity-র সমান হলো। - নিরাপদে ব্যবহার। দুটি reference-ই বেঁচে থাকা object-এর নাম, তাই আউটপুট নিশ্চিত:
5 Dhaka-1205. (বাস্তব কোডে অবশ্যstd::string city = ...;লেখাই সহজ ও পরিষ্কার — এই নিয়ম জানা দরকার মূলত অন্যের কোড বুঝতে।) - ফাংশন মাঝে এলে নিয়ম ভাঙে।
std::maxতার দুটি প্যারামিটারconst int&হিসেবে নেয়, আর বড়টির reference ফেরত দেয়। 3 আর 7-এর temporary bind হয়েছিল ফাংশনের প্যারামিটারে,big-এ নয় — তাই তাদের আয়ু বাড়ে না, পুরো লাইনটি শেষ হতেই মরে যায়।bigহয়ে গেল dangling. - Compile হয়, তবু ভুল। এই লাইন compile হয়, অনেক সময় 7-ও দেখায় — তাই ধরা কঠিন। নিয়মটি সংক্ষেপে: আয়ু বাড়ে শুধু সরাসরি bind হলে, কোনো ফাংশনের ফেরত দেওয়া reference-এর মধ্য দিয়ে নয়। সমাধান:
int big = std::max(3, 7);— মানটি নিজের বাক্সে কপি করে রাখুন।
৭ · Reference বনাম pointer
পর্ব ১৫-এ আপনি pointer শিখবেন — এমন ভ্যারিয়েবল, যার ভেতরে আরেকটি ভ্যারিয়েবলের মেমরি ঠিকানা রাখা থাকে। দুটি দিয়েই “অন্য একটি object-এ কাজ করা” যায়, তাই তুলনাটা এখনই জেনে রাখা ভালো। Pointer-কে ভাবুন একটি ঠিকানা লেখা চিরকুট — চিরকুটে নতুন ঠিকানা লেখা যায়, খালিও রাখা যায়। আর reference হলো ডাকনাম — একজনেরই, চিরকালের।
| বৈশিষ্ট্য | Reference int& r = x; | Pointer int* p = &x; |
|---|---|---|
| বানানোর সময় মান দেওয়া | বাধ্যতামূলক | ঐচ্ছিক (না দিলে বিপজ্জনক) |
| “কিছুই না” অবস্থা | নেই — null reference হয় না | আছে — nullptr |
| অন্য object-এর দিকে সরানো | যায় না | যায়: p = &y; |
| ব্যবহারের লেখা | সাধারণ নামের মতো: r = 5; | *p = 5;, member-এ p->marks |
| গাণিতিক হিসাব | নেই | আছে: p + 1 মানে পরের উপাদান (array-তে) |
& দিয়ে ঠিকানা নিলে | আসল object-এর ঠিকানা | pointer-এর নিজের ঠিকানা (&p) |
| Array বা vector-এ রাখা | std::vector<int&> হয় না | std::vector<int*> চলে |
| যেখানে স্বাভাবিক পছন্দ | ফাংশনের প্যারামিটার, range-for, operator | “থাকতেও পারে, না-ও” এমন ঐচ্ছিক সম্পর্ক; পুরোনো C লাইব্রেরি |
বাস্তব নিয়ম: যখন পারেন reference, যখন দরকার pointer. “একটি জিনিস অবশ্যই আছে, তার ওপর কাজ করো” — reference. “জিনিসটি থাকতেও পারে, না-ও থাকতে পারে” বা “পরে অন্য জিনিসের দিকে সরাতে হবে” — pointer (বা আধুনিক বিকল্প, যা পর্ব ১৭ আর ৩৮-এ দেখবেন)।
Reference C++-এ এসেছিল ১৯৮০-র দশকের শুরুতে, যখন ভাষাটির নাম “C with Classes” থেকে C++ হচ্ছে। Bjarne Stroustrup তাঁর বই The Design and Evolution of C++-এ লিখেছেন, reference আনার প্রধান কারণ ছিল operator overloading (পর্ব ২৩) — যাতে a + b বা v[i] = 5-এর মতো স্বাভাবিক লেখা বড় object-এ কপি ছাড়াই কাজ করে। ধারণাটি তিনি নিয়েছিলেন Algol 68 ভাষা থেকে, তবে একটি বড় পরিবর্তন করে: Algol 68-এ reference-কে নতুন object-এর দিকে সরানো যেত, C++-এ যায় না।
৮ · এক ঝলক সামনে — T&&, rvalue reference
অংশ ৬-এর error-বার্তায় দুটি শব্দ ছিল — lvalue আর rvalue. মোটা দাগে, lvalue হলো নাম ও স্থায়ী ঠিকানা আছে এমন জিনিস (marks, students[0]), যাকে =-এর বাম পাশে বসানো যায়। rvalue হলো অস্থায়ী মান (5, a + b, ফাংশনের ফেরত দেওয়া কোনো std::string), যা এক লাইনের মধ্যেই হারিয়ে যায়। এ পর্যন্ত যে T& দেখেছেন, তার পুরো নাম lvalue reference.
C++11 একটি দ্বিতীয় ধরনের reference এনেছে, দুটি & দিয়ে লেখা: rvalue reference, T&&. এটি শুধু temporary-র সাথে bind হয়:
int x = 10;
int&& t = 5; // ঠিক: 5 একটি temporary
int&& u = x; // error: x-এর নাম আছে, এটি lvalueএর কাজ কী? যে জিনিস এমনিতেই মরতে যাচ্ছে, তার ভেতরের মালপত্র কপি না করে সরিয়ে নেওয়া যায় — এক লাখ ছাত্রের তালিকা কপি না করে শুধু “মালিকানা হাতবদল”। এটাই move semantics, যা আধুনিক C++-কে অনেক দ্রুত করেছে। পুরো গল্প পর্ব ৩৬-এ। এখন শুধু চিনে রাখুন: কোডে && দেখলে সেটি “AND” নয় (যদি টাইপের পাশে বসে), সেটি rvalue reference.
- Reference আর pointer-এর মূল পার্থক্য? — reference বানানোর সময়েই bind হতে হয়, null হয় না, অন্য object-এ সরানো যায় না; pointer-এ তিনটিই সম্ভব, আর pointer-এ গাণিতিক হিসাব চলে।
int& r = x; r = y;-এর পর কী হয়? — r এখনো x-এর নাম; y-এর মান x-এ কপি হয়।- বড় std::string প্যারামিটার কেন
const std::string&? — কপির খরচ নেই, আর ফাংশন ভুল করে বদলাতে পারে না; temporary-ও পাঠানো যায়। - Local ভ্যারিয়েবলের reference ফেরত দিলে কী হয়? — ফাংশন শেষে object মরে যায়, reference dangling হয়; সেটি পড়া undefined behavior.
int& r = 10;কি বৈধ? — না, non-const lvalue reference temporary-র সাথে bind হয় না;const int& r = 10;বৈধ।
৯ · AI-এর যুগে — reference-এর ভুল খুঁজে বের করা
Reference-এর ভুলগুলোর বৈশিষ্ট্য হলো, কোড compile হয় আর ছোট ইনপুটে ঠিকঠাক চলে। ধীরগতি দেখা যায় শুধু বড় ডেটায়, আর dangling reference ধরা পড়ে হয়তো মাসখানেক পর, অন্য compiler-এ। AI টুল প্রায়ই অনলাইনের পুরোনো বা শিক্ষানবিশ কোড দেখে শিখেছে, তাই এই ভুলগুলো বারবার দেখা যায়:
| AI যা করে | সমস্যা | যা হওয়া উচিত |
|---|---|---|
void print(std::vector<Student> list) | প্রতিবার ডাকলে পুরো তালিকা কপি — বড় ডেটায় ধীর, মেমরি নষ্ট | const std::vector<Student>& list |
শুধু পড়ার ফাংশনে std::string& name | ফাংশন ভুল করে বদলাতে পারে; temporary বা "Rahim" পাঠানো যায় না | const std::string& name |
const std::string& getName() { std::string n = ...; return n; } | local-এর reference ফেরত — dangling, undefined behavior | std::string getName() — by value |
for (auto s : students) s.marks += 5; | কপি বদলায়, আসল তালিকা অপরিবর্তিত | for (auto& s : students) |
সব প্যারামিটারে const int& | ছোট টাইপে লাভ নেই, পড়াও কঠিন | int — by value |
ফল ফেরতে void calc(double& out) | ডাকার জায়গায় বোঝা যায় না কী বদলাচ্ছে | double calc() — return দিয়ে |
AI-কে কোড লিখতে বলার সময় প্যারামিটারের নিয়মগুলো আগেই বলে দিন, আর প্রতিটি সিদ্ধান্তের কারণ চেয়ে নিন। তাহলে যাচাই করা সহজ হয়:
AI-এর কোডে reference যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১C-তে তো pointer দিয়েই সব কাজ চলছিল। তাহলে Stroustrup C++-এ আলাদা করে reference আনলেন কেন?
Stroustrup নিজে The Design and Evolution of C++ (১৯৯৪) বইয়ে উত্তর দিয়েছেন: reference আনা হয়েছিল মূলত operator overloading-কে সমর্থন করতে। কারণটা বুঝতে একটি Matrix টাইপ কল্পনা করুন, যার ভেতরে হাজার হাজার সংখ্যা। আমরা চাই লিখতে c = a + b; — গণিতের বইয়ের মতো স্বাভাবিকভাবে।
operator+ যদি প্যারামিটার by value নেয়, প্রতিটি যোগে দুটি বিশাল matrix কপি হবে — অসহনীয় ধীর। যদি pointer নেয়, তাহলে ডাকতে হবে c = &a + &b; — দেখতে কুৎসিত, আর তা সম্ভবও নয়: শুধু pointer-এর মতো built-in টাইপের জন্য operator overload করা যায় না (অন্তত একটি operand class বা enum টাইপের হতে হয়)। দরকার ছিল এমন কিছু, যা ডাকার জায়গায় সাধারণ ভ্যারিয়েবলের মতো দেখায়, কিন্তু ভেতরে ঠিকানার মতো কাজ করে। সেটাই reference: Matrix operator+(const Matrix& x, const Matrix& y).
দ্বিতীয় কারণ — বাম পাশে বসার ক্ষমতা। v[i] = 5; কাজ করতে হলে operator[]-কে এমন কিছু ফেরত দিতে হবে, যার ওপর লেখা যায়। By value ফেরত দিলে লেখা যেত একটি কপিতে। Pointer ফেরত দিলে লিখতে হতো *v[i] = 5;. Reference ফেরত দিলে লেখাটি built-in array-র মতোই স্বাভাবিক। একই কারণে std::cout << a << b-এর শৃঙ্খল কাজ করে — প্রতিটি << stream-এর reference ফেরত দেয়।
নকশায় তিনি একটি সচেতন সিদ্ধান্ত নেন: Algol 68-এ reference-কে পরে নতুন object-এর দিকে সরানো যেত, কিন্তু C++-এ bind হওয়ার পর আর সরানো যাবে না। এতে reference-এর অর্থ সরল থাকে — r = y সবসময় মান কপি করে, কখনো “নাম সরায়” না। Pointer-এর নমনীয়তা যেখানে দরকার, সেখানে pointer তো আছেই।
পরে দেখা গেল reference-এর ব্যবহার operator ছাড়িয়ে অনেক দূর গেছে: const T& প্যারামিটার, range-for, আর C++11-এ T&& দিয়ে move semantics — সবই এই ভিত্তির ওপর দাঁড়ানো। পর্ব ২৩-এ operator overloading শিখতে গিয়ে দেখবেন, প্রায় প্রতিটি operator-এর স্বাক্ষরে reference আছে।
মূল উপলব্ধি: reference কোনো বিলাসিতা নয়, একটি নির্দিষ্ট সমস্যার সমাধান — “নিজের টাইপকে built-in টাইপের মতো স্বাভাবিকভাবে ব্যবহার করা, কপির খরচ ছাড়াই।” ভাষার প্রতিটি বড় বৈশিষ্ট্যের পেছনে এমন একটি “কেন” খুঁজলে সেটি মনে রাখা সহজ হয়।
প্র ০২ফাংশন কিছু বদলাবে, সেটা বোঝাতে reference ভালো, না pointer? Google-এর মতো বড় প্রতিষ্ঠান একসময় pointer-কে কেন পছন্দ করত?
অংশ ৩-এ একটি সমস্যা দেখেছেন: byValue(rahim) আর byRef(rahim) ডাকার জায়গায় হুবহু এক দেখায়। এক লাখ লাইনের কোডবেসে কেউ update(order); পড়ছেন — order কি বদলাবে? ফাংশনের ঘোষণা খুঁজে না দেখে বলা যায় না।
Google-এর C++ Style Guide বহু বছর এই সমস্যার সমাধান করেছিল একটি সহজ নিয়মে: ইনপুট প্যারামিটার হবে by value বা const reference, আর যে প্যারামিটার ফাংশন বদলাবে (output), সেটি হবে pointer. তাহলে ডাকার জায়গায় লিখতে হয় update(&order); — ওই & চিহ্নটিই পাঠককে সতর্ক করে, “এখানে order বদলাতে পারে।” কোড রিভিউতে এটি চোখে পড়ে, যা হাজার হাজার প্রোগ্রামারের দলে খুব মূল্যবান।
কিন্তু এর দামও ছিল। Pointer null হতে পারে, তাই প্রতিটি ফাংশনে ভাবতে হয় “nullptr এলে কী করব?” — অথচ অনেক ক্ষেত্রে null-এর কোনো অর্থই নেই। পরে Google নির্দেশিকা বদলায়; সাম্প্রতিক সংস্করণে বলা আছে, অবশ্য-প্রয়োজনীয় output প্যারামিটার সাধারণত reference হবে, আর pointer শুধু ঐচ্ছিক (থাকতেও পারে, না-ও) প্যারামিটারে।
Bjarne Stroustrup ও Herb Sutter-এর সম্পাদনায় তৈরি C++ Core Guidelines-ও প্রায় একই জায়গায় পৌঁছেছে: সস্তায় কপি হয় এমন টাইপ by value, অন্যগুলো const T&; বদলানো দরকার হলে T&; আর সবচেয়ে গুরুত্বপূর্ণ — output প্যারামিটারের বদলে ফল return করুন। একাধিক ফল দরকার হলে একটি struct বা std::pair ফেরত দিন (পর্ব ১২-এর structured bindings দিয়ে সুন্দরভাবে ধরা যায়)। আর “কোনো object না-ও থাকতে পারে” বোঝাতে T* বা std::optional (পর্ব ৩৮)।
অন্য ভাষাও এই সমস্যা দেখেছে। C#-এ reference প্যারামিটার পাঠাতে ডাকার জায়গাতেও ref লিখতে হয় — Update(ref order) — ফলে দুই দিকের সুবিধাই মেলে। Rust-এ লিখতে হয় &mut order.
মূল উপলব্ধি: API নকশা শুধু “কাজ করে কি না” নয়, “পড়ে বোঝা যায় কি না” — বিশেষ করে বড় দলে। বদলানো এড়াতে পারলে এড়ান (return দিন); বদলাতেই হলে নাম আর ঘোষণা এমন রাখুন যাতে অভিপ্রায় স্পষ্ট হয়, আর আপনার প্রজেক্টের রীতি মেনে চলুন।
প্র ০৩Temporary-র আয়ু বাড়ার নিয়মটি ঠিক কী, কোথায় খাটে না, আর C++-এর নতুন সংস্করণগুলো এ নিয়ে কী বদলেছে?
সাধারণ নিয়ম: একটি temporary — যেমন a + b-এর ফল বা ফাংশনের by value ফেরত দেওয়া object — তৈরি হয় যে “পূর্ণ expression”-এ, সেটি শেষ হলেই (মোটামুটি, পরের ;-এ) মরে যায়। ব্যতিক্রম lifetime extension: temporary-টি যদি সরাসরি একটি const T& বা T&& reference-এর সাথে bind হয়, তাহলে সেটি reference-এর সমান দিন বাঁচে।
কিন্তু “সরাসরি” শব্দটি ভারী। নিয়মটি খাটে না, যখন: (১) temporary ফাংশনের প্যারামিটারে bind হয় — তখন সেটি ফাংশন-ডাকার পূর্ণ expression পর্যন্তই বাঁচে, তাই const int& m = std::max(3, 7); dangling. (২) ফাংশন একটি reference ফেরত দেয় — compiler জানে না সেটি কোন object-এর, তাই কিছু বাড়াতে পারে না। (৩) একটি object-এর member function-এর ফেরত দেওয়া reference ধরলে — const auto& n = makeStudent().name; কাজ করে (সরাসরি member), কিন্তু const auto& n = makeStudent().getName(); (reference ফেরত দেওয়া member function) dangling.
এই নিয়মের একটি বিখ্যাত ব্যবহার ছিল ২০০০ সালের ScopeGuard কৌশল (Andrei Alexandrescu ও Petru Marginean): ফাংশন একটি temporary guard ফেরত দিত, আর const reference দিয়ে ধরে রাখায় সেটি block শেষ পর্যন্ত বাঁচত, তারপর destructor-এ পরিষ্কারের কাজ করত। Herb Sutter ২০০৮ সালে তাঁর “Guru of the Week” লেখায় একে বলেছিলেন “সবচেয়ে গুরুত্বপূর্ণ const”-এর প্রার্থী।
সবচেয়ে কুখ্যাত ফাঁদ ছিল range-for-এ: for (auto& s : getSchool().students()) — যদি students() একটি reference ফেরত দেয়, তাহলে getSchool()-এর temporary লুপ শুরুর আগেই মরে যেত, আর পুরো লুপ চলত মৃত object-এর ওপর। বহু বছরের অভিযোগের পর C++23 নিয়ম বদলেছে: range-for-এর ডান পাশের expression-এ তৈরি সব temporary এখন পুরো লুপ শেষ হওয়া পর্যন্ত বাঁচে। অর্থাৎ একই কোড C++20-এ undefined behavior, C++23-এ নিরাপদ — compiler-এর সংস্করণ আর -std ফ্ল্যাগ এখানে সত্যিই গুরুত্বপূর্ণ।
বাস্তব পরামর্শ: lifetime extension জানা দরকার অন্যের কোড পড়তে আর বাগ খুঁজতে। নিজের কোডে temporary ধরে রাখতে চাইলে সাধারণত সরল পথ ভালো — std::string city = ...; বা auto s = makeStudent();. আধুনিক compiler কপি এড়িয়ে চলে, তাই খরচও প্রায় নেই।
মূল উপলব্ধি: reference নিজে কখনো কোনো object-কে বাঁচিয়ে রাখে না — একমাত্র ব্যতিক্রম এই সরু নিয়ম। “আমার reference যে object-এর নাম, সেটি কে বাঁচিয়ে রাখছে?” — প্রশ্নটির উত্তর দিতে না পারলে value নিন।
প্র ০৪Reference কি “বিনা খরচে” আসে? মেশিনের ভেতরে reference আসলে কী, আর const int& কেন কখনো int-এর চেয়ে ধীর হতে পারে?
C++ standard ইচ্ছা করেই বলে না reference মেমরিতে কীভাবে থাকবে; এমনকি বলে, reference-এর নিজের জন্য জায়গা লাগবে কি না, তা unspecified. এই স্বাধীনতা compiler দুইভাবে কাজে লাগায়।
একই ফাংশনের ভেতরে local reference (int& r = marks;) — compiler জানে r ঠিক কোন বাক্সের নাম, তাই সাধারণত r-কে সরাসরি marks দিয়ে বদলে দেয়। মেশিন কোডে r-এর কোনো চিহ্নই থাকে না — খরচ সত্যিই শূন্য।
ফাংশনের প্যারামিটার বা struct-এর member হিসেবে — এখানে compiler-কে চলার সময় জানতে হয় কোন object, তাই reference কার্যত একটি ঠিকানা হিসেবে যায়। x86-64 Linux-এর মতো প্ল্যাটফর্মের নিয়মে (ABI) একটি reference প্যারামিটার ঠিক pointer-এর মতো একটি রেজিস্টারে ঠিকানা হয়ে পৌঁছায়। একটি int& member থাকলে struct-এর আকার 64-bit সিস্টেমে সাধারণত ৮ বাইট বাড়ে। অর্থাৎ ভেতরে reference প্রায়ই “আড়ালে থাকা pointer” — পার্থক্যটা ভাষার নিয়মে (null নয়, সরানো যায় না), মেশিনে নয়।
তাহলে ছোট টাইপে কেন খরচ? By value int রেজিস্টারেই আসে, সরাসরি ব্যবহার করা যায়। const int& এলে আগে ঠিকানা থেকে মেমরি পড়তে হয় — একটি বাড়তি ধাপ। আরও সূক্ষ্ম সমস্যা aliasing: ধরুন void addAll(std::vector<int>& v, const int& bonus). Compiler নিশ্চিত হতে পারে না যে bonus হয়তো v-এরই একটি উপাদানের নাম নয় — কেউ addAll(v, v[0]) ডাকতে পারেন! তাই লুপের প্রতিবার bonus নতুন করে মেমরি থেকে পড়তে হয়, রেজিস্টারে রেখে দেওয়া যায় না। আর v[0] বদলে গেলে বোনাসও মাঝপথে বদলে যায় — এটা পারফরম্যান্সের পাশাপাশি সঠিকতারও প্রশ্ন।
বড় টাইপে হিসাব উল্টে যায়: একটি ঠিকানা পাঠানো (৮ বাইট) এক লাখ উপাদানের vector কপির চেয়ে বহুগুণ সস্তা। তাই নিয়মটা দাঁড়ায় — সস্তা টাইপ by value, ব্যয়বহুল টাইপ const T&. সীমারেখা মোটামুটি দুই-তিনটি রেজিস্টারের সমান আকার, তবে সন্দেহ হলে মেপে দেখুন (পর্ব ৪৯)।
মূল উপলব্ধি: “zero-cost abstraction” মানে নিখরচা নয়, মানে “হাতে লিখলে এর চেয়ে কম খরচে করতে পারতেন না।” Reference হাতে লেখা pointer-এর সমান খরচ — তার বেশি নয়, তার কমও নয়।
প্র ০৫Java আর Python-এ ফাংশনে object পাঠালে কী হয়? সেখানে কি C++-এর মতো reference আছে?
এই প্রশ্নে ইন্টারভিউতে অনেকে ভুল করেন, কারণ “reference” শব্দটি তিন ভাষায় তিন অর্থে ব্যবহৃত হয়।
Java: Java-র নিয়ম স্পষ্ট — সবকিছু pass by value. কিন্তু object-এর ক্ষেত্রে ভ্যারিয়েবলের ভেতরে object নিজে থাকে না, থাকে তার দিকে একটি “reference” (C++-এর ভাষায় প্রায় একটি pointer). ফাংশনে পাঠানো হয় সেই reference-এর কপি। ফলে ফাংশন object-এর ভেতরের ঘর বদলাতে পারে (s.marks = 80 বাইরে দেখা যায়), কিন্তু ডাকার মানুষের ভ্যারিয়েবলটিকে অন্য object-এর দিকে ঘোরাতে পারে না। তাই Java-তে দুটি Integer অদলবদল করার একটি swap(a, b) ফাংশন লেখা যায় না — যা C++-এ তিন লাইনের কাজ।
Python: Python-এ নাম হলো object-এর গায়ে লাগানো লেবেল। ফাংশনে পাঠালে প্যারামিটার নামটি একই object-এর গায়ে লাগে (একে কেউ বলেন “call by object reference” বা “pass by assignment”). List-এর মতো বদলানো যায় এমন object হলে lst.append(5) বাইরেও দেখা যায়। কিন্তু x += 1 লিখলে, যেখানে x একটি int, Python নতুন একটি int object বানিয়ে শুধু ভেতরের নামটি সেদিকে সরায় — বাইরের ভ্যারিয়েবল অপরিবর্তিত। কারণ int বদলানো যায় না (immutable).
পার্থক্য কোথায়: C++-এর reference হলো ডাকার মানুষের ভ্যারিয়েবলটিরই নাম। r = y সেই ভ্যারিয়েবলের মান বদলে দেয়। Java/Python-এর “reference” বরং C++-এর pointer-এর কাছাকাছি, তবে গাণিতিক হিসাব ছাড়া আর garbage collector-এর পাহারায় — তাই সেখানে dangling হয় না, কিন্তু প্রতিটি object-এর জন্য heap মেমরি আর GC-র খরচ লাগে। C++ আপনাকে বেছে নিতে দেয়: কপি (value), নাম (reference), না ঠিকানা (pointer).
আরও দুটি তুলনা: C#-এ class object Java-র মতো আচরণ করে, কিন্তু ref keyword দিয়ে C++-এর মতো সত্যিকারের reference প্যারামিটারও পাঠানো যায়। Rust-এ & আর &mut borrow আছে, আর তার borrow checker compile-এর সময়েই প্রমাণ করে যে কোনো reference তার object-এর চেয়ে বেশি দিন বাঁচবে না — অর্থাৎ অংশ ৬-এর dangling reference Rust-এ compile-ই হয় না।
মূল উপলব্ধি: ভাষা বদলালে শব্দ একই থাকতে পারে, অর্থ নয়। কোনো ভাষায় জিজ্ঞেস করুন: “ফাংশন কি কপি পায়, ভ্যারিয়েবলের নাম পায়, না object-এর ঠিকানার কপি পায়?” — উত্তরটি জানলে সেই ভাষার অনেক অদ্ভুত আচরণ পরিষ্কার হয়ে যায়।
অনুশীলন
বেশিরভাগ কাজে কোড লিখতে হবে না — আগে অনুমান করুন, তারপর উত্তর মিলিয়ে নিন।
-
আউটপুট অনুমান:
int a = 5; int& r = a; int b = 10; r = b; b = 20; r += 1;— শেষেa,rআরb-এর মান কত?a= 11,r= 11,b= 20.r = b;reseat নয় — b-এর মান 10 a-তে কপি হয়। পরেb = 20শুধু b বদলায়, কারণ r সারাজীবন a-এরই নাম। শেষেr += 1মানে a = 11. -
প্যারামিটার বেছে নিন: প্রতিটি ফাংশনের প্যারামিটার কী হবে, কারণসহ বলুন — (ক)
printReport, যা ছাত্রদের vector শুধু প্রিন্ট করে (খ)applyDiscount, যা একটি পণ্যের দাম (double) আসলেই কমায় (গ)square, যা একটি int-এর বর্গ ফেরত দেয় (ঘ)greet, যা একটি নাম পড়ে স্বাগত বার্তা প্রিন্ট করে।(ক)
const std::vector<Student>&— বড়, শুধু পড়া। (খ)double&— আসলটি বদলাতে হবে (যদিওdouble discounted(double price)লিখে ফেরত দেওয়াও ভালো বিকল্প)। (গ)int— ছোট টাইপ, by value; ফল return. (ঘ)const std::string&— শুধু পড়া, আরgreet("Sumi")-এর মতো লেখাও পাঠানো যাবে (পর্ব ১৪-এরstd::string_view-ও চলবে)। -
বাগ শিকার: একটি AI এই কোড দিয়েছে —
std::vector<int>& passed(std::vector<int> marks) { std::vector<int> result; for (auto m : marks) if (m >= 33) result.push_back(m); return result; }. কয়টি সমস্যা খুঁজে পান? ঠিক রূপ লিখুন।দুটি: (১) বড় সমস্যা — local
result-এর reference ফেরত, ফাংশন শেষে result মরে যায়: dangling, undefined behavior. (২)marksby value — শুধু পড়ার জন্য পুরো vector কপি। ঠিক রূপ:std::vector<int> passed(const std::vector<int>& marks)— ফেরত by value. লুপেরauto mএখানে ঠিক আছে, কারণ int ছোট আর শুধু পড়া হচ্ছে। -
নিজে লিখুন (কোড): উপরের
student_list.cpp-এ একটি ফাংশন যোগ করুনvoid addBonus(... list, int bonus), যা সবার নম্বরে bonus যোগ করবে, আরmain-এর প্রথম লুপের বদলেaddBonus(students, 5);ডাকুন। আউটপুট আগের মতোই আসা উচিত।স্বাক্ষর হবে
void addBonus(std::vector<Student>& list, int bonus)— তালিকা বদলাবে, তাই non-const reference; bonus ছোট, তাই by value. ভেতরেfor (auto& s : list) s.marks += bonus;. পরীক্ষা করে দেখুন: প্যারামিটার থেকে&সরালে compile হবে, কিন্তু নম্বর আর বাড়বে না — কারণ ফাংশন তখন তালিকার একটি কপিতে কাজ করে।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি ফাংশন local ভ্যারিয়েবলের reference ফেরত দিলে যা ঘটে, সেই ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- ফাংশন ডাকা হয়, stack-এ তার frame তৈরি হয়
- Frame-এর ভেতরে local object তৈরি হয়
returnসেই object-এর reference বাইরে পাঠায়- ফাংশন শেষ হয়, frame আর local object মুছে যায়
- ডাকার মানুষ reference পড়েন — undefined behavior
C++-এ int a = 1, b = 2; int& r = a; r = b; b = 3; চলার পর a-এর মান কত?
- 1
- 2
- 3
- Compile error
C++ reference সম্পর্কে কোন কথাটি সঠিক?
- বানানোর সময়েই একটি object-এর সাথে bind করতে হয়
- Reference-কে nullptr দিয়ে খালি রাখা যায়
- পরে r = y; লিখে অন্য ভ্যারিয়েবলের দিকে সরানো যায়
- Reference আসল ভ্যারিয়েবলের একটি স্বাধীন কপি রাখে
একটি C++ ফাংশন শুধু একটি বড় std::vector<std::string> পড়বে, কিছু বদলাবে না। প্যারামিটারের সবচেয়ে উপযুক্ত টাইপ কোনটি?
- std::vector<std::string>
- std::vector<std::string>&
- const std::vector<std::string>&
- std::vector<std::string>&&
C++-এ std::vector<int> v{1, 2, 3}; for (auto x : v) x *= 2; চলার পর v-তে কী থাকে?
- 2, 4, 6
- 2, 2, 3
- Compile error
- 1, 2, 3
একটি C++ ফাংশন তার ভেতরের local std::string-এর reference ফেরত দেয়, আর ডাকার মানুষ সেটি পড়েন। কী হয়?
- compiler string-টির আয়ু বাড়িয়ে দেয়, তাই সব ঠিক থাকে
- Undefined behavior — reference একটি ধ্বংস হওয়া object-এর নাম
- সবসময় compile error হয়
- ডাকার মানুষ string-টির একটি নিরাপদ কপি পান
নিচের কোন C++ লাইনটি সফলভাবে compile হয়?
- int& r = 42;
- int& r;
- const int& r = 42;
- int x = 1; int&& r = x;
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Reference = একটি object-এর দ্বিতীয় নাম, কপি নয়। বানানোর সময়েই bind হয়, পরে সরে না, null হয় না;
r = yমান কপি করে। - ফাংশনে
T&দিলে আসলটি বদলায়,Tদিলে কপি। ডাকার লাইন একই দেখায় — ঘোষণা দেখে বুঝুন। - বড় জিনিস শুধু পড়তে
const T&; ছোট টাইপ by value; নতুন ফলreturnকরে। - Range-for-এ বদলাতে
auto&, পড়তেconst auto&;autoমানে কপি। - Local-এর reference কখনো ফেরত নয় — reference তার object-এর চেয়ে বেশি দিন বাঁচবে না। Temporary-র আয়ু বাড়ে শুধু সরাসরি bind হলে।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ১১ · namespace আর header — বড় প্রোগ্রাম সাজানোপরবর্তী পর্বনামের সংঘর্ষ এড়ানো আর কোড একাধিক ফাইলে ভাগ করা।
- C++ প্রোগ্রামিং · কোর্স হোম৫৫টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- C প্রোগ্রামিং · pointer-এর মূল কথাগভীরে যানঠিকানা,
*আর&— reference-এর ভেতরে যা আসলে ঘটে। - C প্রোগ্রামিং · pointer, array আর ফাংশনপাশের ট্র্যাকC-তে ফাংশন দিয়ে আসল ডেটা বদলানোর পুরোনো উপায়।