智能指针概述
- 从较浅的层面看,智能指针是利用了一种叫做RAII(资源获取即初始化)的技术对普通的指针进行封装,这使得智能指针实质是一个对象,行为表现的却像一个指针。
- 智能指针的作用是防止忘记调用delete释放内存和程序异常地进入catch块忘记释放内存。另外指针的释放时机也是非常有考究的,多次释放同一个指针会造成程序崩溃,这些都可以通过智能指针来解决。
- 智能指针都需要引入< memory>头文件。
- 对智能指针来说,有两条原则:
- 智能指针本身是不能动态分配的,否则它自身有不被释放的风险,进而可能导致它所管理对象不能正确地被释放;
- 在栈上分配智能指针,让它指向堆上动态分配的对象,这样就能保证智能指针所管理的对象能够合理地被释放。
unique_ptr
- unique_ptr是独占式的,即完全拥有它所管理对象的所有权,不和其它的对象共享。
- unique_ptr实现的是专属所有权语义,用于独占它所指向的资源对象的场合。某个时刻只能有一个unique_ptr指向一个动态分配的资源对象,也就是这个资源不会被多个unique_ptr对象同时占有,它所管理的资源只能在unique_ptr对象之间进行移动,不能拷贝,所以它只提供了移动语义。
- 资源对象的生命周期被唯一的一个unique_ptr对象托管着,一旦这个unique_ptr对象被销毁或者变成空对象,或者拥有了另一个资源对象,它所管理的资源对象同时一并销毁。资源对象要么随同unique_ptr对象一起销毁,要么在离开unique_ptr对象的管理范围时被销毁,从而保证了内存不会泄露。 由于unique_ptr禁止了”Copy”语义,所以”res2 = res1;”不能编译通过。
- unique_ptr是以模板形式提供的,它有两种版本:
- 普通版本,即标量版本,用于管理一个动态分配的资源对象;
- 数组版本,是一个偏特化版本,用于管理一个动态分配的数组。
下面是一个unique_ptr的例子,此处的res是在栈上的局部变量,在main()结束时会被销毁,它管理的资源也会被释放掉。
#include <iostream>
#include <memory> // 使用智能指针unique_ptr需引用本头文件
using namespace std;
struct Resource
{
Resource() { cout << "构造Resource\n"; }
~Resource() { cout << "析构Resource\n"; }
};
int main()
{
// 分配一个Resource类对象res,由unique_ptr所有。
unique_ptr<Resource> res{ new Resource() };
return 0;
} // Resource在这里被析构
shared_ptr——可以理解为一个“类模板”
定义
- new出来的内存空间一定要在每条执行路径上delete掉。由于这么做太过复杂,于是引入shared_ptr的概念。
- shared_ptr采用的是引用计数原理来实现多个shared_ptr对象之间共享资源:
shared_ptr在内部会维护着一份引用计数,用来记录该份资源被几个对象共享(资源对象中,有多少个指针指向该资源对象)。
当一个shared_ptr对象被销毁时(调用析构函数),析构函数内就会将该计数减1。如果引用计数减为0后,则表示自己是最后一个使用该资源的shared_ptr对象,必须释放资源。
如果引用计数不是0,就说明自己还有其他对象在使用,则不能释放该资源,否则其他对象就成为野指针。
- 通过shared_ptr的构造函数,可以让shared_ptr的对象托管一个new运算符返回的指针: shared_ptr
ptr(new T); 其中T泛指各种类型,如int, char等。此后,new出来的存储空间就交给ptr托管了,ptr就可以像 int *类型的指针一样使用。*ptr 就是用 new 动态分配的那个对象,而且不必操心释放内存的事情,非常省事! - shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。(因为_ptrcount指向的对象是在堆上,因此所有的线程都能够访问到该资源; 多线程在修改_ptrcount时,则会出现线程安全问题,因此需要在修改_prtcount时需要用锁来保证其数据的正确性)
- shared_ptr对象不能托管指向动态分配的数组的指针,否则会报错。
shared_ptr应用实例:
#include <memory>
#include <iostream>
class A {
public:
int n;
A(int a = 0):n(a) {}
~A() {
cout << n << "析构函数" << endl;
}
};
int main() {
shared_ptr<A> x1(new A(2)); //x1托管A(2)
shared_ptr<A> x2(x1); //x2是由拷贝构造函数初始化的,它也托管A(2)
cout << x1->n << " " << x2->n << endl; //shared_ptr虽然是个对象,但也可以当指针用。这里相当于->重载。本语句输出2 2
shared_ptr<A> x3;
A* p = x1.get(); //p指向A(2)
cout << p->n << endl; //同样输出2
x3 = x1; //x3也托管A(2)
cout << (*x3).n << endl; //输出2 注意本次输出的格式!
x1.reset(); //x1放弃托管A(2)
if(!x1) {
cout << "x1 is null" << endl; //会正常输出本句
}
A* q = new A(3);
x1.reset(q); //x1托管q
cout << x1->n << endl; //输出3
shared_ptr<A> x4(x1); //x4托管A(3)
shared_ptr<A> x5;
//x5.reset(q); 不可以这么写,会报错
x1.reset(); //x1放弃托管A(3)
cout << "---" << endl;
x4.reset(); //x4放弃托管A(3)
return 0; //程序结束,会自动delete掉A(2)
}
weak_ptr
解决shared_ptr的循环引用问题。weak_ptr类的对象它可以指向shared_ptr,并且不会改变shared_ptr的引用计数。一旦最后一个shared_ptr被销毁时,对象就会被释放。
struct ListNode {
weak_ptr<ListNode> _pre;
weak_ptr<ListNode> _next;
};
int main() {
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
node1->_next = node2;
node2->_pre = node1;
cout << "node1的引用计数为" << node1.use_count() << endl;
cout << "node2的引用计数为" << node2.use_count() << endl;
return 0;
}
输出结果为: node1的引用计数为1 node2的引用计数为1
从上述实例中可以看到,weak_ptr对象指向shared_ptr对象时,不会增加shared_ptr中的引用计数,因此当node1销毁掉时,则node1指向的空间就会被销毁掉,node2类似,所以weak_ptr指针可以很好解决循环引用的问题。
在定义双向链表或者在二叉树等有多个指针的时候,如果想要将该类型定义成智能指针,那么结构体内的指针需要定义成weak_ptr类型的指针,防止循环引用的出现。