Как изящно управлять памятью в C++

C++

Управление памятью в С++

C++ является родным языком, и когда дело доходит до родных языков, мы должны говорить об управлении ресурсами, из которых управление памятью является большой проблемой в управлении ресурсами.Поскольку память в куче необходимо выделять и освобождать вручную, необходимо убедиться, что запрошенная память верна.освобождена. Общий принцип для этого "Кто выделяет, кто освобождает", но даже тогда есть утечки памяти, дикие указатели и т.д.

Управляемые языки представили GC (Garbage Collection) для решения этой проблемы, считают ониПамять слишком важна, чтобы оставлять ее программистам. Но у GC часто есть свои проблемы для нативной разработки. С другой стороны, сообщество Native часто критикует GC, говоря, чтоПамять слишком важна, чтобы оставлять ее машине.

C++ предлагает компромиссное решение, то есть: он не полностью отдан ни машине, ни полностью программисту, но программист теперь указывает, как это сделать в коде, и когда это сделать, как гарантировать, что это будет сделано. be done Если он выполняется, он передается компилятору для определения.

прежде всегоC++98Предоставляет языковой механизм: когда объект выходит за пределы области видимости, автоматически вызывается его деструктор. Затем Бьерн Страуструп, отец C++, определилRAII(Получение ресурсов — это инициализация) (то есть ресурсы, необходимые при создании объекта, должны быть инициализированы в конструкторе, а ресурсы должны быть освобождены при разрушении объекта).RAII говорит нам, что классы должны использоваться для инкапсуляции ресурсов и управления ими.

Следуя этой идее, первый прием управления памятью, который следует ввести, заключается в использованииумный указатель

умный указатель

Что касается управления памятью, Boost был первым, кто внедрил интеллектуальные указатели промышленного уровня, и сегодняшние интеллектуальные указатели (shared_ptr и unique_ptr)C++11Часть этого, проще говоря, с интеллектуальными указателями, удаления почти никогда не должны появляться в вашем коде C++.

Хотя интеллектуальный указатель называется «указателем» и ведет себя как указатель, по сути, это класс. Как было сказано ранее:

RAII говорит нам, что классы должны использоваться для инкапсуляции ресурсов и управления ими.

Интеллектуальные указатели получают контроль над памятью при инициализации объекта и автоматически освобождают память при его уничтожении для правильного управления памятью.

В С++ 11,shared_ptrиunique_ptrДва наиболее часто используемых интеллектуальных указателя, оба должны включать файлы заголовков.<memory>

unique_ptr

unique_ptrуникален и подходит для хранения динамически размещаемых старых массивов в стиле C. При объявлении переменной используйтеautoиmake_uniqueЭффективность согласования выше. также,unique_ptrКак следует из названия, ресурс должен иметь только одинunique_ptrконтроль, и его следует использовать, когда требуется передача контроляstd::move, указатель, потерявший управление, больше не может продолжать доступ к ресурсу.

#include <iostream>
#include <memory>

using namespace std;

int main()
{
    int size = 5;
    auto x1 = make_unique<int[]>(size);
    unique_ptr<int[]> x2(new int[size]);        // 两种声明unique_ptr的方式

    x1[0] = 5;
    x2[0] = 10;                                 // 像指针一样赋值

    for(int i = 0; i < size; ++i)
        cout << x1[i] << endl;                  // 输出: 5 0 0 0 0

    auto x3 = std::move(x1);                    // 转移x1的所有权
    for(int i = 0; i < size; ++i)
        cout << x3[i] << endl;                  // 输出: 5 0 0 0 0

}

unique_ptrПри уничтожении объекта управляемые ресурсы освобождаются, а при передаче управления есть особый случай, на который следует обратить внимание, то есть никогда не передавать управление локальной переменной. Потому что локальная переменная будет уничтожена после выхода из области видимости, тем самым освободив ресурс, и возникнет ошибка, когда внешний мир захочет получить доступ к освобожденному ресурсу. Следующий пример иллюстрирует эту ситуацию

#include <iostream>
#include <memory>

using namespace std;

class A
{
public:
    A():a(new int(10))                      // 初始化a为10
    {
        cout << "Create A..." << endl;
    }

    ~A()
    {
        cout << "Destroy A..." << endl;
        delete a;                           // 释放a
    }

    int* a;
};

void move_unique_ptr_to_local_unique_ptr(unique_ptr<A>& uptr)
{
    auto y(std::move(uptr));                // 转移所有权
}                                           // 函数结束,y进行析构,便释放了A的资源

int main()
{
    auto x = make_unique<A>();
    move_unique_ptr_to_local_unique_ptr(x);

    cout << *(x->a) << endl;                  // 内存访问错误,x中的资源以及被局部变量释放了
}

shared_ptr

shared_ptrиспользование иunique_ptrпохожий. использоватьautoиmake_sharedЭффективность согласования выше. Кроме того, сunique_ptrразница в том,shared_ptrПамять управляется подсчетом ссылок, поэтому ресурс может иметь несколькоshared_ptrСсылка в то же время, и когда счетчик ссылок равен 0, освободить ресурс (счетчик ссылок можно использовать сuse_countсмотреть)

void copy_shared_ptr_to_local_shared_ptr(shared_ptr<A>& sptr)
{
    auto y(sptr);                                                         // 复制shared_ptr,拥有同一片资源
    cout << "After copy, use_count : " << sptr.use_count() << endl;       // After copy, use_count : 2
}

int main()
{
    auto x = make_shared<A>();
    cout << "use_count: " << x.use_count() << endl;          // use_count: 1
    copy_shared_ptr_to_local_shared_ptr(x);
    cout << *(x->a) << endl;                                 // 内存未被释放,可以正常访问
}

unique_ptrиshared_ptrВы также можете указать, как освобождать память, что значительно облегчает управление такими ресурсами, как файлы и сокеты.

#include <iostream>
#include <memory>

using namespace std;

void fclose_deletor(FILE* f)
{
    cout << "close a file" << endl;
    fclose(f);
}

int main()
{
    unique_ptr<FILE, decltype(&fclose_deletor)> file_uptr( fopen("abc.txt", "w"),  &fclose_deletor);
    shared_ptr<FILE> file_sptr( fopen("abc.txt", "w"),  fclose_deletor);
}

умный указательunique_ptrиshared_ptrИспользование парадигмы RAII обеспечивает большое удобство для нашего управления памятью, но при ее использовании есть некоторые недостатки (Четыре смертных греха C++), среди которых самой неприятной проблемой я считаю: загрязнение интерфейса.

Например, я хочу передатьint*Перейдите к функции, так как право собственности находится на умном указателе, чтобы обеспечить правильную передачу права собственности, я должен изменить тип параметра функции наunique_ptr<int>. Точно так же возвращаемое значение имеет аналогичную ситуацию.

В приведенной выше ситуации, если на ранней стадии разработки ясно, что все указатели используют умные указатели, это не является большой проблемой. Но большая часть текущего кода основана на старом коде.При вызове старого кода вам нужно использовать интеллектуальный указатель вgetметод для возврата контролируемого ресурса. называетсяgetЭто означает, что умный указатель теряет полный контроль над ресурсом, то есть больше не может гарантировать правильное освобождение ресурса.

Scope Guard

Несмотря на то, что парадигма RAII хороша, пользоваться ею непросто, во многих случаях мы не хотим с большой помпой писать класс для closeHandle, ReleaseDC и т. д., умные указатели облегчают нам управление памятью, но они все равно принадлежат категория "указатели", правильно Неуказательные ресурсы неудобны в использовании, а также существует проблема загрязнения интерфейса.Поэтому в это время мы часто выпускаем функции вручную, потому что боимся неприятностей.Один недостаток ручной настройки заключается в том, что если мы находимся между заявкой на ресурс и выпуском ресурса, возникает исключение, тогда выпуск не произойдет. Кроме того, при ручном выпуске необходимо вызывать функцию выпуска при всех возможных выходах из функции.На случай, если кто-то когда-нибудь изменит код, есть еще одно место.returnreturnЕсли вы забудете вызвать функцию освобождения раньше, произойдет утечка ресурса. В идеале мы хотели бы иметь возможность использовать его так:

#include <fstream>
using namespace std;

void foo()
{
    fstream file("abc.txt", ios::binary);
    ON_SCOPE_EXIT{ file.close() };
}

ON_SCOPE_EXITКод внутри как деструктор: как бы он не вышел, он будет выполнен, например

Первоначально этоScopeGuardкогда идея была предложена, из-заC++ не имеет хорошего механизма для поддержки этой идеи, а его реализация очень громоздка и несовершенна. Позже был выпущен C++11 в сочетании с C++11.Лямбда-функция и tr1::function могут упростить ее реализацию

class ScopeGuard
{
public:
    explicit ScopeGuard(std::function<void()> onExitScope)
        : onExitScope_(onExitScope)
    { }

    ~ScopeGuard()
    {
        onExitScope_();
    }

private:
    std::function<void()> onExitScope_;

private: // noncopyable
    ScopeGuard(ScopeGuard const&) = delete;
    ScopeGuard& operator=(ScopeGuard const&) = delete;
};

Этот класс очень прост в использовании.Вы даете ему std::function, который отвечает за его выполнение при деструкции.Большая часть этого std::function является лямбдой, например:

void foo()
{
    fstream file("abc.txt", ios::binary);
    ScopeGuard on_exit([&]{
        file.close();
    });
}

on_exitбудет выполняться при уничтоженииfile.close. Чтобы избежать проблем с присвоением имени этому объекту, вы можете определить макрос, который смешивает номера строк, поэтому вы определяете по одному за раз.ScopeGuardОбъекты имеют уникальные имена:

#define SCOPEGUARD_LINENAME_CAT(name, line) name##line
#define SCOPEGUARD_LINENAME(name, line) SCOPEGUARD_LINENAME_CAT(name, line)
#define ON_SCOPE_EXIT(callback) ScopeGuard SCOPEGUARD_LINENAME(EXIT, __LINE__)(callback)

с тех пор, какON_SCOPE_EXITПосле этого становится очень удобно подавать заявки и освобождать ресурсы на C++.

fstream file("abc.txt", ios::binary);
ON_SCOPE_EXIT( [&] { file.close(); })

auto* x = new A()
ON_SCOPE_EXIT( [&] { delete x; })

Преимущество этого заключается в том, что код, который обращается за ресурсами, и освобождает ресурсы, находится близко друг к другу и никогда не будет забыт. область выхода, мы всегда будем ресурсы могут быть освобождены правильно.

Leaked Object Detector

Наиболее распространенной причиной утечек памяти является создание нового ресурса и забвение удаления.Хотя интеллектуальные указатели и охранники области действия могут эффективно помочь нам правильно освободить память, из-за различных причин и ограничений все еще будет проблема забывания освободить память. , Как контролировать Что делать с неправильно освобожденной памятью?Может быть, нам нужен детектор утечки объектов, который уведомляет нас, когда происходит утечка.

В частности, мы надеемся, что это может иметь следующие последствия:

int main()
{
   auto* x = new A();
} // 报错,因为没有delete

В исходном коде JUCE я нашелLeakedObjectDetectorкласс, который делает то, что мы хотим.LeakedObjectDetectorСчетчик поддерживается внутри, вOwnerClassПри создании счетчик +1,OwnerClassПри уничтожении счетчик -1

template <typename OwnerClass>
class LeakedObjectDetector
{
public:
    LeakedObjectDetector() noexcept
    {
        ++(getCounter().num_objects);
    }

    LeakedObjectDetector(const LeakedObjectDetector&) noexcept
    {
        ++(getCounter().num_objects);
    }

    ~LeakedObjectDetector()
    {
        if(--(getCounter().num_objects) < 0)
        {
            cerr << "*** Dangling pointer deletion! Class: " << getLeakedObjectClassName() << endl;

            assert(false);
        }
    }

private:
    class LeakCounter
    {
    public:
        LeakCounter() = default;

        ~LeakCounter()
        {
            if(num_objects > 0)
            {
                cerr << "*** Leaked object detected: " << num_objects << " instance(s) of class" << getLeakedObjectClassName() << endl;
                assert(false);
            }
        }

        atomic<int> num_objects{0};
    };

    static const char* getLeakedObjectClassName()
    {
        return OwnerClass::getLeakedObjectClassName();
    }

    static LeakCounter& getCounter() noexcept
    {
        static LeakCounter counter;
        return counter;
    }
};

Поскольку счетчик является статическим, его жизненный цикл длится от начала программы до конца программы, поэтому в конце программы счетчик уничтожается, и деструктор выносит решение.Если счетчик > 0, это означает, что экземпляр создан, но не выпущен.

Другое суждениеLeakedObjectDetectorВ деструкторе если счетчик меньше 0 значит много раз удалялся

Кроме того, если есть утечка памяти или несколько удалений, используйтеassertфорсировать перерыв

Очень удобно использовать с макросами

#define LINENAME_CAT(name, line) name##line
#define LEAK_DETECTOR(OwnerClass) \
        friend class LeakedObjectDetector<OwnerClass>;  \
        static const char* getLeakedObjectClassName() noexcept { return #OwnerClass; } \
        LeakedObjectDetector<OwnerClass>  LINENAME_CAT(leakDetector, __LINE__);

class A
{
public:
    A() = default;

private:
    LEAK_DETECTOR(A);
};

просто используйтеLEAK_DETECTOR(ClassName), вы можете контролировать правильность освобождения памяти класса, например

int main()
{
    auto* x = new A();
    return 0;
}
// 忘记delete,出现警告:
// *** Leaked object detected: 1 instance(s) of classA
// Assertion failed: (false), function ~LeakCounter, file /Users/hw/Development/work/leaked_object_detector/main.cpp, line 44.
int main()
{
    auto* x = new A();

    delete x;
    delete x;
    return 0;
}
// 多次delete,出现警告
// *** Dangling pointer deletion! Class: A
// Assertion failed: (false), function ~LeakedObjectDetector, file /Users/hw/Development/work/leaked_object_detector/main.cpp, line 29.

Суммировать

В C++ управление памятью полуавтоматическое, вам нужно указать программе, как это сделать, и компилятор гарантированно сделает это правильно. После введения трех вышеупомянутых методов управления памятью, вот небольшое резюме

  • RAII говорит нам, что мы должны использовать классы для инкапсуляции ресурсов, чтобы гарантировать, что ресурсы инициализируются при инициализации класса, а ресурсы высвобождаются при уничтожении класса Поэтому рассмотрите возможность использования класса, такого как вектор, для замены собственного указателя массива.
  • Используйте интеллектуальные указатели как можно чаще, но будьте осторожны с передачей права собственности
  • Используйте средства защиты области для управления локальными ресурсами, которые могут гарантировать, что независимо от того, как вы выйдете из области, ресурсы могут быть правильно освобождены.
  • LeakedObjectDetectorУмеет следить за правильным освобождением памяти и выдавать предупреждения при утечке ресурсов.Если вы переживаете, что это снизит эффективность работы, то добавлять его нужно не во все классы, а когда вы подозреваете, что в классе есть утечка памяти , затем добавьте его