введение
Когда я недавно работал над проектом C++, произошла серьезная утечка памяти, из-за которой я потерял себя, как показано на следующем рисунке:
После отладки наконец выяснилось, что место утечки памяти было в часто вызываемой функции.Есть определенная вероятность, что четыре указателя не были освобождены.Размер каждого указателя должен соответствовать ширине памяти, то есть, каждый указатель 64 бита и 8 байт, четыре указателя 32 байта. А небольшая 32-байтная утечка может накапливать энергию, которая может достигать десятков гигабайт пространства, пока не съест всю память. В этой статье представлен метод обнаружения утечек памяти без использования других средств обнаружения, а также рассказывается об использовании интеллектуальных указателей в STL.Метод утечки памяти запроса
Что такое утечка памяти
Утечка памяти объясняется в Википедии следующим образом:
В компьютерных науках утечка памяти — это отказ программы освободить память, которая больше не используется из-за недосмотра или ошибки. Утечка памяти относится не к физическому исчезновению памяти, а к тому, что после того, как приложение выделяет определенный сегмент памяти из-за ошибки проектирования, оно теряет контроль над сегментом памяти до освобождения сегмента памяти, что приводит к пустой трате времени. объем памяти.
Основная причина утечек памяти в C++ заключается в том, что после обращения программиста к памяти (malloc(), new
), бесполезное пространство памяти не освобождается вовремя, и даже указатель уничтожается, так что пространство памяти в этой области вообще не может быть освобождено.
Как только вы узнаете причину утечки памяти, вы сможете узнать, как бороться с утечкой памяти, а именно: не забудьте освободить неиспользуемое пространство памяти и сохранить его до Нового года, если вы его не освободите!
Утечки памяти могут иметь серьезные последствия:
- После запуска программа со временем занимает все больше памяти и, наконец, аварийно завершает работу, когда память становится недоступной;
- Программа потребляет много памяти, из-за чего другие программы не могут нормально работать;
- Программа потребляет много памяти, из-за чего потребители выбирают чужую программу вместо вашей;
- Программисты, которые часто допускали баги с утечками памяти, были уволены из компании и обеднели.
Как узнать, есть ли в моей программе утечка памяти?
В соответствии с причиной утечки памяти и ее плохими последствиями мы можем узнать, есть ли утечка памяти в программе, по ее основным проявлениям: после долгой работы программы использование памяти медленно и непрерывно растет, но на самом деле в вашей логике нет утечки памяти.Так много памяти требуется.
Как найти место утечки?
-
Согласно принципу, мы можем сначала просмотреть наш собственный код и использовать функцию «Найти» для запроса
new
иdelete
, чтобы увидеть, освобождаются ли приложение памяти и выпуск парами, что позволяет быстро найти некоторые утечки памяти с более простой логикой. -
Если утечка памяти по-прежнему происходит, вы можете судить по записи, соответствует ли количество примененных и освобожденных объектов. Добавить статическую переменную в класс
static int count;
выполнить в конструктореcount++;
выполнить в деструктореcount--;
, уничтожив все классы до конца программы, а затем выведя статические переменные, чтобы увидеть, равно ли значение count 0. Если оно равно 0, проблема не в нем. Если это не 0, объект этого типа полностью не высвобождается.. -
Проверить, полностью ли освобождено пространство, заявленное в классе, особенно если родительский класс наследуется, и проверить, вызывается ли деструктор родительского класса в пространстве памяти подкласса.
-
Для временного пространства, заявленного в функции, внимательно проверьте, нет ли места, где функция выскакивает заранее и память не освобождается.
Умные указатели для STL
Чтобы уменьшить количество утечек памяти, в STL используются интеллектуальные указатели для уменьшения утечек. Обычно в STL есть четыре типа интеллектуальных указателей:
класс указателя | служба поддержки | Примечание |
---|---|---|
unique_ptr |
C++ 11 | Интеллектуальные указатели с уникальной семантикой владения объектом |
shared_ptr |
C++ 11 | Смарт-указатели с общей семантикой владения объектом |
weak_ptr |
C++ 11 | слабая ссылка на объект, управляемый std::shared_ptr |
auto_ptr |
Удалено в С++ 17 | Интеллектуальные указатели со строгой семантикой владения объектом |
так какauto_ptr
В C++17 он убран, программистам, ориентированным на будущее, лучше уменьшить частоту его использования в коде, я не буду здесь изучать этот тип. Также из-заweak_ptr
даshared_ptr
Слабая ссылка, поэтому main можно разделить только на два указателя.unique_ptr
иshared_ptr
.
std::unique_ptr — это интеллектуальный указатель, который владеет и управляет другим объектом через указатель и освобождает объект, когда unique_ptr выходит за пределы области действия. Объект освобождается с помощью связанного с ним средства удаления, когда происходит одно из следующих событий:
- Уничтожен управляемый объект unique_ptr
- Назначьте другой указатель управляемому объекту unique_ptr с помощью operator= или reset().
std::shared_ptr — это интеллектуальный указатель, который поддерживает совместное владение объектом через указатель. Несколько объектов shared_ptr могут владеть одним и тем же объектом. Объект уничтожается, а его память освобождается, когда происходит одно из следующих событий:
- Последний оставшийся shared_ptr, содержащий объект, уничтожается;
- Последний оставшийся shared_ptr, содержащий объект, назначается другому указателю с помощью operator= или reset().
unique_ptr
Это эксклюзивный объект-указатель. В любой момент ресурс может быть занят только одним указателем. Когда unique_ptr покидает область действия, содержимое, содержащееся в указателе, освобождается.
Создайте
unique_ptr<int> uptr( new int );
unique_ptr<int[ ]> uptr( new int[5] );
//声明,可以用一个指针显示的初始化,或者声明成一个空指针,可以指向一个类型为T的对象
shared_ptr<T> sp;
unique_ptr<T> up;
//赋值,返回相对应类型的智能指针,指向一个动态分配的T类型对象,并且用args来初始化这个对象
make_shared<T>(args);
make_unique<T>(args); //注意make_unique是C++14之后才有的
//用来做条件判断,如果其指向一个对象,则返回true否则返回false
p;
//解引用
*p;
//获得其保存的指针,一般不要用
p.get();
//交换指针
swap(p,q);
p.swap(q);
//release()用法
//release()返回原来智能指针指向的指针,只负责转移控制权,不负责释放内存,常见的用法
unique_ptr<int> q(p.release()) // 此时p失去了原来的的控制权交由q,同时p指向nullptr
//所以如果单独用:
p.release()
//则会导致p丢了控制权的同时,原来的内存得不到释放
//则会导致//reset()用法
p.reset() // 释放p原来的对象,并将其置为nullptr,
p = nullptr // 等同于上面一步
p.reset(q) // 注意此处q为一个内置指针,令p释放原来的内存,p新指向这个对象
Класс соответствует требованиям для MoveConstructible и MoveAssignable, но не для CopyConstructible или CopyAssignable. Таким образом, вы не можете использовать оператор = и конструктор копирования, можно использовать только оператор перемещения.
Demo
#include <iostream>
#include <vector>
#include <memory>
#include <cstdio>
#include <fstream>
#include <cassert>
#include <functional>
struct B {
virtual void bar() { std::cout << "B::bar\n"; }
virtual ~B() = default;
};
struct D : B
{
D() { std::cout << "D::D\n"; }
~D() { std::cout << "D::~D\n"; }
void bar() override { std::cout << "D::bar\n"; }
};
// 消费 unique_ptr 的函数能以值或以右值引用接收它
std::unique_ptr<D> pass_through(std::unique_ptr<D> p)
{
p->bar();
return p;
}
void close_file(std::FILE* fp) { std::fclose(fp); }
int main()
{
std::cout << "unique ownership semantics demo\n";
{
auto p = std::make_unique<D>(); // p 是占有 D 的 unique_ptr
auto q = pass_through(std::move(p));
assert(!p); // 现在 p 不占有任何内容并保有空指针
q->bar(); // 而 q 占有 D 对象
} // ~D 调用于此
std::cout << "Runtime polymorphism demo\n";
{
std::unique_ptr<B> p = std::make_unique<D>(); // p 是占有 D 的 unique_ptr
// 作为指向基类的指针
p->bar(); // 虚派发
std::vector<std::unique_ptr<B>> v; // unique_ptr 能存储于容器
v.push_back(std::make_unique<D>());
v.push_back(std::move(p));
v.emplace_back(new D);
for(auto& p: v) p->bar(); // 虚派发
} // ~D called 3 times
std::cout << "Custom deleter demo\n";
std::ofstream("demo.txt") << 'x'; // 准备要读的文件
{
std::unique_ptr<std::FILE, void (*)(std::FILE*) > fp(std::fopen("demo.txt", "r"),
close_file);
if(fp) // fopen 可以打开失败;该情况下 fp 保有空指针
std::cout << (char)std::fgetc(fp.get()) << '\n';
} // fclose() 调用于此,但仅若 FILE* 不是空指针
// (即 fopen 成功)
std::cout << "Custom lambda-expression deleter demo\n";
{
std::unique_ptr<D, std::function<void(D*)>> p(new D, [](D* ptr)
{
std::cout << "destroying from a custom deleter...\n";
delete ptr;
}); // p 占有 D
p->bar();
} // 调用上述 lambda 并销毁 D
std::cout << "Array form of unique_ptr demo\n";
{
std::unique_ptr<D[]> p{new D[3]};
} // 调用 ~D 3 次
}
Выходной результат:
unique ownership semantics demo
D::D
D::bar
D::bar
D::~D
Runtime polymorphism demo
D::D
D::bar
D::D
D::D
D::bar
D::bar
D::bar
D::~D
D::~D
D::~D
Custom deleter demo
x
Custom lambda-expression deleter demo
D::D
D::bar
destroying from a custom deleter...
D::~D
Array form of unique_ptr demo
D::D
D::D
D::D
D::~D
D::~D
D::~D
shared_ptr
Есть два способа созданияshared_ptr
: Используйте макрос make_shared, чтобы ускорить процесс создания. Поскольку shared_ptr активно выделяет память и ведет счетчик ссылок, make_shared делает работу по созданию более эффективной.
void main( )
{
shared_ptr<int> sptr1( new int );
shared_ptr<int> sptr2 = make_shared<int>(100);
}
уничтожить
shared_ptr по умолчанию вызывает delete для освобождения связанных ресурсов. Если пользователь выбирает другую стратегию уничтожения, он может свободно указать стратегию построения этого shared_ptr. В этом сценарии shared_ptr указывает на набор объектов, но при выходе за пределы области видимости деструктор по умолчанию вызывает delete для освобождения ресурса. На самом деле, мы должны вызвать delete[] для уничтожения массива. Пользователь может указать общий этап выпуска, вызвав функцию, например лямбда-выражение.
void main( )
{
shared_ptr<Test> sptr1( new Test[5],
[ ](Test* p) { delete[ ] p; } );
}
УведомлениеСтарайтесь не создавать shared_ptr с необработанными указателями, чтобы избежать ошибок, вызванных разными группами.
void main( )
{
// 错误
int* p = new int;
shared_ptr<int> sptr1( p); // count 1
shared_ptr<int> sptr2( p ); // count 1
// 正确
shared_ptr<int> sptr1( new int ); // count 1
shared_ptr<int> sptr2 = sptr1; // count 2
shared_ptr<int> sptr3;
sptr3 =sptr1 // count 3
}
циклическая ссылка
Поскольку Shared_ptr является указателем на несколько указателей, могут возникать циклические ссылки, в результате чего память не может быть освобождена после выхода за пределы области.
class B;
class A
{
public:
A( ) : m_sptrB(nullptr) { };
~A( )
{
cout<<" A is destroyed"<<endl;
}
shared_ptr<B> m_sptrB;
};
class B
{
public:
B( ) : m_sptrA(nullptr) { };
~B( )
{
cout<<" B is destroyed"<<endl;
}
shared_ptr<A> m_sptrA;
};
//***********************************************************
void main( )
{
shared_ptr<B> sptrB( new B ); // sptB count 1
shared_ptr<A> sptrA( new A ); // sptB count 1
sptrB->m_sptrA = sptrA; // sptB count 2
sptrA->m_sptrB = sptrB; // sptA count 2
}
// 超出定义域
// sptA count 1
// sptB count 2
demo
#include <iostream>
#include <memory>
#include <thread>
#include <chrono>
#include <mutex>
struct Base
{
Base() { std::cout << " Base::Base()\n"; }
// 注意:此处非虚析构函数 OK
~Base() { std::cout << " Base::~Base()\n"; }
};
struct Derived: public Base
{
Derived() { std::cout << " Derived::Derived()\n"; }
~Derived() { std::cout << " Derived::~Derived()\n"; }
};
void thr(std::shared_ptr<Base> p)
{
std::this_thread::sleep_for(std::chrono::seconds(1));
std::shared_ptr<Base> lp = p; // 线程安全,虽然自增共享的 use_count
{
static std::mutex io_mutex;
std::lock_guard<std::mutex> lk(io_mutex);
std::cout << "local pointer in a thread:\n"
<< " lp.get() = " << lp.get()
<< ", lp.use_count() = " << lp.use_count() << '\n';
}
}
int main()
{
std::shared_ptr<Base> p = std::make_shared<Derived>();
std::cout << "Created a shared Derived (as a pointer to Base)\n"
<< " p.get() = " << p.get()
<< ", p.use_count() = " << p.use_count() << '\n';
std::thread t1(thr, p), t2(thr, p), t3(thr, p);
p.reset(); // 从 main 释放所有权
std::cout << "Shared ownership between 3 threads and released\n"
<< "ownership from main:\n"
<< " p.get() = " << p.get()
<< ", p.use_count() = " << p.use_count() << '\n';
t1.join(); t2.join(); t3.join();
std::cout << "All threads completed, the last one deleted Derived\n";
}
возможный выход
Base::Base()
Derived::Derived()
Created a shared Derived (as a pointer to Base)
p.get() = 0xc99028, p.use_count() = 1
Shared ownership between 3 threads and released
ownership from main:
p.get() = (nil), p.use_count() = 0
local pointer in a thread:
lp.get() = 0xc99028, lp.use_count() = 3
local pointer in a thread:
lp.get() = 0xc99028, lp.use_count() = 4
local pointer in a thread:
lp.get() = 0xc99028, lp.use_count() = 2
Derived::~Derived()
Base::~Base()
All threads completed, the last one deleted Derived
weak_ptr
std::weak_ptr — это интеллектуальный указатель, который содержит не владеющую («слабую») ссылку на объект, управляемый std::shared_ptr. Должен быть преобразован в std::shared_ptr перед доступом к указанному объекту.
std::weak_ptr используется для выражения концепции временного владения: когда к объекту нужно обращаться только тогда, когда он существует, и он может быть удален другими в любое время, std::weak_ptr может использоваться для отслеживания объекта. Когда требуется временное владение, он преобразуется в std::shared_ptr.В это время, если исходный std::shared_ptr уничтожается, время жизни объекта будет продлено до тех пор, пока временный std::shared_ptr также не будет уничтожен.
Другое использование std::weak_ptr — разрыв циклических ссылок на объекты, управляемые std::shared_ptr. Если такое кольцо потеряно (например, нет указателя на внешний общий указатель в кольце), счетчик ссылок shared_ptr не может достичь нуля, и происходит утечка памяти. Этого можно избежать, сделав один из указателей в кольце слабым указателем.
Создайте
void main( )
{
shared_ptr<Test> sptr( new Test ); // 强引用 1
weak_ptr<Test> wptr( sptr ); // 强引用 1 弱引用 1
weak_ptr<Test> wptr1 = wptr; // 强引用 1 弱引用 2
}
Назначение одного weak_ptr другому weak_ptr увеличивает счетчик слабых ссылок. Итак, когда shared_ptr покидает область видимости, ресурсы в нем высвобождаются, что в это время произошло со weak_ptr, указывающим на shared_ptr? weak_ptr просрочен (истек). Как определить, указывает ли weak_ptr на действительный ресурс, есть два метода:
- Вызовите use-count(), чтобы получить счетчик ссылок, этот метод возвращает только счетчик сильных ссылок, а не счетчик слабых ссылок.
- Вызовите метод expired(). Быстрее, чем вызов метода use_count().
Вызовите lock() из weak_ptr, чтобы получить shared_ptr, или напрямую конвертируйте weak_ptr в shared_ptr.
Решить проблему циклической ссылки shared_ptr
class B;
class A
{
public:
A( ) : m_a(5) { };
~A( )
{
cout<<" A is destroyed"<<endl;
}
void PrintSpB( );
weak_ptr<B> m_sptrB;
int m_a;
};
class B
{
public:
B( ) : m_b(10) { };
~B( )
{
cout<<" B is destroyed"<<endl;
}
weak_ptr<A> m_sptrA;
int m_b;
};
void A::PrintSpB( )
{
if( !m_sptrB.expired() )
{
cout<< m_sptrB.lock( )->m_b<<endl;
}
}
void main( )
{
shared_ptr<B> sptrB( new B );
shared_ptr<A> sptrA( new A );
sptrB->m_sptrA = sptrA;
sptrA->m_sptrB = sptrB;
sptrA->PrintSpB( );
}
Ловушки интеллектуального указателя STL/недостаточно умный
- Попробуйте использовать make_shared/make_unique вместо нового
std::shared_ptr
В реализации используется технология refcount, поэтому будет счетчик (управляющий блок, используемый для управления данными) и указатель, указывающий на данные. Итак, выполнениеstd::shared_ptr<A> p2(new A)
Когда сначала применяется память данных, а затем применяется блок внутреннего управления, то есть два запроса памяти, иstd::make_shared<A>()
Он заключается в том, чтобы выполнить приложение памяти только один раз и объединить приложения блока данных и управления.
-
Не инициализируйте (или не сбрасывайте) несколько интеллектуальных указателей с помощью одного и того же встроенного указателя.
-
Не удаляйте указатель, возвращаемый функцией get().
-
Не инициализируйте/сбрасывайте другой интеллектуальный указатель с помощью get()
-
Для ресурсов, управляемых умными указателями, он удалит только память, выделенную new по умолчанию.Если она не выделена new, она должна быть передана удалению.
-
Не передавайте этот указатель интеллектуальному управлению указателями.
Что случилось со следующим кодом? Все та же ошибка. Собственный указатель this доставляется как m_sp, так и p для управления, что приведет к двойному удалению указателя this. Здесь стоит отметить, что упомянутое выше управление доставкой на m_sp и p некорректно, что не означает, что несколько shared_ptrs не могут одновременно занимать однотипные ресурсы. Совместное использование ресурсов между shared_ptr достигается путем копирования и назначения интеллектуального указателя shared_ptr, потому что это может привести к обновлению счетчика; и если он инициализируется непосредственно через собственный указатель, это приведет к тому, что m_sp и p не будут знать о существовании каждого из них. совсем другое, но оба управляют одним и тем же местом. Это эквивалентно «приглашению двух богов в храм».
class Test{ public: void Do(){ m_sp = shared_ptr<Test>(this); } private: shared_ptr<Test> m_sp; }; int main() { Test* t = new Test; shared_ptr<Test> p(t); p->Do(); return 0; }
-
Не управляйте собственным указателем на несколько shared_ptr или unique_ptr.
Мы знаем, что при использовании собственного указателя для инициализации интеллектуального указателя объект интеллектуального указателя рассматривает собственный указатель как ресурс, управляемый им самим. Другими словами, это означает: после инициализации нескольких интеллектуальных указателей эти интеллектуальные указатели отвечают за освобождение памяти. Затем это приведет к тому, что собственный указатель будет выпущен несколько раз! !
```C++
int* ptr = new int;
shared_ptr<int> p1(ptr);
shared_ptr<int> p2(ptr);
//p1,p2析构的时候都会释放ptr,同一内存被释放多次!
```
- Вместо того, чтобы использовать новое пространство, вам нужно настроить удаление
Следующий код пытается передать динамическую память, сгенерированную malloc, в shared_ptr для управления; очевидно, есть проблема, delete и malloc неверны! ! ! Итак, нам нужно передать пользовательский deleter[](int* p){ free(p); } в shared_ptr.
```C++
int main()
{
int* pi = (int*)malloc(4 * sizeof(int));
shared_ptr<int> sp(pi);
return 0;
}
```
- попробуй не использовать get()
Конструктор интеллектуальных указателей предоставляет интерфейс get(), так что интеллектуальные указатели также могут адаптироваться к связанным функциям, используемым собственными указателями. Этот дизайн можно назвать хорошим или неудачным. Поскольку по принципу замыкания инкапсуляции мы передаем нативный указатель в управление смарт-указателем, мы не должны и не можем получить нативный указатель, потому что единственным менеджером нативного указателя должен быть смарт-указатель. Вместо любого другого кода в области клиентской логики. Таким образом, мы должны быть особенно осторожны при использовании get(), и запрещено использовать собственный указатель, возвращаемый get(), для инициализации других интеллектуальных указателей или их освобождения. (можно только использовать, но не управлять). Следующий код нарушает это правило:
int main()
{
shared_ptr<int> sp(new int(4));
shared_ptr<int> pp(sp.get());
return 0;
}
Reference
- cppreference.com
- Умные указатели C++11 от Kabbalah's Tree
- Интеллектуальные указатели для стандартов C++11 и C++14
- Правило 21. Используйте std::make_unique и std::make_shared поверх new напрямую
- Управление ресурсами динамической памяти С++, на которое следует обратить внимание (5) - ловушка интеллектуального указателя