Sobes.tech
Middle+

Как да се справим с изтичане на памет, когато възникне изключение в конструктора при извикване на оператора new?

sobes.tech AI

Отговор от AI

Ако в конструктора на обект, създаден чрез new, възникне изключение, паметта, заделена за самия обект с operator new, не се освобождава автоматично. Това води до изтичане на памет.

За решаване на този проблем могат да се приложат подходи, базирани на RAII (Resource Acquisition Is Initialization):

  1. "Умни" указатели (Smart Pointers): Използване на std::unique_ptr или std::shared_ptr. Паметта се управлява от "умен" указател, който в своя деструктор освобождава ресурса (паметта), дори ако конструкторът на обекта, към който сочи, е изхвърлил изключение.

    #include <memory>
    #include <stdexcept>
    
    class MyClass {
    public:
        MyClass() {
            // Имитация на възникване на изключение в конструктора
            throw std::runtime_error("Constructor exception");
        }
    };
    
    void func_with_leak_potential() {
        // Без умния указател, паметта ще изтече при изключение в конструктора
        MyClass* obj = new MyClass(); 
        // Ако MyClass() хвърли изключение, този delete няма да бъде извикан
        delete obj; 
    }
    
    void func_with_no_leak() {
        // Използване на std::unique_ptr
        // Паметта, заделена с operator new за MyClass, ще бъде освободена от деструктора на unique_ptr
        // дори ако MyClass хвърли изключение
        std::unique_ptr<MyClass> obj = std::make_unique<MyClass>(); 
    }
    
  2. Помощни класове (Helper Classes/Scopes): Създаване на малки класове, чиято задача е управлението на ресурси в техните конструктори и деструктори. В контекста на конструктор, който може да хвърли изключение, такъв клас може временно да притежава ресурс (например, суров указател) и да го освободи в своя деструктор, ако основният конструктор не е успешен.

    #include <stdexcept>
    #include <iostream>
    
    // Помощен клас за управление на суров указател
    class PtrGuard {
    private:
        int* ptr_;
        bool released_;
    public:
        PtrGuard(int* p) : ptr_(p), released_(false) {}
        
        ~PtrGuard() {
            if (!released_ && ptr_) {
                std::cout << "Изтриване на ptr_ от Guard\n";
                delete ptr_;
            }
        }
        
        int* get() const { return ptr_; }
        
        void release() { released_ = true; } // Предаване на собственост
    };
    
    class MyClassWithResource {
    private:
        int* resource_;
    public:
        MyClassWithResource() : resource_(nullptr) {
            // Заделяне на ресурс
            resource_ = new int(10);
            
            // Използване на Guard за управление на ресурса до пълното конструиране
            PtrGuard guard(resource_); 
    
            // Имитация на изключение след задължителното заделяне
            // Ако тук възникне изключение, guard.release() няма да бъде извикана,
            // и ресурсът ще бъде освободен в деструктора на guard
            throw std::runtime_error("Constructor part exception");
            
            // Ако няма изключение, "прехвърляме собственост" на MyClassWithResource
            guard.release(); 
            // Ресурс_ сега ще се управлява от деструктора на MyClassWithResource
        }
        
        ~MyClassWithResource() {
            if (resource_) {
                std::cout << "Изтриване на resource_ от MyClassWithResource\n";
                delete resource_;
            }
        }
    };
    
    void func_with_no_leak_with_guard() {
        try {
            MyClassWithResource* obj = new MyClassWithResource();
            // Ако няма изключение, трябва да се направи delete
            delete obj; 
        } catch (const std::runtime_error& e) {
            std::cerr << "Засечена изключение: " << e.what() << std::endl;
        }
    }
    

Предпочитаният и по-идиоматичен подход в съвременния C++ е използването на "умни" указатели (std::unique_ptr и std::shared_ptr), тъй като те предоставят надеждно и по-малко многословно решение за управление на паметта и други ресурси при изключения. Използването на std::make_unique (C++14 и нагоре) или std::make_shared също е безопасно по отношение на изключения, тъй като алокацията на памет и извикването на конструктора стават атомарно.