رفتن به محتوا

حافظه پویا: 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 تخصیص خام است. آن را مطالعه کنید تا در کد قدیمی بشناسیدش، اما هرگز آن را در کد جدید ننویسید — به جای آن از اشاره‌گرهای هوشمند استفاده کنید.

new_delete.cpp
#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 استفاده شود.

placement_new.cpp
#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 استفاده کنید — آن‌ها تخصیص و ساخت را به عنوان یک عملیات واحد و ایمن در برابر استثنا انجام می‌دهند.