c++内存泄漏原因及解决办法(智能指针)

时间:2023-03-08 20:59:03

内存泄漏

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

内存泄露的种类

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

堆内存泄漏

  1. 在类的构造函数和析构函数中没有匹配的调用new和delete函数
  2. 没有正确地清除嵌套的对象指针
  3. 在释放对象数组时在delete中没有使用方括号(delete []p)

    方括号是告诉编译器这个指针指向的是一个对象数组,同时也告诉编译器正确的对象地址值并调用对象的析构函数,如果没有方括号,那么这个指针就被默认为只指向一个对象,对象数组中的其他对象的析构函数就不会被调用,结果造成了内存泄露。如果在方括号中间放了一个比对象数组大小还大的数字,那么编译器就会调用无效对象(内存溢出)的析构函数,会造成堆的奔溃。如果方括号中间的数字值比对象数组的大小小的话,编译器就不能调用足够多个析构函数,结果会造成内存泄露。

    释放单个对象、单个基本数据类型的变量或者是基本数据类型的数组不需要大小参数,释放定义了析构函数的对象数组才需要大小参数

  4. 指向对象的指针数组不等同于对象数组

    对象数组是指:数组中存放的是对象,只需要delete []p,即可调用对象数组中的每个对象的析构函数释放空间

    指向对象的指针数组是指:数组中存放的是指向对象的指针,不仅要释放每个对象的空间,还要释放每个指针的空间,delete []p只是释放了每个指针,但是并没有释放对象的空间,正确的做法,是通过一个循环,将每个对象释放了,然后再把指针释放了。

  5. 缺少拷贝构造函数
  6. 缺少重载赋值运算符c++内存泄漏原因及解决办法(智能指针)
  7. 运算符重载

    a. 返回栈上对象的引用或者指针(也即返回局部对象的引用或者指针)。导致最后返回的是一个空引用或者空指针,因此变成野指针

    b. 返回内部静态对象的引用。

    c. 返回一个泄露内存的动态分配的对象。导致内存泄露,并且无法回收

    解决这一类问题的办法是重载运算符函数的返回值不是类型的引用,二应该是类型的返回值,即不是 int&而是int

  8. 没有将基类的析构函数定义为虚函数当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露

智能指针

  写一个new语句时,一般就会立即把delete语句直接也写了,但是我们不能避免程序还未执行到delete时就跳转了或者在函数中没有执行到最后的delete语句就返回了,如果我们不在每一个可能跳转或者返回的语句前释放资源,就会造成内存泄露。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域时,类会自动调用析构函数,析构函数会自动释放资源

auto_ptr(已弃用)

  1. get:Get pointer (public member function )
  2. operator*:Dereference object (public member function )
  3. operator->:Dereference object member (public member function )
  4. operator=:Release and copy auto_ptr (public member function )
  5. relase:Release pointer (public member function )
  6. reset:Deallocate object pointed and set new value (public member function )
  7. conversion operator:Conversion operators (public member function )

  智能指针可以像类的原始指针一样访问类的public成员,成员函数get()返回一个原始的指针,成员函数reset()重新绑定指向的对象,而原来的对象则会被释放。访问auto_ptr的成员函数时用的是“.”,访问指向对象的成员时用的是“->”。我们也可用声明一个空智能指针auto_ptr<Test>ptest();

  当我们对智能指针进行赋值时,如ptest2 = ptest,ptest2会接管ptest原来的内存管理权,ptest会变为空指针,如果ptest2原来不为空,则它会释放原来的资源,基于这个原因,应该避免把auto_ptr放到容器中,因为算法对容器操作时,很难避免STL内部对容器实现了赋值传递操作,这样会使容器中很多元素被置为NULL。判断一个智能指针是否为空不能使用if(ptest == NULL),应该使用if(ptest.get() == NULL)

  成员函数release,这个函数只是把智能指针赋值为空,但是它原来指向的内存并没有被释放,相当于它只是释放了对资源的所有权,从下面的代码执行结果可以看出,析构函数没有被调用。想要在中途释放资源,而不是等到智能指针被析构时才释放,我们可以使用ptest.reset()

  因此对于auto_ptr的控制权转移的实现没有核心元素的支持,但是还是实现了auto_ptr的移动语义,这样带来的一些问题是拷贝构造函数和复制操作重载函数不够完美,具体体现就是把auto_ptr作为函数参数,传进去的时候控制权转移,转移到函数参数,当函数返回的时候并没有一个控制权移交的过程,所以过了函数调用则原先的auto_ptr已经失效了

unique_ptr

http://www.cplusplus.com/reference/memory/unique_ptr/

  使用move()把unique_ptr传入函数,这样你就知道原先的unique_ptr已经失效了.移动语义本身就说明了这样的问题,比较坑爹的是标准描述是说对于move之后使用原来的内容是未定义行为,并非抛出异常,所以还是要靠人肉遵守游戏规则.再一个,auto_ptr不支持传入deleter,所以只能支持单对象(delete object),而unique_ptr对数组类型有偏特化重载,并且还做了相应的优化,比如用[]访问相应元素等.

  是个独享所有权的智能指针,它提供了严格意义上的所有权:

  1. 拥有它指向的对象
  2. 无法进行复制构造,无法进行复制赋值操作。即无法使两个unique_ptr指向同一个对象。但是可以进行移动构造和移动赋值操作
  3. 保存指向某个对象的指针,当它本身被删除释放的时候,会使用给定的删除器释放它指向的对象

  unique_ptr 可以实现如下功能:

  1. 为动态申请的内存提供异常安全
  2. 将动态申请的内存所有权传递给某函数
  3. 从某个函数返回动态申请内存的所有权
  4. 在容器中保存指针
  5. auto_ptr 应该具有的功能

  不能使用两个智能指针赋值操作,应该使用std::move; 而且它可以直接用if(ptest == NULL)来判断是否空指针;release、get、reset等用法也和auto_ptr一致,使用函数的返回值赋值时,可以直接使用=, 这里使用c++11 的移动语义特性。另外注意的是当把它当做参数传递给函数时(使用值传递,应用传递时不用这样),传实参时也要使用std::move,比如foo(std::move(ptest))。它还增加了一个成员函数swap用于交换两个智能指针的值

share_ptr

http://www.cplusplus.com/reference/memory/shared_ptr/

  资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。出了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。

weak_ptr

http://www.cplusplus.com/reference/memory/weak_ptr/?kw=weak_ptr

  weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

class B;
class A
{
public:
    shared_ptr<B> pb_;
    ~A()
    {
        cout<<"A delete\n";
    }
};
class B
{
public:
    shared_ptr<A> pa_;
    ~B()
    {
        cout<<"B delete\n";
    }
};

void fun()
{
    shared_ptr<B> pb(new B());
    shared_ptr<A> pa(new A());
    pb->pa_ = pa;
    pa->pb_ = pb;
    cout<<pb.use_count()<<endl;
    cout<<pa.use_count()<<endl;
}

int main()
{
    fun();
    ;
}

  可以看到fun函数中pa ,pb之间互相引用,两个资源的引用计数为2,当要跳出函数时,智能指针pa,pb析构时两个资源引用计数会减一,但是两者引用计数还是为1,导致跳出函数时资源没有被释放(A B的析构函数没有被调用),如果把其中一个改为weak_ptr就可以了,我们把类A里面的shared_ptr<B> pb_; 改为weak_ptr<B> pb_; 运行结果如下,这样的话,资源B的引用开始就只有1,当pb析构时,B的计数变为0,B得到释放,B释放的同时也会使A的计数减一,同时pa析构时使A的计数减一,那么A的计数为0,A得到释放。

  不能通过weak_ptr直接访问对象的方法,比如B对象中有一个方法print(),我们不能这样访问,pa->pb_->print(); 英文pb_是一个weak_ptr,应该先把它转化为shared_ptr,如:shared_ptr<B> p = pa->pb_.lock();    p->print();