为什么引入智能指针?
首先看一个函数:
void func(int n)
{
int *p = new int[n];
return;
}
相信大家肯定都能一眼看出该函数的缺陷:在函数体内动态分配了堆上的内存,却没有在函数结束前释放,导致了内存泄露。这个问题的解决也很简单,只要添加一行代码即可:
void func(int n)
{
int *p = new int[n];
delete []p;
return;
}
但这样的做法需要程序员时刻保持警惕,有时候代码一多,稍不留神就会忘记释放内存,所以有没有什么更好的方案呢?智能指针就是为此而引入的。
智能指针的作用
C++中的智能指针一共有四种:auto_ptr,unique_ptr,shared_ptr,weak_ptr。使用它们需要包含memory头文件,它们都可以用来管理动态分配的内存,以auto_ptr为例:
//常规指针
int *p = new int;
//智能指针
auto_ptr<int> ap(new int);
从上面的用法中可以看出,智能指针实际上一种行为类似于指针的类模板。上面的代码中,我们用auto_ptr<int>这个类定义了一个类似指针的对象ap,并将new int作为构造函数的参数传入。当智能指针对象ap过期时,将自动调用析构函数并使用delete来释放内存,从而无需由程序员记住要在稍后释放这些内存。
了解了智能指针要解决的问题和基本的作用原理后,下面来具体看一下四种智能指针的区别:
auto_ptr
首先看如下几行简单的代码:
int *p = new int;
*p = 626;
int *q = p;
cout << *p << endl;
cout << *q << endl;
//输出如下:
//626
//626
可以看到正确输出,而如果换成用auto_ptr改写上述代码会如何呢?
auto_ptr<int> p(new int);
*p = 626;
auto_ptr<int> q = p;
cout << *p << endl;
cout << *q << endl;
结果是不能正确输出*p,会在输出*p时发生访问冲突。为什么会这样呢?智能指针怎么变成智障指针了?其实这里正是智能指针的特点所在,auto_ptr引入了一个所有权的概念,即对于用auto_ptr分配的一片动态内存,只能有一个智能指针拥有它。
我们试想一下如果不采用这种策略的话会怎样:假设同时有两个智能指针都指向同一片动态分配的内存,当它们都过期时,各自的析构函数都将调用delete删除内存,就会造成对同一片内存释放两次,这是不能接受的。
因此,通过引入所有权的概念,在对auto_ptr智能指针进行赋值操作时,所有权就会发生转移,也就避免了刚才两次释放内存的问题。
unique_ptr
上面介绍的auto_ptr虽然解决了两次释放内存的问题,但还是可能在访问被剥夺所有权的智能指针时,产生访问冲突的问题,而且这个问题只有在程序运行期间才能发现,所以还是有点不那么智能。于是,秉承着“早发现,早治疗”的精神,unique_ptr出现了。
unique_ptr和auto_ptr的不同之处或者说是优于auto_ptr的地方在于——一般不允许所有权的转让,即如下操作是不被允许的:
unique_ptr<int> p(new int);
unique_ptr<int> q = p;
上面的赋值操作将在编译阶段出现错误。但如果非要想把所有权转移怎么办,可以使用C++的一个标准库函数std::move()。
此外,unique_ptr还有另外一个优点(unique_ptr独有),就是允许使用new []来分配内存。
unique_ptr<int []> up(new int[10]);
shared_ptr
auto_ptr和unique_ptr各自都不同程度地解决了对同一片内存多次释放的问题,但是这种解决方案相比常规指针其实是增加了许多限制的,因为auto_ptr和unique_ptr不允许两个指针指向同一片内存,但常规指针是允许的。为了避免这种限制,又引入了shared_ptr。
相比前两者,shared_ptr更加智能,它采用记录指向同一片内存的的智能指针的个数的策略,即引用计数。例如,在shared_ptr之间赋值时,计数将加一,指针过期时,计数将减一,只有当计数为0时,才会调用delete。
虽然shared_ptr很智能,但也会存在一些更麻烦的错误——循环引用。看如下代码:
class Son;
class Father
{
public:
shared_ptr<Son> sp_son;
}
class Son
{
public:
shared_ptr<Father> sp_father;
}
int main()
{
shared_ptr<Father> sp_f(new Father);//Father对象引用计数加1
shared_ptr<Son> sp_s(new Son);//Son对象引用计数加1
sp_f->sp_son = sp_s;//Son对象引用计数加1
sp_s->sp_father = sp_f;//Father对象引用计数加1
}
//---程序结束后---
//sp_f过期,Father对象引用计数减一,为1,不调用析构函数
//sp_s过期,Son对象引用计数减一,为1,不调用析构函数
//Father对象和Son对象的内存仍然存在,出现内存泄露
从上述代码中可以看出,由于Father对象和Son对象相互引用,导致程序结束后,各自的引用计数也不为0,也就不会调用析构函数。
weak_ptr
weak_ptr可以用来解决上述循环引用的问题。weak_ptr不会增加所引用对象的引用计数,只要我们把上面的Father类和Son类中任意一个类的shared_ptr成员改成weak_ptr成员,就会使得,在程序结束时,某个对象的引用计数为0,这样析构函数就会被调用。
class Son;
class Father
{
public:
weak_ptr<Son> wp_son;//改成了weak_ptr!!!!!!
}
class Son
{
public:
shared_ptr<Father> sp_father;
}
int main()
{
shared_ptr<Father> sp_f(new Father);//Father对象引用计数加1
shared_ptr<Son> sp_s(new Son);//Son对象引用计数加1
sp_f->wp_son = sp_s;//不会增加Son对象引用计数!!!!!!
sp_s->sp_father = sp_f;//Father对象引用计数加1
}
//---程序结束后---
//sp_f过期,Father对象引用计数减一,为1,不调用析构函数
//sp_s过期,Son对象引用计数减一,为0,调用析构函数,所以Son对象的成员sp_father的析构函数也被调用(但此时Father对象的引用计数仍不为0)
//Father对象和Son对象的内存都被释放
weak_ptr虽然也是智能指针,但是它并没有重载解引用运算符*和指针运算符->,但它有两个很重要的函数:
1)lock函数
shared_ptr<int> sp(new int);
weak_ptr<int> wp = sp;
auto sp2 = wp.lock();
通过lock函数可以获得一个shared_ptr指针,这样即便没有解引用运算符*和指针运算符->,wp也能访问对象了。
2)empired函数
用来判断所引用的对象是否存在