सामग्री पर जाएं

डायनैमिक मेमोरी: new, delete और उनके ख़तरे

जानें कि new और delete के साथ डायनैमिक एलोकेशन कैसे काम करता है, मेमोरी लीक, डबल-फ़्री और यूज़-आफ़्टर-फ़्री के ख़तरों को समझें, और देखें कि आधुनिक C++ में रॉ new/delete की संख्या शून्य क्यों होनी चाहिए।

new और delete के साथ डायनैमिक एलोकेशन

स्टैक (stack) तेज़ होता है, लेकिन साइज़ और स्कोप में सीमित होता है। जब आपको ऐसे ऑब्जेक्ट चाहिए जो अपने बनाने वाले स्कोप से ज़्यादा समय तक जीवित रहें, या जिनका साइज़ कंपाइल-टाइम पर पता न हो, तब आप हीप (heap) (फ़्री स्टोर) पर एलोकेट करते हैं।

new T(args) एक T के लिए मेमोरी एलोकेट करता है, उसका कंस्ट्रक्टर कॉल करता है, और T* रिटर्न करता है। delete p डिस्ट्रक्टर कॉल करता है और मेमोरी को मुक्त करता है। ऐरे के लिए: new T[n] और delete[] p। new को delete[] के साथ, या new[] को delete के साथ मिलाना अपरिभाषित व्यवहार (undefined behavior) है — कंपाइलर इसे नहीं पकड़ेगा।

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
}

प्लेसमेंट new

प्लेसमेंट new बिना एलोकेट किए किसी निर्दिष्ट मेमोरी एड्रेस पर ऑब्जेक्ट बनाता है। इसका उपयोग एलोकेटर, मेमोरी पूल और std::vector जैसे कंटेनर अंदरूनी तौर पर करते हैं। आपको डिस्ट्रक्टर स्पष्ट रूप से कॉल करना होगा — प्लेसमेंट-new से बनाए गए ऑब्जेक्ट पर delete का उपयोग नहीं करना चाहिए।

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 आपको तीन तरह के बग के जोखिम में डालता है:

1. मेमोरी लीक (Memory Leak) — new से एलोकेट करना लेकिन delete कभी कॉल न करना। मेमोरी OS को कभी वापस नहीं लौटती (प्रोसेस के समाप्त होने तक)। लंबे समय तक चलने वाले सर्वर में यह अंततः उपलब्ध सारी RAM खा जाता है।

2. डबल फ़्री — एक ही पॉइंटर पर दो बार delete कॉल करना। दूसरा delete हीप एलोकेटर के आंतरिक डेटा स्ट्रक्चर को करप्ट कर देता है, जिससे अक्सर बहुत बाद में असंबंधित कोड में क्रैश होते हैं। इसे डीबग करना बेहद कठिन है।

3. यूज़-आफ़्टर-फ़्री — delete के बाद मेमोरी को एक्सेस करना। हो सकता है वह मेमोरी किसी दूसरे एलोकेशन के लिए दोबारा इस्तेमाल हो चुकी हो, इसलिए आप किसी दूसरे ऑब्जेक्ट का डेटा पढ़ या लिख रहे हो सकते हैं। यह एक सुरक्षा कमज़ोरी (CVE-class बग) है।

सामान्य गलती

इस पर विचार करें: f(new A(), new B())। कंपाइलर आर्ग्युमेंट का मूल्यांकन किसी भी क्रम में कर सकता है और उन्हें आपस में इंटरलीव भी कर सकता है:

1. A के लिए मेमोरी एलोकेट करें
2. B के लिए मेमोरी एलोकेट करें
3. A को कंस्ट्रक्ट करें
4. B को कंस्ट्रक्ट करें

अगर चरण 4 में एक्सेप्शन फेंका जाता है, तो A लीक हो जाता है — कोई भी उस पर delete कॉल नहीं करता। यहाँ तक कि f(std::shared_ptr(new A()), std::shared_ptr(new B())) में भी C++17 से पहले यह समस्या थी (C++17 गारंटी देता है कि एक आर्ग्युमेंट का मूल्यांकन दूसरे आर्ग्युमेंट के मूल्यांकन के साथ आपस में मिश्रित (इंटरलीव) नहीं होता; हालाँकि आर्ग्युमेंट किस क्रम में मूल्यांकित होंगे, यह अब भी अनिर्दिष्ट है)। समाधान: हमेशा std::make_unique या std::make_shared का उपयोग करें — ये एलोकेशन और कंस्ट्रक्शन को एक ही, एक्सेप्शन-सेफ़ ऑपरेशन के रूप में करते हैं।

कस्टम एलोकेटर: एक परिचय

डिफ़ॉल्ट new/delete ग्लोबल एलोकेटर (आमतौर पर malloc/free) का उपयोग करते हैं, जो सामान्य-उद्देश्य (general-purpose) है लेकिन हर पैटर्न के लिए सर्वोत्तम नहीं। कस्टम एलोकेटर ये सुविधाएँ देते हैं:

- एरीना/लीनियर एलोकेटर — बस एक पॉइंटर आगे बढ़ाएँ, और सब कुछ एक साथ मुक्त करें। हर फ़्रेम के गेम डेटा के लिए बढ़िया।
- पूल एलोकेटर — निश्चित साइज़ के ब्लॉक पहले से एलोकेट कर लेते हैं। O(1) alloc/free, कोई फ़्रैगमेंटेशन नहीं।
- स्टैक एलोकेटर — LIFO एलोकेशन, बेहद तेज़।

C++ कंटेनर एलोकेटर टेम्पलेट पैरामीटर स्वीकार करते हैं: std::vector>। C++17 में std::pmr (polymorphic memory resources) नेमस्पेस एक रनटाइम-पॉलीमॉर्फ़िक एलोकेटर फ़्रेमवर्क देता है, जो टेम्पलेट पैरामीटर की समस्या से बचाता है।