由于C++不支持垃圾自动回收机制,程序员必须手动释放动态申请的空间,否则会发生内存泄漏,这无疑对编程提出了更高的要求。为了解决令人头疼的内存泄漏问题,STL引入了智能指针。
智能指针实际上是一个类模板,对普通指针进行了一层封装,模板参数是指针指向的类型,通过重载->和*两个操作符使智能指针的用法与普通指针相同。通过析构函数释放指针指向的空间,使得内存管理完全由智能指针自动完成,无需手动释放。
由于C++11抛弃了传统的智能指针auto_ptr,因此在新代码中最好不要使用auto_ptr。C++11常用的三个智能指针包括:unique_ptr独享指针、shared_ptr共享指针和weak_ptr若指针。
独享指针unique_ptr唯一拥有所指向对象的所有权,不支持拷贝和赋值操作,因此不能用unique_ptr对另一个智能指针初始化或赋值,而只能通过move函数将其所有权转移给其他智能指针,确保不和其他智能指针指向同一个对象。
unique<int> p1(new int(1));
unique<int> p2=p1; //错误,不能进行赋值操作
unique<int> p2=std::move(p1); //正确,可以通过move函数转移所有权
共享指针shared_ptr是最常见的智能指针,多个shared_ptr共享所指向对象的所有权,通过引用计数管理指向同一对象的智能指针个数,每增加一个智能指针指向对象时,引用计数加1,当指向同一对象的所有智能指针的生命周期都结束时,引用计数为0,此时释放对象的内存空间。
弱指针weak_ptr配合shared_ptr一起使用,weak_ptr可以与shared_ptr指向同一个对象,但是不改变引用计数的值。
shared_ptr<int> p1(new int(1)); //引用计数为1
shared_ptr<int> p2=p1; //引用计数为2
weak_ptr<int> p3=p2; //引用计数仍然为2
根据三种智能指针的性质不难看出,当对象无须共享所有权时,应该使用unique_ptr独享指针;当对象需要共享所有权时,应该使用shared_ptr指针;当需要与shared_ptr共享对象所有权而又不想改变引用计数时,应该使用weak_ptr弱指针。
案例分析
两个shared_ptr指针所指向对象的数据成员中,如果含有指向对方对象的shared_ptr指针则会产生环状引用。环状引用导致释放资源时发生死锁,引用计数不会降为0,造成对象空间无法释放。环状引用的定义比较晦涩,下面通过一个例子来解释:
class A
{
public:
shared_ptr<B> bptr;
~A()
{
cout<<"~A()"<<endl;
}
};
class B
{
public:
shared_ptr<A> aptr;
~B()
{
cout<<"~B()"<<endl;
}
};
int main()
{
shared_ptr<A> a(new A());
shared_ptr<B> b(new B());
a->bptr=b;
b->aptr=a;
return 0;
}
程序中通过new创建了一个类A的对象和一个类B的对象并通过shared_ptr指向它们,之后通过指向另一个对象的智能指针对自身的数据成员ptr进行赋值,因此两个对象的引用计数都为2。
当程序退出时,main函数中创建的智能指针由于生命周期结束,其所指向对象的引用计数减1,但是由于环状引用,对象内部的智能指针的生命周期都不会结束,两个对象的引用计数始终为1。
具体来讲,就是对象A中的智能指针bptr只有在对象A析构之后才会结束其生命周期,从而将对象B的引用计数降为0;而对象A并不会被销毁,因为对象A销毁的条件是指向A的智能指针的引用计数降为0,而对象B中指向A的智能指针只有在B对象析构后才会结束其生命周期,从而将对象A的引用计数降为0。
因此上述逻辑就成了一个死结,就好像两个人打架都揪着对方的头发,并且叫嚣着只要对方松手自己就松手,但是两个人谁也不愿意首先让步,结果这两个人始终保持着同样的姿势,除非某一方实在坚持不住放弃了。
程序中不存在某个对象坚持不住首先释放的情况,因此必须处理环状引用问题。解决环状引用问题的钥匙就是weak_ptr,因为weak_ptr不会增加对象的引用计数。
class A
{
public:
weak_ptr<B> bptr;
~A()
{
cout<<"~A()"<<endl;
}
};
class B
{
public:
weak_ptr<A> aptr;
~B()
{
cout<<"~B()"<<endl;
}
};
int main()
{
shared_ptr<A> a(new A());
shared_ptr<B> b(new B());
a->bptr=b;
b->aptr=a;
return 0;
}
将类中的智能指针类型由shared_ptr改为weak_ptr,在main函数中初始化两个智能指针时会将对象的引用计数加1,但是对类中的数据成员进行赋值时不会增加对象的引用计数。程序退出时,main函数中创建的两个智能指针生命周期结束,对象的引用计数由1减为0,对象空间释放。
unique_ptr和auto_ptr
首先,auto_ptr存在潜在的安全问题。auto_ptr允许赋值操作,只是赋值操作的含义是将指针指向对象的所有权转移给另一个auto_ptr指针,原指针在失去对象的所有权后成为空指针,如果后续程序错误的使用了这个空指针可能会发生潜在的问题,而unique_ptr从根本上禁止了赋值操作。
既然unique_ptr不支持赋值操作,那么如果函数的返回值是unique_ptr怎么办呢?是否能将函数返回值声明成unique_ptr呢?
实际上unique_ptr在赋值问题上做了折中:如果赋值给一个临时变量,则允许进行赋值操作。因为临时变量在赋值操作后会立即销毁,不会被使用,也就不会产生安全问题。
unique_ptr<int> p1(new int(1));
unique_ptr<int> p2=p1; //不允许
unique_ptr<int> p3=unique_ptr<int> (new int(1)); //允许
其次auto_ptr不能作为容器的元素。由于容器中的对象需要支持拷贝构造函数,拷贝构造函数的参数为const类型,值不能改变,而auto_ptr在赋值时肯定会修改参数值,因为auto_ptr需要将参数中的指针置空,避免两个auto_ptr指向同一个对象,而unique_ptr解决了这个问题。
vector<auto_ptr<int>> vs; //不允许
vector<unique_ptr<int>> vs; //允许
最后auto_ptr不适用于动态数组。由于动态数组使用delete[ ]释放数组中所有元素的空间,而auto_ptr在释放对象空间时默认使用delete操作符,只会释放动态数组首元素的空间,造成内存泄漏,而unique_ptr则会正确使用delete[ ]释放整个动态数组的空间。
auto_ptr<int> p1(new int[10]); //不允许
unique_ptr<int> p2(new int[10]); //允许