C++11 -- 智能指针
目录
前言
一、什么是智能指针?
二、C++11中的智能指针
2.1、unique_ptr
2.2.1、循环引用问题
2.3、weak_ptr
2.4、其他问题
2.5、总结
前言
什么是内存泄漏?
内存泄漏是指申请了空间却没能正确的释放,导致程序在运行过程中失去了对该内存的控制,造成的资源浪费。比如:new了,没delete;malloc了,没free。
而智能指针解决了手动内存管理的痛点。
一、什么是智能指针?
智能指针的概念和用法很简单,就是把申请资源和释放资源的工作交给类去完成,从而达到在作用域自动调用构造申请资源,出作用域自动调用析构释放资源,也叫RAII机制。目的是为了防止内存泄漏。不需要手动的调delete
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内 存、文件句柄、网络连接、互斥量等等)的简单技术。
如下:
template<class T>
class SmartPtr
{
public:SmartPtr(T* ptr = nullptr):_ptr(ptr){}~SmartPtr(){if (_ptr){cout << "释放资源" << endl;delete _ptr;}}private:T* _ptr;
};int main()
{SmartPtr<int> ptr(new int); // 出作用域自动调用析构,释放资源return 0;
}
那么,类中只要再重载一些指针操作(*, ->),这个类实例的对象也就能像指针一样操作了。
template<class T>
class SmartPtr
{
public:// RAII// 资源交给对象管理,对象生命周期内,资源有效,对象生命周期到了,释放资源SmartPtr(T* ptr = nullptr):_ptr(ptr){}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}~SmartPtr(){if (_ptr){cout << "delete " << _ptr << endl;delete _ptr;}}private:T* _ptr;
};int main()
{SmartPtr<int> p1(new int(4));SmartPtr<int> p2(new int(3));cout << *p1 << endl; // 输出 4cout << *p2 << endl; // 输出 3return 0;
}
输出结果:
但是对于上面的类,还有问题:
- 拷贝问题:对于编译器默认生成的赋值重载函数,完成的是浅拷贝,会有重复释放和内存泄漏问题
来看看C++11库中的智能指针是如何解决的
二、C++11中的智能指针
2.1、unique_ptr
C++11中的unique_ptr解决拷贝的方案是,直接禁掉了拷贝构造和赋值运算符重载,简单除暴
没有赋值和拷贝的场景还是可以用这个的
2.2、shared_ptr
shared_ptr的解决方案是,用引用计数来记录当前资源被多少对象访问。实际就是增加了个类似 int* count 的成员变量,每增加一个对象对资源的管理,则count++,一个对象失去对该资源的管理则count--。
- 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
- 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
比如:
int main()
{lsg::shared_ptr<int> p1(new int(4));lsg::shared_ptr<int> p2(p1);lsg::shared_ptr<int> p3(new int(6));return 0;
}
注意:引用计数的类型不能是static int和int,因为不同的shared_ptr需要管理不同对象的引用计数,每个对象独立计数
底层类似下面,重点看拷贝构造和赋值运算符重载:
namespace lsg
{template<class T>class shared_ptr{public:// RAII// 资源交给对象管理,对象生命周期内,资源有效,对象生命周期到了,释放资源shared_ptr(T* ptr = nullptr):_ptr(ptr), _count(new int(1)){}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr), _count(sp._count){++(*_count);}// 注意自己给自己赋值的情况shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (_ptr == sp._ptr)return *this;if (--(*count) == 0){delete _ptr;delete _count;}_ptr = sp._ptr;_count = sp._count;++(*_count);return *this;}~shared_ptr(){if (--(*_count) == 0){cout << "delete " << _ptr << endl;delete _ptr;delete _count;}}private:T* _ptr;int* _count;};
}
但上面代码并不是完全正确的,当多个线程同时对一个资源的对象赋值或拷贝构造,会有线程安全问题,因为(*count)++,(*_count)-- 并不是原子的。
2.2.1、循环引用问题
shared_ptr在绝大部分场景下是没问题的,但是下面这种情况会有问题
struct Node
{int val = 0;shared_ptr<Node> _next = nullptr; // 理解成Node* _nextshared_ptr<Node> _prev = nullptr; // 理解成Node* _prev
};int main()
{shared_ptr<Node> sp1(new Node);shared_ptr<Node> sp2(new Node);// 循环引用问题sp1->_next = sp2;sp2->_prev = sp1;return 0;
}
刚开始,sp1和sp2初始化时,引用计数都为1。但是,当sp1->_next 指向 sp2时,sp2的引用计数会加1,因为对同个资源多了个访问对象。同理,sp1的引用计数也会加1。那么sp1和sp2出作用域时,自动调用析构,析构让引用计数减减为一,此时不会释放sp1和sp2。导致内存泄漏。这就是循环引用问题。
注意:循环引用问题,编译器时不会报错的
如何解决呢??? ---> weak_ptr
2.3、weak_ptr
weak_ptr是专门用来解决shared_ptr循环引用问题的,本身并没有RAII机制。不是用来单独使用的
解决的原理是,提供了个以shared_ptr为参数的拷贝构造和赋值,不增加引用计数。可以访问资源,但不参与资源的管理
struct Node
{int val = 0;weak_ptr<Node> _next; // 理解成Node* _nextweak_ptr<Node> _prev; // 理解成Node* _prev
};int main()
{shared_ptr<Node> sp1(new Node);shared_ptr<Node> sp2(new Node);sp1->_next = sp2;sp2->_prev = sp1;// 打印引用计数cout << sp1.use_count() << endl; // 输出1cout << sp2.use_count() << endl; // 输出1return 0;
}
底层类似:
template<class T>
class weak_ptr
{
public:weak_ptr():_ptr(nullptr){}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}weak_ptr(const shared_ptr<T>& sp):_ptr(sp.get()){}weak_ptr<T>& operator=(const shared_ptr<T>& sp){_ptr = sp.get();return *this;}private:T* _ptr;
};
2.4、其他问题
标准智能指针默认使用 delete/delete[] 释放内存,但许多资源并非通过new分配
- C库分配的内存(malloc, calloc)
- 操作系统资源(文件句柄,套接字)
- 第三方库分配的资源
class A
{
public:A(int a = 0):_a(a){cout << "A(int a = 0)" << endl;}~A(){cout << this;cout << " ~A()" << endl;}int _a;
};template<class T>
struct Del
{void operator()(const T* ptr){delete[] ptr;}
};int main()
{// 会报错,因为delete释放lsg::shared_ptr<A> sp1(new A[5], Del<A>());lsg::shared_ptr<A> sp2((A*)malloc(sizeof(A)));lsg::shared_ptr<FILE> sp3(fopen("test.cpp", "r"));return 0;
}
所以,官方提供了第二个模板参数,叫定制删除器,申请的自定义类型的资源时,由我们提供的方法来释放资源
智能指针的定制删除器,允许我们自定义当智能指针管理对象生命周期结束时的清理行为。就是指定自定义类型的释放方式。
实现大概如下:
namespace lsg
{template<class T>class shared_ptr{public:// RAII// 资源交给对象管理,对象生命周期内,资源有效,对象生命周期到了,释放资源shared_ptr(T* ptr = nullptr):_ptr(ptr), _count(new int(1)){}template<class D>shared_ptr(T* ptr, D del):_ptr(ptr), _count(new int(1)), _del(del){}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr), _count(sp._count),_del(sp._del){++(*_count);}shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (_ptr == sp._ptr)return *this;if (--(*_count) == 0){delete _ptr;delete _count;}_ptr = sp._ptr;_count = sp._count;_del = sp._del;++(*_count);return *this;}~shared_ptr(){if (--(*_count) == 0){//delete _ptr;_del(_ptr); // 定制删除器delete _count;}}T& operator*() { return *_ptr;}T* operator->() { return _ptr;}int use_count() const { return *_count;}T* get() const { return _ptr;}private:T* _ptr;int* _count;function<void(T*)> _del = [](T* ptr) {delete ptr;};};
}
2.5、总结
unique_ptr:禁止拷贝,日常使用,不需要拷贝的场景
shared_ptr:用引用计数来支持拷贝,需要拷贝的场景使用,但要小心循环引用导致的内存泄漏
weak_ptr:专门用来解决shared_ptr循环引用的问题,但不支持RAII,可以访问资源,但不能单独 管理资源