文章目录
动态内存管理经常出现问题:一种是忘记释放内存,会造成内存泄漏;引用释放的内存,产生引用非法内存的指针。引入智能指针解决上面的问题,自动释放所指向的对象。shared_ptr 允许多个指针指向同一个对象,unique_ptr 则“独占”所指向的对象。weak_ptr 是一种弱引用,指向shared_ptr所管理的对象,这三种智能指针都定义在memory头文件中。
1 shared_ptr
1.1 shared_ptr 用法
- 使用 make_shared 函数,此函数在动态内存中分配一个对象并初始化它,返回指向此对象的 shared_ptr。
- 使用普通指针调用成员函数创建。
shared_ptr<int> p3 = make_shared<int>(42);
shared_ptr<int>pp(new int(10));
每个 shared_ptr 都有一个引用计数,我们拷贝一个 shared_ptr,计数加 1。当我们给 shared_ptr 赋予一个新值或是 shared_ptr 被销毁时,计数减 1,当计数器变为 0 时,自动释放所管理的对象。
- shared_ptr 和 unique_ptr 都支持的操作
- shared_ptr 独有的操作
- 定义和更改 shared_ptr 的其他方法
使用注意:
1、不要混合使用普通指针和智能指针
如果混合使用的话,智能指针自动释放之后,普通指针有时就会变成悬空指针,当将一个shared_ptr 绑定到一个普通指针时,应该使用 shared_ptr 进行操作,不应该再使用内置指针访问内存。
2、不能直接将指针赋值给 shared_ptr,他们一个是指针,一个是类对象
3、不要使用 get 初始化另一个智能指针或为智能指针赋值
shared_ptr<int> p(new int(42)); //引用计数为1
int *q = p.get(); //正确:但使用q时要注意,不要让它管理的指针被释放
{
shared_ptr(q); //未定义:两个独立的share_ptr指向相同的内存
} //程序块结束,q 被销毁,它指向的内存被释放
int foo = *p; //未定义,p指向的内存已经被释放了
p 和 q 指向相同的一块内存,由于是相互独立创建,各自的引用计数都是 1,当 q 所在的程序块结束时,q 被销毁,导致内存被释放,这时候 p 就变成一个空悬指针,使用将发生未定义的行为,当 p 被销毁时,这块空间会被二次 delete
1.2 实现原理
我们来看看是怎么实现的:
template<class T>
class SharedPtr //模拟实现shared_ptr
{
public:
SharedPtr(T* tmp = nullptr) : _ptr(tmp), count(new int(1)) { }
~SharedPtr()
{
if (--(*count) == 0)
{
delete _ptr;
delete count;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
SharedPtr(SharedPtr<T>& tmp) : _ptr(tmp._ptr), count(tmp.count)
{
(*count)++;
}
SharedPtr<T>& operator = (SharedPtr<T>& tmp)
{
if (_ptr != tmp._ptr) //排除自己给自己赋值的可能
{
if (--(*count) == 0) //先要判断原来的空间是否需要释放
{
delete _ptr;
delete count;
}
_ptr = tmp._ptr;
count = tmp.count;
(*count)++;
}
return *this; //考虑连等的可能
}
private:
T* _ptr;
int* count;
};
这里先要说说记录引用计数为什么是指针:为了使各个对象都使用同一个变量标记,所以不能直接用整形记录。有人说那行,用个静态的吧,那也不行,用静态的就真正是所有变量都公用一个变量了.比如指针 a,b指向内存块 A,指针 c,d,e 指向内存块 B,本来 a,b 的引用计数应该是 2,c,d,e 的引用计数应该是 3,要使用静态的就都变成 5 了。这里要注意,所以最好的办法就是使用指针。
1.3 shared_ptr 所导致的循环引用问题
循环引用时会造成内存泄漏,首先我们来了解一下什么是循环引用,这里用链表的例子来实现一下:
struct ListNode
{
int _data;
shared_ptr<ListNode> _prev;
shared_ptr<ListNode> _next;
ListNode(int x)
:_data(x)
, _prev(NULL)
,_next(NULL)
{}
~ListNode()
{
cout << "~ListNode" << endl;
}
};
int main()
{
shared_ptr<ListNode> cur(new ListNode(1));
shared_ptr<ListNode> next(new ListNode(2));
cur->_next = next;
next->_prev = cur;
cout << "cur" << " " << cur.use_count() << endl;
cout << "next" << " " << next.use_count() << endl;
return 0;
}
C++ 库为了解决这个问题,定义了 weak_ptr,专门用于辅助 shared_ptr 来解决引用计数的问题。当 shared_ptr 内部要监视其他的 shared_ptr 对象时,就采用 weak_ptr,不会使被监视的引用计数增加,且当被监视的对象析构后就自动失效。
2 unique_ptr
2.1 unique_ptr 的使用
某个时刻只能有一个 unique_ptr 指向一个给定对象,unique_ptr 不支持普通的拷贝或赋值操作。
- unique_ptr 的操作
虽然我们不能拷贝或者赋值 unique_ptr,但是可以通过调用 release 或 reset 将指针所有权从一个(非const)unique_ptr 转移给另一个 unique_ptr
//将所有权从p1转移给p2,release将p1置为空
unique_ptr<string> p2(p1.release());
//将所有权从p3转移到p2,
unique_ptr<string>p3(new string("Trex"));
p2.reset(p3.release());
2.2 实现原理
unique_ptr 原理:直接把拷贝构造/赋值函数定义为 private,且只声明不实现。类成员无法调用这两个函数。
template<class T>
class UniquePtr //模拟实现unique_ptr
{
public:
UniquePtr(T* tmp = nullptr) : _ptr(tmp) { }
~UniquePtr()
{
if (_ptr)
delete _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
UniquePtr(const UniquePtr<T>& tmp) = delete;
UniquePtr<T>& operator=(const UniquePtr<T>& tmp) = delete;
T* _ptr;
};
3 weak_ptr
weak_ptr 它指向 shared_ptr 管理的对象,weak_ptr 不改变引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放,即使有weak_ptr指向对象,对象还是会被释放。
- weak_ptr的操作
4 智能指针是线程安全的吗
- 对于 unique_ptr,由于只是在当前代码块范围内有效。所以不涉及线程安全的问题。
- 对于 shared_ptr,多个对象要同时共用一个引用计数变量,所以会存在线程安全的问题,但是标准库实现的时候考虑到了这一点,使用了基于原子操作(CAS)的方式来保证 shared_ptr 能够高效,原子的操作引用计数。
5 小结
1、有拷贝构造/赋值的情况,推荐使用 shared_ptr
2、不需要拷贝构造/赋值的时候,可以使用 unique_ptr
3、类内有访问其他 shared_ptr 对象时,使用 weak_ptr,可以不改变引用计数。