Управление памятью в С++
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 и т. д., умные указатели облегчают нам управление памятью, но они все равно принадлежат категория "указатели", правильно Неуказательные ресурсы неудобны в использовании, а также существует проблема загрязнения интерфейса.Поэтому в это время мы часто выпускаем функции вручную, потому что боимся неприятностей.Один недостаток ручной настройки заключается в том, что если мы находимся между заявкой на ресурс и выпуском ресурса, возникает исключение, тогда выпуск не произойдет. Кроме того, при ручном выпуске необходимо вызывать функцию выпуска при всех возможных выходах из функции.На случай, если кто-то когда-нибудь изменит код, есть еще одно место.return
,иreturn
Если вы забудете вызвать функцию освобождения раньше, произойдет утечка ресурса. В идеале мы хотели бы иметь возможность использовать его так:
#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
Умеет следить за правильным освобождением памяти и выдавать предупреждения при утечке ресурсов.Если вы переживаете, что это снизит эффективность работы, то добавлять его нужно не во все классы, а когда вы подозреваете, что в классе есть утечка памяти , затем добавьте его