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

new আর delete — heap মেমরি, আর কেন এড়িয়ে চলবেন

Dynamic Memory with new and delete — and Why Modern C++ Avoids Them

Taught in Bangla, this lesson covers dynamic memory in C++: new and delete, how new differs from malloc, four classic bugs such as leaks and double delete, why manual delete breaks with exceptions and early returns, why to avoid naked new, and sanitizer tools.

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

  • Stack আর heap — দুই ধরনের মেমরি, আর প্রতিটির জিনিস কতক্ষণ বাঁচে
  • new/delete আর new[]/delete[] ঠিক কী করে, আর malloc/free-এর সাথে পার্থক্য
  • চারটি ক্লাসিক বাগ — leak, double delete, use-after-free, ভুল জোড়ার delete — প্রতিটি একটি ছোট ঘটনায়
  • কেন exception আর early return হাতে লেখা delete-কে ভঙ্গুর করে, আর std::bad_alloc কী
  • “Naked new” কেন নিরুৎসাহিত, তার বদলে কী লিখবেন, আর AI-এর লেখা মেমরি-কোড কীভাবে যাচাই করবেন

১ · সমস্যা: কতজন গ্রাহক, আগে জানা নেই

পর্ব ১৫-এ আপনি pointer দেখেছেন — এমন ভ্যারিয়েবল, যার ভেতরে আরেকটি object-এর ঠিকানা থাকে। সেখানে প্রতিটি pointer এমন কোনো ভ্যারিয়েবলের দিকে দেখাচ্ছিল, যা আগে থেকেই আছে। এবার প্রশ্নটা উল্টো: যদি object-টিই আগে থেকে না থাকে?

ধরুন আপনি একটি ছোট কুরিয়ার কোম্পানির সফটওয়্যার লিখছেন। প্রতিদিন সকালে প্রোগ্রাম একটি ফাইল থেকে গ্রাহকের তালিকা পড়ে। কোনো দিন ৩০ জন, ঈদের আগে হয়তো ৩০ হাজার। কোড লেখার সময় আপনি জানেন না কতজন — জানা যাবে কেবল প্রোগ্রাম চলার সময়, ফাইল খোলার পর। আবার মাসের শেষে একটি “Report” object বানাতে হবে, যা একটি ফাংশনে তৈরি হয়ে অন্য অনেক ফাংশনে ঘুরবে — অর্থাৎ যে ফাংশন তাকে বানিয়েছে, তার চেয়ে বেশি দিন বাঁচতে হবে।

এই দুটি চাহিদা — চলার সময় ঠিক হওয়া আকার আর ফাংশনের চেয়ে লম্বা আয়ু — সাধারণ local ভ্যারিয়েবল দিয়ে মেটে না। এর জন্য লাগে dynamic memory. কেন লাগে, তা বুঝতে আগে দেখুন একটি C++ প্রোগ্রাম মেমরির কোন দুটি এলাকা সবচেয়ে বেশি ব্যবহার করে।

Stack — নিজে থেকে গোছানো তাক

প্রতিটি ফাংশন ডাকলে stack-এ তার একটি stack frame তৈরি হয়; local ভ্যারিয়েবল সেখানে থাকে। ফাংশন শেষ হলেই frame মুছে যায় — আপনাকে কিছু করতে হয় না। খুব দ্রুত, কিন্তু আকার ছোট (Linux-এ সাধারণত ৮ MB, Windows-এ ১ MB) আর আয়ু ফাংশনের সাথে বাঁধা।

আয়ু = ফাংশনের আয়ু

Heap — চেয়ে নেওয়া গুদাম

Heap (C++-এর আনুষ্ঠানিক নাম free store) হলো বড় একটি এলাকা, যেখান থেকে প্রোগ্রাম চলার সময় যত খুশি জায়গা চেয়ে নেওয়া যায়। এখানের জিনিস ফাংশন শেষ হলেও বেঁচে থাকে — যতক্ষণ না কেউ সেটি ফেরত দেয়।

আয়ু = যতক্ষণ না ফেরত দেন

পাড়ায় বিয়ের অনুষ্ঠান। নিজের বাসার চেয়ার-টেবিল (stack) সবসময় হাতের কাছে, অনুষ্ঠান শেষে ঘরেই থাকে, কাউকে ফেরত দিতে হয় না। কিন্তু ৫০০ অতিথির জন্য ডেকোরেটরের কাছ থেকে চেয়ার ভাড়া (heap) আনতে হয়। কতগুলো লাগবে, সেটা অতিথির সংখ্যা জানার পর ঠিক হয়; আর ভাড়ার চেয়ার যতদিন খুশি রাখা যায়। কিন্তু শর্ত একটাই — অনুষ্ঠান শেষে চেয়ারগুলো ঠিক একবার ফেরত দিতে হবে। ফেরত না দিলে ডেকোরেটরের গুদাম খালি হতে থাকে (leak). একই চেয়ার দুবার “ফেরত” লিখলে হিসাব গোলমাল (double delete). আর ফেরত দেওয়ার পরও যদি অতিথি সেই চেয়ারে বসতে যান — চেয়ার তখন হয়তো অন্য কোনো বাড়িতে (use-after-free).

C++-এ heap থেকে জায়গা চাওয়া হয় new দিয়ে, ফেরত দেওয়া হয় delete দিয়ে। সবচেয়ে ছোট উদাহরণ — একটি int:

void f() {
    int* p = new int(5);   // heap-এ একটি int, মান 5; ঠিকানা থাকে p-তে
    *p = 7;                // pointer দিয়ে heap-এর int-এ লেখা
    delete p;              // heap-এর জায়গা ফেরত
}                          // p নিজে (stack-এ) এখানে মুছে যায়

এখানে দুটি আলাদা জিনিস আছে, আর এদের গুলিয়ে ফেলাই বেশিরভাগ ভুলের উৎস: pointer p নিজে থাকে stack-এ, আর যে int-এর দিকে সে দেখায়, সেটি থাকে heap-এ। ধাপে ধাপে দেখুন, প্রতিটি ধাপে আগে নিজে অনুমান করুন:

void f() {ফাংশন শুরু
STACK                         HEAP
[ f-এর frame ]                (ফাঁকা জায়গা, allocator-এর হাতে)
  p @1000 = ?
new int(5) — ধাপ কজায়গা জোগাড়
STACK                         HEAP
[ f-এর frame ]
  p @1000 = ?                 @7000: [ ৪ বাইট, এখনো মান নেই ]
                              <- allocator একটি ফাঁকা block খুঁজে দিল
new int(5) — ধাপ খমান বসানো, ঠিকানা ফেরত
STACK                         HEAP
[ f-এর frame ]
  p @1000 = 7000  -------->   @7000: 5
*p = 7;pointer দিয়ে heap-এ লেখা
STACK                         HEAP
[ f-এর frame ]
  p @1000 = 7000  -------->   @7000: 7
delete p;block ফেরত গেল, p বদলায়নি
STACK                         HEAP
[ f-এর frame ]
  p @1000 = 7000  ----?--->   @7000: [ ফেরত দেওয়া — আর আপনার নয় ]
  <- p এখন dangling: ঠিকানা আছে, object নেই
}ফাংশন শেষ
STACK                         HEAP
[ f-এর frame মুছে গেছে, p-ও ]   (সব পরিষ্কার)

delete না লিখলে:
[ frame মুছে গেছে ]           @7000: 7  <- ঠিকানা কারও কাছে নেই: leak
  1. ফাংশন শুরু। f() ডাকতেই stack-এ তার frame তৈরি হলো, ভেতরে p-এর জন্য ৮ বাইট (64-bit সিস্টেমে একটি ঠিকানার মাপ)। Heap-এ এখনো আমাদের কিছু নেই। ঠিকানাগুলো উদাহরণমাত্র।
  2. new-এর প্রথম কাজ: জায়গা। new int(5) প্রথমে allocator-কে বলে “একটি int-এর মাপের জায়গা দাও।” Allocator heap-এর হিসাবের খাতা দেখে ধরুন ঠিকানা 7000-এ ৪ বাইট দিল।
  3. দ্বিতীয় কাজ: object তৈরি, তারপর ঠিকানা ফেরত। সেই জায়গায় মান 5 বসে (class হলে এখানে constructor চলত)। তারপর পুরো new লেখাটির ফল হয় ঠিকানা 7000, যা p-তে জমা হয়। Heap-এর int-টির নিজের কোনো নাম নেই — তাকে পাওয়ার একমাত্র উপায় এই ঠিকানা।
  4. ব্যবহার। *p = 7; মানে “p-তে যে ঠিকানা আছে, সেখানে 7 লেখো।” Stack-এর p বদলায়নি, বদলেছে heap-এর int.
  5. delete — জায়গা ফেরত, কিন্তু pointer অপরিবর্তিত। delete p; ঠিকানা 7000-এর int-কে ধ্বংস করে আর জায়গাটি allocator-কে ফেরত দেয়। খেয়াল করুন: p-এর ভেতরে এখনো 7000 লেখা! delete pointer-কে খালি করে না। এখন p একটি dangling pointer — আর ব্যবহার করলে undefined behavior. দরকার হলে নিজে p = nullptr; লিখুন।
  6. ফাংশন শেষ — দুই রকম পরিণতি। Stack-এর frame নিজে থেকেই মুছে যায়, সাথে p. আমরা delete করেছিলাম, তাই সব পরিষ্কার। কিন্তু delete না লিখলে? Frame-এর সাথে ঠিকানাটিও হারিয়ে যেত, আর heap-এ ৪ বাইট পড়ে থাকত, যা আর কেউ কখনো ফেরত দিতে পারবে না — এটাই memory leak.
মূল ধারণা

Stack-এর জিনিসের আয়ু ঠিক করে কোডের গঠন — যে block-এ জন্ম, সেই block শেষে মৃত্যু। Heap-এর জিনিসের আয়ু ঠিক করেন আপনি — new থেকে delete পর্যন্ত। এই স্বাধীনতাই heap-এর শক্তি, আবার এটাই তার বিপদ: যে দায়িত্ব compiler নিত, তা এখন আপনার কাঁধে। পুরো পর্বের প্রশ্ন একটাই — প্রতিটি heap block কে, কখন, ঠিক একবার ফেরত দেবে?

২ · new আর delete — জায়গা আর জীবন একসাথে

C++-এ new expression-এর কয়েকটি রূপ আছে। সবগুলোর ফল একটি pointer:

লেখাকী বানায়ফেরত দেওয়ার জোড়া
new intএকটি int, মান অনির্দিষ্ট (আবর্জনা)delete p;
new int(5) বা new int{5}একটি int, মান 5delete p;
new Report("Oct")একটি Report, constructor-এ "Oct" পাঠিয়েdelete r;
new int[n]পাশাপাশি n টি int, মান অনির্দিষ্টdelete[] a;
new int[n]() বা new int[n]{}n টি int, সবগুলো 0delete[] a;
new Customer[n]n টি Customer, প্রতিটির default constructor চালিয়েdelete[] c;

শেষ কলামটিই এই পর্বের প্রথম কঠিন নিয়ম: একটি object-এর new-এর সাথে delete, array-র new[]-এর সাথে delete[]। কোনো pointer দেখে compiler বুঝতে পারে না সেটি একটি object-এর, না একটি array-র প্রথম উপাদানের — দুটোর টাইপই Customer*. তাই কোনটি লিখবেন, সে দায়িত্ব আপনার। অংশ ৪-এ দেখবেন জোড়া না মিললে কী হয়।

new আসলে দুটি কাজ করে

new Report("Oct") এক লাইনের লেখা, কিন্তু ভেতরে দুটি আলাদা ধাপ:

  1. জায়গা জোগাড়: একটি Report-এর মাপের কাঁচা মেমরি চাওয়া হয় (ভেতরে operator new নামের একটি ফাংশন এ কাজ করে, যা সাধারণত শেষ পর্যন্ত malloc-এর মতো allocator-কেই ডাকে)।
  2. জীবন দেওয়া: সেই জায়গায় Report-এর constructor চলে — member-গুলো তৈরি হয়, std::string-এর ভেতরের লেখা বসে। এর পরেই জিনিসটি সত্যিকারের একটি object.

আর delete expression ঠিক উল্টো ক্রমে কাজ করে: আগে destructor চালিয়ে object-কে তার নিজের রিসোর্স (যেমন std::string-এর ভেতরের heap মেমরি) ফেরত দেওয়ার সুযোগ দেয়, তারপর জায়গাটি allocator-কে ফেরত দেয়। Constructor আর destructor আপনি পর্ব ২১-এ বিস্তারিত শিখবেন; এখনকার জন্য এটুকু জানলেই চলবে — constructor হলো object জন্মের মুহূর্তে নিজে থেকে চলা ফাংশন, আর destructor (~Report()) মৃত্যুর মুহূর্তে নিজে থেকে চলা ফাংশন।

নিচের প্রোগ্রামে constructor আর destructor নিজের নাম প্রিন্ট করে, যাতে দেখা যায় কোন লাইনে কোন object জন্মায় আর মরে। এখানে সব delete সঠিকভাবে লেখা — এটি “হাতে লিখলে ঠিক কীভাবে লিখতে হয়”-এর নমুনা:

#include <print>
#include <string>

struct Report {
    std::string title;
    explicit Report(const char* t) : title(t) { std::println("ctor: {}", title); }
    ~Report() { std::println("dtor: {}", title); }
};

int main() {
    Report* r = new Report("Sales-Oct");     // jayga + constructor
    std::println("using {}", r->title);
    delete r;                                // destructor + jayga ferot
    r = nullptr;                             // dangling na rakhar obhyash

    Report* many = new Report[2]{Report("A"), Report("B")};
    std::println("array ready");
    delete[] many;                           // new[] er jora: delete[]
}

আউটপুটের ক্রম খেয়াল করুন: ctor: Sales-Oct, using Sales-Oct, তারপর delete r;-এর লাইনে dtor: Sales-Oct. Destructor চলেছে ঠিক যখন আপনি বলেছেন — আগেও না, পরেও না। Array-র বেলায় ctor: A, ctor: B, array ready, তারপর dtor: B আর dtor: A — delete[] প্রতিটি উপাদানের destructor চালায়, আর তা উল্টো ক্রমে, শেষে-বানানোটি আগে। r->title মানে “r যে object-এর দিকে দেখায়, তার title” — পর্ব ১৫-এর (*r).title-এর সংক্ষিপ্ত রূপ। এই প্রোগ্রাম সঠিক, কিন্তু লক্ষ করুন — প্রতিটি new-এর জন্য একটি করে delete মনে রাখতে হয়েছে। অংশ ৬-এ দেখবেন একই কাজ delete না লিখেই কীভাবে হয়।

delete nullptr নিরাপদ: খালি pointer-কে delete করলে কিছুই ঘটে না, তাই if (p) delete p; লেখার দরকার নেই। এজন্যই delete-এর পর p = nullptr; একটি ভালো অভ্যাস — ভুল করে আবার delete করলেও ক্ষতি হয় না। তবে এটি শুধু এই একটি pointer-কে রক্ষা করে; একই ঠিকানার আরেকটি কপি অন্য কোথাও থাকলে সেটি তখনো dangling (অংশ ৪)।

৩ · new বনাম malloc — শুধু জায়গা, না জায়গা আর জীবন?

C থেকে আসা অনেকে ভাবেন, new হলো malloc-এর একটু সুন্দর নাম। আসলে পার্থক্য মৌলিক। malloc একটি সাধারণ ফাংশন, যা শুধু বাইট গোনে — “৪০ বাইট দাও।” সে জানেই না সেই ৪০ বাইটে কী থাকবে, তাই কোনো constructor চালায় না। new একটি ভাষার অংশ, যে টাইপ জানে — “একটি Report বানাও।”

বৈশিষ্ট্যnew / deletemalloc / free
কীভাষার operator<cstdlib>-এর ফাংশন
আকার কে হিসাব করেcompiler, টাইপ থেকেআপনি: malloc(n * sizeof(T))
ফেরতের টাইপসঠিক টাইপের pointer, T*void* — C++-এ cast লাগে
Constructor / destructorচালায়চালায় না
ব্যর্থ হলেstd::bad_alloc exception ছোড়েNULL ফেরত দেয়
আকার বদলানোসরাসরি উপায় নেই (vector ব্যবহার করুন)realloc
মেশানো যায়?না। new-এর জিনিস free দিয়ে, বা malloc-এর জিনিস delete দিয়ে ফেরত দেওয়া undefined behavior

Constructor না চলার পরিণতি গুরুতর। ধরুন Report-এর ভেতরে একটি std::string আছে। malloc দিয়ে জায়গা নিয়ে সরাসরি r->title = "Oct"; লিখলে, std::string-টি কখনো তৈরিই হয়নি — তার ভেতরের pointer আর আকারের ঘরে আবর্জনা। সেই আবর্জনায় লিখতে গেলে undefined behavior. আবার free destructor চালায় না, তাই string-এর নিজের heap মেমরি কখনো ফেরত যায় না। এজন্য C++ Core Guidelines-এর নিয়ম R.10 সোজা বলে: malloc আর free এড়িয়ে চলুন। এগুলো দরকার হয় কেবল C লাইব্রেরির সাথে কাজ করতে, যখন সেই লাইব্রেরি নিজে free করবে।

C জানলে

Heap-এর ধারণা হুবহু এক — চেয়ে নেওয়া, ঠিকানা রাখা, ঠিক একবার ফেরত দেওয়া; leak, double free আর use-after-free-ও একই বাগ। পার্থক্য হলো C++-এর new টাইপ জানে, cast চায় না, constructor চালায় আর ব্যর্থ হলে NULL না দিয়ে exception ছোড়ে। আর C-তে যেখানে malloc/realloc দিয়ে নিজে বাড়তে-পারা array বানাতেন, C++-এ সেটি std::vector করে দেয়। Heap-এর মানচিত্র আর allocator-এর ভেতরের কথা গভীরে জানতে C কোর্সের ডায়নামিক মেমরি পর্বটি দেখুন।

৪ · চারটি ক্লাসিক বাগ — যেগুলো compile হয়, তবু ভুল

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

১. Leak — কখনো ফেরত নয়

প্রতিটি অর্ডারের জন্য একটি Receipt বানানো হলো, কিন্তু delete নেই। দিনে এক লাখ অর্ডার — এক লাখ হারানো block. সার্ভার কয়েক দিন চলার পর মেমরি ফুরিয়ে বসে পড়ে।

শূন্যবার ফেরত

২. Double delete — দুবার ফেরত

দুটি pointer একই Customer-এর দিকে; দুই জায়গার কোড দুজনেই “আমার কাজ শেষ” ভেবে delete করল। Allocator-এর হিসাবের খাতা নষ্ট হয় — প্রায়ই crash, কখনো আরও খারাপ।

দুবার ফেরত

৩. Use-after-free — ফেরতের পর ব্যবহার

Delete করার পর পুরোনো pointer দিয়ে নাম প্রিন্ট। ঠিকানায় এখন হয়তো অন্য কারও ডেটা — ভুল নাম, crash, বা নিরাপত্তা-ছিদ্র।

dangling pointer

৪. ভুল জোড়া — new[] দিয়ে নিয়ে delete

new Customer[n]-এর পর delete c; লেখা, বা new-এর জিনিস free করা। Undefined behavior — সাধারণত বাকি destructor চলে না, কখনো heap নষ্ট হয়।

জোড়া মেলেনি

১. Leak — যে ঠিকানা হারিয়ে গেছে

void handleOrder(int id) {
    Receipt* r = new Receipt(id);
    r->print();
}   // delete নেই। r মুছে গেল, Receipt রয়ে গেল heap-এ — চিরকালের জন্য

Leak-এর বিশেষত্ব হলো, এটি তাৎক্ষণিক কোনো ভুল দেখায় না। ছোট প্রোগ্রাম কয়েক সেকেন্ড চলে শেষ হয়, আর শেষ হলে অপারেটিং সিস্টেম পুরো প্রোগ্রামের মেমরি এমনিতেই ফেরত নেয় — তাই কেউ টের পায় না। কিন্তু যে প্রোগ্রাম মাসের পর মাস চলে — bKash-এর মতো লেনদেনের সার্ভার, একটি মোবাইল অ্যাপ, একটি গেম — সেখানে প্রতিটি leak জমতে জমতে মেমরি ফুরিয়ে দেয়। আর leak শুধু মেমরি নয়: Receipt-এর destructor যদি একটি ফাইল বন্ধ করার কথা থাকে, সেই ফাইলও খোলা থেকে যায়।

২ ও ৩. Double delete আর use-after-free — একই গল্পের দুই প্রান্ত

এই দুটি বাগ প্রায় সবসময় একই কারণে জন্মায়: একই heap block-এর ঠিকানা একাধিক জায়গায় আছে, কিন্তু কে মালিক, তা স্পষ্ট নয়। একটি গ্রাহক-তালিকার প্রোগ্রামে current আর lastViewed দুটি pointer একই গ্রাহকের দিকে দেখাচ্ছে। সময়রেখা ধরে দেখুন:

1Customer* current = new Customer("Rahim");2Customer* lastViewed = current;3delete current;  current = nullptr;4Customer* other = new Customer("Karim");5std::println("{}", lastViewed->name);6delete lastViewed;
heap block @5000 আর pointer-গুলো
@5000
current
lastViewed
other
  1. এক মালিক। Heap-এ Rahim নামের একটি Customer তৈরি হলো, ঠিকানা ধরুন 5000. ঠিকানাটি শুধু current-এর কাছে।
  2. ঠিকানার কপি। lastViewed = current; object কপি করে না, শুধু ঠিকানা কপি করে। এখন দুটি pointer একই Rahim-এর দিকে। কোডের কোথাও লেখা নেই, এদের মধ্যে কে মালিক — যে শেষে delete করবে।
  3. একজন delete করল। current-এর মালিক ভেবে কেউ delete করে current = nullptr;-ও লিখলেন — ভালো অভ্যাস। কিন্তু সেটি শুধু current-কে বাঁচায়। lastViewed-এর ভেতরে এখনো 5000 — সে এখন dangling, আর কেউ তা জানে না।
  4. Allocator জায়গাটি আবার দিল। ফেরত পাওয়া জায়গা allocator পরের অনুরোধেই দিয়ে দিতে পারে — প্রায়ই একই মাপের অনুরোধে ঠিক সেই ঠিকানাই। নতুন Karim বসল 5000-এ। (কোন ঠিকানা আসবে, standard নিশ্চিত করে না; এটি একটি সম্ভাব্য ঘটনা।)
  5. Use-after-free. lastViewed->name পড়ল 5000 থেকে — আর পেল Karim! প্রোগ্রাম crash করেনি, ভুল তথ্য দেখিয়েছে। এটিই use-after-free-এর সবচেয়ে বিপজ্জনক রূপ: নীরবে ভুল। হ্যাকাররা ঠিক এই কৌশলে মুক্ত হওয়া জায়গায় নিজেদের সাজানো ডেটা বসিয়ে প্রোগ্রামকে ভুল পথে চালান।
  6. Double delete — অন্যের ঘর ভাঙা। delete lastViewed; লেখকের চোখে Rahim-কে ফেরত দেওয়া, বাস্তবে Karim-কে ধ্বংস করল। এখন other dangling. Karim জায়গা না পেলে এটি সরাসরি একই block-এর দ্বিতীয় delete — double delete — যা allocator-এর খাতা নষ্ট করে। দুই ক্ষেত্রেই undefined behavior. মূল কারণ লাইন ২-এ: মালিকানা অস্পষ্ট।

Undefined behavior মানে “প্রোগ্রাম crash করবে” নয়। মানে, C++ standard কোনো প্রতিশ্রুতি দেয় না — ফল আপনার মেশিনে ঠিক, পরীক্ষকের মেশিনে ভুল, optimization চালু করলে অন্যরকম হতে পারে। তাই এই পর্বের ভুল কোডগুলো অনলাইন compiler-এ “চালিয়ে দেখে” কিছু প্রমাণ হয় না; একবার ঠিক চলা মানে কিছুই নয়। এগুলো ধরার সঠিক যন্ত্র অংশ ৭-এর sanitizer.

৪. ভুল জোড়া — কেন delete[] আলাদা

Customer* list = new Customer[100];
// ...
delete list;     // ভুল! হওয়া উচিত delete[] list;

কেন দুই রকম delete? কারণ new Customer[100]-এর পর delete-এর কাজ ১০০টি destructor চালানো — আর তার জন্য জানতে হবে “কয়টি?” কিন্তু pointer-এ শুধু প্রথম উপাদানের ঠিকানা আছে। তাই অনেক implementation new[]-এর সময় array-র ঠিক আগে কয়েক বাইটে সংখ্যাটি (১০০) লুকিয়ে লিখে রাখে, আর delete[] সেখান থেকে পড়ে। সাধারণ delete ধরে নেয় একটিই object, ওই লুকানো সংখ্যার কথা জানে না — ফলে সাধারণত মাত্র একটি destructor চলে, বাকি ৯৯টির রিসোর্স leak হয়, আর ভুল ঠিকানা allocator-কে ফেরত দেওয়ায় heap নষ্টও হতে পারে। ঠিক কী ঘটবে, তা নির্ভর করে compiler-এর ওপর; standard শুধু বলে — undefined behavior. GCC আর Clang অনেক সময় এমন স্পষ্ট ভুলে -Wmismatched-new-delete সতর্কবার্তা দেয়, কিন্তু pointer যখন অন্য ফাংশন ঘুরে আসে, তখন কেউ ধরতে পারে না।

৫ · Exception আর early return — হাতে লেখা delete কেন ভঙ্গুর

ধরুন আপনি সাবধানী। প্রতিটি new-এর নিচে একটি delete লিখেছেন, ঠিক জোড়া মিলিয়ে। তবু বিপদ কাটেনি — কারণ ফাংশন সবসময় শেষ লাইন পর্যন্ত পৌঁছায় না। ফাংশন থেকে বেরোনোর পথ অনেক: মাঝখানে একটি return, লুপের ভেতরের break, বা সবচেয়ে লুকানো — একটি exception. কুরিয়ার কোম্পানির গ্রাহক-তালিকা লোড করার এই ফাংশনটি দেখুন (exception-এর পুরো গল্প পর্ব ৩৭-এ; এখন শুধু জানুন, throw হলে ফাংশন সেই লাইনেই থেমে যায় আর নিয়ন্ত্রণ সোজা বাইরে কোথাও চলে যায়):

1bool loadCustomers(const std::string& path) {2    Customer* list = new Customer[500];3    std::ifstream file(path);4    if (!file) return false;         // delete বাদ!5    parseAll(file, list);            // throw করতে পারে6    delete[] list;7    return true;8}
heap-এর হিসাব
ডাক ১-এর block
ডাক ২-এর block
মোট leak
  1. ফাংশন শুরু। প্রথমবার loadCustomers("today.csv") ডাকা হলো। আপাতত সব স্বাভাবিক।
  2. Heap থেকে ৫০০ জনের জায়গা। new Customer[500] জায়গা নিল আর ৫০০টি constructor চালাল। ঠিকানা শুধু local list-এর কাছে — যা stack-এ, ফাংশন শেষে মুছে যাবে।
  3. ফাইল খোলা গেল না। হয়তো ফাইলের নাম ভুল, বা ফোল্ডারে নেই। file এখন ব্যর্থ অবস্থায়।
  4. Early return — leak. লেখক তাড়াতাড়ি ফেরার পথ রেখেছেন (পর্ব ০৮-এর guard clause — যা ভালো অভ্যাস!)। কিন্তু return false; লাইন ৬-কে এড়িয়ে যায়। Frame মুছে গেল, list হারাল, ৫০০ Customer heap-এ চিরকাল পড়ে রইল। কোড রিভিউতে এটি ধরা সহজ — কিন্তু ভাবুন, ফাংশনে এমন ছয়টি return থাকলে?
  5. দ্বিতীয় ডাক — এবার ফাইল আছে। আবার ৫০০ জনের জায়গা নেওয়া হলো, ফাইল খুলল, parseAll পড়া শুরু করল। এখানে কোনো return নেই — দেখে মনে হয় এবার লাইন ৬-এ পৌঁছাবেই।
  6. Exception — অদৃশ্য প্রস্থানপথ। ফাইলের ৩১৭ নম্বর লাইনে ভুল ফরম্যাট, parseAll exception ছুড়ল। তখন stack unwinding শুরু হয়: ফাংশনের frame একে একে বন্ধ হয়, stack-এর object-গুলোর destructor চলে (file ঠিকঠাক বন্ধ হয়, কারণ সে stack-এর object!)। কিন্তু লাইন ৬ সাধারণ একটি statement, destructor নয় — সেটি চলে না। আরও ৫০০ জনের leak. লক্ষ করুন, কোডে throw শব্দটি এই ফাংশনে কোথাও লেখা নেই — বিপদটা লুকিয়ে ছিল অন্য ফাংশনে।

শেষ ধাপের পর্যবেক্ষণটিই সমাধানের চাবি: file-এর destructor চলেছে, list-এর delete চলেনি। Stack-এর object যেভাবেই ফাংশন শেষ হোক — স্বাভাবিকভাবে, return-এ, বা exception-এ — নিজের destructor চালায়। তাহলে heap-এর block-এর দায়িত্ব যদি এমন একটি stack-object-কে দেওয়া যায়, যার destructor-এ delete লেখা? এই ধারণার নাম RAII, আর পরের অংশে এটিই দেখবেন।

মেমরি না পেলে — std::bad_alloc

Heap অসীম নয়। সাধারণ new যখন জায়গা জোগাড় করতে পারে না, তখন nullptr ফেরত দেয় না — <new>-এ সংজ্ঞায়িত std::bad_alloc exception ছোড়ে। তাই C-এর অভ্যাসে if (p == nullptr) পরীক্ষা সাধারণ new-এর পরে অর্থহীন — ব্যর্থ হলে প্রোগ্রাম ওই লাইন পর্যন্ত পৌঁছায়ই না। কেউ exception না চাইলে new (std::nothrow) Customer[n] লিখতে পারেন; তখন ব্যর্থ হলে nullptr আসে, আর পরীক্ষা করা আপনার দায়িত্ব।

try {
    auto* big = new double[n];       // n খুব বড় হলে bad_alloc
    // ...
    delete[] big;
} catch (const std::bad_alloc&) {
    std::println("not enough memory for {} values", n);
}

বাস্তবে একটি সূক্ষ্মতা আছে: Linux সাধারণত মেমরির প্রতিশ্রুতি আগে দেয়, আসল জায়গা দেয় ব্যবহারের সময় (একে বলে overcommit). ফলে সেখানে বিশাল new-ও প্রায়ই সফল হয়, আর মেমরি সত্যিই ফুরালে অপারেটিং সিস্টেম প্রোগ্রামটিকেই বন্ধ করে দিতে পারে। তাই bad_alloc সবচেয়ে নিশ্চিতভাবে দেখা যায় অসম্ভব বড় অনুরোধে, বা মেমরি-সীমা বাঁধা পরিবেশে। Embedded সিস্টেমে, যেখানে exception বন্ধ রাখা হয়, সেখানে nothrow রূপ বা নিজস্ব allocator বেশি প্রচলিত।

৬ · “Naked new” কেন নয় — আর তার বদলে কী

এখন পর্যন্ত যা দেখলেন, তার সারকথা: হাতে new/delete লেখা মানে প্রতিটি block-এর জন্য — প্রতিটি return-পথে, প্রতিটি সম্ভাব্য exception-এ, প্রতিটি কপি করা pointer-এর ক্ষেত্রে — মাথায় হিসাব রাখা “কে, কখন, একবার ফেরত দেবে।” মানুষ এ হিসাব নির্ভুলভাবে রাখতে পারে না, অন্তত বড় কোডবেসে বছরের পর বছর ধরে নয়। তাই Bjarne Stroustrup ও Herb Sutter-এর সম্পাদিত C++ Core Guidelines-এর নিয়ম R.11: “Avoid calling new and delete explicitly” — new আর delete সরাসরি ডাকবেন না। কোনো মালিক-object ছাড়া কাঁচা pointer-এ রাখা new-কে বলে naked new.

এর মানে heap ব্যবহার বন্ধ নয় — মানে heap-এর প্রতিটি block-এর একজন মালিক-object থাকবে, যার destructor ফেরত দেবে। এই মালিকানার (ownership) কাজ করে দেওয়ার জন্য standard library-তে তৈরি টাইপ আছে:

আপনি যা চানপুরোনো উপায়আধুনিক উপায়
চলার সময় ঠিক হওয়া আকারের তালিকাnew Customer[n] … delete[]std::vector<Customer> (পর্ব ১৩)
লেখা (নাম, ঠিকানা)new char[len + 1] … delete[]std::string (পর্ব ১৪)
একটি object, একজন মালিকnew Report(...) … deleteauto r = std::make_unique<Report>(...)
একটি object, কয়েকজন ভাগীদার মালিকহাতে গোনা “কে শেষে delete করবে”std::make_shared<Report>(...)
শুধু দেখা, মালিক নয়Report*Report& বা Report* — কখনো delete নয়

std::unique_ptr হলো একটি smart pointer — pointer-এর মতো -> দিয়ে ব্যবহার করা যায়, কিন্তু সে নিজে একটি stack-object, আর তার destructor-এ লেখা আছে delete. std::make_unique (C++14) heap-এ object বানিয়ে সরাসরি তার হাতে দেয়, তাই আপনার কোডে new শব্দটিও থাকে না। ভাগাভাগি মালিকানার জন্য আছে std::shared_ptr. এগুলো কীভাবে কাজ করে, কেন unique_ptr কপি করা যায় না, আর কখন কোনটি — সেটা পর্ব ১৭-এর পুরো বিষয়। এখানে শুধু ফলাফলটি দেখুন — অংশ ৫-এর একই ব্যর্থতার পথ, এবার মালিক-object দিয়ে:

std::vector<Customer> list(500);মালিক: list (stack-এ)
STACK                              HEAP
[ loadCustomers ]
  list  { data: 7000, size: 500 } ---> @7000: ৫০০ Customer
  <- vector নিজেই heap থেকে নিয়েছে
if (!file) return false;early return
STACK                              HEAP
[ loadCustomers ] বন্ধ হচ্ছে...
  list.~vector()  চলল  --->     @7000: ৫০০ destructor, তারপর ফেরত
                                   leak: 0
parseAll(file, list); // throwexception
STACK                              HEAP
[ loadCustomers ] unwinding...
  file.~ifstream()  চলল          (ফাইল বন্ধ)
  list.~vector()    চলল  --->   @7000: ফেরত
                                   leak: 0
auto r = std::make_unique<Report>("Monthly");একটি object, একজন মালিক
STACK                              HEAP
[ build ]
  r  (unique_ptr) ---------------> @9000: Report "Monthly"
ফাংশন যেভাবেই শেষ হোক:
  r.~unique_ptr() → delete       @9000: ~Report(), তারপর ফেরত
  1. মালিক একটি stack-object. std::vector<Customer> list(500); — vector নিজে stack-এ থাকে, ভেতরে heap থেকে ৫০০ জনের জায়গা নেয়। দেখতে অংশ ৫-এর মতোই, পার্থক্য একটাই: heap block-এর মালিক এখন একটি object, যার destructor আছে। আর কোডে new নেই।
  2. Early return — তবু পরিষ্কার। ফাইল না খুললে return false;. Frame বন্ধ হওয়ার সময় list-এর destructor নিজে থেকে চলে: ৫০০টি Customer ধ্বংস করে, জায়গা ফেরত দেয়। আপনি কিছুই লেখেননি, তবু leak শূন্য।
  3. Exception — তবু পরিষ্কার। Stack unwinding-এর সময় যেমন file-এর destructor চলেছিল, তেমনি এবার list-এরও চলে। যে প্রস্থানপথ আগে delete-কে এড়িয়ে যেত, এখন সেটিও ফেরত দেওয়ার পথ। কয়টি return আছে, কে throw করে — আর হিসাব রাখতে হয় না।
  4. একটি object-এর জন্য unique_ptr. Report-এর মতো একক object-এর মালিক std::unique_ptr. যেভাবেই ফাংশন শেষ হোক, r-এর destructor delete চালায়। নিয়মটি একটি বাক্যে: heap-এর প্রতিটি block-এর একজন মালিক-object, আর ফেরত দেওয়া সেই মালিকের destructor-এর কাজ।

এবার নিজে চালিয়ে দেখুন। একই Report, এবার std::make_unique দিয়ে, আর একটি ফাংশন যেখানে ইচ্ছা করে মাঝপথে return রাখা হয়েছে। সাথে একটি গ্রাহক-তালিকা, যা নিজের heap নিজেই সামলায়:

#include <memory>
#include <print>
#include <string>
#include <vector>
struct Report {
    std::string title;
    explicit Report(const char* t) : title(t) { std::println("ctor: {}", title); }
    ~Report() { std::println("dtor: {}", title); }
};
void build(bool fileOk) {
    auto r = std::make_unique<Report>("Monthly");   // malik: r
    if (!fileOk) return;                            // early return - tobu dtor cholbe
    std::println("built {}", r->title);
}
int main() {
    std::vector<std::string> customers{"Rahim", "Karim", "Sumi"};
    customers.push_back("Nadia");                   // vector nijei heap samlay
    std::println("{} customers", customers.size());
    build(false);
    build(true);
}

আউটপুট: 4 customers, তারপর প্রথম ডাকে ctor: Monthly আর সাথে সাথে dtor: Monthly — early return হলেও destructor চলেছে। দ্বিতীয় ডাকে ctor: Monthly, built Monthly, dtor: Monthly. পুরো প্রোগ্রামে একটিও new বা delete নেই, অথচ heap ব্যবহার হয়েছে অনেকবার: vector গ্রাহকদের জন্য (আর push_back-এ দরকার হলে বড় জায়গায় সরেছে), std::string লম্বা নামের জন্য, unique_ptr Report-এর জন্য। প্রতিটির মালিক নিজের destructor-এ ফেরত দিয়েছে। main শেষে customers-ও নিজে থেকে পরিষ্কার হয়।

তাহলে new কি কখনোই লিখবেন না? প্রায় কখনোই না — অ্যাপ্লিকেশন কোডে। ব্যতিক্রম মূলত লাইব্রেরি বা নিজের container লেখার মতো নিচু স্তরের কাজ, আর পুরোনো কোডবেস বা C লাইব্রেরির সাথে সংযোগ। সেখানেও new-এর ফল সাথে সাথে কোনো মালিক-object-এর হাতে তুলে দেওয়া রীতি। আর কাঁচা pointer (T*) আধুনিক কোডে থাকে শুধু “দেখার” জন্য — Core Guidelines-এর ভাষায়, raw pointer মালিক নয় (R.3); তাকে delete করা আপনার কাজ নয়।

৭ · আরও কিছু জানা দরকার — placement new আর বাগ ধরার যন্ত্র

Placement new — শুধু চিনে রাখুন

new দুটি কাজ করে — জায়গা আর জীবন। কখনো কখনো জায়গা আগে থেকেই আছে, শুধু জীবন দরকার। যেমন std::vector ভেতরে আগে একসাথে অনেকটা কাঁচা জায়গা নিয়ে রাখে (capacity), তারপর push_back-এর সময় ঠিক পরের ঘরে একটি object তৈরি করে। এর জন্য আছে placement new: new (ঠিকানা) T(...) — নতুন মেমরি নেয় না, দেওয়া ঠিকানায় শুধু constructor চালায়। এর destructor হাতে ডাকতে হয়, আর জায়গা ফেরত দিতে হয় আলাদাভাবে। এটি লাইব্রেরি-লেখকদের যন্ত্র; সাধারণ কোডে আপনার দেখা পাওয়ার কথা নয়, আর নিজে লেখার দরকারও পড়বে না।

যে বাগ চোখে দেখা যায় না, তা যন্ত্র দিয়ে ধরুন

অংশ ৪-এর বাগগুলো প্রায়ই নীরব থাকে। তাই পেশাদার C++ দলগুলো চোখের ওপর ভরসা করে না, যন্ত্র ব্যবহার করে:

যন্ত্রকীভাবে চালাবেনযা ধরে
AddressSanitizer (ASan)g++ -g -fsanitize=address app.cppuse-after-free, double delete, ভুল জোড়ার delete, array-র সীমার বাইরে লেখা; Linux-এ সাথে leak-ও
LeakSanitizerASan-এর সাথে নিজে থেকে (Linux), বা -fsanitize=leakপ্রোগ্রাম শেষে যে block-এর ঠিকানা কারও কাছে নেই
Valgrind (Memcheck)valgrind --leak-check=full ./appleak, use-after-free, double free, মান না দেওয়া মেমরি পড়া — recompile ছাড়াই, তবে অনেক ধীর

অংশ ৪-এর use-after-free কোড ASan দিয়ে চালালে লাইন ৫-এই প্রোগ্রাম থেমে যায়, আর বার্তায় লেখা থাকে heap-use-after-free, সাথে তিনটি ঠিকানার ইতিহাস: কোথায় পড়া হলো, কোথায় delete হয়েছিল, আর কোথায় new হয়েছিল। কোনো leak থাকলে প্রোগ্রাম শেষে আসে detected memory leaks আর কোন লাইনের new থেকে কত বাইট হারাল। এগুলোর ব্যবহার, ডিবাগার আর টেস্টের সাথে, পর্ব ৪৮-এ দেখবেন। এখনকার পরামর্শ: শেখার সময় নিজের প্রোগ্রাম মাঝে মাঝে -fsanitize=address দিয়ে চালান — sanitizer হলো সেই শিক্ষক, যে প্রতিটি মেমরি-ভুল লাইন নম্বরসহ ধরিয়ে দেয়।

new আর delete C++-এর প্রায় শুরু থেকে আছে — ১৯৮০-র দশকের গোড়ায় “C with Classes”-এই Stroustrup এগুলো যোগ করেন, যাতে heap-এ object বানালেও constructor আর destructor নিশ্চিতভাবে চলে, যা malloc পারত না। আধুনিক বিকল্প এসেছে অনেক পরে: std::auto_ptr ছিল C++98-এ, কিন্তু তার কপি-আচরণ বিভ্রান্তিকর হওয়ায় C++11-এ std::unique_ptr আর std::shared_ptr আসে, আর auto_ptr C++17-এ বাদ পড়ে। std::make_shared এসেছে C++11-এ, কিন্তু std::make_unique — যা দেখতে সহজতম — এসেছে C++14-এ; Herb Sutter পরে বলেছেন C++11-এ এটি না থাকা আংশিকভাবে নজর এড়িয়ে যাওয়ার ফল।

পরীক্ষা ও ইন্টারভিউতে
  • new আর malloc-এর পার্থক্য? — new টাইপ জানে, constructor চালায়, cast লাগে না, ব্যর্থ হলে bad_alloc ছোড়ে; malloc শুধু বাইট দেয়, ব্যর্থ হলে NULL. একটির জিনিস অন্যটি দিয়ে ফেরত দেওয়া যায় না।
  • delete আর delete[]-এর পার্থক্য? — delete একটি object-এর destructor চালায়; delete[] array-র প্রতিটি উপাদানের (উল্টো ক্রমে)। new[]-এর সাথে delete লেখা undefined behavior.
  • Memory leak কী, আর কীভাবে এড়াবেন? — heap-এর block ফেরত না দিয়ে তার ঠিকানা হারানো; vector/string/unique_ptr-এর মতো মালিক-object ব্যবহার করে (RAII).
  • Dangling pointer কী? — যে pointer এমন ঠিকানা রাখে, যেখানের object delete হয়ে গেছে; তা দিয়ে পড়া বা লেখা use-after-free.
  • delete p;-এর পর p-এর মান কী? — বদলায় না, পুরোনো ঠিকানাই থাকে; তাই প্রয়োজনে নিজে p = nullptr; লিখতে হয়। আর delete nullptr নিরাপদ।

৮ · AI-এর যুগে — AI-এর লেখা মেমরি-কোড যাচাই

অনলাইনে C++-এর বিপুল পরিমাণ পুরোনো কোড, টিউটোরিয়াল আর পাঠ্যবইয়ের উদাহরণ আছে, যেগুলো C++11-এর আগের রীতিতে লেখা — সর্বত্র new আর delete. AI টুল সেখান থেকেই শিখেছে, তাই “একটি dynamic array বানাও” বললে প্রায়ই সোজা new int[n] লিখে দেয়। কোড compile হয়, ছোট পরীক্ষায় ঠিক চলে — ভুল ধরা পড়ে শুধু ব্যর্থতার পথে বা দীর্ঘ সময় চালালে। সাধারণ ভুলগুলো:

AI যা করেসমস্যাযা হওয়া উচিত
int* a = new int[n]; … delete a;ভুল জোড়া — undefined behaviorstd::vector<int> a(n);
মাঝপথে return বা throw, delete শুধু শেষেব্যর্থতার পথে leakমালিক-object: vector বা std::make_unique
Report* makeReport() — new করে কাঁচা pointer ফেরতকে delete করবে, স্বাক্ষর দেখে বোঝা যায় নাstd::unique_ptr<Report> ফেরত, বা সরাসরি Report by value
class-এর member-এ char* name = new char[...]কপি হলে দুটি object একই block — double deletestd::string name;
new-এর পর if (p == nullptr)অর্থহীন — সাধারণ new ব্যর্থ হলে bad_alloc ছোড়েপরীক্ষা বাদ দিন, বা ইচ্ছা হলে std::nothrow
ছোট, স্থির আকারের জিনিসও new দিয়েঅকারণে ধীর আর ঝুঁকিপূর্ণসাধারণ local ভ্যারিয়েবল (stack)

AI-কে কোড লিখতে বলার সময় মেমরির নিয়ম আগেই বেঁধে দিন, আর প্রতিটি heap block-এর মালিক কে, সেটা ব্যাখ্যা করতে বলুন:

আমি C++23 শিখছি (g++, -Wall -Wextra). একটি প্রোগ্রাম লেখো যা একটি ফাইল থেকে গ্রাহকের তালিকা পড়বে (struct Customer { std::string name; std::string phone; }), গ্রাহকের সংখ্যা আগে জানা নেই, তারপর একটি Report object বানিয়ে সারাংশ প্রিন্ট করবে। নিয়ম: ১. কোডে কোথাও new বা delete লিখবে না (C++ Core Guidelines R.11 মেনে)। ২. তালিকার জন্য std::vector, লেখার জন্য std::string; একক heap object দরকার হলে শুধু std::make_unique. ৩. ফাইল না খুললে বা ভুল ফরম্যাট হলে early return বা exception হবে — তখনও যেন কোনো leak না হয়। ৪. কোনো ফাংশন কাঁচা pointer (T*) দিয়ে মালিকানা ফেরত দেবে না। ৫. heap দরকার না হলে সাধারণ local ভ্যারিয়েবল ব্যবহার করবে। কোডের পরে একটি তালিকায় বলো: প্রতিটি heap ব্যবহারের মালিক কোন object, আর প্রতিটি ব্যর্থতার পথে সেটি কীভাবে ফেরত যায়।

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

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

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

প্র ০১Java, C#, Go, Python — প্রায় সবার garbage collector আছে। C++-এ ডিফল্টভাবে নেই কেন? আর C++11-এ যে GC-সমর্থন যোগ হয়েছিল, তা C++23-এ বাদ পড়ল কেন?

একটি garbage collector (GC) প্রোগ্রাম চলার ফাঁকে খুঁজে দেখে কোন heap object-এর কাছে আর পৌঁছানোর পথ নেই, তারপর সেগুলো নিজেই ফেরত নেয়। এতে leak আর dangling-এর বড় অংশ উধাও হয়। তাহলে C++ কেন নেয়নি? কয়েকটি কারণ একসাথে কাজ করেছে।

প্রথমত, zero-overhead নীতি। Stroustrup-এর নকশার মূল কথা: যে বৈশিষ্ট্য আপনি ব্যবহার করেন না, তার খরচ আপনি দেবেন না। GC-র খরচ আছে — বাড়তি মেমরি (অনেক GC ভালো চলতে প্রয়োজনের কয়েক গুণ heap চায়), আর মাঝে মাঝে প্রোগ্রাম থামিয়ে বা সমান্তরালে খোঁজার CPU-সময়। গাড়ির ব্রেকের নিয়ন্ত্রক, পেসমেকার, শেয়ারবাজারের ট্রেডিং ইঞ্জিন বা গেমের প্রতিটি ফ্রেমে এই অনিশ্চিত বিরতি গ্রহণযোগ্য নয়।

দ্বিতীয়ত, destructor-এর নিশ্চয়তা। C++-এ object ঠিক কখন মরবে, তা জানা — scope শেষে, বা delete-এ। এই নিশ্চয়তার ওপরই RAII দাঁড়িয়ে: ফাইল বন্ধ, lock ছাড়া, নেটওয়ার্ক সংযোগ বন্ধ — সব destructor-এ। GC শুধু মেমরি সামলায়, আর কখন সামলাবে বলা যায় না; তাই Java-কে ফাইল বন্ধ করতে আলাদা try-with-resources আনতে হয়েছে। C++-এর উত্তর ছিল: RAII দিয়ে মেমরিসহ সব রিসোর্স একভাবে সামলাও। Stroustrup তাঁর FAQ-তে মজা করে লিখেছেন, C++ তাঁর প্রিয় garbage-collected ভাষা, কারণ এটি খুব কম garbage তৈরি করে।

তৃতীয়ত, C-এর উত্তরাধিকার। C++ কোড pointer-কে integer-এ রূপান্তর করতে পারে, ঠিকানার কিছু bit-এ অন্য তথ্য লুকাতে পারে (pointer tagging), এমনকি XOR করে দুটি ঠিকানা একসাথে রাখতে পারে। এমন অবস্থায় GC নিশ্চিত হতে পারে না কোন মান আসলে pointer. Hans Boehm-এর বিখ্যাত conservative collector C/C++-এর জন্য বহু দশক ধরে আছে — সেটি সন্দেহজনক সব মানকে pointer ধরে নিরাপদ দিকে থাকে — কিন্তু তা ঐচ্ছিক লাইব্রেরি, ভাষার অংশ নয়।

C++11-এ একটি ছোট আপস যোগ হয়েছিল: “garbage collection ও reachability-ভিত্তিক leak detection-এর ন্যূনতম সমর্থন” — std::declare_reachable জাতীয় কয়েকটি ফাংশন, যাতে কোনো implementation চাইলে GC যোগ করতে পারে। বাস্তবে GCC, Clang বা MSVC — কোনো প্রধান compiler এর ভিত্তিতে GC বানায়নি; ফাংশনগুলো কিছুই করত না। এক দশক পর প্রস্তাব P2186 যুক্তি দেখায় যে এই অংশ কেউ ব্যবহার করে না, শুধু ভাষাকে জটিল করে — আর C++23 সেটি সরিয়ে দেয়। এটি standard থেকে কোনো বৈশিষ্ট্য বাদ পড়ার বিরল উদাহরণগুলোর একটি।

মূল উপলব্ধি: C++ মেমরির সমস্যা উপেক্ষা করেনি — অন্য পথে সমাধান বেছেছে: চলার সময়ের একটি পাহারাদার নয়, compile-এর সময়েই নির্ধারিত মালিকানা (RAII, smart pointer). দাম হলো, নিয়মটি আপনাকে মানতে হয়; লাভ হলো, খরচ আর সময় দুটোই আগে থেকে জানা।

প্র ০২Heap থেকে জায়গা নেওয়া stack-এর চেয়ে কতটা ধীর, আর কেন? পারফরম্যান্স-সচেতন কোড এই খরচ কীভাবে এড়ায়?

Stack-এ জায়গা নেওয়া প্রায় বিনা খরচে। ফাংশন শুরুতে compiler একটি রেজিস্টার (stack pointer) থেকে frame-এর মাপ বিয়োগ করে — একটি মাত্র মেশিন-নির্দেশে সব local ভ্যারিয়েবলের জায়গা হয়ে যায়, আর ফেরার সময় যোগ করে ফেরত। কোন জায়গা ফাঁকা, খোঁজার দরকার নেই, কারণ stack সবসময় এক প্রান্ত থেকে বাড়ে-কমে। তার ওপর stack-এর ওপরের অংশ প্রায় সবসময় CPU-র cache-এ গরম থাকে।

Heap-এ কাজ অনেক বেশি। new-কে allocator-এর কাছে যেতে হয়: অনুরোধের মাপ অনুযায়ী উপযুক্ত তালিকা বাছা, একটি ফাঁকা block খুঁজে বের করা, হিসাবের খাতা হালনাগাদ করা। একাধিক thread একসাথে চাইলে তাদের মধ্যে সমন্বয় লাগে। হাতে জায়গা না থাকলে অপারেটিং সিস্টেমের কাছে নতুন মেমরি চাইতে হয় (Linux-এ brk বা mmap system call) — যা আরও ব্যয়বহুল; glibc-এর malloc ডিফল্টভাবে ১২৮ KB-র চেয়ে বড় অনুরোধ সরাসরি mmap দিয়ে মেটায়। আধুনিক allocator ছোট অনুরোধ সাধারণত কয়েক দশ ন্যানোসেকেন্ডে সারে — একক অনুরোধে সামান্য, কিন্তু একটি লুপে লাখবার হলে প্রোগ্রামের বড় সময় এখানেই যায়। তার ওপর heap-এর object মেমরিতে ছড়িয়ে থাকে, তাই pointer ধরে ধরে হাঁটলে cache miss বাড়ে, যা প্রায়ই allocation-এর চেয়েও বড় খরচ।

পারফরম্যান্স-সচেতন কোড যা করে: (১) ছোট, স্থির আকারের জিনিস stack-এ রাখে — std::array, সাধারণ local object. (২) একবারে অনেকটা নেয়: আকার আগে জানা থাকলে v.reserve(n), যাতে push_back বারবার বড় জায়গায় না সরে। (৩) লুপের ভেতরে বারবার বানানো-ভাঙার বদলে একটি object বাইরে রেখে পুনর্ব্যবহার করে। (৪) Library-ও সাহায্য করে: libstdc++-এর std::string ১৫ অক্ষর পর্যন্ত লেখা heap-এ না নিয়ে object-এর ভেতরেই রাখে (small string optimization) — তাই “Rahim”-এর মতো ছোট নামে কোনো allocation-ই হয় না। (৫) বিশেষ ক্ষেত্রে C++17-এর std::pmr দিয়ে আগে থেকে নেওয়া একটি বড় buffer থেকে খুব দ্রুত জায়গা দেওয়া।

তবে stack-এর সীমাও মনে রাখুন: Linux-এ main thread-এর stack সাধারণত ৮ MB, Windows-এ ১ MB. ফাংশনের ভেতরে int big[10'000'000]; লিখলে stack overflow — প্রোগ্রাম crash. বড় বা চলার সময় ঠিক হওয়া আকার heap-এরই কাজ।

মূল উপলব্ধি: heap খারাপ নয়, কিন্তু বিনা মূল্যে নয়। নিয়ম: যা stack-এ ধরে, stack-এ রাখুন; heap লাগলে কম বার, বড় টুকরোয় নিন — আর অনুমান নয়, profiler দিয়ে মেপে সিদ্ধান্ত নিন (পর্ব ৪৯)।

প্র ০৩Memory fragmentation কী? মোট ফাঁকা মেমরি যথেষ্ট থাকার পরেও কেন একটি বড় new ব্যর্থ হতে পারে, আর বড় সার্ভার ও গেম ইঞ্জিন এ সমস্যা কীভাবে সামলায়?

একটি সিনেমা হলের কথা ভাবুন। ৩০০ আসনের ১০০টি ফাঁকা, কিন্তু ছড়িয়ে-ছিটিয়ে — এখানে দুটি, ওখানে একটি। এখন ১০ জনের একটি পরিবার পাশাপাশি বসতে চায়। মোট ফাঁকা আসন অনেক, তবু তাদের জায়গা নেই। Heap-এ এই অবস্থার নাম memory fragmentation — আরও নির্দিষ্টভাবে external fragmentation: ফাঁকা জায়গা আছে, কিন্তু টানা নয়। আর new int[n] সবসময় টানা জায়গা চায়।

এটি জন্মায় বিভিন্ন মাপের object বিভিন্ন সময়ে বানানো আর ভাঙা থেকে। ছোট ছোট block-এর মাঝে মাঝে কিছু দীর্ঘজীবী object থেকে গেলে, তাদের চারপাশের ফাঁকা অংশ আর জোড়া লাগে না। আরেক ধরন internal fragmentation: allocator হিসাবের সুবিধার জন্য অনুরোধকে নির্দিষ্ট মাপে গোল করে — ২৫ বাইট চাইলে হয়তো ৩২ দেয় — বাকি ৭ বাইট নষ্ট। মাসের পর মাস চলা সার্ভারে এই দুটি মিলে প্রক্রিয়ার মেমরি ধীরে ধীরে বাড়তে পারে, এমনকি কোনো leak না থাকলেও — যা দেখে অনেকে ভুল করে leak খোঁজেন।

সমাধানের একটি বড় অংশ allocator-এর নকশায়। Jason Evans-এর jemalloc (প্রথমে FreeBSD-এর জন্য, পরে Firefox ও Facebook-এ ব্যবহৃত), Google-এর tcmalloc আর Microsoft-এর mimalloc — এরা অনুরোধকে মাপ-শ্রেণিতে ভাগ করে, প্রতিটি শ্রেণির জন্য আলাদা পাতা রাখে, যাতে একই মাপের জিনিস পাশাপাশি থাকে আর ফাঁকা হলে সহজে পুনর্ব্যবহার হয়। অনেক কোম্পানি শুধু allocator বদলে মেমরি ব্যবহার আর গতিতে উল্লেখযোগ্য উন্নতি পেয়েছে বলে জানিয়েছে।

দ্বিতীয় অংশ প্রোগ্রামের নকশায়। গেম ইঞ্জিন প্রায়ই প্রতিটি ফ্রেমের অস্থায়ী জিনিসের জন্য একটি “arena” রাখে — ফ্রেমের শুরুতে একটি বড় টুকরো, তার ভেতরে শুধু সামনে এগিয়ে জায়গা দেওয়া, আর ফ্রেম শেষে পুরোটা একবারে খালি। একই মাপের অনেক object (গুলি, কণা, শত্রু) থাকলে “pool” — আগে থেকে বানানো নির্দিষ্ট সংখ্যক ঘর। C++17-এর std::pmr::monotonic_buffer_resource আর unsynchronized_pool_resource ঠিক এই ধারণাগুলোর standard রূপ, আর std::pmr::vector এদের থেকে জায়গা নিতে পারে।

মূল উপলব্ধি: মেমরি শুধু “কতটা”-র প্রশ্ন নয়, “কীভাবে সাজানো”-রও। একই আয়ুর জিনিস একসাথে রাখুন, অসংখ্য ছোট আলাদা allocation এড়ান — vector-এর মতো টানা container এজন্যই প্রায়ই সেরা পছন্দ।

প্র ০৪AddressSanitizer বা Valgrind কীভাবে বোঝে কোন block leak হয়েছে, বা কোন পড়াটা use-after-free? এরা কি সব ভুল ধরতে পারে?

AddressSanitizer (ASan) Google-এর Konstantin Serebryany ও সহকর্মীরা তৈরি করেন; ২০১২ সালের USENIX সম্মেলনে এর গবেষণাপত্র প্রকাশিত হয়, আর এটি এখন GCC ও Clang দুটোরই অংশ। এর মূল কৌশল shadow memory: প্রোগ্রামের প্রতি ৮ বাইট মেমরির জন্য আলাদা এক বাইটের একটি “ছায়া” রাখা হয়, যাতে লেখা থাকে ওই ৮ বাইটে প্রবেশ বৈধ কি না। compile-এর সময় compiler প্রতিটি মেমরি পড়া-লেখার আগে ছায়াটি পরীক্ষার কোড বসিয়ে দেয়। প্রতিটি heap block-এর দুই পাশে কিছু নিষিদ্ধ জায়গা (redzone) রাখা হয় — array-র সীমা পেরোলেই ধরা। delete হওয়া block সাথে সাথে আবার দেওয়া হয় না; কিছুদিন “কোয়ারেন্টিনে” নিষিদ্ধ হিসেবে রাখা হয়। ফলে অংশ ৪-এর মতো পুরোনো pointer দিয়ে পড়লে ছায়া বলে “এটি মুক্ত হওয়া জায়গা” — heap-use-after-free. খরচ: প্রোগ্রাম সাধারণত প্রায় দুই গুণ ধীর হয়, মেমরি বেশ খানিকটা বেশি লাগে — তাই টেস্টে চালানো হয়, গ্রাহকের হাতে দেওয়া প্রোগ্রামে নয়।

LeakSanitizer কাজ করে প্রোগ্রাম শেষে, অনেকটা garbage collector-এর “চিহ্নিত করা” ধাপের মতো: stack, global ভ্যারিয়েবল আর রেজিস্টার থেকে শুরু করে যত মান heap block-এর ঠিকানার মতো দেখায়, সেগুলো ধরে ধরে পৌঁছানো যায় এমন সব block চিহ্নিত করে। যে block-এ কোনো পথেই পৌঁছানো যায় না, সেটি leak — আর ASan আগেই মনে রেখেছিল সেটি কোন লাইনের new-এ জন্মেছিল।

Valgrind (Julian Seward-এর তৈরি, প্রথম প্রকাশ ২০০২) পুরো অন্য পথে চলে: recompile না করে প্রোগ্রামটিকে একটি কৃত্রিম CPU-তে চালায়, প্রতিটি নির্দেশ অনুবাদ করে তার সাথে পরীক্ষা জুড়ে দেয়। এর Memcheck অংশ প্রতিটি বাইটের জন্য রাখে “বৈধ কি না” আর “মান দেওয়া হয়েছে কি না।” Leak-কে ভাগ করে “definitely lost”, “indirectly lost”, “possibly lost” আর “still reachable”-এ। দাম হলো গতি — প্রোগ্রাম কয়েক দশ গুণ ধীর হয়ে যায়।

সীমা: এরা শুধু সেই ভুল ধরে, যে পথ টেস্টের সময় সত্যিই চলেছে। অংশ ৫-এর ভুল ফাইলের পথ টেস্টে কেউ না চালালে, সেই leak কেউ দেখবে না। কোয়ারেন্টিন শেষ হয়ে জায়গা পুনর্ব্যবহার হয়ে গেলে কিছু use-after-free-ও এড়িয়ে যেতে পারে। তাই sanitizer হলো জাল, প্রমাণ নয়। এজন্য Google-এর মতো প্রতিষ্ঠান sanitizer-এর সাথে fuzzing চালায় — লাখো এলোমেলো ইনপুট দিয়ে নতুন নতুন পথ খোলা।

মূল উপলব্ধি: যন্ত্র ভুল খুঁজে দেয়, কিন্তু ভুল না হওয়ার নকশা দেয় না। সবচেয়ে ভালো কৌশল দুই স্তরের: নকশায় naked new বাদ দিয়ে ভুলের সুযোগ কমানো, আর sanitizer দিয়ে যা বাকি থাকে তা ধরা।

প্র ০৫Rust-এ GC নেই, তবু use-after-free compile-ই হয় না। Java বা Python GC দিয়ে, Swift reference গুনে মেমরি সামলায়। C++-এর পথ এদের থেকে কোথায় আলাদা, আর এই পার্থক্য আজ কেন এত আলোচিত?

মেমরি ফেরত দেওয়ার প্রশ্নে ভাষাগুলো মোটামুটি চারটি পথ নিয়েছে। Tracing GC (Java, C#, Go): চলার সময় পৌঁছানো যায় না এমন object খুঁজে ফেরত নেওয়া। dangling আর double free প্রায় অসম্ভব, কিন্তু খরচ চলার সময়ে আর ফেরতের মুহূর্ত অনিশ্চিত। Reference counting (Python-এর মূল কৌশল, Swift-এর ARC): প্রতিটি object গুনে রাখে কতজন তাকে ধরে আছে, শূন্য হলে সাথে সাথে ফেরত। নিশ্চিত সময়, কিন্তু প্রতিটি কপিতে গোনার খরচ, আর দুটি object একে অপরকে ধরে রাখলে (cycle) কখনো শূন্য হয় না — তাই Python-এর পাশে একটি আলাদা cycle collector-ও আছে। C++-এর std::shared_ptr হুবহু এই কৌশল, ঐচ্ছিকভাবে।

Rust-এর পথ — ownership যা compiler যাচাই করে। Rust-এ প্রতিটি মানের ঠিক একজন মালিক; মালিক scope ছাড়লে মান ফেরত যায় (Rust-এ বলে Drop — যা আসলে C++-এর RAII-রই রূপ)। অন্যরা মান “ধার” (borrow) নিতে পারে, আর borrow checker compile-এর সময়েই প্রমাণ করে যে কোনো ধার মালিকের চেয়ে বেশি দিন বাঁচবে না। ফলে অংশ ৪-এর lastViewed-এর মতো কোড Rust-এ compile-ই হয় না। চলার সময়ে কোনো বাড়তি খরচ নেই — দাম দিতে হয় শেখার কষ্টে আর compiler-এর সাথে “তর্ক” করে।

C++-এর পথ — একই ধারণা, কিন্তু আপনার শৃঙ্খলায়। std::unique_ptr আর vector দিয়ে C++-এ Rust-এর মতোই একক মালিকানা লেখা যায়, আর তার খরচও শূন্যের কাছাকাছি। পার্থক্য হলো, C++ compiler আপনাকে বাধ্য করে না: কাঁচা pointer কপি করা, delete করা, dangling reference রাখা — সবই বৈধ C++. নিয়ম মানা না-মানা আপনার আর আপনার দলের ওপর।

কেন এখন এত আলোচনা: Microsoft-এর নিরাপত্তা দল ২০১৯ সালে জানায়, তাদের পণ্যে বছরের পর বছর যত নিরাপত্তা-ত্রুটি ঠিক করা হয়েছে, তার প্রায় ৭০% মেমরি-নিরাপত্তার ভুল; Google-এর Chromium দলও গুরুতর ত্রুটির প্রায় একই অনুপাত জানিয়েছে, যার বড় অংশ use-after-free. এর পর ২০২২ সালে যুক্তরাষ্ট্রের NSA, আর ২০২৪ সালে হোয়াইট হাউসের একটি প্রতিবেদন memory-safe ভাষার দিকে যাওয়ার সুপারিশ করে। C++ সম্প্রদায়ের উত্তর বহুমুখী: Core Guidelines আর তাদের পরীক্ষক টুল, standard library-র নিরাপদ মোড (hardening), আর ভাষাকে আরও নিরাপদ করার নানা প্রস্তাব — যা পর্ব ৪৩-এ দেখবেন।

মূল উপলব্ধি: প্রতিটি ভাষাকেই “কে কখন ফেরত দেবে” প্রশ্নের উত্তর দিতে হয় — চলার সময়ের পাহারাদার দিয়ে, গোনা দিয়ে, compiler-এর প্রমাণ দিয়ে, না প্রোগ্রামারের শৃঙ্খলা দিয়ে। আধুনিক C++ লেখা মানে সেই শৃঙ্খলাকে অভ্যাস বানানো: মালিক-object ছাড়া কোনো heap block নয়।

অনুশীলন

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

  1. আউটপুট অনুমান: উপরের Report struct (constructor-এ ctor:, destructor-এ dtor: প্রিন্ট করে) ধরে নিন। main-এ লেখা আছে: Report* a = new Report("X"); Report b("Y"); std::println("end"); — আর কিছু নয়। আউটপুটে কয়টি লাইন আসবে, কোন ক্রমে?

    চারটি লাইন: ctor: X, ctor: Y, end, dtor: Y. b stack-এর object, তাই main শেষে তার destructor নিজে থেকে চলে। কিন্তু a-র Report heap-এ, কেউ delete করেনি — dtor: X কখনো আসে না। প্রোগ্রাম শেষে অপারেটিং সিস্টেম মেমরি ফেরত নেয় ঠিকই, কিন্তু destructor চলে না; destructor-এ ফাইল সেভ করার কাজ থাকলে সেটিও হতো না। এটি একটি leak, আর LeakSanitizer এটি রিপোর্ট করবে।

  2. Stack, না heap? প্রতিটির জন্য বলুন কোথায় রাখবেন আর কীভাবে লিখবেন — (ক) একটি লুপের গণক i (খ) ফাইল থেকে পড়া গ্রাহকের তালিকা, সংখ্যা অজানা (গ) একটি ৫০ MB ছবির পিক্সেল (ঘ) একটি Report, যা একটি ফাংশনে বানিয়ে ডাকার মানুষকে ফেরত দিতে হবে।

    (ক) Stack — সাধারণ int i. (খ) Heap, কিন্তু std::vector<Customer>-এর মাধ্যমে — vector নিজে stack-এ, তার উপাদান heap-এ। (গ) Heap — stack-এর সীমা (৮ MB বা ১ MB) ছাড়িয়ে যায়; std::vector<std::uint8_t> pixels(size);. (ঘ) প্রায়ই heap-এর দরকারই নেই — Report makeReport() লিখে by value ফেরত দিন, আধুনিক compiler কপি এড়িয়ে যায়। যদি heap সত্যিই লাগে (যেমন পর্ব ২৫-এর polymorphism-এ), তাহলে std::unique_ptr<Report> ফেরত দিন — কাঁচা Report* নয়।

  3. বাগ শিকার: একটি AI এই ফাংশন দিয়েছে — double average(const std::string& path) { int n = countLines(path); double* marks = new double[n]; if (n == 0) return 0.0; readMarks(path, marks, n); double sum = 0; for (int i = 0; i < n; ++i) sum += marks[i]; delete marks; return sum / n; }. কয়টি মেমরি-সমস্যা খুঁজে পান? ঠিক রূপ কী?

    তিনটি: (১) new double[n]-এর সাথে delete — ভুল জোড়া, undefined behavior; হওয়া উচিত delete[]. (২) n == 0 হলে early return-এ delete বাদ — leak. (৩) readMarks যদি exception ছোড়ে (যেমন ফাইল না পেলে), তখনও leak. তিনটিই এক ধাক্কায় সমাধান হয়: std::vector<double> marks(n); — কোনো delete লাগবে না। readMarks-কে marks.data() দিন, বা আরও ভালো, সেটিকে std::vector<double>& নিতে বদলান।

  4. নিজে লিখুন (কোড): অংশ ২-এর manual_new_delete.cpp থেকে সব new আর delete সরিয়ে ফেলুন: একক Report-এর জন্য std::make_unique, আর দুটি Report-এর array-র জন্য std::vector<Report>. আউটপুটে আগের মতোই প্রতিটি ctor-এর জোড়া dtor আসছে কি না দেখুন।

    লিখুন auto r = std::make_unique<Report>("Sales-Oct"); (#include <memory> যোগ করে) আর std::vector<Report> many; many.reserve(2); many.emplace_back("A"); many.emplace_back("B");. পার্থক্য দুটি লক্ষ করবেন: (১) dtor: Sales-Oct এখন আসে main-এর শেষে, কারণ r তখনই scope ছাড়ে — আগে চাইলে r.reset(); লিখুন। (২) vector-এর উপাদান ধ্বংস হওয়ার ক্রম standard বেঁধে দেয়নি; libstdc++-এ সাধারণত A আগে, তারপর B. reserve না দিলে দ্বিতীয় emplace_back-এ vector বড় জায়গায় সরতে পারে, তখন কপি বা move-এর কারণে বাড়তি ctor/dtor লাইন দেখবেন — কেন, তা পর্ব ২২ আর ৩৬-এ।

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

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

ক্রমReport* r = new Report("Oct"); থেকে delete r; পর্যন্ত যা ঘটে, সেই ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Allocator heap থেকে একটি Report-এর মাপের জায়গা দেয়
  2. সেই জায়গায় Report-এর constructor চলে
  3. Object-এর ঠিকানা pointer r-এ জমা হয়
  4. r->title দিয়ে object ব্যবহার হয়
  5. delete r;-এ প্রথমে destructor চলে
  6. তারপর জায়গাটি allocator-কে ফেরত দেওয়া হয়

C++-এ int* a = new int[50]; দিয়ে নেওয়া মেমরি ফেরত দেওয়ার সঠিক উপায় কোনটি?

  • free(a);
  • delete a;
  • delete[] a;
  • a = nullptr;

C++-এ new আর C-এর malloc-এর মূল পার্থক্য কোনটি?

  • new stack-এ জায়গা নেয়, malloc heap-এ
  • new জায়গা নেওয়ার পর constructor চালায়, malloc চালায় না
  • new-এর মেমরি নিজে থেকেই ফেরত যায়
  • ব্যর্থ হলে new nullptr দেয়, malloc exception ছোড়ে

C++-এ একটি সাধারণ new প্রয়োজনীয় মেমরি জোগাড় করতে না পারলে কী হয়?

  • nullptr ফেরত দেয়
  • প্রোগ্রাম সাথে সাথে compile error দেখায়
  • পুরোনো object মুছে জায়গা খালি করে
  • std::bad_alloc exception ছোড়ে

C++-এ Customer* p = new Customer; Customer* q = p; delete p; delete q; — শেষ লাইনে কী ঘটে?

  • Double delete — undefined behavior
  • q ইতিমধ্যে nullptr, তাই কিছুই ঘটে না
  • q-র নিজস্ব Customer কপিটি ফেরত যায়
  • সবসময় compile error হয়

একটি C++ ফাংশন শুরুতে new দিয়ে একটি buffer নেয় আর শেষ লাইনে delete[] করে। মাঝখানে ডাকা একটি ফাংশন exception ছুড়লে buffer-এর কী হয়?

  • Stack unwinding-এর সময় delete[] নিজে থেকে চলে
  • Garbage collector সেটি পরে ফেরত নেয়
  • delete[] চলে না, buffer leak হয়
  • কোডটি compile হয় না

আধুনিক C++-এ একটি Report object heap-এ বানাতে হবে, যার মালিক থাকবে ঠিক একজন। C++ Core Guidelines অনুযায়ী সবচেয়ে উপযুক্ত লেখা কোনটি?

  • Report* r = new Report("Oct");
  • auto r = std::make_unique<Report>("Oct");
  • Report* r = (Report*)malloc(sizeof(Report));
  • auto r = std::make_shared<Report>("Oct");

শব্দকোষ

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

মনে রাখুন

  1. Stack-এর আয়ু ঠিক করে কোডের গঠন, heap-এর আয়ু ঠিক করেন আপনি। Pointer নিজে stack-এ থাকতে পারে, তার দেখানো object heap-এ।
  2. new = জায়গা + constructor; delete = destructor + জায়গা ফেরত। new[]-এর জোড়া delete[]; new আর malloc-এর জিনিস মেশানো যায় না।
  3. প্রতিটি heap block ঠিক একবার ফেরত: শূন্যবার = leak, দুবার = double delete, ফেরতের পর ব্যবহার = use-after-free. delete pointer-কে খালি করে না।
  4. Early return আর exception হাতে লেখা delete এড়িয়ে যায়, কিন্তু stack-object-এর destructor সবসময় চলে। সাধারণ new ব্যর্থ হলে std::bad_alloc.
  5. Naked new নয় (Core Guidelines R.11): তালিকায় std::vector/std::string, একক object-এ std::make_unique; সন্দেহ হলে -fsanitize=address.

পরের পর্বে: এই পর্বের শেষ ধাপে দেখেছেন, মালিক-object নিজের destructor-এ heap ফেরত দেয়। কিন্তু সেটি ঠিক কীভাবে কাজ করে? RAII নীতি, std::unique_ptr কেন কপি হয় না, std::shared_ptr কীভাবে মালিক গোনে, আর কখন কোনটি — মালিকানার পুরো নিয়ম।

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

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