c++ - من يقوم بحذف الذاكرة المخصصة أثناء عملية "جديدة" والتي لديها استثناء في المنشئ؟




exception memory-leaks (6)

أعتقد أنه من السخرية أن يقوم مُنشئ بإثارة استثناء. هل يمكن أن يكون لديك قيمة إرجاع واختبارها في الرئيسي الخاص بك؟

class Blah
{
   public:

   Blah()
       {
           if Error
           {
              this.Error = "oops";
           }
        }
};

void main()
{
Blah* b = NULL;

b = new Blah();

if (b.Error == "oops")
{
   delete (b);
   b = NULL;
}

أنا حقا لا أستطيع أن أصدق أنني لم أستطع العثور على إجابة واضحة لهذا ...

كيف يمكنك تحرير الذاكرة المخصصة بعد طرح منشئ فئة C ++ استثناءً ، في حالة بدء تشغيله باستخدام عامل التشغيل new . على سبيل المثال:

class Blah
{
public:
  Blah()
  {
    throw "oops";
  }
};

void main()
{
  Blah* b = NULL;
  try
  {
    b = new Blah();
  }
  catch (...)
  {
    // What now?
  }
}

عندما حاولت ذلك ، b هو NULL في كتلة الصيد (الذي من المنطقي).

عند تصحيح الأخطاء ، لاحظت أن conrol يدخل روتين تخصيص الذاكرة قبل أن تصل إلى المنشئ.

يبدو أن هذا على موقع MSDN يؤكد ذلك :

عند استخدام جديد لتخصيص الذاكرة لكائن فئة C ++ ، يتم استدعاء منشئ الكائن بعد تخصيص الذاكرة.

لذا ، ضع في اعتبارك أن المتغير المحلي b لا يتم تعيينه أبداً (أي NULL في كتلة catch) كيف تحذف الذاكرة المخصصة؟

سيكون من الجيد أيضًا الحصول على إجابة عبر منصة على هذا. بمعنى ، ما الذي تقوله مواصفات C ++؟

التوضيح: أنا لا أتحدث عن الحالة التي خصص فيها الفصل نفسه للذاكرة في c'tor ومن ثم يلقي. وأنا أقدر أنه في هذه الحالات لن يتم استدعاء d'tor. أنا أتحدث عن الذاكرة المستخدمة لتخصيص الكائن ( Blah في حالتي).


إذا قام المُنشئ بطرح الذاكرة المخصصة للكائن ، فسيتم إعادتها تلقائيًا إلى النظام.

لاحظ لن يتم استدعاء destructor من الطبقة التي أدخلت.
ولكن سيتم أيضاً استدعاء destructor من أي فئة أساسية (حيث تم إكمال منشئ الأساسي).

ملحوظة:
كما لاحظ معظم الناس الآخرين قد تحتاج إلى بعض تنظيف.

الأعضاء الذين تمت تهيئتهم بشكل كامل سوف يطلق عليهم دمارهم ، ولكن إذا كان لديك أي عضو في مؤشر RAW تملكه (أي الحذف في destructor) ، يجب عليك القيام ببعض التنظيف قبل القيام بالرمي (سبب آخر لعدم استخدام مؤشرات RAW في صفك).

#include <iostream>

class Base
{
    public:
        Base()  {std::cout << "Create  Base\n";}
        ~Base() {std::cout << "Destroy Base\n";}
};

class Deriv: public Base
{
    public:
        Deriv(int x)    {std::cout << "Create  Deriv\n";if (x > 0) throw int(x);}
        ~Deriv()        {std::cout << "Destroy Deriv\n";}
};

int main()
{
    try
    {
        {
            Deriv       d0(0);  // All constructors/Destructors called.
        }
        {
            Deriv       d1(1);  // Base constructor and destructor called.
                                // Derived constructor called (not destructor)
        }
    }
    catch(...)
    {
        throw;
        // Also note here.
        // If an exception escapes main it is implementation defined
        // whether the stack is unwound. By catching in main() you force
        // the stack to unwind to this point. If you can't handle re-throw
        // so the system exception handling can provide the appropriate
        // error handling (such as user messages).
    }
}

المدة الطويلة والقصيرة هي أنه إذا لم تقم بإجراء أي تخصيصات للكيانات الأخرى في الكائن (كما في المثال الخاص بك) ، فسيتم حذف الذاكرة التي تم تخصيصها تلقائيًا. ومع ذلك ، يجب التعامل مع أي عبارات جديدة (أو أي شيء آخر يقوم بإدارة الذاكرة بشكل مباشر) في عبارة catch في المُنشئ ، وإلا يتم حذف الكائن دون حذف التخصيصات اللاحقة ، ويكون لديك ، يا صديقي ، تسريبًا.


المشكلة المذكورة قديمة قدم الطريق إلى روما ، لاستخدام قول هولندي. لقد تعاملت مع المشكلة وتخصيص ذاكرة لكائن قد طرح الاستثناء يبدو كما يلي:

try
{
    std::string *l_string =
        (_heap_cleanup_tpl<std::string>(&l_string),
        new std::string(0xf0000000, ' '));
    delete l_string;
}
catch(std::exception &)
{
}

قبل المكالمة الفعلية للمدراء new ، يتم إنشاء كائن مجهول (مؤقت) ، والذي يتلقى عنوان الذاكرة المخصصة من خلال مشغل جديد معرف من قبل المستخدم (انظر بقية هذه الإجابة). في حالة تنفيذ البرنامج العادي ، يمرر الكائن المؤقت نتيجة المشغل الجديد (الكائن الذي تم إنشاؤه حديثًا l_string بالكامل ، في حالتنا سلسلة طويلة جدًا جدًا) إلى l_string المتغير. في حالة حدوث استثناء ، لا يتم تمرير القيمة ، ولكن يقوم destructor للكائن المؤقت بحذف الذاكرة (دون استدعاء ofcourse destructor للكائن الرئيسي).

إنها طريقة غامضة بعض الشيء للتعامل مع هذه القضية ، لكنها تعمل. قد تنشأ مشاكل لأن هذا الحل يتطلب مشغل جديد يحدده المستخدم ومشغل حذف محدد من قبل المستخدم ليتماشى معه. وسيتعين على المشغلين الجدد / المحذورين من قبل المستخدم استدعاء تطبيق C ++ - القياسي للمكتبة الجديدة / حذف المشغلين ، لكني تركت ذلك من أجل الإيجازية واعتمدت على malloc() و free() بدلاً من ذلك.

إنها ليست هي الحل النهائي ، ولكن أعتقد أنه من الجدير العمل على هذا الأمر.

ملاحظة: كان هناك ميزة "غير موثقة" في التعليمات البرمجية أدناه ، لذلك قمت بتحسين.

رمز الكائن المؤقت كالتالي:

class _heap_cleanup_helper
{
    public:
    _heap_cleanup_helper(void **p_heap_block) :
        m_heap_block(p_heap_block),
        m_previous(m_last),
        m_guard_block(NULL)
    {
        *m_heap_block = NULL;
        m_last = this;
    }
    ~_heap_cleanup_helper()
    {
        if (*m_heap_block == NULL) operator delete(m_guard_block);
        m_last = m_previous;
    }
    void **m_heap_block, *m_guard_block;
    _heap_cleanup_helper *m_previous;
    static _heap_cleanup_helper *m_last;
};

_heap_cleanup_helper *_heap_cleanup_helper::m_last;

template <typename p_alloc_type>
class _heap_cleanup_tpl : public _heap_cleanup_helper
{
    public:
    _heap_cleanup_tpl(p_alloc_type **p_heap_block) :
        _heap_cleanup_helper((void **)p_heap_block)
    {
    }
};

المشغل الجديد المعرفة من قبل المستخدم هو كما يلي:

void *operator new (size_t p_cbytes)
{
    void *l_retval = malloc(p_cbytes);

    if (
        l_retval != NULL &&
        *_heap_cleanup_helper::m_last->m_heap_block == NULL &&
        _heap_cleanup_helper::m_last->m_guard_block == NULL
    )
    {
        _heap_cleanup_helper::m_last->m_guard_block = l_retval;
    }
    if (p_cbytes != 0 && l_retval == NULL) throw std::bad_alloc();

    return l_retval;
}

void operator delete(void *p_buffer)
{
    if (p_buffer != NULL) free(p_buffer);
}

من C ++ 2003 Standard 5.3.4 / 17 - جديد:

إذا تم إنهاء أي جزء من عملية تهيئة الكائن الموصوفة أعلاه عن طريق طرح استثناء ووظيفة إلغاء توزيع مناسبة ، يتم استدعاء وظيفة إلغاء التخصيص لتحرير الذاكرة التي كان يتم فيها بناء الكائن ، وبعد ذلك يستمر الاستثناء في الانتشار في السياق من التعبير الجديد. إذا تعذر العثور على دالة إلغاء توافق مطابقة لا لبس فيها ، فإن نشر الاستثناء لا يتسبب في تحرير ذاكرة الكائن. [ملاحظة: هذا مناسب عندما لا تقوم وظيفة تخصيص التخصيص بتخصيص الذاكرة ؛ خلاف ذلك ، فمن المحتمل أن ينتج عنه حدوث تسرب للذاكرة. ]

لذلك قد يكون هناك تسرب أو لا يكون - يعتمد ذلك على ما إذا كان يمكن العثور على deallocator مناسب (وهو ما يحدث عادةً ، ما لم يتم إلغاء عامل جديد / حذف) .في حالة وجود deallocator مناسب ، يكون المترجم مسؤولاً للتوصيلات في مكالمة إليه إذا كان منشئ يلقي.

لاحظ أن هذا أكثر أو أقل لا علاقة له بما يحدث للموارد التي تم الحصول عليها في المنشئ ، وهذا هو أول محاولة لي في الإجابة التي تمت مناقشتها - وهو سؤال تمت مناقشته في العديد من الأسئلة الشائعة والمقالات والإعلانات.


يجب عليك الرجوع إلى الأسئلة المتشابهة here here . أساسا إذا كان منشئ يلقي استثناء كنت آمنة أن يتم تحرير ذاكرة الكائن نفسه مرة أخرى. على الرغم من أنه ، إذا تم المطالبة بذاكرة أخرى أثناء إنشاء المنشئ ، فأنت لوحدك لتحريره قبل مغادرة المُنشئ بالاستثناء.

بالنسبة لسؤالك ، تقوم منظمة الصحة العالمية بحذف الذاكرة ، والإجابة هي الرمز وراء المشغل الجديد (الذي يتم إنشاؤه بواسطة المحول البرمجي). إذا أدركت الاستثناء الذي يترك المُنشئ ، فعليه استدعاء جميع أعضاء الفرقة الذين دمروا (كما تم بناؤه بالفعل بنجاح قبل استدعاء رمز المُنشئ) وتحرير ذاكرتهم (يمكن القيام به بشكل متكرر مع استدعاء الدامور ، على الأرجح عن طريق استدعاء حذف مناسب عليها) وكذلك تحرير الذاكرة المخصصة لهذه الفئة نفسها. ثم يجب إعادة الاستثناء catched من constructor إلى المتصل جديد . بالطبع قد يكون هناك المزيد من العمل الذي يجب القيام به ولكن لا يمكنني سحب كل التفاصيل من رأسي لأنها تصل إلى كل تطبيق المجمع.





constructor