# چجوری تو سی پلاس پلاس از شر مموری لیک (Memory Leak) خلاص بشیم؟ ببینید رفقا، بیاید رک و پوستکنده حرف بزنیم. سی پلاس پلاس (C++) یه زبون شاهکاره؛ ترکیبی از قدرت خام C و قابلیتها و ساختار مدرن C++ است. این زبان به شما اجازه میده تا نه فقط کد بنویسید، بلکه **نحوه اجرای** اون کد رو هم دیکته کنید. سرعتش در سطح ناسازگاری با سختافزار پایینه و اگه درست استفاده بشه، هیچ چیز نمیتونه جلوی اوج پرفورمنسش رو بگیره. اما این قدرتِ مطلق یه قیمت داره؛ و اون قیمت، معمولاً به شکل **مموری لیک (Memory Leak)** ظاهر میشه. تو زبانهایی مثل Python یا Java، شما کد مینویسید و ول میکنید؛ یه دستیار باهوش (Garbage Collector) میاد و بعد از مدتی میگه: "آها، این حافظهای که کسی دیگه سراغش نمیره، باید آزاد شه." اما در ++C؟ **شما خود اون دستیاری هستید!** شما باید هوشیار باشید. مموری لیک دقیقاً یعنی چی؟ یعنی شما یه فضای مشخص (مثلاً ۱ مگابایت) از RAM رو با دستوراتی مثل `new` یا `malloc` درخواست دادید، ولی به دلایلی، هیچوقت و در هیچ نقطهای از اجرای برنامه، اون حافظه رو پس ندادید. وقتی این اتفاق تو یک حلقه بیفته که میلیونها بار تکرار میشه، سیستم عامل شما کمکم خفه میشه و برنامه داره با یه نشت دائمی منابع (Resource Exhaustion) میمیره! بیاید اول ببینیم یه سناریوی کلاسیکِ وحشتناک چطور مموری لیک ایجاد میکنه. ### مثال کلاسیک: دیوانگی `new` بدون `delete` فرض کنید ما داریم یه سری دادههای بزرگ رو در زمان اجرا (Runtime) به حافظه هیپ (Heap) میگیریم: ```cpp void classicLeakFunction() { // درخواست ۱۰۰ هزار واحد از حافظه برای نگهداری اعداد صحیح int* largeArray = new int[100000]; std::cout << "حافظه تخصیص داده شد.\n"; if (someCriticalCheck) { // اوپس! شرط درست بود، پس میایم اینجا. اما اگر یه خطایی رخ بده یا تابع زودتر برگرده... return; // <--- مشکل اصلی: تابع قبل از رسیدن به این خط خارج میشه! } // فقط در صورتی که شرط بالا غلط باشه، کد اینجا اجرا میشه و حافظه آزاد میشه. delete[] largeArray; // اگر زودتر برگردیم، این دستور هرگز اجرا نمیشه! std::cout << "حافظه با موفقیت آزاد شد.\n"; } ``` **تحلیل فنی:** اگه `someCriticalCheck` درست باشه، تابع از داخل میپره و **Destructor** اون آبجکت (یا در اینجا، خودِ اجرای بلوک کد) فراخوانی نمیشه که دستور `delete[] largeArray` رو اجرا کنه. نتیجه؟ ۱۰۰ هزار عدد صحیح، برای همیشه تو هیپ گیر افتادن! --- ## چطور این کابوس رو شکست بدیم؟ ما سه سطح اصلی از دفاع در برابر مموری لیک داریم که باید طبق اولویت استفاده کنیم. ### ۱. قهرمان مطلق: RAII (Resource Acquisition Is Initialization) این مفهوم، سنگ بنای برنامهنویسی مدرن ++C است. حرف سادهاش اینه: **"هر چیزی که میخوای مالکیتش رو بگیری، حتماً در سازنده (Constructor) بگیر و قول بده تو مخرب (Destructor) پسش بدی."** RAII به شما این اطمینان رو میده: "تا وقتی آبجکتِ ما وجود داره، من مسئول نگهداری منابعشم. حتی اگه Exception پرت شه یا تابع وسط راه برگرده، **من قطعا Destructor خودم رو صدا میزنم و حافظه رو آزاد میکنم.**" **مثال RAII برای پاک کردن آرایه:** ```cpp class ManagedArray { private: int* data; size_t size_; public: // سازنده (Resource Acquisition): حافظه رو میگیریم. ManagedArray(size_t size) : size_(size) { data = new int[size]; std::cout << "RAII: آرایه با موفقیت تخصیص یافت.\n"; } // مخرب (Resource Release): حافظه رو پس میدیم. ~ManagedArray() { delete[] data; std::cout << "RAII: حافظه به درستی آزاد شد!\n"; } // اگر بخواهید، اپراتور کپی را هم باید تعریف کنید تا از Shallow Copy جلوگیری شود. // ... (Copy Constructor و Copy Assignment Operator) }; void raiiSafeFunction() { ManagedArray myArray(50000); // حافظه اینجا گرفته میشه std::cout << "--- داخل اسکوپ تابع ---\n"; if (true /* فرض میکنیم این شرط درست باشه */) { // چون از RAII استفاده کردیم، حتی اگه اینجا return کنیم... return; } // اگر به اینجا برسیم همونطور میمونه. std::cout << "--- رسیدیم به پایان اسکوپ ---\n"; }; // <--- وقتی تابع تموم میشه، myArray نابود میشه و مخرب (Destructor) خودش کارش رو انجام میده! ``` ### ۲. ابزارهای هوشمند: Smart Pointers (از C++11 به بعد) اینها بهترین شکل اجرای RAII هستن؛ شما دیگه مجبور نیستید خودتون کلاس بنویسید تا حافظه رو مدیریت کنید. کتابخانه استاندارد (`<memory>`) این کار رو برای شما انجام میده. #### الف) `std::unique_ptr`: مالکیت انحصاری (The Exclusive Owner) این پوینتر میگه: "فقط یه نفر مالک این حافظه است، و اون شخص منم." وقتی شیء `unique_ptr` نابود میشه، حتماً باید حافظهای که نگه داشته رو آزاد کنه. **نکته حرفهای:** همیشه از `std::make_unique` برای ساختن استفاده کنید. چون خودِ تابع `make_unique` مسئول تخصیص و بازگرداندن پوینتر هست، احتمال اشتباهات شما صفر میشه. ```cpp #include <memory> // ... (فرض میکنیم کلاس Player وجود داره) void uniquePtrTest() { std::cout << "\n--- تست Unique Ptr ---\n"; // ساخت با استفاده از make_unique: بهترین روش! std::unique_ptr<Player> p1 = std::make_unique<Player>(); p1->attack(); // کپی کردن مجاز نیست (به جز انتقال مالکیت) // std::unique_ptr<Player> p2 = p1; // کامپایلر خطا میده! // انتقال مالکیت: این پوینتر دیگه مسئول نیست، بلکه اجازه میده دیگری مسئول بشه. std::unique_ptr<Player> p3 = std::move(p1); if (p1 == nullptr) { // p1 حالا خالی شده و مقدارش null است std::cout << "P1 اکنون مالکیت را به P3 منتقل کرده است.\n"; } } // وقتی این تابع تموم میشه، پوینتر p3 نابود میشه و Player خودش پاک میشه. ``` #### ب) `std::shared_ptr`: اشتراکگذاری مسئولیت (The Shared Responsibility) وقتی چند بخش از کد نیاز دارن به یه شیء دسترسی داشته باشن، شما نمیخواید مالکیت رو فقط در یک جا نگه دارید. `shared_ptr` این کار رو انجام میده با مکانیزم **Reference Counting**. * **Count بالا میره:** هر وقت کپی جدیدی از `shared_ptr` گرفتید. * **Count پایین میاد:** هر وقت یه شیء دیگر از اون پوینتر خارج یا نابود شد. * **پاکسازی:** وقتی Count به صفر رسید، حافظه آزاد میشه! ```cpp #include <memory> // ... (فرض میکنیم کلاس Player وجود داره) void sharedPtrTest() { std::cout << "\n--- تست Shared Ptr ---\n"; // ساخت با استفاده از make_shared: این روش بهینه ترین است، چون پوینتر و شیء رو همزمان تخصیص میده! std::shared_ptr<Player> ptrA = std::make_shared<Player>(); std::cout << "Count شروع: " << ptrA.use_count() << "\n"; // خروجی: 1 { // کپی کردن مجاز است و Count را افزایش می دهد. std::shared_ptr<Player> ptrB = ptrA; std::cout << "Count پس از کپی (B): " << ptrA.use_count() << "\n"; // خروجی: 2 // حالا یه شیء موقت دیگه هم ایجاد میکنیم که بهش اشاره داره. auto tempPtr = std::make_shared<Player>(); std::cout << "Count پس از Temp: " << ptrA.use_count() + tempPtr.use_count() << "\n"; // خروجی: 3 } // ptrB اینجا نابود میشه -> Count کم میشه به ۲ std::cout << "Count بعد از پایان بلوک B: " << ptrA.use_count() << "\n"; // خروجی: 2 (چون tempPtr هنوز وجود داره) // اگر یه shared_ptr دیگه هم اینجا نابود بشه، Count به ۱ میرسه. } // وقتی این تابع تموم میشه، ptrA نابود میشه و چون آخرین اشارهگر بود، حافظه پاک میشه! ``` #### ج) `std::weak_ptr`: ناظر بیتأثیر (The Observer) اینجا نقطه اوج تکنیکهاست. اگر شما دو شیء دارید که هر دو به همدیگه اشاره میکنن با استفاده از **`shared_ptr`**، چی میشه؟ فرض کنید: `NodeA -> shared_ptr<NodeB>` و `NodeB -> shared_ptr<NodeA>`. وقتی تابع تموم میشه، هیچکدوم نابود نمیشن چون کانتر هر دو همیشه حداقل ۱ هست! **این یه مموری لیکِ بسیار ظریف (Circular Reference Leak) است.** راه حل؟ یکی از اونها رو به `std::weak_ptr` تبدیل کنید. `weak_ptr` فقط "نگاه میکنه"؛ یعنی اطلاعات شیء رو میخونه، ولی در شمارشگر مالکیت شرکت نمیکنه. ```cpp #include <memory> // ... (فرض میکنیم کلاس Node وجود داره) class Node { public: std::shared_ptr<Node> next; // این پوینتر مسئول بودن اصلی است std::weak_ptr<Node> prev; // این فقط یه ناظر بیتأثیره! int id; Node(int i) : id(i) {} }; void weakPtrCycleTest() { std::cout << "\n--- تست Weak Ptr (شکستن چرخه) ---\n"; // ایجاد دو Node با مالکیت مشترک auto node1 = std::make_shared<Node>(1); auto node2 = std::make_shared<Node>(2); // اتصال: A به B اشاره میکنه (مالکیت رو بالا میبره) node1->next = node2; // و B به A اشاره میکنه، اما با weak_ptr (فقط مشاهده میکنه) node2->prev = node1; // <--- کلید حل مشکل! std::cout << "قبل از خروج: Node 1 Count: " << node1.use_count() << ", Node 2 Count: " << node2.use_count() << "\n"; // هر دو باید ۲ باشند } // وقتی این تابع تموم میشه، چون هیچ کدام به هم وابسته نبودن (Weak/Shared)، هردو با خیال راحت پاک میشن! ``` --- ## روشهای عملیاتی: وقتی کد شما قدیمی و دستکاری شده است اگر وارد یه پروژه شدی که از سال ۲۰۰۵ نوشته شده و همه جا پر از `new` و `delete` دستیه، نمیتونی کلش رو در یک روز با RAII عوض کنی. باید ابزار استفاده کنی. ### ۱. AddressSanitizer (ASan) – شاهکار کامپایلر این بهترین دوست شماست. فقط کافیه به کامپایلر GCC یا Clang بگید که این قابلیت رو فعال کنه: ```bash g++ -g -O1 -fsanitize=address your_source_file.cpp -o my_program ./my_program ``` **چی اتفاق میفته؟** برنامه شما اجرا میشه، و اگه حتی یه بایت مموری هم اشتباه آزاد یا نشت کرده باشه، ASan در لحظه کرش میخوره و بهت **دقیقاً خط کد و نوع عملیات (Heap Allocation/Deallocation)** رو نشون میده. ### ۲. Valgrind – بازرس جامع سیستم اگه از محیط لینوکس استفاده میکنید، Valgrind یه ابزار کلاسیک و بیرقیب است: ```bash valgrind --leak-check=full --show-leak-kinds=all ./my_program ``` **چی اتفاق میفته؟** Valgrind کد شما رو خط به خط اجرا میکنه و در نهایت یه گزارش طولانی میدهد. تو این گزارش، با عبارتهایی مثل `definitely lost` (کاملاً نشت کرده) یا `possibly lost` (احتمالاً نشت کرده)، دقیقاً متوجه میشید مشکل از کجاست. ### ۳. استفاده از ابزارهای IDE اگر در VSCode, Visual Studio یا CLion کار میکنید، اکثر این محیطها قابلیت اجرای خودکار ASan یا Valgrind رو دارن و به شما یه دکمه سبز برای "اجرا با تشخیص حافظه" میدن! --- ## جمعبندی نهایی (چک لیست عملی) اگر از امروز شروع کنید، باید این اصول رو در ذهن داشته باشید: | تکنیک | هدف اصلی | چه زمانی استفاده کنیم؟ | مزیت فنی کلیدی | | :--- | :--- | :--- | :--- | | **RAII** | تضمین آزادسازی منبع | همیشه؛ به عنوان فریمورک اصلی. | حتی در زمان پرتاب Exception، حافظه آزاد میشه. | | **`std::unique_ptr`** | مالکیت تک و انحصاری | وقتی مطمئنید فقط یه شیء مسئول پاکسازی است. | سربار (Overhead) بسیار کم؛ نزدیکترین به پوینتر خام (O(1)). | | **`std::shared_ptr`** | اشتراکگذاری منابع | وقتی چندین بخش برنامه نیاز به دسترسی همزمان دارند. | مدیریت خودکار توسط Reference Count. | | **`std::weak_ptr`** | مشاهده بدون مالکیت | زمانی که با `shared_ptr` یک چرخه (Cycle) ایجاد شده است. | جلوگیری از نشت حافظه در وابستگیهای حلقوی. | | **ASan/Valgrind** | اشکالزدایی عملیاتی | وقتی کد شما پیچیده و قدیمی است و نمیتوانید همه جا را بازنویسی کنید. | شناسایی دقیق خطوط مموری لیک با گزارش کامل. | با پیروی از این اصول، دیگه نیازی نیست نگران باشید که برنامه شما یهو بعد از اجرای ۱۰۰۰ بار حلقه، به دلیل فراموشی یک `delete`، کرش کنه. شما کنترل حافظه رو در دست خودتون دارید و این یعنی قدرت واقعی C++!