यदि मूल वर्ग को कोई कच्ची गतिशील मेमोरी आवंटित नहीं होती है, तो बेस क्लास को वर्चुअल विध्वंसक होने की आवश्यकता क्यों है?


12

निम्न कोड स्मृति रिसाव का कारण बनता है:

#include <iostream>
#include <memory>
#include <vector>

using namespace std;

class base
{
    void virtual initialize_vector() = 0;
};

class derived : public base
{
private:
    vector<int> vec;

public:
    derived()
    {
        initialize_vector();
    }

    void initialize_vector()
    {
        for (int i = 0; i < 1000000; i++)
        {
            vec.push_back(i);
        }
    }
};

int main()
{
    for (int i = 0; i < 100000; i++)
    {
        unique_ptr<base> pt = make_unique<derived>();
    }
}

यह मेरे लिए बहुत मायने नहीं रखता था, क्योंकि वर्ग व्युत्पन्न कोई कच्ची गतिशील मेमोरी आवंटित नहीं करता है, और unique_ptr स्वयं को डीलॉलेट करता है। मुझे लगता है कि क्लास बेस के निहित विध्वंसक को व्युत्पन्न के बजाय बुलाया जा रहा है, लेकिन मुझे नहीं लगता कि यहां समस्या क्यों है। अगर मुझे व्युत्पन्न के लिए एक स्पष्ट विध्वंसक लिखना था, तो मैं vec के लिए कुछ भी नहीं लिखूंगा।


4
यदि आप मैन्युअल रूप से लिखे गए हैं, तो आप केवल एक विध्वंसक मान रहे हैं; यह धारणा दोषपूर्ण है: भाषा ~derived()वीसी के विध्वंसक को एक प्रतिनिधि प्रदान करती है । वैकल्पिक रूप से, आप मान रहे हैं कि unique_ptr<base> ptव्युत्पन्न विध्वंसक को पता चल जाएगा। एक आभासी विधि के बिना, यह मामला नहीं हो सकता। हालांकि एक यूनिक_प्रटर को एक डिलीट फंक्शन दिया जा सकता है जो कि बिना रनटाइम प्रतिनिधित्व के एक टेम्प्लेट पैरामीटर है, और यह सुविधा इस कोड के लिए किसी काम की नहीं है।
आमोन

क्या हम कोड को छोटा बनाने के लिए उसी लाइन पर ब्रेसिज़ लगा सकते हैं? अब मुझे स्क्रॉल करना है।
laike9m

जवाबों:


14

जब संकलक delete _ptr;अंदर के unique_ptrविनाशकर्ता (जहां _ptrसूचक में संग्रहीत होता है unique_ptr) के अंदर निहित को निष्पादित करने के लिए जाता है , तो यह ठीक दो चीजें जानता है:

  1. हटाए जाने वाली वस्तु का पता।
  2. सूचक का प्रकार जो _ptrहै। चूंकि पॉइंटर अंदर है unique_ptr<base>, इसका मतलब _ptrहै कि प्रकार का है base*

यह सब संकलक जानता है। इसलिए, यह देखते हुए कि यह एक प्रकार की वस्तु को हटा रहा है base, यह आह्वान करेगा ~base()

तो ... वह हिस्सा जहां वह उस derviedवस्तु को नष्ट करता है जिसे वह वास्तव में इंगित करता है? क्योंकि यदि संकलक को यह पता नहीं है कि यह नष्ट हो रहा है derived, तो यह बिल्कुल भी नहीं जानता derived::vec है कि इसे नष्ट कर दिया जाए। इसलिए आपने इसे आधा छोड़ कर वस्तु को तोड़ दिया है।

संकलक यह नहीं मान सकता है कि base*नष्ट होने वाला वास्तव में एक है derived*; सब के बाद, वहाँ से प्राप्त की गई कक्षाओं की संख्या हो सकती है base। यह कैसे पता चलेगा कि यह base*वास्तव में किस प्रकार को इंगित करता है?

कंपाइलर को क्या करना है, यह जानने के लिए सही डिस्ट्रक्टोर को कॉल करें (हां, derivedएक डिस्ट्रक्टर है। जब तक आप = deleteडिस्ट्रक्टर नहीं होते, हर क्लास में एक डिस्ट्रक्टर होता है, चाहे आप एक लिखें या नहीं)। ऐसा करने के लिए, इसे baseनष्ट करने के लिए विध्वंसक कोड का सही पता प्राप्त करने के लिए संग्रहीत जानकारी का उपयोग करना होगा , वास्तविक वर्ग के निर्माता द्वारा निर्धारित की गई जानकारी। फिर उसे इस जानकारी का उपयोग base*करके पॉइंटर को संबंधित derivedवर्ग के पते में परिवर्तित करना होगा (जो अलग पते पर हो सकता है या नहीं भी हो सकता है। हां, वास्तव में)। और तब यह उस विध्वंसक को आह्वान कर सकता है।

उस तंत्र का मैंने अभी वर्णन किया है? इसे आम तौर पर "वर्चुअल डिस्पैच" कहा जाता है: उर्फ, वह चीज जो किसी भी समय virtualतब होती है जब आप किसी फ़ंक्शन को आधार वर्ग के लिए पॉइंटर / संदर्भ के रूप में चिह्नित फ़ंक्शन कहते हैं ।

यदि आप एक व्युत्पन्न वर्ग फ़ंक्शन को कॉल करना चाहते हैं जब आपके पास सभी बेस क्लास पॉइंटर / संदर्भ होते हैं, तो उस फ़ंक्शन को घोषित किया जाना चाहिए virtual। विध्वंसक मूल रूप से इस संबंध में अलग नहीं हैं।


0

विरासत

वंशानुक्रम का पूरा बिंदु कई अलग-अलग कार्यान्वयनों के बीच एक सामान्य इंटरफ़ेस और प्रोटोकॉल साझा करना है, ताकि किसी व्युत्पन्न वर्ग के उदाहरण को किसी भी अन्य व्युत्पन्न प्रकार से किसी अन्य उदाहरण के लिए पहचान लिया जा सके।

C ++ वंशानुक्रम में भी इसे लागू करने के विवरण के साथ लाता है, अंकन (या अंकन नहीं) विध्वंसक के रूप में आभासी एक ऐसा कार्यान्वयन विस्तार है।

बांधने का कार्य

अब जब एक फंक्शन, या इसके किसी विशेष केस जैसे कि कंस्ट्रक्टर या डिस्ट्रक्टर को बुलाया जाता है, तो कंपाइलर को यह चुनना चाहिए कि कौन सा फंक्शन कार्यान्वयन था। फिर उसे मशीन कोड जनरेट करना होगा जो इस आशय का अनुसरण करता है।

इसे काम करने का सबसे सरल तरीका यह होगा कि आप संकलन समय पर फ़ंक्शन का चयन करें और बस पर्याप्त मशीन कोड का उत्सर्जन करें ताकि किसी भी मूल्य की परवाह किए बिना, जब कोड का वह टुकड़ा निष्पादित हो जाए, तो यह हमेशा फ़ंक्शन के लिए कोड चलाता है। यह वंशानुक्रम को छोड़कर महान काम करता है।

यदि हमारे पास एक फ़ंक्शन के साथ बेस क्लास है (कंस्ट्रक्टर या डिस्ट्रक्टर सहित कोई भी फ़ंक्शन हो सकता है) और आपका कोड इस पर एक फ़ंक्शन को कॉल करता है, तो इसका क्या मतलब है?

अपने उदाहरण से लेते हुए, यदि आपने initialize_vector()कंपाइलर को यह तय करना है कि क्या आप वास्तव में पाए गए कार्यान्वयन को कॉल करने के लिए हैं Base, या कार्यान्वयन पाया गया है Derived। इसे तय करने के दो तरीके हैं:

  1. पहला यह तय करना है कि क्योंकि आप एक Baseप्रकार से बुलाए गए हैं , इसलिए आपका मतलब कार्यान्वयन में है Base
  2. दूसरा यह तय करना है कि क्योंकि Baseटाइप किए गए मूल्य में संग्रहित मूल्य का रनटाइम प्रकार हो सकता है Base, या Derivedयह कि निर्णय किस कॉल को करना है, रनटाइम पर कॉल किया जाना चाहिए (प्रत्येक बार जब इसे कहा जाता है)।

इस बिंदु पर संकलक भ्रमित है, दोनों विकल्प समान रूप से मान्य हैं। यह virtualमिश्रण में आता है। जब यह कीवर्ड कंपाइलर पिक ऑप्शन 2 प्रस्तुत करता है तो कोड को वास्तविक मान के साथ चलाने तक सभी संभावित कार्यान्वयनों के बीच निर्णय लेने में देरी होती है। जब यह कीवर्ड अनुपस्थित है तो कंपाइलर विकल्प 1 चुनता है क्योंकि यह अन्यथा सामान्य व्यवहार है।

कंपाइलर अभी भी वर्चुअल फ़ंक्शन कॉल के मामले में विकल्प 1 चुन सकता है। लेकिन केवल अगर यह साबित कर सकता है कि यह हमेशा मामला है।

कंस्ट्रक्टर और डिस्ट्रक्टर्स

तो हम एक वर्चुअल कंस्ट्रक्टर को क्यों नहीं निर्दिष्ट करते?

अधिक सहज कैसे संकलक के लिए निर्माता की समान कार्यान्वयन के बीच चुनेंगे Derivedऔर Derived2? यह बहुत आसान है, यह नहीं हो सकता। कोई पूर्व-मौजूदा मूल्य नहीं है जिससे कंपाइलर सीख सकता है कि वास्तव में क्या इरादा था। प्री-एक्सिसिटिंग वैल्यू नहीं है क्योंकि यह कंस्ट्रक्टर का काम है।

तो हमें एक आभासी विध्वंसक को निर्दिष्ट करने की आवश्यकता क्यों है?

अधिक सहजता से संकलक कार्यान्वयन के लिए Baseऔर के बीच चयन कैसे करेगा Derived? वे केवल फ़ंक्शन कॉल हैं, इसलिए फ़ंक्शन कॉल व्यवहार होता है। एक घोषित वर्चुअल डिस्ट्रक्टर के बिना कंपाइलर Baseरनटाइम प्रकार की परवाह किए बिना डिस्ट्रक्टर को सीधे बाँधने का निर्णय करेगा ।

कई संकलक में, यदि व्युत्पन्न किसी भी डेटा सदस्यों की घोषणा नहीं करता है, और न ही अन्य प्रकारों से विरासत में मिला है, तो ~Base()वसीयत में व्यवहार उपयुक्त होगा, लेकिन इसकी गारंटी नहीं है। यह पूरी तरह से घटित होने से काम करेगा, बहुत कुछ एक फ्लेमथ्रोवर के सामने खड़ा होने जैसा कि अभी तक प्रज्वलित नहीं हुआ था। आप थोड़ी देर के लिए ठीक हैं।

C ++ में किसी भी आधार या इंटरफ़ेस प्रकार को घोषित करने का एकमात्र सही तरीका आभासी विध्वंसक घोषित करना है, ताकि सही विध्वंसक को उस प्रकार के पदानुक्रम के किसी भी उदाहरण के लिए बुलाया जाए। यह उस आवृत्ति को सही ढंग से साफ़ करने के लिए आवृत्ति के सबसे अधिक ज्ञान के साथ फ़ंक्शन की अनुमति देता है।

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.