以下四个智能指针是我们常用的智能指针
1.auto_ptr 管理权转移,带有缺陷的设计。尽量不要使用它
2.unique_ptr 防拷贝,高效简洁。不需要拷贝/赋值,尽量使用它
3.shared_ptr 共享(引用计数)管理、支持赋值/拷贝。缺陷:循环使用(针对循环使用有weak_ptr).
4.weak_ptr辅助shared_ptr解决循环引用,不增加引用计数。
为什么要引进智能指针?
- 在C和C++中,通过访问指针对象存储的地址,可以实现对内存的直接操作。
- 我们都知道我们在动态开辟了一块空间以后,需要手动delete,否则会发生内存泄漏的问题
- 但在实际工程中,可能存在一些特殊的情况,很可能我们的程序没有执行到delete
- 比如我们在程序中访问了无效的内存地址,除以0,或者在delete之前程序就已经return了
- 前两种情况,程序会抛出异常,而第三种情况,程序过早地return导致我们没有释放资源
- 更甚者,开发人员很可能会忘记调用free或delete来释放对应的内存
- 虽然内存泄漏问题会在程序结束时解决,但是这对于一个服务器程序确是致命的(服务器程序不会经常重启)
- 因此引入了智能指针,自动调用delete释放资源,而不需要手动去调用,减轻了开发人员的
智能指针的原理(RAII,资源申请即初始化)
- 智能指针只是RAII中相当重要的一个例子,当然还有别的例子
- 智能指针确保在任何情况下,动态分配的内存都能得到正确的释放,从而将开发人员从这项任务中解放出来(我们不用手动地调用delete去释放智能指针)
- 这包括程序因为异常而中断,原本用于释放内存的代码被跳过的场景(既然我们不用手动释放了,自然也不用担心这个问题了)
- 用一个动态分配的对象的地址来初始化智能指针,在析构的时候释放内存,就确保了这一点
- 因为析构函数总是会被执行,这样所包含的内存也将总是会被释放
- 因此智能指针其实不是指针,而是一个类,只是这个类的构造函数的参数,是动态开辟出来空间的地址(用new的返回值的指针来初始化智能指针,达到资源申请即初始化的目的)
- 可以这么理解,智能指针 = 指针+RAII,RAII将指针封装成一个类,通过构造函数和析构函数管理指针,防止内存泄漏(因为一个类在出了类的作用域之后会自动调用类的析构函数)
auto_ptr(自动指针,特点:管理权转移)
class Test{
public:
Test(int a = 0)
: _a(a)
{}
~Test(){
cout << "Calling destructor" << endl;
}
public:
int _a;
};
int main() {
auto_ptr<Test> ptr(new Test(5));
cout << ptr->_a << endl;
return 0;
}
auto_ptr完成了智能指针的基本功能,在申请资源的时候,调用构造函数进行初始化,达到资源申请即初始化的目的,同时当main函数结束时,ptr出了这个作用域也会自动调用析构函数来释放资源。
但是同时,auto_ptr因为它本身的特点(这个指针通过在拷贝构造和赋值运算符重载时采用管理权转移实现),会出现很多问题。
void Fun(auto_ptr<Test> ptr1) {
cout << ptr1->_a << endl;
}
int main() {
auto_ptr<Test> ptr(new Test(5));
cout << ptr->_a << endl;
Fun(ptr);
cout << ptr->_a << endl;
return 0;
}
上面的程序会崩溃。本来ptr拥有一块自己的内存,当Fun函数调用后,ptr关联的内存的所有权转移给了Fun函数的参数ptr1,ptr1是ptr的一份拷贝(因为这里采用的是值传递),ptr1拥有的是原来ptr拥有的内存块。当Fun函数执行完以后,ptr1拥有的内存块也就因为智能指针的特性随之释放了。而ptr这个对象在函数传参时它的所有权就没有了,它变成了一个空对象,它内部的指针也变成了一个空指针,这个时候再尝试去调用ptr->_a,就是是解引用空指针,程序崩溃。
然而上述在拷贝之后使用原来指针的情况是经常发生的!因此auto_ptr非常不被推荐使用
下面是关于auto_ptr的模拟实现
template<class T>
class Auto_ptr{
public:
//构造函数
Auto_ptr(T* ptr)
:_ptr(ptr)
{}
//拷贝构造函数
Auto_ptr(Auto_ptr<T>& a)
:_ptr(a._ptr){
//因为要转移指针权限,原来的指针就没有权限了
//如果在这里不像函数体内部这样将a中的_ptr置为NULL,那么a中的_ptr就是指向了一块已经释放了的空间
//再调用a的析构,此时就会出现重复delete的问题
//可以尝试着把下面一行注释掉,这样程序就会崩溃了(如果进行了拷贝构造)
a._ptr = NULL;
}
//赋值运算符重载
Auto_ptr<T>& operator=(Auto_ptr<T>& a){
_ptr = a._ptr;
a._ptr = NULL;
return *this;
}
//析构函数
~Auto_ptr(){
std::cout << "In ~Auto_ptr()" << std::endl;
if(_ptr){
//这里一定要对_ptr判空,如果不为空才delete
delete _ptr;
_ptr = NULL;
}
}
//把这个_ptr设为public方便测试
T* _ptr;
};
因此,在这里总结一下auto_ptr的缺点,缺点太多啦,不推荐使用。
- 容易在拷贝之后对原来的auto_ptr进行 -> 或 * 操作
- auto_ptr不能指向一组对象,就是说它不能和操作符new[]一起使用。因为auto_ptr的析构函数是使用delete实现的而不是delete []。因此当我们创建一个数组对象时,就会发生错误。
- auto_ptr不能和标准容器(vector,list,map….)一起使用(标准容器在分配内存的时候,必须要能够拷贝构造容器的元素。而且拷贝构造的时候,不能修改原来元素的值(可以记得拷贝构造函数中里面的元素都是const T&)。而auto_ptr在拷贝构造的时候,一定会修改元素的值)
关于第三个缺点,可以看下面的代码
//注意下面的代码是在VS2017环境下测试的,可能因为编译器不同,下面的代码就直接编译不过
int main() {
vector< auto_ptr<int> > v(5);
int i = 0;
for (; i<5; i++)
{
v[i] = auto_ptr<int>(new int(i));
}
vector< auto_ptr<int> > v1(v);
return 0;
}
原来的v中的auto_ptr都变为空了,但这并不是我们的本意,我们不想改变原有容器。
unique_ptr(唯一指针,特点:防拷贝)
- 针对auto_ptr的缺陷(访问原指针可能导致程序崩溃),因此有了unique_ptr
- unique_ptr通过限制拷贝构造和赋值运算的方式规避 auto_ptr 的缺陷(只声明不定义,并且定义为private)
- unique_ptr遵循着独占语义。在任何时间点,资源只能唯一地被一个unique_ptr占有(通过禁止拷贝语义, 只有移动语义来实现)
- 应该优先使用unique_ptr,可以避免不经意的权限传递(防拷贝,编译时就不会通过)
- 记住unique_ptr不提供赋值语义(拷贝赋值和拷贝构造都不可以),只支持转移语义
创建
unique_ptr<int> uptr( new int );
unique_ptr相比于auto_ptr,它还可以创建数组对象的特殊方法,当指针离开作用域时,调用delete []来代替delete。当创建unique_ptr时,这一组对象被视作模板参数的部分。这样,程序员就不需要再提供一个指定的析构方法,如下:
unique_ptr<int[ ]> uptr( new int[5] );
可以通过下面的转移语义来转移所有权
unique_ptr<int> ptr(new int(5));
unique_ptr<int> ptr1 = move(ptr);//转移了以后ptr就是一个空对象,内部指针是空指针
接口
unique_ptr提供的接口和传统指针差不多,但是不支持指针运算。
unique_ptr提供一个release()的方法,释放所有权。release和reset的区别在于,release仅仅释放所有权但不释放资源,reset也释放资源。
shared_ptr(共享指针,特点:引用计数,支持赋值/拷贝)
- 这是使用频率最高的一个智能指针,在C++11标准被引入标准库
- shared_ptr基本上类似于unique_ptr,关键不同处在于shared_ptr可以进行拷贝和赋值操作。
- 它可以和其他shared_ ptr类型的智能指针共享所有权,shared_ptr记录了有多少个shared_ptr在引用同一个对象,当引用对象的最后一个智能指针被摧毁后,对象才会被释放。
- 这个计数被称为强引用,shared_ptr还有一个弱引用,在讲weak_ptr的时候会说到
- 因为所有权可以在shared_ ptr之间共享(采用引用计数共享),任何一个共享指针都可以被复制,因此就可以在标准容器中存储智能指针了(shared_ptr在拷贝构造和赋值操作的时候,只会引起公用的引用计数的+1,不存在拷贝构造和赋值操作的参数不能是const的问题)
- 与unique_ ptr类似的是,它的析构函数也是delete
创建
shared_ptr<int> ptr( new int );
使用make_shared宏来加速创建的过程。
shared_ptr<int> ptr = make_shared<int>(100);
两种创建方式的区别:默认的构造函数会申请两次内存,而make_shared只会申请一次内存。因为shared_ptr内部有一个引用计数以及存放数据的内存,等于说有两部分内存,默认构造函数为数据内存和引用计数每个分别申请一次内存,而make_shared将数据和引用计数的内存申请放到一起。
关于make_shared的缺点
make_shared 只分配一次内存, 这看起来很好. 减少了内存分配的开销. 问题来了, weak_ptr 会保持控制块(强引用, 以及弱引用的信息)的生命周期, 而因此连带着保持了对象分配的内存, 只有最后一个 weak_ptr 离开作用域时, 内存才会被释放. 原本强引用减为 0 时就可以释放的内存, 现在变为了强引用, 弱引用都减为 0 时才能释放, 意外的延迟了内存释放的时间. 这对于内存要求高的场景来说, 是一个需要注意的问题
析构
如果用户采用一个不一样的析构策略时,可以自由指定构造这个shared_ptr的策略
shared_ptr<Test> sptr1( new Test[5],
[ ](Test* p) { delete[ ] p; } );
上面的代码表示创建了一个数组对象,并采用了指定的delete []来析构
接口
- use_count() :可以得到shared_ptr的引用计数(强引用计数,strong_ref)
- get() :获取到对象的资源,注意这是获取到的是一个指向申请到的内存的指针
- reset() :释放关联内存块的所有权,如果是最后一个指向该资源的shared_ptr,就释放这块内存
- unique() :判断是否是唯一指向当前内存的shared_ptr
- operator bool : 判断当前的shared_ptr是否指向一个内存块,可以直接用于if表达式(if(shared_ptr))
下面是关于shared_ptr的模拟实现
template<class T>
class Shared_ptr{
public:
Shared_ptr(T* ptr)
:_ptr(ptr),
_ref(new int(1))
{}
Shared_ptr(const Shared_ptr<T>& a)
:_ptr(a._ptr),
_ref(a._ref)
{
std::cout << "拷贝前计数:" << *_ref << std::endl;
//让引用计数++
(*_ref)++;
std::cout << "拷贝后计数:" << *_ref << std::endl;
}
Shared_ptr<T>& operator=(const Shared_ptr<T>& a){
if(_ptr != a._ptr){
int *tmp = _ref;
std::cout << "赋值前原有对象计数:" << *_ref << std::endl;
if(--(*_ref) == 0){
//因为当前引用对象要去引用a对象的引用计数了
//如果当前引用对象只剩一个了,即这个即将要去引用a对象的对象
//那么当前引用对象的计数就为0,此时需要释放该对象
delete _ptr;
delete _ref;
}
std::cout << "赋值后原有对象计数:" << *tmp << std::endl;
std::cout << "赋值前被赋值对象计数:" << *a._ref << std::endl;
_ptr = a._ptr;
_ref = a._ref;
(*_ref)++;
std::cout << "赋值前被赋值对象计数:" << *_ref << std::endl;
}
}
~Shared_ptr(){
if(--(*_ref) == 0){
delete _ptr;
delete _ref;
_ptr = NULL;
_ref = NULL;
}
}
int* _ref;
T* _ptr;
};
但是shared_ptr也是会产生一些问题
int main() {
shared_ptr<int> sptr1(new int); //引用计数为1
shared_ptr<int> sptr2 = sptr1; //引用计数为2
shared_ptr<int> sptr3;
sptr3 = sptr1; //引用计数为3
//析构的时候依次析构sptr3,sptr2,sptr1,引用计数依次减到0,并释放资源
return 0;
}
上面的代码没有问题,下面的代码就有问题了
int main() {
int* p = new int;
shared_ptr<int> sptr1(p);
shared_ptr<int> sptr2(p);
return 0;
}
因为sptr2并不是拷贝sptr1或者通过sptr1赋值过来的,因此两个shared_ptr的引用计数其实都是1,在析构的时候,sptr2先析构,并释放了资源,但是sptr1也是由这个资源初始化的,并且它的引用计数并不是0,所以析构sptr1的时候,就会出现重复释放的问题,释放已经释放了的内存
避免上述问题的方法就是尽量不要从一个裸指针中创建shared_ptr。
关于shared_ptr的循环引用问题
当下面的代码运行时,会出现无限循环的问题
struct Node //链表节点的定义
{
int _data;
shared_ptr<Node> _prev;
shared_ptr<Node> _next;
Node(int data)
:_data(data),
_prev(NULL),
_next(NULL)
{}
};
void Test() {
shared_ptr<Node> p1(new Node(1));
cout << p1.use_count() << endl;
shared_ptr<Node> p2(new Node(2));
cout << p2.use_count() << endl;
//让p2是p1的next
p1->_next = p2;
cout << p2.use_count() << endl;
//让p1是p2的prev
p2->_prev = p1;
cout << p1.use_count() << endl;
}
int main() {
Test();
return 0;
}
在Test中,引用计数的变化情况如下
在p1,p2离开作用域时它们的引用计数还是1,也就是说,因为它们的引用计数是1,所以它们的资源其实都没有释放!!!
因此针对上述可能出现的循环引用的问题,引入了weak_ptr
weak_ptr(弱指针,特点:辅助shared_ptr使用)
- weak_ptr总是通过shared_ptr来初始化的
- weak_ptr 拥有共享语义和不包含语义。这意味着,weak_ptr可以共享shared_ptr持有的资源。所以可以从一个包含资源的shared_ptr创建weak_ptr。同时,weak_ptr不支持普通指针包含的*,->操作。它并不包含资源所以也不允许程序员操作资源
- 因此shared_ptr并不实际拥有对象的内存,实际拥有的是通过lock接口返回的shared_ptr对象
- weak_ptr不控制对象的生命期,但是它可以知道对象是否还活着。如果对象还活着(没有被释放),那么它可以返回一个有效的shared_ptr来使用,否则返回一个空的shared_ptr
创建
可以以shared_ptr作为参数构造weak_ptr。从shared_ptr创建一个weak_ptr增加了共享指针的弱引用计数(weak_ref,这个引用计数之前介绍过,是专门为了weak_ptr设计的)。但是当shared_ptr离开作用域时,这个计数(弱引用计数)不作为是否释放资源的依据。换句话说,就是除非强引用计数变为0,才会释放掉指针指向的资源
int main() {
shared_ptr<int> sptr(new int(5)); //强引用计数+1
weak_ptr<int> wptr(sptr); //强引用计数不变,弱引用计数+1
weak_ptr<int> wptr1 = wptr; //强引用计数不变,弱引用计数+1
return 0;
}
将一个weak_ptr赋给另一个weak_ptr会增加弱引用计数
所以,当shared_ptr离开作用域时,其内的资源释放了,这时候指向该shared_ptr的weak_ptr发生了什么?weak_ptr过期了(expired)
int main() {
shared_ptr<int> sptr(new int(5)); //强引用计数+1
weak_ptr<int> wptr(sptr); //强引用计数不变,弱引用计数+1
weak_ptr<int> wptr1 = wptr; //强引用计数不变,弱引用计数+1
sptr.reset(); //强引用计数变为0,弱引用计数不变
return 0;
}
调用sptr.reset()之后
如何判断weak_ptr是否指向有效资源,有两种方法:
- 调用use_count()去获取引用计数,该方法只返回强引用计数,并不返回弱引用计数。
- 调用expired()方法。比调用use_count()方法速度更直观,如果资源有效,返回true,无效则返回false
从weak_ptr调用lock()可以得到shared_ptr
int main() {
shared_ptr<int> sptr(new int(5));
weak_ptr<int> wptr(sptr);
weak_ptr<int> wptr1 = wptr;
shared_ptr<int> sptr2 = wptr.lock();
return 0;
}
从weak_ptr中获取shared_ptr增加强引用计数(上面的图中因为调用了lock强引用从1变成了2)。
所以对于上述的测试代码,只要将结构体中的shared_ptr改成weak_ptr就可以了
struct Node //链表节点的定义
{
int _data;
weak_ptr<Node> _prev;
weak_ptr<Node> _next;
Node(int data)
:_data(data)
{}
};
void Test() {
shared_ptr<Node> p1(new Node(1));
shared_ptr<Node> p2(new Node(2));
//让p2是p1的next
p1->_next = weak_ptr<Node>(p2);
//让p1是p2的prev
p2->_prev = weak_ptr<Node>(p1);
}
int main() {
Test();
return 0;
}
在Test中,引用计数的变化情况如下
因为p1和p2的强引用计数都是0了,所以它们的资源就被释放,很好地解决了循环引用问题。