स्मृति रिसाव के कारण 'नए' का उपयोग क्यों होता है?


131

मैंने C # पहले सीखा, और अब मैं C ++ से शुरू कर रहा हूं। जैसा कि मैं समझता हूं, newसी ++ में ऑपरेटर सी # में एक के समान नहीं है।

क्या आप इस नमूना कोड में मेमोरी लीक का कारण बता सकते हैं?

class A { ... };
struct B { ... };

A *object1 = new A();
B object2 = *(new B());

जवाबों:


464

क्या हो रहा है

जब आप लिखते हैं कि T t;आप स्वचालित संग्रहण अवधि केT साथ एक प्रकार की वस्तु बना रहे हैं । दायरे से बाहर जाने पर यह अपने आप साफ हो जाएगा।

जब आप लिखते हैं कि new T()आप गतिशील भंडारण अवधि केT साथ प्रकार की एक वस्तु बना रहे हैं । यह स्वचालित रूप से साफ नहीं होगा।

सफाई के बिना नया

इसे deleteसाफ करने के लिए आपको इसे एक पॉइंटर पास करना होगा:

हटाने के साथ नया

हालाँकि, आपका दूसरा उदाहरण बदतर है: आप पॉइंटर को डीफ़र कर रहे हैं, और ऑब्जेक्ट की कॉपी बना रहे हैं। इस तरह से आप पॉइंटर के साथ बनाई गई ऑब्जेक्ट को खो देते हैं new, इसलिए आप कभी भी इसे हटा नहीं सकते हैं, भले ही आप चाहते हों!

deref के साथ नयापन

तुम्हे क्या करना चाहिए

आपको स्वचालित भंडारण अवधि पसंद करनी चाहिए। एक नई वस्तु चाहिए, बस लिखें:

A a; // a new object of type A
B b; // a new object of type B

यदि आपको डायनेमिक स्टोरेज अवधि की आवश्यकता है, तो पॉइंटर को एक स्वचालित स्टोरेज अवधि ऑब्जेक्ट में आवंटित ऑब्जेक्ट में स्टोर करें जो इसे स्वचालित रूप से हटा देता है।

template <typename T>
class automatic_pointer {
public:
    automatic_pointer(T* pointer) : pointer(pointer) {}

    // destructor: gets called upon cleanup
    // in this case, we want to use delete
    ~automatic_pointer() { delete pointer; }

    // emulate pointers!
    // with this we can write *p
    T& operator*() const { return *pointer; }
    // and with this we can write p->f()
    T* operator->() const { return pointer; }

private:
    T* pointer;

    // for this example, I'll just forbid copies
    // a smarter class could deal with this some other way
    automatic_pointer(automatic_pointer const&);
    automatic_pointer& operator=(automatic_pointer const&);
};

automatic_pointer<A> a(new A()); // acts like a pointer, but deletes automatically
automatic_pointer<B> b(new B()); // acts like a pointer, but deletes automatically

स्वचालित_पॉइंट के साथ नयाकरण

यह एक सामान्य मुहावरा है जो बहुत-वर्णनात्मक नाम RAII ( संसाधन अधिग्रहण इज इनिशियलाइज़ेशन ) द्वारा जाता है। जब आप एक ऐसे संसाधन का अधिग्रहण करते हैं, जिसे आपको सफाई की आवश्यकता होती है, तो आप इसे स्वचालित भंडारण अवधि की वस्तु में चिपका देते हैं, इसलिए आपको इसे साफ करने के बारे में चिंता करने की आवश्यकता नहीं है। यह किसी भी संसाधन पर लागू होता है, यह मेमोरी, खुली फाइलें, नेटवर्क कनेक्शन, या जो भी आप कल्पना करते हैं।

यह automatic_pointerबात पहले से ही विभिन्न रूपों में मौजूद है, मैंने इसे केवल एक उदाहरण देने के लिए प्रदान किया है। मानक पुस्तकालय में एक बहुत ही समान वर्ग मौजूद है जिसे कहा जाता है std::unique_ptr

नाम का एक पुराना (प्री-सी ++ 11) भी है, auto_ptrलेकिन अब इसे हटा दिया गया है क्योंकि इसमें एक अजीब नकल व्यवहार है।

और फिर कुछ और भी होशियार उदाहरण हैं, जैसे std::shared_ptr, कई पॉइंटर्स को एक ही ऑब्जेक्ट के लिए अनुमति देता है और केवल तब ही इसे साफ करता है जब अंतिम पॉइंटर नष्ट हो जाता है।


4
@ user1131997: खुशी है कि आपने यह एक और सवाल किया। जैसा कि आप देख सकते हैं कि टिप्पणियों में व्याख्या करना बहुत आसान नहीं है :)
आर। मार्टिनो फर्नांडीस

@ R.MartinhoFernandes: उत्कृष्ट उत्तर। सिर्फ एक सवाल। आपने ऑपरेटर * () फ़ंक्शन के संदर्भ में रिटर्न का उपयोग क्यों किया?
विध्वंसक

@Destructor देर से जवाब: डी। संदर्भ के आधार पर वापस आने से आप सूचक को संशोधित कर सकते हैं, इसलिए आप *p += 2सामान्य सूचक के साथ ऐसा कर सकते हैं, जैसे कि आप। यदि यह संदर्भ से वापस नहीं आता है, तो यह सामान्य सूचक के व्यवहार की नकल नहीं करेगा, जो यहाँ है।
आर। मार्टिनो फर्नांडिस

"स्वचालित स्टोरेज अवधि ऑब्जेक्ट में आवंटित ऑब्जेक्ट को पॉइंटर को स्टोर करने की सलाह देने के लिए बहुत बहुत धन्यवाद, जो इसे स्वचालित रूप से हटा देता है।" यदि केवल इस तरीके को सीखने के लिए कोडर्स की आवश्यकता होती है, तो इससे पहले कि वे किसी भी C ++ को संकलित करने में सक्षम हों!
एंडी

35

एक कदम से कदम स्पष्टीकरण:

// creates a new object on the heap:
new B()
// dereferences the object
*(new B())
// calls the copy constructor of B on the object
B object2 = *(new B());

तो इसके अंत तक, आपके पास ढेर पर एक ऑब्जेक्ट है, जिसके पास कोई पॉइंटर नहीं है, इसलिए इसे हटाना असंभव है।

अन्य नमूना:

A *object1 = new A();

यदि आप deleteआवंटित स्मृति को भूल जाते हैं तो केवल एक स्मृति रिसाव है :

delete object1;

C ++ में स्वचालित भंडारण वाली वस्तुएं हैं, जो स्टैक पर बनाई गई हैं, जो स्वचालित रूप से निपट जाती हैं, और गतिशील भंडारण के साथ ऑब्जेक्ट्स, ढेर पर, जिसे आप आवंटित करते हैं newऔर अपने आप को मुक्त करने के लिए आवश्यक हैं delete। (यह सब मोटे तौर पर रखा गया है)

सोचें कि आपके पास deleteआवंटित हर वस्तु के लिए होना चाहिए new

संपादित करें

यह सोचने के लिए आओ, object2स्मृति रिसाव होना जरूरी नहीं है।

निम्नलिखित कोड सिर्फ एक बिंदु बनाने के लिए है, यह एक बुरा विचार है, कभी भी इस तरह के कोड को पसंद न करें:

class B
{
public:
    B() {};   //default constructor
    B(const B& other) //copy constructor, this will be called
                      //on the line B object2 = *(new B())
    {
        delete &other;
    }
}

इस मामले में, चूंकि otherसंदर्भ द्वारा पारित किया गया है, यह उसी बिंदु पर इंगित की गई सटीक वस्तु होगी new B()। इसलिए, &otherपॉइंटर को हटाकर उसका पता प्राप्त करने से मेमोरी खाली हो जाएगी।

लेकिन मैं इस पर जोर नहीं दे सकता, ऐसा मत करो। यह सिर्फ एक बिंदु बनाने के लिए यहां है।


2
मैं भी यही सोच रहा था: हम इसे लीक न करने के लिए हैक कर सकते हैं लेकिन आप ऐसा नहीं करना चाहेंगे। object1 को या तो लीक नहीं करना है, क्योंकि इसका निर्माता खुद को किसी प्रकार की डेटा संरचना से जोड़ सकता है जो इसे किसी बिंदु पर हटा देगा।
कैशोकॉ

2
यह हमेशा सिर्फ एसओ को उन "लिखने के लिए लुभाना है" ऐसा करना संभव है, लेकिन जवाब नहीं! :-) मुझे लग रहा है
कोस

11

दो "ऑब्जेक्ट" दिए गए:

obj a;
obj b;

वे स्मृति में एक ही स्थान पर कब्जा नहीं करेंगे। दूसरे शब्दों में,&a != &b

एक के मूल्य को दूसरे में बदलने से उनका स्थान नहीं बदलेगा, लेकिन यह उनकी सामग्री को बदल देगा:

obj a;
obj b = a;
//a == b, but &a != &b

वास्तव में, पॉइंटर "ऑब्जेक्ट्स" उसी तरह काम करते हैं:

obj *a;
obj *b = a;
//a == b, but &a != &b

अब, अपने उदाहरण को देखें:

A *object1 = new A();

का मान निर्दिष्ट new A()कर रहा है object1। मान एक सूचक है, अर्थ है object1 == new A(), लेकिन &object1 != &(new A())। (ध्यान दें कि यह उदाहरण मान्य कोड नहीं है, यह केवल स्पष्टीकरण के लिए है)

क्योंकि पॉइंटर का मूल्य संरक्षित है, हम इसे इंगित करने वाली मेमोरी को मुक्त कर सकते हैं: delete object1;हमारे नियम के कारण, यह वही व्यवहार करता है delete (new A());जिसका कोई रिसाव नहीं है।


आपके दूसरे उदाहरण के लिए, आप पॉइंट-टू ऑब्जेक्ट को कॉपी कर रहे हैं। मान उस वस्तु की सामग्री है, न कि वास्तविक सूचक। जैसा कि हर दूसरे मामले में होता है &object2 != &*(new A())

B object2 = *(new B());

हमने आवंटित मेमोरी को पॉइंटर खो दिया है, और इस तरह हम इसे मुक्त नहीं कर सकते हैं। delete &object2;ऐसा लगता है कि यह काम करेगा, लेकिन क्योंकि &object2 != &*(new A()), यह इसके बराबर नहीं है delete (new A())और इसलिए अमान्य है।


9

C # और Java में, आप किसी भी वर्ग का एक उदाहरण बनाने के लिए नए का उपयोग करते हैं और फिर आपको इसे बाद में नष्ट करने के बारे में चिंता करने की आवश्यकता नहीं है।

C ++ में एक कीवर्ड "नया" भी है जो एक ऑब्जेक्ट बनाता है लेकिन जावा या C # के विपरीत, यह ऑब्जेक्ट बनाने का एकमात्र तरीका नहीं है।

C ++ में ऑब्जेक्ट बनाने के दो तंत्र हैं:

  • स्वचालित
  • गतिशील

स्वचालित निर्माण के साथ आप एक स्कोप वाले वातावरण में वस्तु बनाते हैं: - एक फ़ंक्शन में या - एक वर्ग (या संरचना) के सदस्य के रूप में।

एक समारोह में आप इसे इस तरह बनाएंगे:

int func()
{
   A a;
   B b( 1, 2 );
}

एक वर्ग के भीतर आप सामान्य रूप से इसे इस तरह से बनाएंगे:

class A
{
  B b;
public:
  A();
};    

A::A() :
 b( 1, 2 )
{
}

पहले मामले में, गुंजाइश ब्लॉक से बाहर निकलने पर ऑब्जेक्ट स्वचालित रूप से नष्ट हो जाते हैं। यह एक फंक्शन या फंक्शन का स्कोप-ब्लॉक हो सकता है।

उत्तरार्द्ध मामले में ऑब्जेक्ट बी ए के उदाहरण के साथ एक साथ नष्ट हो जाता है जिसमें यह एक सदस्य है।

ऑब्जेक्ट को नए के साथ आवंटित किया जाता है जब आपको ऑब्जेक्ट के जीवनकाल को नियंत्रित करने की आवश्यकता होती है और फिर इसे नष्ट करने के लिए इसे हटाने की आवश्यकता होती है। RAII नामक तकनीक के साथ, आप उस बिंदु पर वस्तु के विलोपन का ध्यान रखते हैं जिसे आप इसे एक स्वचालित वस्तु के भीतर रखकर बनाते हैं, और उस स्वचालित वस्तु के विध्वंसक के प्रभावी होने की प्रतीक्षा करते हैं।

ऐसी ही एक वस्तु एक साझा_प्रकार है जो एक "निपुण" तर्क का आह्वान करेगा लेकिन केवल तभी जब वस्तु को साझा करने वाले साझा_प्रकार के सभी उदाहरण नष्ट हो जाते हैं।

सामान्य तौर पर, जब तक आपके कोड में कई नए कॉल हो सकते हैं, आपके पास हटाने के लिए सीमित कॉल होनी चाहिए और हमेशा यह सुनिश्चित करना चाहिए कि इन्हें विध्वंसक या "डीलेटर" ऑब्जेक्ट से कहा जाता है जो स्मार्ट-पॉइंटर्स में डाल दिए जाते हैं।

आपके विध्वंसक भी अपवाद कभी नहीं फेंकने चाहिए।

यदि आप ऐसा करते हैं, तो आपके पास कुछ मेमोरी लीक होंगे।


4
से अधिक automaticऔर है dynamic। वहाँ भी है static
मूविंग डक

9
B object2 = *(new B());

यह रेखा रिसाव का कारण है। चलिए इसे थोड़ा अलग करते हैं ..

object2 टाइप B का एक वेरिएबल है, जो एड्रेस 1 पर संग्रहीत है (हां, मैं यहां मनमाने नंबर चुन रहा हूं)। दाईं ओर, आपने एक नई B, या B के किसी ऑब्जेक्ट के लिए एक पॉइंटर मांगा है। प्रोग्राम खुशी से आपको यह देता है और आपके नए B को 2 पता करने के लिए असाइन करता है और एड्रेस 3 में एक पॉइंटर भी बनाता है। अब पता 2 में डेटा को एक्सेस करने का एकमात्र तरीका पता 3 में सूचक के माध्यम से है। अगला, आपने सूचक *को उस डेटा को प्राप्त करने के लिए उपयोग किया है जो सूचक इंगित कर रहा है (पता 2 में डेटा)। यह प्रभावी रूप से उस डेटा की एक प्रति बनाता है और इसे object2 को असाइन करता है, पता 1 में असाइन किया गया है। याद रखें, यह एक COPY है, मूल नहीं।

अब, यहाँ समस्या है:

आपने वास्तव में उस पॉइंटर को कभी भी संग्रहीत नहीं किया है कहीं भी आप इसका उपयोग कर सकते हैं! एक बार यह असाइनमेंट समाप्त हो जाने के बाद, पॉइंटर (एड्रेस 3 में मेमोरी, जिसे आपने एड्रेस 2 एक्सेस करने के लिए इस्तेमाल किया था) स्कोप से बाहर है और आपकी पहुंच से परे है! अब आप उस पर डिलीट कॉल नहीं कर सकते हैं और इसलिए पता 2 में मेमोरी को साफ नहीं कर सकते हैं। आपके पास जो कुछ बचा है, वह पता 1 से पता 2 के डेटा की एक प्रति है। स्मृति में बैठे हुए दो समान बातें। एक आप एक्सेस कर सकते हैं, दूसरा आप नहीं कर सकते (क्योंकि आपने इसके लिए रास्ता खो दिया है)। इसलिए यह मेमोरी लीक है।

मैं आपको अपने C # बैकग्राउंड से आने का सुझाव दूंगा कि आप C ++ काम में कितने पॉइंटर्स पर पढ़ते हैं। वे एक उन्नत विषय हैं और उन्हें समझने में थोड़ा समय लग सकता है, लेकिन उनका उपयोग आपके लिए अमूल्य होगा।


8

यदि यह आसान बनाता है, तो कंप्यूटर मेमोरी को एक होटल की तरह समझें और प्रोग्राम ऐसे ग्राहक हैं जो ज़रूरत पड़ने पर कमरे किराए पर लेते हैं।

इस होटल के काम करने का तरीका यह है कि आप एक कमरा बुक करते हैं और कुली को बताते हैं कि आप कब निकल रहे हैं।

यदि आप कुली को बताए बिना एक कमरा बुक करते हैं और कुली को बताए बिना सोचते हैं कि कमरा अभी भी उपयोग में है और किसी को भी इसका इस्तेमाल नहीं करने देंगे। इस मामले में एक कमरे का रिसाव है।

यदि आपका प्रोग्राम मेमोरी आवंटित करता है और इसे डिलीट नहीं करता है (यह केवल इसका उपयोग करना बंद कर देता है) तो कंप्यूटर सोचता है कि मेमोरी अभी भी उपयोग में है और किसी और को इसका उपयोग करने की अनुमति नहीं देगा। यह एक मेमोरी लीक है।

यह एक सटीक सादृश्य नहीं है, लेकिन यह मदद कर सकता है।


5
मैं उस सादृश्य को काफी पसंद करता हूं, यह बिल्कुल सही नहीं है, लेकिन निश्चित रूप से यह उन लोगों के लिए स्मृति लीक को समझाने का एक अच्छा तरीका है जो इसके लिए नए हैं!
एडम

1
मैंने लंदन में ब्लूमबर्ग में एक वरिष्ठ इंजीनियर के लिए एक साक्षात्कार में एचआर लड़की को मेमोरी लीक की व्याख्या करने के लिए इसका इस्तेमाल किया था। मुझे उस साक्षात्कार के माध्यम से मिला क्योंकि मैं वास्तव में एक गैर प्रोग्रामर को मेमोरी लीक (और थ्रेडिंग मुद्दों) को समझाने में सक्षम था, जिसे वह समझ गया था।
स्टीफन

7

बनाते समय object2आप नए के साथ बनाई गई ऑब्जेक्ट की एक प्रति बना रहे हैं, लेकिन आप भी (कभी नहीं सौंपे गए) पॉइंटर खो रहे हैं (इसलिए बाद में इसे हटाने का कोई तरीका नहीं है)। इससे बचने के लिए, आपको object2एक संदर्भ बनाना होगा ।


3
किसी वस्तु को हटाने के लिए एक संदर्भ का पता लेने के लिए यह अविश्वसनीय रूप से बुरा अभ्यास है। स्मार्ट पॉइंटर का इस्तेमाल करें।
टॉम व्हिटॉक

3
अविश्वसनीय रूप से बुरा अभ्यास, एह? आपको क्या लगता है कि स्मार्ट पॉइंटर्स पर्दे के पीछे का उपयोग करते हैं।
ब्लिंडी

3
@ बेलिंडी स्मार्ट पॉइंटर्स (कम से कम शालीनता से लागू किए गए) सीधे पॉइंटर्स का उपयोग करते हैं।
लुचियन ग्रिगोर

2
ठीक है, पूरी तरह से ईमानदार होने के लिए, पूरा आइडिया महान नहीं है, है ना? असल में, मुझे यह भी पक्का नहीं है कि ओपी में आजमाया गया पैटर्न वास्तव में कितना उपयोगी होगा।
मारियो

7

ठीक है, आप एक मेमोरी लीक बनाते हैं यदि आप किसी बिंदु पर उस मेमोरी को मुक्त नहीं करते हैं जो आपने newउस मेमोरी को पॉइंटर पास करके ऑपरेटर का उपयोग करके आवंटित की हैdelete

उपरोक्त दो मामलों में:

A *object1 = new A();

यहाँ आप deleteमेमोरी खाली करने के लिए उपयोग नहीं कर रहे हैं , इसलिए यदि और जब आपकीobject1 पॉइंटर स्कोप से बाहर हो जाता है, तो आपके पास मेमोरी लीक होगा, क्योंकि आपने पॉइंटर खो दिया होगा और इसलिए आप deleteऑपरेटर का उपयोग नहीं कर सकते ।

और यहाँ

B object2 = *(new B());

आप द्वारा लौटाए गए पॉइंटर को छोड़ रहे हैं new B(), और इसलिए उस पॉइंटर को deleteमेमोरी से मुक्त करने के लिए कभी भी पास नहीं किया जा सकता है। इसलिए एक और स्मृति रिसाव।


7

यह यह लाइन है जो तुरंत लीक हो रही है:

B object2 = *(new B());

यहां आप एक नया बना रहे हैं B ढेर पर वस्तु , फिर स्टैक पर एक कॉपी बना रहे हैं। जो ढेर पर आवंटित किया गया है, उसे अब एक्सेस नहीं किया जा सकता है और इसलिए रिसाव।

यह पंक्ति तुरंत टपकी नहीं है:

A *object1 = new A();

वहाँ एक रिसाव होगा यदि आप हालांकि कभी नहीं deleteobject1


4
गतिशील / स्वचालित भंडारण की व्याख्या करते समय कृपया ढेर / स्टैक का उपयोग न करें।
पब्बी

2
@ सार्वजनिक उपयोग क्यों नहीं करते? डायनेमिक / ऑटोमैटिक स्टोरेज के कारण हमेशा ढेर होता है, स्टैक नहीं? और इसीलिए स्टैक / हीप के बारे में विस्तार से बताने की आवश्यकता नहीं है, क्या मैं सही हूं?

4
@ user1131997 हीप / स्टैक कार्यान्वयन विवरण हैं। वे इस बारे में जानना महत्वपूर्ण हैं, लेकिन इस प्रश्न के लिए अप्रासंगिक हैं।
पबि

2
हम्म मैं इसके लिए एक अलग उत्तर देना चाहता हूं, अर्थात मेरा जैसा ही लेकिन जो आप सबसे अच्छा सोचते हैं उसके साथ ढेर / स्टैक की जगह। मुझे यह जानने में दिलचस्पी होगी कि आप इसे कैसे समझाना पसंद करेंगे।
मटजग्लय
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.