使用Deleaker检测c++内存泄漏

使用Deleaker检测c++内存泄漏

内存泄漏

1.内存泄漏的定义

内存泄漏(memory leak)是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

2.内存泄漏的后果

  • 程序运行后,随着时间占用了更多的内存,最后无内存可用而崩溃;
  • 只发生一次的小的内存泄漏可能不会被注意,但泄漏大量内存的程序或泄漏日益增多的程序可能会表现出程序性能降低,也会因为单个程序内存占用不断升高从而导致其他程序崩溃,而其他应用无法发现问题

3.内存泄漏的主要类型

  • 堆内存泄漏 (Heap leak),程序运行中根据需要分配通过malloc,realloc new等从堆中分配的一块内存,使用完后必须通过调用对应的 free或者delete 删掉。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak.
  • 系统资源泄露(Resource Leak).主要指程序使用系统分配的资源比如 Bitmap,handle ,SOCKET等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定。

其他资源对象的解释可以参考windows资源管理(内核对象/GDI对象/user对象)
如下图,使用deleaker可以检测到所有类型的内存泄漏
image.png

4.内存泄漏的检测方法

  • windows平台
  1. win平台使用工具软件delaker,deleaker使用插件方式集成到vs中,原理是通过比较不同时刻内存快照的方式来定位到内存泄漏点
  2. 使用crtdbg.h中的api,具体的使用方法可以访问使用CRT库查找内存泄漏
  • linux平台

valgrind 一个强大开源的程序检测工具

5.解决内存泄漏方法

最有效的办法就是使用智能指针(Smart Pointer)。因为智能指针可以自动删除分配的内存。智能指针和普通指针类似,只是不需要手动释放指针,而是通过智能指针自己管理内存的释放,这样就不用担心内存泄漏的问题了。
智能指针的原理主要可以从三个层面分析:

  • 从较浅的层面看,智能指针是利用了一种叫做RAII(资源获取即初始化)的技术对普通的指针进行封装,这使得智能指针实质是一个对象,行为表现的却像一个指针。
  • 智能指针的作用是防止忘记调用delete释放内存和程序异常的进入catch块忘记释放内存。另外指针的释放时机也是非常有考究的,多次释放同一个指针会造成程序崩溃,这些都可以通过智能指针来解决
  • 引用计数机制,对每一块 new 出来的资源都维护一个计数,保存所有指向它的指针数量,这个数就叫做引用计数。新增指针指向这块资源,引用计数自增;指针释放或者改去指向别的地方,引用计数自减。当引用计数减到零,就去释放资源

智能指针

先看一段简单代码

class TestClass
{
public:
	TestClass() {
		std::cout << "TestClass" << std::endl;
	};
	~TestClass() {
		std::cout << "~TestClass" << std::endl;
	};
};

void test_ptr() {
	auto* ptr = new TestClass[2];
    //delete[] ptr;
	return;
}

void test_smartptr() {
	auto ptr = std::make_unique<TestClass[]>(2);
	return;
}

int main()
{
	std::cout << "1******1" << std::endl;
	test_ptr();
	std::cout << "2******2" << std::endl;
	test_smartptr();
	std::cout << "3******3" << std::endl;
	return 0;
}

运行结果:
image.png
使用智能指针时,在离开函数作用域后,就算不调用delete[],内存也可以得到释放.

智能指针相关知识可以参考以下几篇文章加深理解

一般有四种智能指针:

指针类别支持备注
unique_ptrC++ 11拥有独有对象所有权语义的智能指针
shared_ptrC++ 11拥有共享对象所有权语义的智能指针
weak_ptrC++ 11到 std::shared_ptr 所管理对象的弱引用
auto_ptrC++ 17中移除拥有严格对象所有权语义的智能指针

因为 auto_ptr 已经在 C++ 17 中移除,对于面向未来的程序员来说,最好减少在代码中出现该使用的频次吧,这里我便不再研究该类型。又因为weak_ptrshared_ptr的弱引用,所以,主要的只能指针分为两个unique_ptrshared_ptr

std::unique_ptr 是通过指针占有并管理另一对象,并在 unique_ptr 离开作用域时释放该对象的智能指针。在下列两者之一发生时用关联的删除器释放对象:

  • 销毁了管理的 unique_ptr 对象
  • 通过 operator= 或 reset() 赋值另一指针给管理的 unique_ptr 对象。

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) 的要求。 因此不可以使用 = 操作和拷贝构造函数,仅能使用移动操作。

关于移动构造,移动赋值可以访问c++11 移动构造_移动赋值_拷贝构造_拷贝赋值调用时机探究

Demo
#include <cassert>
#include <cstdio>
#include <fstream>
#include <functional>
#include <iostream>
#include <memory>
#include <vector>

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主动分配内存并且保存引用计数(reference count),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[]来销毁这个数组。用户可以通过调用一个函数,例如一个lamda表达式,来指定一个通用的释放步骤。

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 是多个指向的指针,可能出现循环引用,导致超出了作用域后仍有内存未能释放。

#include <vector>
#include <iostream>
#include <memory>

using std::endl;
using std::cout;
using std::vector;
using std::shared_ptr;
using std::unique_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 test() {
	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
}
void main() {
	cout << "1******1\n";
	test();
	cout << "2******2\n";
}

可以看到test()函数结束后,内存并未释放.
image.png

demo
#include <chrono>
#include <iostream>
#include <memory>
#include <mutex>
#include <thread>


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会增加弱引用计数(weak reference count)。 所以,当shared_ptr离开作用域时,其内的资源释放了,这时候指向该shared_ptr的weak_ptr发生了什么?weak_ptr过期了(expired)。如何判断weak_ptr是否指向有效资源,有两种方法:

  • 调用use-count()去获取引用计数,该方法只返回强引用计数,并不返回弱引用计数。
  • 调用expired()方法。比调用use_count()方法速度更快。

从weak_ptr调用lock()可以得到shared_ptr或者直接将weak_ptr转型为shared_ptr

解决 shared_ptr 循环引用问题
#include <vector>
#include <iostream>
#include <memory>

using std::endl;
using std::cout;
using std::vector;
using std::shared_ptr;
using std::unique_ptr;
using std::weak_ptr;

class B;
class A {
public:
	A() : m_sptrB() { cout << " A is construte" << endl; };
	~A() { cout << " A is destroyed" << endl; }
	shared_ptr<B> m_sptrB;
};
class B {
public:
	B() : m_sptrA() { cout << " B is construte" << endl; };
	~B() { cout << " B is destroyed" << endl; }
	weak_ptr<A> m_sptrA;
};
//***********************************************************

void test() {
	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
}
void main() {
	cout << "1******1\n";
	test();
	cout << "2******2\n";
}

使用weak_ptr后可以看到内存准确的释放
image.png
image.png

STL 智能指针的陷阱/不够智能的地方

  1. 尽量用make_shared/make_unique,少用new

std::shared_ptr在实现的时候使用的refcount技术,因此内部会有一个计数器(控制块,用来管理数据)和一个指针,指向数据。因此在执行std::shared_ptr<A> p2(new A) 的时候,首先会申请数据的内存,然后申请内控制块,因此是两次内存申请,而std::make_shared<A>()则是只执行一次内存申请,将数据和控制块的申请放到一起。

  1. 不要使用相同的内置指针来初始化(或者reset)多个智能指针
  2. 不要delete get()返回的指针
  3. 不要用get()初始化/reset另一个智能指针
  4. 智能指针管理的资源它只会默认删除new分配的内存,如果不是new分配的则要传递给其一个删除器
  5. 不要把this指针交给智能指针管理

以下代码发生了什么事情呢?还是同样的错误。把原生指针 this 同时交付给了 m_sp 和 p 管理,这样会导致 this 指针被 delete 两次。 这里值得注意的是:以上所说的交付给m_sp 和 p 管理不对,并不是指不能多个shared_ptr同时占有同一类资源。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;
}
  1. 不要把一个原生指针给多个shared_ptr或者unique_ptr管理

我们知道,在使用原生指针对智能指针初始化的时候,智能指针对象都视原生指针为自己管理的资源。换句话意思就说:初始化多个智能指针之后,这些智能指针都担负起释放内存的作用。那么就会导致该原生指针会被释放多次!!

int* ptr = new int;
shared_ptr<int> p1(ptr);
shared_ptr<int> p2(ptr);
//p1,p2析构的时候都会释放ptr,同一内存被释放多次!
  1. 不是使用new出来的空间要自定义删除器

以下代码试图将malloc产生的动态内存交给shared_ptr管理;显然是有问题的,delete 和 malloc 牛头不对马嘴!!! 所以我们需要自定义删除器[](int* p){ free§; }传递给shared_ptr。

int main()
{
    int* pi = (int*)malloc(4 * sizeof(int));
    shared_ptr<int> sp(pi);
    return 0;
}
  1. 尽量不要使用 get()

智能指针设计者之处提供get()接口是为了使得智能指针也能够适配原生指针使用的相关函数。这个设计可以说是个好的设计,也可以说是个失败的设计。因为根据封装的封闭原则,我们将原生指针交付给智能指针管理,我们就不应该也不能得到原生指针了;因为原生指针唯一的管理者就应该是智能指针。而不是客户逻辑区的其他什么代码。 所以我们在使用get()的时候要额外小心,禁止使用get()返回的原生指针再去初始化其他智能指针或者释放。(只能够被使用,不能够被管理)。而下面这段代码就违反了这个规定:

int main()
{
    shared_ptr<int> sp(new int(4));
    shared_ptr<int> pp(sp.get());
    return 0;
}

Deleaker的使用

获取14天试用注册码

访问delaker官网点击FREE TRAIL,然后填写一个邮箱,就可以获取14天注册码

image.png
邮箱里面有详细的注册方法也可以使用临时邮箱获取,方便14天到期后可以再次申请

Deleaker检测内存泄漏演示

演示demo

#include <iostream>
#include <memory>

void test_ptr() {
	int* ptr = new int[10];
	return;
}

void test_smartptr() {
	auto ptr = std::make_unique<int[]>(10);
	return;
}

int main()
{
	std::cout << "1" << std::endl;
	test_ptr();
	test_smartptr();
	std::cout << "2" << std::endl;
	return 0;
}

image.png

启动调试,在第一个断点处点击Take Snapshot,照一张快照

image.png

然后运行到第二个断点处再照一张
image.png
可以看到检测到了在 test_ptr() 函数中, 检测到了内存泄漏.而使用unique_ptr的test_smartptr()函数中没有检测到内存泄漏.

网狐内存泄漏

上面的演示代码很简单,真实的项目情况复杂很多,

示例1:

GameRecordPacket结构体定义修改
image.png
vector容器中原本存储结构体,修改为存储智能指针
image.png

这里的 std::vector<GameRecordPacket> m_UserReplayRecord 修改为std::vector<shared_ptr<GameRecordPacket>> m_UserReplayRecord 
vector为什么不适合存储类对象可以访问STL容器里存放对象还是指针,主要是因为

  • vector在insert或者push_back操作时是拷贝的工作方式,所以你放进去再拿出来,访问的就不是原来的那个对象
  • 如果一个类的数据非常多,或者包含其他复杂自定义类型,那么此时类的拷贝构造的开销是非常大的
  • 声明了一个存放基类对象的容器,如果此时向容器中插入子类的对象,那么子类特有的那些内容就会被无情剥离
  • 容器存放普通指针,容易造成内存泄漏

所以综合来看,最好的方式就是在容器中存放shared_ptr指针.
image.png
右侧修改后的代码,在307行,执行clear()方法时,会自动调用析构函数,释放内存,不用显式释放.

示例2:

image.png
右侧修改后的代码,在5104行 m_ReplayRecordData = nullptr 会释放掉内存,并且把指针置为nulllptr
运行到5018行,离开作用域后,data智能指针临时变量销毁前也会释放char[]内存

内存分配器

tcmalloc

关于tcmalloc使用的好处和原理,可以参考:

mimalloc

和tcmalloc一样也属于内存分配器,可以当做内存池来使用,比tcmalloc开源时间要晚,具体的可以访问
microsoft/mimalloc

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值