C++智能指针就是为应对内存泄露问题,帮助管理new出来的对象,管理内存,C++11 标准在充分借鉴和吸收了 boost 库中智能指针的设计思想,引入了三种类型的智能指针,即 std::unique_ptr、std::shared_ptr 和 std::weak_ptr。所有的智能指针类(包括 std::unique_ptr)均包含于头文件 中。
std::unique_ptr
std::unique_ptr表示对内存对象的唯一指针,禁止拷贝与赋值语义。std::unique_ptr对其持有的堆内存具有唯一拥有权,也就是说引用计数永远是 1, std::unique_ptr 对象销毁时会释放其持有的堆内存。可以使用以下方式初始化一个 std::unique_ptr 对象:
std::unique_ptr<int> sp1(new int(123)); // 初始化方式1
std::unique_ptr<int> sp2; // 初始化方式2
sp2.reset(new int(123));
std::unique_ptr<int> sp3 = std::make_unique<int>(123); // 初始化方式3,推荐使用,C++14才引入std::make_unique<>()
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(std::make_unique<int>(123));;
std::unique_ptr<int> sp2(sp1);//无法通过编译
std::unique_ptr<int> sp3;
sp3 = sp1; //无法通过编译
虽然禁止复制语义,但是可以通过一个函数返回一个 std::unique_ptr:
#include <memory>
std::unique_ptr<int> func(int val){
std::unique_ptr<int> up(new int(val));
return up;
}
int main(){
std::unique_ptr<int> sp1 = func(123); // 从 func 函数中得到一个 std::unique_ptr 对象,然后返回给 sp1
return 0;
}
std::unique_ptr 不能复制,但是可以通过移动构造std::move()转移内存对象
#include <memory>
int main(){
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);
return 0;
}
std::unique_ptr 不仅可以持有一个堆对象,也可以持有一组堆对象:
#include <iostream>
#include <memory>
int main(){
// 创建10个int类型的堆对象
std::unique_ptr<int[]> sp1(new int[10]); // 形式1
std::unique_ptr<int[]> sp2; // 形式2
sp2.reset(new int[10]);
std::unique_ptr<int[]> sp3(std::make_unique<int[]>(10)); // 形式3
for (int i = 0; i < 10; ++i){
sp1[i] = i;
sp2[i] = i;
sp3[i] = i;
}
for (int i = 0; i < 10; ++i){
std::cout << sp1[i] << ", " << sp2[i] << ", " << sp3[i] << std::endl;
}
return 0;
}
自定义释放内存
自定义智能指针对象持有的资源的释放函数,默认情况下,智能指针对象在析构时只会释放其持有的堆内存(调用 delete 或者 delete[]),但是假设这块堆内存代表的对象还对应一种需要回收的资源(如操作系统的套接字句柄、文件句柄等),可以通过自定义智能指针的资源释放函数。假设现在有一个 Socket 类,对应着操作系统的套接字句柄,在回收时需要关闭该对象,可以自定义智能指针对象的资源析构函数
#include <iostream>
#include <memory>
class Socket {
public:
Socket() {
}
~Socket() {
}
void close() {
}
};
int main() {
auto deletor = [](Socket *pSocket) {
pSocket->close(); //关闭句柄
// ...
delete pSocket;
};
std::unique_ptr<Socket, void (*)(Socket * pSocket)> spSocket(new Socket(), deletor); // 自定义传递销毁指针时需要执行的函数
return 0;
}
可以使用decltype(deletor)让编译器自己推导 deletor 的类型
std::unique_ptr<Socket, decltype(deletor)> spSocket(new Socket(), deletor);
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 基本相同。
// 初始化方式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);
和std::unique_ptr一样,应该优先使用 std::make_shared去初始化一个std::shared_ptr*对象。
#include <iostream>
#include <memory>
class A {
public:
A() {
std::cout << "A constructor" << std::endl;
}
~A() {
std::cout << "A destructor" << std::endl;
}
};
int main() {
{
// 初始化方式1
std::shared_ptr<A> sp1(new A());
std::cout << "use count: " << sp1.use_count() << std::endl;
// 初始化方式2
std::shared_ptr<A> 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<A> sp3 = sp1;
std::cout << "use count: " << sp1.use_count() << std::endl;
}
std::cout << "use count: " << sp1.use_count() << std::endl;
}
return 0;
}
/**
A constructor
use count: 1
use count: 2
use count: 1
use count: 2
use count: 1
A destructor
*/
- sp1 构造时,同时触发对象 A 的构造,因此 A 的构造函数会执行;此时只有一个 sp1 对象引用 A 对象
- 之后利用 sp1 拷贝一份 sp2,引用计数为2,调用 sp2 的 reset() 方法,sp2 释放对资源对象 A 的引用,引用计数值再次变为 1
- 利用 sp1 再次创建 sp3,引用计数变为 2,之后sp3 出了其作用域被析构,资源 A 的引用计数递减 1,引用计数为 1
- 最后sp1 出了其作用域被析构,在其析构时递减资源 A 的引用计数至 0,并析构资源 A 对象,因此类 A 的析构函数被调用。
std::enable_shared_from_this
实际开发中,有时候需要在类中返回包裹当前对象(this)的一个std::shared_ptr对象给外部使用,有如此需求的类只要继承自std::enable_shared_from_this模板对象即可
#include <iostream>
#include <memory>
class A : public std::enable_shared_from_this<A> {
public:
A() {
std::cout << "A constructor" << std::endl;
}
~A() {
std::cout << "A destructor" << std::endl;
}
std::shared_ptr<A> getSelf() {
return shared_from_this();
}
};
int main() {
std::shared_ptr<A> sp1(new A());
std::shared_ptr<A> sp2 = sp1->getSelf();
std::cout << "use count: " << sp1.use_count() << std::endl;
return 0;
}
类 A 的继承std::enable_shared_from_this 并提供一个getSelf()方法返回自身的 std::shared_ptr对象,在 getSelf()中调用shared_from_this()即可。std::enable_shared_from_this用起来比较方便,但是也存在很多不易察觉的陷阱。
不应该共享栈对象的 this 给智能指针对象
假设将上面代码 main 函数生成 A 对象的方式改成一个栈变量,即:
int main() {
A a;
std::shared_ptr<A> sp2 = a.getSelf();
std::cout << "use count: " << sp2.use_count() << std::endl;
return 0;
}
运行修改后的代码会发现程序在 std::shared_ptr sp2 = a.getSelf(); 产生崩溃。这是因为,智能指针管理的是堆对象,栈对象会在函数调用结束后自行销毁,因此不能通过shared_from_this()将该对象交由智能指针对象管理。切记:智能指针最初设计的目的就是为了管理堆对象的(即那些不会自动释放的资源) 。
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, 资源永远不会释放)。
#include <iostream>
#include <memory>
int main(){
//创建一个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;
return 0;
}
use count: 1
use count: 1
use count: 1
use count: 1
无论通过何种方式创建 std::weak_ptr 都不会增加资源的引用计数,因此每次输出引用计数的值都是 1。
std::weak_ptr 提供了一个 **expired()方法来检测对象是否被销毁了,返回 true,说明其引用的资源已经不存在了;返回 false,说明该资源仍然存在,这个时候可以使用 std::weak_ptr 的lock()**方法得到一个 **std::shared_ptr**对象然后继续操作资源:
//tmpConn_ 是一个 std::weak_ptr<TcpConnection> 对象
//tmpConn_引用的TcpConnection已经销毁,直接返回
if (tmpConn_.expired())
return;
std::shared_ptr<TcpConnection> conn = tmpConn_.lock();
if (conn){
//对conn进行操作,省略...
}
std::weak_ptr的正确使用场景是那些资源如果可能就使用,如果不可使用则不用的场景,它不参与资源的生命周期管理。例如,网络分层结构中,Session 对象(会话对象)利用 Connection 对象(连接对象)提供的服务工作,但是 Session 对象不管理 Connection 对象的生命周期,Session 管理 Connection 的生命周期是不合理的,因为网络底层出错会导致 Connection 对象被销毁,此时 Session 对象如果强行持有 Connection 对象与事实矛盾。
std::weak_ptr 的应用场景,经典的例子是订阅者模式或者观察者模式中。这里以订阅者为例来说明,消息发布器只有在某个订阅者存在的情况下才会向其发布消息,而不能管理订阅者的生命周期。
class Subscriber{
};
class SubscribeManager{
public:
void publish(){
for (const auto& iter : m_subscribers){
if (!iter.expired()){
//TODO:给订阅者发送消息
}
}
}
private:
std::vector<std::weak_ptr<Subscriber>> m_subscribers;
};