一文彻底掌握智能指针
前言:
大家好,今天继续分享一篇基础的智能指针的使用,在分享这篇之前,大家可以看之前分享的两种智能指针:C++智能指针学习(一), 今天我们来分享剩下的两个智能指针:
std::share_ptr
std::weak_ptr
在分享之前,简单回忆一下智能指针的概念:
智能指针:它是存储指向动态分配(堆)对象指针的类,用于生存期控制,能够确保在离开指针所在作用域时,自动正确的销毁动态内存分配的对象,防止内存泄漏。
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 的示例:
int main()
{
//初始化方式1
std::shared_ptr sp1(new int(123));
//初始化方式2
std::shared_ptr sp2;
sp2.reset(new int(123));
//初始化方式3
std::shared_ptr sp3;
sp3 = std::make_shared(123);
return 0;
}
和 std::unique_ptr 一样,你应该优先使用 std::make_shared 去初始化一个 std::shared_ptr 对象。
再来看另外一段代码:
#include
#include
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;
}
A constructor
use count: 1
use count: 2
use count: 1
use count: 2
use count: 1
A destructor
上述代码 22 行 sp1 构造时,同时触发对象 A 的构造,因此 A 的构造函数会执行。
此时只有一个 sp1 对象引用 22 行 new 出来的 A 对象(为了叙述方便,下文统一称之为资源对象 A),因此代码 24 行打印出来的引用计数值为 1。
代码 27 行,利用 sp1 拷贝一份 sp2,导致代码 28 行打印出来的引用计数为 2。
代码 30 行调用 sp2 的 reset() 方法,sp2 释放对资源对象 A 的引用,因此代码 31 行打印的引用计数值再次变为 1。
代码 34 行 利用 sp1 再次 创建 sp3,因此代码 35 行打印的引用计数变为 2。
程序执行到 36 行以后,sp3 出了其作用域被析构,资源 A 的引用计数递减 1,因此 代码 38 行打印的引用计数为 1。
程序执行到 39 行以后,sp1 出了其作用域被析构,在其析构时递减资源 A 的引用计数至 0,并析构资源 A 对象,因此类 A 的析构函数被调用。
1、std::enable_shared_from_this
#include
#include
class A : public std::enable_shared_from_this
{
public:
A()
{
std::cout << "A constructor" << std::endl;
}
~A()
{
std::cout << "A destructor" << std::endl;
}
std::shared_ptr getSelf()
{
return shared_from_this();
}
};
int main()
{
std::shared_ptr sp1(new A());
std::shared_ptr sp2 = sp1->getSelf();
std::cout << "use count: " << sp1.use_count() << std::endl;
return 0;
}
std::enable_shared_from_this 用起来比较方便,但是也存在很多不易察觉的陷阱。
2、陷阱一:不应该共享栈对象的 this 给智能指针对象
假设我们将上面代码 main 函数 25 行生成 A 对象的方式改成一个栈变量,即:
//其他相同代码省略...
int main()
{
A a;
std::shared_ptrsp2 = a.getSelf();
std::cout << "use count: " << sp2.use_count() << std::endl;
return 0;
}
切记:智能指针最初设计的目的就是为了管理堆对象的(即那些不会自动释放的资源)。
3、陷阱二:避免 std::enable_shared_from_this 的循环引用问题
#include
#include
class A : public std::enable_shared_from_this
{
public:
A()
{
m_i = 9;
//注意:
//比较好的做法是在构造函数里面调用shared_from_this()给m_SelfPtr赋值
//但是很遗憾不能这么做,如果写在构造函数里面程序会直接崩溃
std::cout << "A constructor" << std::endl;
}
~A()
{
m_i = 0;
std::cout << "A destructor" << std::endl;
}
void func()
{
m_SelfPtr = shared_from_this();
}
public:
int m_i;
std::shared_ptr m_SelfPtr;
};
int main()
{
{
std::shared_ptr spa(new A());
spa->func();
}
return 0;
}
乍一看上面的代码好像看不出什么问题,让我们来实际运行一下看看输出结果:
A constructor
我们发现在程序的整个生命周期内,只有 A 类构造函数的调用输出,没有 A 类析构函数的调用输出,这意味着 new 出来的 A 对象产生了内存泄漏!
std::weak_ptr:
std::weak_ptr 是一个不控制资源生命周期的智能指针,是对对象的一种弱引用,只是提供了对其管理的资源的一个访问手段,引入它的目的为协助 std::shared_ptr 工作。
#include
#include
int main()
{
//创建一个std::shared_ptr对象
std::shared_ptr sp1(new int(123));
std::cout << "use count: " << sp1.use_count() << std::endl;
//通过构造函数得到一个std::weak_ptr对象
std::weak_ptr sp2(sp1);
std::cout << "use count: " << sp1.use_count() << std::endl;
//通过赋值运算符得到一个std::weak_ptr对象
std::weak_ptr sp3 = sp1;
std::cout << "use count: " << sp1.use_count() << std::endl;
//通过一个std::weak_ptr对象得到另外一个std::weak_ptr对象
std::weak_ptr 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。
// tmpConn_ 是一个 std::weak_ptr 对象
// tmpConn_ 引用的TcpConnection已经销毁,直接返回
if (tmpConn_.expired())
return;
std::shared_ptr conn = tmpConn_.lock();
if (conn)
{
//对conn进行操作,省略...
}
#include
class A
{
public:
void doSomething()
{
}
};
int main()
{
std::shared_ptrsp1(new A());
std::weak_ptr sp2(sp1);
//正确代码
if (sp1)
{
//正确代码
sp1->doSomething();
(*sp1).doSomething();
}
//正确代码
if (!sp1)
{
}
//错误代码,无法编译通过
//if (sp2)
//{
// //错误代码,无法编译通过
// sp2->doSomething();
// (*sp2).doSomething();
//}
//错误代码,无法编译通过
//if (!sp2)
//{
//}
return 0;
}
std::weak_ptr 的应用场景,经典的例子是订阅者模式或者观察者模式中。这里以订阅者为例来说明,消息发布器只有在某个订阅者存在的情况下才会向其发布消息,而不能管理订阅者的生命周期。
class Subscriber
{
};
class SubscribeManager
{
public:
void publish()
{
for (const auto &iter : m_subscribers)
{
if (!iter.expired())
{
//TODO:给订阅者发送消息
}
}
}
private:
std::vector> m_subscribers;
};
智能指针的大小:
#include
#include
#include
int main()
{
std::shared_ptr sp0;
std::shared_ptr sp1;
sp1.reset(new std::string());
std::unique_ptr sp2;
std::weak_ptr sp3;
std::cout << "sp0 size: " << sizeof(sp0) << std::endl;
std::cout << "sp1 size: " << sizeof(sp1) << std::endl;
std::cout << "sp2 size: " << sizeof(sp2) << std::endl;
std::cout << "sp3 size: " << sizeof(sp3) << std::endl;
return 0;
}
Visual Studio 2019 (32bit) 运行结果:
sp0 size:8
sp1 size:8
sp2 size:4
sp3 size:8
sp0 size:16
sp1 size:16
sp2 size:8
sp3 size:16
在 32 位机器上,std_unique_ptr 占 4 字节,std::shared_ptr 和 std::weak_ptr 占 8 字节。
在 64 位机器上,std_unique_ptr 占 8 字节,std::shared_ptr 和 std::weak_ptr 占 16 字节。
也就是说,std_unique_ptr 的大小总是和原始指针大小一样,std::shared_ptr 和 std::weak_ptr 大小是原始指针的一倍。
智能指针使用注意事项:
#include
class Subscriber
{
};
int main()
{
Subscriber *pSubscriber = new Subscriber();
std::unique_ptr spSubscriber(pSubscriber);
delete pSubscriber;
return 0;
}
这段代码利用创建了一个堆对象 Subscriber,然后利用智能指针 spSubscriber 去管理之,可以却私下利用原始指针销毁了该对象,这让智能指针对象 spSubscriber 情何以堪啊?
记住,一旦智能指针对象接管了你的资源,所有对资源的操作都应该通过智能指针对象进行,不建议再通过原始指针进行操作了。
当然,除了 std::weak_ptr 之外,std::unique_ptr 和 std::shared_ptr 都提供了获取原始指针的方法——get() 函数。
int main()
{
Subscriber *pSubscriber = new Subscriber();
std::unique_ptr spSubscriber(pSubscriber);
//pTheSameSubscriber和pSubscriber指向同一个对象
Subscriber *pTheSameSubscriber = spSubscriber.get();
return 0;
}
前面的例子,一定让你觉得非常容易知道一个智能指针的持有的资源是否还有效,但是还是建议在不同场景谨慎一点,有些场景是很容易造成误判。例如下面的代码:
#include
#include
class T
{
public:
void doSomething()
{
std::cout << "T do something..." << m_i << std::endl;
}
private:
int m_i;
};
int main()
{
std::shared_ptr sp1(new T());
const auto &sp2 = sp1;
sp1.reset();
//由于sp2已经不再持有对象的引用,程序会在这里出现意外的行为
sp2->doSomething();
return 0;
}
你一定仍然觉得这个例子也能很明显地看出问题,ok,让我们把这个例子放到实际开发中再来看一下:
//连接断开
void MonitorServer::OnClose(const std::shared_ptr &conn)
{
std::lock_guard guard(m_sessionMutex);
for (auto iter = m_sessions.begin(); iter != m_sessions.end(); ++iter)
{
//通过比对connection对象找到对应的session
if ((*iter)->GetConnectionPtr() == conn)
{
m_sessions.erase(iter);
//注意这里:程序在此处崩溃
LOGI("monitor client disconnected: %s", conn->peerAddress().toIpPort().c_str());
break;
}
}
}
该段程序会在代码 12 行处崩溃,崩溃原因是调用了 conn->peerAddress() 方法。为什么这个方法的调用可能会引起崩溃?现在可以一目了然地看出了吗?
我们知道,为了减小编译依赖加快编译速度和生成二进制文件的大小,C/C++ 项目中一般在 *.h 文件对于指针类型尽量使用前置声明,而不是直接包含对应类的头文件。例如:
//Test.h
//在这里使用A的前置声明,而不是直接包含A.h文件
class A;
class Test
{
public:
Test();
~Test();
private:
A *m_pA;
};
同样的道理,在头文件中当使用智能指针对象作为类成员变量时,也应该优先使用前置声明去引用智能指针对象的包裹类,而不是直接包含包含类的头文件。
//Test.h
#include
//智能指针包裹类A,这里优先使用A的前置声明,而不是直接包含A.h
class A;
class Test
{
public:
Test();
~Test();
private:
std::unique_ptrm_spA;
};
Modern C/C++ 已经变为 C/C++ 开发的趋势,希望能善用和熟练这些智能指针对象。
智能指针的简单实现:
/*
* 计数器
* Counter对象就是用来申请一块内存存储引用计数
* m_refCount是SharedPtr的引用计数
* m_weakCount是WeakPtr的引用计数
* 当m_weakCount为0时删除Counter对象
*/
template
class Counter
{
friend class SharedPtr;
friend class WeakPtr;
public:
Counter() : m_refCount(0), m_weakCount(0) {}
virtual ~Counter() {}
private:
Counter(const Counter &) = delete;
Counter &operator=(const Counter &) = delete;
private:
atomic_uint m_refCount; // #shared,原子操作
atomic_uint m_weakCount; // #weak,原子操作
};
/*
* SharedPtr的简单实现
*/
template
class SharedPtr
{
friend class WeakPtr;
public:
/*
* 构造函数,用原生指针构造
*/
SharedPtr(T *ptr) : m_ptr(ptr), m_cnt(new Counter)
{
if (ptr)
{
m_cnt->m_refCount = 1;
}
cout << "Ptr Construct S." << endl;
}
~SharedPtr()
{
release();
}
/*
* 拷贝构造函数,用另一个SharedPtr对象构造
*/
SharedPtr(const SharedPtr &s)
{
m_ptr = s.m_ptr;
s.m_cnt->m_refCount++;
m_cnt = s.m_cnt;
cout << "S Copy Construct S." << endl;
}
/*
* 拷贝构造函数,用另一个WeakPtr对象构造
* 为了WeakPtr对象调用自己的lock()方法将自己传进来构造一个SharedPtr返回
*/
SharedPtr(const WeakPtr &w)
{
m_ptr = w.m_ptr;
w.m_cnt->m_refCount++;
m_cnt = w.m_cnt;
cout << "W Copy Construct S." << endl;
}
/*
* 赋值构造函数,用另一个SharedPtr对象构造
*/
SharedPtr &operator=(const SharedPtr &s)
{
if (this != s)
{
this->release();
m_ptr = s.m_ptr;
s.m_cnt->m_refCount++;
m_cnt = s.m_cnt;
cout << "S Assign Construct S." << endl;
}
return *this;
}
T &operator*()
{
return *m_ptr;
}
T *operator->()
{
return m_ptr;
}
protected:
void release()
{
m_cnt->m_refCount--;
if (m_cnt->m_refCount < 1)
{
delete m_ptr;
m_ptr = nullptr;
cout << "SharedPtr Delete Ptr." << endl;
if (m_cnt->m_weakCount < 1)
{
delete m_cnt;
m_cnt = nullptr;
cout << "SharedPtr Delete Cnt." << endl;
}
cout << "SharedPtr Release." << endl;
}
}
private:
T *m_ptr;
Counter *m_cnt;
};
template
class WeakPtr
{
public:
/*
* 构造函数,用SharedPtr对象构造
*/
WeakPtr(SharedPtr &s) : m_ptr(s.m_ptr), m_cnt(s.m_cnt)
{
m_cnt->m_weakCount++;
cout << "S Construct W." << endl;
}
/*
* 构造函数,用WeakPtr对象构造
*/
WeakPtr(WeakPtr &w) : m_ptr(w.m_ptr), m_cnt(w.m_cnt)
{
m_cnt->m_weakCount++;
cout << "W Construct W." << endl;
}
~WeakPtr()
{
release();
}
/*
* 赋值构造函数,用另一个SharedPtr对象构造
*/
WeakPtr &operator=(SharedPtr &s)
{
release();
m_cnt = s.m_cnt;
m_cnt->m_weakCount++;
m_ptr = s.m_ptr;
cout << "S Assign Construct W." << endl;
return *this;
}
/*
* 赋值构造函数,用另一个WeakPtr对象构造
*/
WeakPtr &operator=(WeakPtr &w)
{
if (this != &w)
{
release();
m_cnt = w.m_cnt;
m_cnt->m_weakCount++;
m_ptr = w->m_ptr;
cout << "W Assign Construct W." << endl;
}
return *this;
}
/*
* WeakPtr通过lock函数获得SharedPtr
*/
SharedPtr &lock()
{
return SharedPtr(*this);
}
/*
* 检查SharedPtr是否已过期
*/
bool expired()
{
if (m_cnt)
{
if (m_cnt->m_refCount > 0)
return false;
}
return true;
}
private:
WeakPtr() = delete; WeakPtr禁止默认构造,只能从SharedPtr或者WeakPtr构造
T &operator*() = delete; //WeakPtr禁止*
T *operator->() = delete; //WeakPtr禁止->
private:
void release()
{
if (m_cnt)
{
m_cnt->m_weakCount--;
if (m_cnt->m_weakCount < 1 && m_cnt->m_refCount < 1)
{
delete m_cnt;
m_cnt = nullptr;
cout << "Delete Cnt." << endl;
}
cout << "WeakPtr Release." << endl;
}
}
private:
T *m_ptr;
Counter *m_cnt;
};
上面的实现可能不是非常严谨,仅实现了常用的的函数接口而已,但其主要的目的是为了更深刻的了解智能指针的原理,这样才能更有把握的使用智能指针,只有了解它的内部实现,对于使用中的一些坑才能有效避免。
文章相关参考:https://blog.csdn.net/code_peak/article/details/119722167
很多人搞不清 C++ 中的 delete 和 delete[ ] 的区别
Java、C++ 内存模型都不知道,还敢说自己是高级工程师?