حافظه پویا: new، delete و خطرات آنها
نحوه کار تخصیص حافظه پویا با new و delete را بیاموزید، خطرات نشت حافظه، آزادسازی مضاعف (double-free) و استفاده پس از آزادسازی (use-after-free) را درک کنید و دلیل اینکه C++ مدرن باید صفر new/delete خام داشته باشد را کشف کنید.
تخصیص حافظه پویا با new و delete
پشته (stack) سریع است اما از نظر اندازه و دامنه محدود است. وقتی به اشیایی نیاز دارید که بیشتر از دامنه ایجادکنندهشان عمر کنند، یا اندازهشان در زمان کامپایل مشخص نباشد، روی هیپ (free store) تخصیص حافظه میدهید.
new T(args) حافظهای برای یک T تخصیص میدهد، سازنده آن را فراخوانی میکند و یک T* برمیگرداند. delete p مخرب (destructor) را فراخوانی کرده و حافظه را آزاد میکند. برای آرایهها: new T[n] و delete[] p. عدم تطابق new با delete[] یا new[] با delete رفتار تعریفنشده است — کامپایلر این را تشخیص نمیدهد.
new، delete و انواع آرایهای آنها
این API تخصیص خام است. آن را مطالعه کنید تا در کد قدیمی بشناسیدش، اما هرگز آن را در کد جدید ننویسید — به جای آن از اشارهگرهای هوشمند استفاده کنید.
#include <iostream>
#include <cstring>
struct Widget {
int id;
Widget(int i) : id(i) { std::cout << "Widget " << id << " created\n"; }
~Widget() { std::cout << "Widget " << id << " destroyed\n"; }
};
int main() {
// Single object
Widget* w = new Widget(1);
std::cout << "Using widget " << w->id << '\n';
delete w; // destructor called, memory freed
// Array
Widget* arr = new Widget[3]{{10}, {20}, {30}};
for (int i = 0; i < 3; ++i)
std::cout << arr[i].id << ' ';
std::cout << '\n';
delete[] arr; // MUST use delete[] for new[]
// UNDEFINED BEHAVIOR — don't do these:
// delete arr; // wrong: used delete instead of delete[]
// delete[] w; // wrong: used delete[] instead of delete
}Placement new
Placement new یک شیء را در آدرس حافظه خاصی بدون تخصیص حافظه میسازد. توسط تخصیصدهندهها، استخرهای حافظه و ظروفی مانند std::vector بهصورت داخلی استفاده میشود. باید مخرب را بهصورت صریح فراخوانی کنید — delete نباید روی اشیاء ساختهشده با placement new استفاده شود.
#include <iostream>
#include <new> // required for placement new
#include <cstddef> // std::byte
#include <memory> // std::align
struct Sensor {
int id;
double reading;
Sensor(int i, double r) : id(i), reading(r) {
std::cout << "Sensor " << id << " init\n";
}
~Sensor() { std::cout << "Sensor " << id << " shutdown\n"; }
};
int main() {
// Pre-allocate raw storage (aligned)
alignas(Sensor) std::byte buffer[sizeof(Sensor)];
// Construct in pre-allocated memory
Sensor* s = new (buffer) Sensor(42, 3.14);
std::cout << "Reading: " << s->reading << '\n';
// Must destroy explicitly — do NOT use delete
s->~Sensor(); // explicit destructor call
// delete s; // WRONG — memory was not allocated by new
}سه باگ مرگبار حافظه
new/delete خام شما را در معرض سه دسته باگ قرار میدهد:
۱. نشت حافظه (Memory Leak) — تخصیص با new بدون فراخوانی delete. حافظه هرگز به سیستمعامل بازگردانده نمیشود (تا پایان فرآیند). در سرور با اجرای طولانی، این در نهایت تمام RAM موجود را مصرف میکند.
۲. آزادسازی مضاعف (Double Free) — فراخوانی delete روی یک اشارهگر دو بار. delete دوم ساختارهای داده داخلی تخصیصدهنده هیپ را خراب میکند، که اغلب منجر به خرابیهایی بسیار بعدتر در کد نامرتبط میشود. اشکالزدایی این بسیار دشوار است.
۳. استفاده پس از آزادسازی (Use-After-Free) — دسترسی به حافظه پس از delete. حافظه ممکن است برای تخصیص دیگری بازاستفاده شده باشد، پس ممکن است دادههای شیء دیگری را بخوانید یا بنویسید. این یک آسیبپذیری امنیتی (باگ کلاس CVE) است.
در نظر بگیرید: f(new A(), new B()). کامپایلر ممکن است آرگومانها را به هر ترتیبی ارزیابی کند و ممکن است آنها را درهم بیامیزد:
1. تخصیص حافظه برای A
2. تخصیص حافظه برای B
3. ساخت A
4. ساخت B
اگر مرحله ۴ استثنا پرتاب کند، A نشت میکند — هیچکس delete آن را فراخوانی نمیکند. حتی f(std::shared_ptr(new A()), std::shared_ptr(new B())) قبل از C++17 این مشکل را داشت (که تضمین میکند هر آرگومان قبل از بعدی کاملاً ارزیابی شود). راهحل: همیشه از std::make_unique یا std::make_shared استفاده کنید — آنها تخصیص و ساخت را به عنوان یک عملیات واحد و ایمن در برابر استثنا انجام میدهند.
مروری بر تخصیصدهندههای سفارشی
new/delete پیشفرض از تخصیصدهنده سراسری (معمولاً malloc/free) استفاده میکنند که عمومی است اما برای همه الگوها بهینه نیست. تخصیصدهندههای سفارشی فراهم میکنند:
- تخصیصدهندههای آرنا/خطی — یک اشارهگر را جلو میبرند، همه چیز را یکجا آزاد میکنند. عالی برای دادههای هر فریم بازی.
- تخصیصدهندههای استخر — بلوکهای با اندازه ثابت را از پیش تخصیص میدهند. تخصیص/آزادسازی O(1)، بدون تکهتکه شدن (fragmentation).
- تخصیصدهندههای پشتهای — تخصیص LIFO، بسیار سریع.
ظروف C++ پارامترهای قالب تخصیصدهنده میپذیرند: std::vector. فضای نام std::pmr (منابع حافظه چندریختی) در C++17 یک چارچوب تخصیصدهنده چندریختی زمان اجرا فراهم میکند که مشکل پارامتر قالب را حل میکند.
در C++ مدرن (C++14 و بعد از آن)، کد برنامهای خوبنوشته باید صفر new و delete خام داشته باشد:
- مالکیت انحصاری: از std::make_unique استفاده کنید
- مالکیت اشتراکی: از std::make_shared استفاده کنید
- آرایههای پویا: از std::vector استفاده کنید
- رشتهها: از std::string استفاده کنید
- اشیاء اختیاری: از std::optional استفاده کنید
new/delete خام ممکن است در کد کتابخانه (تخصیصدهندهها، ظروف سفارشی) ظاهر شود، اما کد برنامهای هرگز نباید به آنها نیاز داشته باشد. اگر new را در بازبینی کد میبینید، بپرسید چرا — تقریباً همیشه جایگزین امنتری وجود دارد.
- عدم تطابق
new/delete[]یاnew[]/deleteرفتار تعریفنشده است - نشت حافظه، آزادسازی مضاعف و استفاده پس از آزادسازی سه باگ مرگبار حافظه هستند
f(new A(), new B())در صورت پرتاب استثنا توسط سازنده میتواند نشت کند (قبل از C++17 حتی با پوشش shared_ptr)- Placement new در آدرس مشخصی میسازد — بهصورت دستی نابود کنید، هرگز از
deleteاستفاده نکنید - کد برنامهای C++ مدرن باید صفر
new/deleteخام داشته باشد — از اشارهگرهای هوشمند و ظروف استفاده کنید
آزمون — دانش خود را بسنجید
(15 امتیاز)1. اگر از `delete` روی حافظه تخصیصیافته با `new[]` استفاده کنید چه اتفاقی میافتد؟
2. چرا `f(new A(), new B())` میتواند حافظه را نشت کند؟
3. شیء ساختهشده با placement new را چگونه باید نابود کرد؟