C++11 新特性之智能指针
参考:万字长文全面详解现代C++智能指针:原理、应用和陷阱_深入c++指针-CSDN博客
智能指针其实就是帮助我们进行内存管理(自动释放堆内存),避免造成内存泄漏的工具
1. 定义
-
原理:核心思想就是将要在堆内存中创建的对象放到栈中来进行管理,具体做法就是使用模板类封装指针,在类中分配内存,析构函数中释放内存,这个模板对象建立在栈中,当作用域结束自动调用该对象的析构函数。
unique
中没有拷贝构造和赋值构造,只有构造初始化shared
只有第一个使用构造初始化,其他的shared对这个对象使用拷贝构造和赋值构造实现多个指针指向同一块的地址和统计个数,每个对象析构的时候计数减1,直到为0才会释放那块内存weak
只能使用shared指针或者weak指针来拷贝构造或赋值构造(weakcount++),weak对象析构时将weakcount减1 -
智能指针可以代替使用
new
和delete
,当不想显式的管理内存时,不需要自己来调用delete
-
使用智能指针添加头文件
#include<memory>
2. unique_ptr
2.1 定义
- 也叫作用域指针,当超出作用域时就会销毁,并调用
delete
- 也叫唯一指针,意思是不可以复制,因为如果有两个指针指向同一片内存,那么有一个死了,另一个还在指向那块内存
- 不可以复制,所以也不能作为函数参数,和作为返回值,这些都将被复制副本
2.2 实现原理
原理:unique_ptr
的模板类中不能拷贝和赋值,对应的拷贝构造函数和赋值运算符函数已定义删除
// Disable copy from lvalue.
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
#include <iostream>
#include <utility> // for std::move
template<typename T>
class UniquePtr {
private:
T* ptr; // 原始指针
public:
// 构造函数
explicit UniquePtr(T* p = nullptr) : ptr(p) {}
// 禁止复制构造函数
UniquePtr(const UniquePtr&) = delete;
// 禁止复制赋值操作符
UniquePtr& operator=(const UniquePtr&) = delete;
// move移动构造函数
UniquePtr(UniquePtr&& other) noexcept : ptr(other.ptr) {
other.ptr = nullptr; // 将源指针置为空
}
// move移动赋值操作符
UniquePtr& operator=(UniquePtr&& other) noexcept {
if (this != &other) {
delete ptr; // 释放当前持有的资源
ptr = other.ptr; // 转移所有权
other.ptr = nullptr; // 将源指针置为空
}
return *this;
}
// 析构函数
~UniquePtr() {
delete ptr; // 释放资源
}
// 重载 * 操作符
T& operator*() const {
return *ptr;
}
// 重载 -> 操作符
T* operator->() const {
return ptr;
}
// 获取原始指针
T* get() const {
return ptr;
}
// 释放所有权并返回原始指针
T* release() {
T* temp = ptr;
ptr = nullptr;
return temp;
}
// 重新设置指针
void reset(T* p = nullptr) {
if (ptr != p) {
delete ptr; // 释放当前持有的资源
ptr = p; // 设置新的指针
}
}
};
2.3 使用方法
-
创建
unique_ptr
指针:因为没有了拷贝构造和赋值构造,所以只有使用c++14中的make_unique
和自身的构造函数初始化std::unqiue_ptr<Entity> entity = std::make_unique<Entity>(); //如果对象有带参构造函数,参数写括号里面 std::unqiue_ptr<Entity> entity(new Entity()); //使用模板类的构造函数构造,因为没有了赋值构造,所以不能用`=new entity()`
使用make_unique会更安全一点,如果构造失败将会返回异常信息,而使用自身构造函数,不会报错
-
释放所有权 :
sp.release()
,返回raw pointer
(裸指针,非智能指针)unique_ptr<int> p1 = make_unique<int>(1); int* a = p1.release(); std::cout<<*a<<std::endl; delete a; // you need to delete it manually
-
重置所有权:
sp.reset()
,指向空指针unique_ptr<int> p1 = make_unique<int>(1); p1.reset(); std::cout<<p1.get()<<std::endl; // 0
-
转移控制权:
std::move()
可以将一个unique_ptr
转移给另一个unique_ptr
或者shared_ptr
。转移后,原来的unique_ptr
将不再拥有对内存的控制权,将变为空指针。std::unique_ptr<int> p1 = std::make_unique<int>(0); std::unique_ptr<int> p2 = std::move(p1); // now, p1 is nullptr
-
std::shared_ptr
和std::unique_ptr
共有操作方法 用途 p.get() 返回p中保存的指针,不会影响p的引用计数。 p.reset() 释放p指向的对象,将p置为空。 p.reset(q) 释放p指向的对象,令p指向q。 p.reset(new T) 释放p指向的对象,令p指向一个新的对象。 p.swap(q) 交换p和q中的指针。 swap(p, q) 交换p和q中的指针。 p.operator*() 解引用p。 p.operator->() 成员访问运算符,等价于(*p).member。 p.operator bool() 检查p是否为空指针。 std::unique_ptr<int> p1 = std::make_unique<int>(42); std::unique_ptr<int> p2 = std::make_unique<int>(44); int* p = p1.get(); std::cout<<*p<<std::endl; // 42 p1.swap(p2); std::cout<<*p1<<std::endl; // 44 std::cout<<*p2<<std::endl; // 42 p1.reset(); std::cout<<p1.get()<<std::endl; // 0, first call get(), then call operator bool()
2.4 使用时机
std::unique_ptr<T>
比std::shared_ptr<T>
具有更小的内存,而且不需要维护引用计数,因此它的性能更好。当我们需要一个独占的指针时,应该优先使用std::unique_ptr<T>
。
3. shared_ptr
3.1 定义
std::shared_ptr<T>
是一个类模板,它的对象行为像指针,但是它还能记录有多少个对象共享它管理的内存对象。多个std::shared_ptr<T>
可以共享同一个对象。使用了引用计数,当最后一个std::shared_ptr<T>
被销毁时,它会自动释放它所指向的对象。
sp1初始化对象,为数据申请内存,sp2对sp1使用拷贝构造,它们就指向同一个内存,内存对象的引用计数为2。当sp1被销毁时,引用计数减为1,sp2仍然指向该对象。当sp2被销毁时,引用计数减为0,内存对象被销毁。
3.2 实现原理
-
shared_ptr
类模板中定义了两个指针,一个指针是所管理的数据的地址;还有一个指针是控制块的地址,包括引用引用计数(即shared_ptr计数)、weak_ptr计数、删除器(Deleter)、分配器(Allocator);因为不同shared_ptr
指针需要共享相同的内存对象,因此引用计数的存储是在堆上的。而unique_ptr
只有一个指针成员,指向所管理的数据的地址。因此一个shared_ptr
对象的大小是raw_pointer
大小的两倍。// 32位编译器下 std::cout<<sizeof(std::shared_ptr<int>)<<std::endl; // 8 std::cout<<sizeof(std::unique_ptr<int>)<<std::endl; // 4
-
代码实现:
这里引用计数只是简单地用了一个
int
类型的内存空间,省略了weak_ptr
的计数、删除器和分配器,不考虑多线程的情况template<typename T> class shared_ptr { private: T* m_ptr; // points to the actual data int* m_refCount; // reference count public: // 构造函数constructor shared_ptr(T* ptr = nullptr) : m_ptr(ptr), m_refCount(new int(1)) {} // 拷贝构造函数copy constructor shared_ptr(const shared_ptr& other) : m_ptr(other.m_ptr), m_refCount(other.m_refCount) { // increase the reference count (*m_refCount)++; } // 析构函数destructor ~shared_ptr() { // decrease the reference count (*m_refCount)--; // 只有引用计数到0才会释放成员内存 if the reference count is zero, delete the pointer if (*m_refCount == 0) { delete m_ptr; delete m_refCount; } } // 重载赋值函数 overload operator=() shared_ptr& operator=(const shared_ptr& other) { // check self-assignment if (this != &other) { // decrease the reference count for the old pointer (*m_refCount)--; // if the reference count is zero, delete the pointer if (*m_refCount == 0) { delete m_ptr; delete m_refCount; } // copy the data and reference pointer and increase the reference count m_ptr = other.m_ptr; m_refCount = other.m_refCount; // increase the reference count (*m_refCount)++; } return *this; } //其他的一些函数跟unique_ptr中类似 };
3.3 使用方法
-
构造
shared_ptr
的方式:使用make_shared
,不建议使用模板类中的构造函数new
一个对象std::shared_ptr<Entity> sharedEntity = std::make_shared<Entity>(); //若Entity有参构造,则把参数放入()中
不建议使用new来构造的原因是,
shared_ptr
会分配一块内存叫做控制块,用来存储引用计数,如果使用了new
,那么会先给new entity()
做内存分配,然后再给shard_ptr
的控制内存块分配而使用
make_shared
的好处是它只进行一次内存分配 -
std::shared_ptr<T>
的内置方法方法 用途 make_shared(args) 返回一个shared_ptr,指向一个动态分配的类型为T的对象,使用args初始化此对象。 shared_ptrp(q) p是q的拷贝,此操作递增q中的计数器。q中的指针必须能转换为T*。 shared_ptrp = q p是q的拷贝,此操作递增q中的计数器。q中的指针必须能转换为T*。 p.unique() 如果p.use_count()为1,返回true,否则返回false。 p.use_count() 返回与p共享对象的智能指针数量。 std::shared_ptr<int> sp1 = std::make_shared<int>(42); std::cout<<sp1.unique()<<std::endl; // 1 std::shared_ptr<int> sp2 = sp1; std::shared_ptr<int> sp3(sp1); std::shared_ptr<int> sp4(new int(44)); // Not recommended std::cout<<sp1.use_count()<<std::endl; // 3 sp1.reset(); std::cout<<sp1.use_count()<<std::endl; // 0 std::cout<<sp2.use_count()<<std::endl; // 2
2.4 使用时机
通常用于一些资源创建昂贵比较耗时的场景, 比如涉及到文件读写、网络连接、数据库连接等。当需要共享资源的所有权时,例如,一个资源需要被多个对象共享,但是不知道哪个对象会最后释放它,这时候就可以使用std::shared_ptr<T>
。
4. weak_ptr
4.1 定义
- 与
shared_ptr
联用,它是一种弱引用,指向shared_ptr所管理的对象,而不影响所指对象的生命周期,不增加引用计数 weak_ptr
只能用来查看对象,不能做对对象的修改操作- 一旦监视的对象被销毁,那么
weak_ptr
就会释放。
4.2 实现原理
- 不能带有原始指针的构造函数,只能使用
weak_ptr
和shared_ptr
进行构造
template<class T>
class WeakPtr
{
private:
void release()
{
cout << "into WeakPtr release" << endl;
if (cnt)
{
cnt->w--; //cnt->w管理块的weakcount cnt->s是管理块的sharedcount
if (cnt->w <1 && cnt->s <1)
{
cout << "weakptr release" << endl;
cnt = nullptr;
}
}
}
T* _ptr; //监听的shared的数据指针
Counter* cnt; //复制的shared的管理块指针
public://给出默认构造和拷贝构造,其中拷贝构造不能有从原始指针进行构造
WeakPtr()
{
_ptr = 0;
cnt = 0;
cout << "WeakPtr construct " << endl;
}
//使用sharedptr进行构造
WeakPtr(SharePtr<T>& s) :
_ptr(s._ptr), cnt(s.cnt)
{
cout << "w con s" << endl;
cnt->w++;
}
//使用weakptr拷贝构造
WeakPtr(WeakPtr<T>& w) :
_ptr(w._ptr), cnt(w.cnt)
{
cnt->w++;
}
//析构函数
~WeakPtr()
{
release();
}
//赋值函数重载
WeakPtr<T>& operator =(WeakPtr<T> & w)
{
if (this != &w)
{
release();
cnt = w.cnt;
cnt->w++;
_ptr = w._ptr;
}
return *this;
}
//赋值重载函数重载
WeakPtr<T>& operator =(SharePtr<T> & s)
{
cout << "w = s" << endl;
release();
cnt = s.cnt;
cnt->w++;
_ptr = s._ptr;
return *this;
}
//lock:返回shared_ptr来进行引用操作
SharePtr<T> lock()
{
return SharePtr<T>(*this);
}
//判断引用的那个shared_ptr是否为空,如果为空就需要释放掉这个weak指针
bool expired()
{
if (cnt)
{
if (cnt->s >0)
{
cout << "empty " << cnt->s << endl;
return false;
}
}
return true;
}
friend class SharePtr<T>;//方便weak_ptr与share_ptr设置引用计数和赋值。
};
4.3 使用方法
-
只能指向已创建的
shared_ptr
指针 -
使用
lock()
来读取引用对象;weak_ptr
对它所指向的shared_ptr
所管理的对象没有所有权,不能对它解引用,因此若要读取引用对象,必须要转换成shared_ptr
。 C++中提供了lock函数来实现该功能。如果对象存在,lock()
函数返回一个指向共享对象的shared_ptr
,否则返回一个空shared_ptr
。 -
使用
expired()
来判断所指对象是否已被释放,如果所指对象已经被释放,expired()返回true,否则返回false。std::shared_ptr<int> sp1(new int(22)); std::shared_ptr<int> sp2 = sp1; std::weak_ptr<int> wp = sp1; // point to sp1 std::cout<<wp.use_count()<<std::endl; // 2 if(!wp.expired()){ std::shared_ptr<int> sp3 = wp.lock(); std::cout<<*sp3<<std::endl; // 22 }
-
std::weak_ptr
也可以作为std::shared_ptr
的构造函数参数;如果std::weak_ptr
指向的对象已经被释放,那么std::shared_ptr
的构造函数会抛出std::bad_weak_ptr
异常std::shared_ptr<int> sp1(new int(22)); std::weak_ptr<int> wp = sp1; // point to sp1 std::shared_ptr<int> sp2(wp); std::cout<<sp2.use_count()<<std::endl; // 2 sp1.reset(); std::shared_ptr<int> sp3(wp); // throw std::bad_weak_ptr
-
std::weak_ptr
一些内置方法方法 用途 use_count() 返回与之共享对象的shared_ptr的数量 expired() 检查所指对象是否已经被释放 lock() 返回一个指向共享对象的shared_ptr,若对象不存在则返回空shared_ptr owner_before() 提供所有者基于的弱指针的排序 reset() 释放所指对象 swap() 交换两个weak_ptr对象
4.4 使用时机
-
用来缓存对象
-
避免循环引用问题
- 循环引用是指两个或多个对象之间通过
shared_ptr
相互引用,形成了一个环,导致它们的引用计数都不为0,从而导致内存泄漏。
- 循环引用是指两个或多个对象之间通过
class Subject {
private:
std::vector<std::shared_ptr<IObserver>> observers_;
}
class IObserver {
private:
std::shared_ptr<Subject> subject_;
//std::weak_ptr<Subject> subject_; //修改方法:其中一个改为weak_ptr
};
在观察者模式中使用shared_ptr可能会出现循环引用,在上面的程序中,IObserver对象和Subject对象相互引用,导致它们的引用计数都不为0,从而导致内存泄漏
-
解决办法:将Observer类中的subject_成员变量改为
weak_ptr
,这样就打破循环引用,不会导致内存无法正确释放了。 -
实现单例模式
5. 其他问题
5.1 尽量使用make_shared来创建shared_ptr指针
std::make_shared<T>
是更安全的做法。std::make_shared<T>
是一个函数模板,它在动态内存中分配一个对象并初始化它,返回指向此对象的std::shared_ptr<T>
。std::make_shared<T>
的好处是它只进行一次内存分配,而std::shared_ptr<T>(new T)
则进行两次内存分配,一次是为T分配内存,另一次是为std::shared_ptr<T>
的控制块分配内存。因此,std::make_shared<T>
是更好的选择。
std::shared_ptr<int> sp(new int(42)); // exception unsafe
//当new int(42)抛出异常时,sp将不会被创建,从而对应new分配的内存也不会释放,从而导致内存泄漏。
5.2 智能指针和裸指针的性能区别
shared_ptr
由于占据更多内存,且需要通过原子操作维护引用计数,因此效率是比较慢的。在不开启编译器优化的时候,是比new操作慢10倍,此时不应该使用make_shared、shared_ptr
。开启优化后,也大概慢2-3倍。unique_ptr、make_unique
、带少许偏差的make_shared
几乎和new、delete
具有一样的性能。unique_ptr
自动管理内存资源,而几乎没有额外开销。因此效率和new、delete
几乎一样。
5.3 shared_ptr的线程安全问题
如果多个线程同时拷贝同一个 shared_ptr
对象,不会有问题,因为 shared_ptr
的引用计数是线程安全的。但是如果多个线程同时修改同一个 shared_ptr
对象,不是线程安全的。因此,如果多个线程同时访问同一个 shared_ptr
对象,并且有写操作,需要使用互斥量来保护。
5.4 避免使用同一个裸指针初始化多个shared_ptr
多个shared_ptr
由同一个raw pointer
创建时会导致生成两个独立的引用计数控制块
int* p = new int(0);
std::shared_ptr<int> sp1(p);
std::shared_ptr<int> sp2(p);
std::cout<<sp1.use_count()<<std::endl; // 1
std::cout<<sp2.use_count()<<std::endl; // 1