1、std::auto_ptr
std::auto_ptr真正容易让人误用的地方是其不常用的复制语义,即当复制一个std::auto_ptr对象时(拷贝复制或operator =复制),原对象所持有的堆内存对象也会转移给复制出来的对象。示例代码如下:
- //测试拷贝构造函数
- std::auto_ptr<int> sp1(new int(8));
- std::auto_ptr<int> sp2(sp1);
- if (sp1.get() != NULL)
- {
- std::cout << " sp1 is not empty" << std::endl;
- }
- else {
- std::cout << " sp1 is empty" << std::endl;
- }
-
- if (sp2.get() != NULL)
- {
- std::cout << " sp2 is not empty" << std::endl;
- }
- else {
- std::cout << " sp2 is empty" << std::endl;
- }
-
- //测试赋值构造
- std::auto_ptr<int> sp3(new int(8));
- std::auto_ptr<int> sp4;
- sp4 = sp3;
- if (sp3.get() != NULL)
- {
- std::cout << " sp3 is not empty" << std::endl;
- }
- else {
- std::cout << " sp3 is empty" << std::endl;
- }
-
- if (sp4.get() != NULL)
- {
- std::cout << " sp4 is not empty" << std::endl;
- }
- else {
- std::cout << " sp4 is empty" << std::endl;
- }
上述代码中分别利用拷贝构造(sp1=>sp2)和赋值构造(sp3=>sp4)来创建新的std::auto_ptr对象,因此sp1 持有的堆对象被转移给sp2,sp3持有的堆对象被转移给sp4。因此输出为:
- sp1 is empty
- sp2 is not empty
- sp3 is empty
- sp4 is not empty
例如:
std::vector
当用算法对容器操作的时候(如最常见的容器元素遍历),很难避免不对容器中的元素实现赋值传递,这样便会使容器中多个元素被置为空指针,这不是我们希望看到的,可能会造成一些意想不到的错误。
2、std::unique_ptr
std::unique_ptr对其持有的堆内存具有唯一拥有权,也就是说引用计数永远是1,std::unique_ptr对象销毁时会释放其持有的堆内存。可以使用以下方式初始化一个std::unique_ptr对象:
- //1
- std::unique_ptr<int> sp1(new int(123));
- //2
- std::unique_ptr<int> sp2;
- sp2.reset(new int(123));
- //3
- std::unique_ptr<int> sp3 = std::make_unique<int>(123);
应该尽量使用初始化方式3的方式去创建一个std::unique_ptr而不是方式1和2,因为形式3更安全。
std::unique_ptr禁止复制语义,为了达到这个效果,std::unique_ptr类的拷贝构造函数和赋值运算符(operator =)被标记为 =delete。
- template <class T>
- class unique_ptr
- {
- //省略其他代码...
-
- //拷贝构造函数和赋值运算符被标记为delete
- unique_ptr(const unique_ptr&) = delete;
- unique_ptr& operator=(const unique_ptr&) = delete;
- };
因此,下列代码是无法通过编译的:
- std::unique_ptr<int> sp1(new int(123));
-
- //报错
- std::unique_ptr<int> sp2(sp1);
-
- std::unique_ptr<int> sp3;
- //报错
- sp3 = sp2;
禁止复制语义也存在特例,即可以通过一个函数返回一个std::unique_ptr。
既然std::unique_ptr不能复制,那么如何将一个std::unique_ptr对象持有的堆内存转移给另外一个呢?答案是使用移动构造,示例代码如下:
- std::unique_ptr<int> sp1(std::make_unique<int>(123));
-
- std::unique_ptr<int> sp2(std::move(sp1));
-
- std::unique_ptr<int> sp3;
- sp3 = std::move(sp2);
以上代码利用std::move将sp1持有的堆内存(值为123)转移给sp2,再把sp2转移给sp3。最后,sp1和sp2不再持有堆内存的引用,变成一个空的智能指针对象。并不是所有的对象的std::move操作都有意义,只有实现了移动构造函数(Move Constructor)或移动赋值运算符(operator =)的类才行,而std::unique_ptr正好实现了这二者。
3、std::shared_ptr
std::unique_ptr对其持有的资源具有独占性,而std::shared_ptr持有的资源可以在多个std::shared_ptr之间共享,每多一个std::shared_ptr对资源的引用,资源引用计数将增加1,每一个指向该资源的std::shared_ptr对象析构时,资源引用计数减1,最后一个std::shared_ptr对象析构时,发现资源计数为0,将释放其持有的资源。多个线程之间,递增和减少资源的引用计数是安全的。(注意:这不意味着多个线程同时操作std::shared_ptr引用的对象是安全的)。std::shared_ptr提供了一个use_count()方法来获取当前持有资源的引用计数。除了上面描述的,std::shared_ptr用法和std::unique_ptr基本相同。
初始化std::shared_ptr的示例:
- //初始化方式1
- std::shared_ptr<int> sp1(new int(123));
-
- //初始化方式2
- std::shared_ptr<int> sp2;
- sp2.reset(new int(123));
-
- //初始化方式3
- std::shared_ptr<int> sp3;
- sp3 = std::make_shared<int>(123);
- class A
- {
- public:
- A()
- {
- std::cout << "A constructor" << std::endl;
- }
-
- ~A()
- {
- std::cout << "A destructor" << std::endl;
- }
- };
- int main()
- {
- {
- //初始化方式1
- std::shared_ptr sp1(new A());
-
- std::cout << "use count: " << sp1.use_count() << std::endl;
-
- //初始化方式2
- std::shared_ptr sp2(sp1);
- std::cout << "use count: " << sp1.use_count() << std::endl;
-
- sp2.reset();
- std::cout << "use count: " << sp1.use_count() << std::endl;
-
- {
- std::shared_ptr sp3 = sp1;
- std::cout << "use count: " << sp1.use_count() << std::endl;
- }
-
- std::cout << "use count: " << sp1.use_count() << std::endl;
- }
- return 0;
- }
上述代码sp1构造时,同时触发对象A的构造,因此A的构造函数会执行;
此时只有一个sp1对象引用sp1 new出来的A对象(为了叙述方便,下文统一称之为资源对象A),因此count第一次打印出来的引用计数值为1;
利用sp1拷贝一份sp2,导致count第二次打印出来的引用计数为2;
调用sp2的reset()方法,sp2释放对资源对象A的引用,因此coun第三次打印的引用计数值再次变为1;
利用sp1再次创建sp3,因此count第四次打印的引用计数变为2;
sp3出了其作用域被析构,资源A的引用计数递减1,因此count第五次打印的引用计数为1;
sp1出了其作用域被析构,在其析构时递减资源A的引用计数至0,并析构资源A对象,因此类A的析构函数被调用。
输出结果:
- A constructor
- use count: 1
- use count: 2
- use count: 1
- use count: 2
- use count: 1
- A destructor
4、std::weak_ptr
std::weak_ptr是一个不控制资源生命周期的智能指针,是对对象的一种弱引用,只是提供了对其管理的资源的一个访问手段,引入它的目的为协助std::shared_ptr工作。
std::weak_ptr可以从一个std::shared_ptr或另一个std::weak_ptr对象构造,std::shared_ptr可以直接赋值给std::weak_ptr ,也可以通过std::weak_ptr的lock()函数来获得std::shared_ptr。它的构造和析构不会引起引用计数的增加或减少。std::weak_ptr可用来解决std::shared_ptr相互引用时的死锁问题,即两个std::shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0, 资源永远不会释放。
- //创建一个std::shared_ptr对象
- std::shared_ptr<int> sp1(new int(123));
- std::cout << "use count: " << sp1.use_count() << std::endl;
-
- //通过构造函数得到一个std::weak_ptr对象
- std::weak_ptr<int> sp2(sp1);
- std::cout << "use count: " << sp1.use_count() << std::endl;
-
- //通过赋值运算符得到一个std::weak_ptr对象
- std::weak_ptr<int> sp3 = sp1;
- std::cout << "use count: " << sp1.use_count() << std::endl;
-
- //通过一个std::weak_ptr对象得到另外一个std::weak_ptr对象
- std::weak_ptr<int> sp4 = sp2;
- std::cout << "use count: " << sp1.use_count() << std::endl;
- use count: 1
- use count: 1
- use count: 1
- use count: 1
无论通过何种方式创建std::weak_ptr都不会增加资源的引用计数,因此每次输出引用计数的值都是1。
既然,std::weak_ptr不管理对象的生命周期,那么其引用的对象可能在某个时刻被销毁了,如何得知呢?std::weak_ptr提供了一个expired()方法来做这一项检测,返回true,说明其引用的资源已经不存在了;返回false,说明该资源仍然存在,这个时候可以使用std::weak_ptr 的lock()方法得到一个std::shared_ptr对象然后继续操作资源。
std::weak_ptr类没有重写operator->和operator方法,因此不能像std::shared_ptr或std::unique_ptr一样直接操作对象,同时std::weak_ptr类也没有重写operator bool()操作,因此也不能通过std::weak_ptr对象直接判断其引用的资源是否存在。
之所以std::weak_ptr不增加引用资源的引用计数来管理资源的生命周期,是因为即使它实现了以上说的几个方法,调用它们仍然是不安全的,因为在调用期间,引用的资源可能恰好被销毁了,这样可能会造成比较棘手的错误和麻烦。
因此,std::weak_ptr的正确使用场景是那些资源如果可用就使用,如果不可用则不使用的场景,它不参与资源的生命周期管理。例如,网络分层结构中,Session对象(会话对象)利用Connection对象(连接对象)提供的服务来进行工作,但是Session对象不管理Connection对象的生命周期,Session管理Connection的生命周期是不合理的,因为网络底层出错会导致Connection对象被销毁,此时Session对象如果强行持有Connection对象则与事实矛盾。
std::weak_ptr的应用场景,经典的例子是订阅者模式或者观察者模式中。这里以订阅者为例来说明,消息发布器只有在某个订阅者存在的情况下才会向其发布消息,而不能管理订阅者的生命周期。