当前位置:网站首页>智能指针-使用、避坑和实现
智能指针-使用、避坑和实现
2022-08-02 13:39:00 【程序员编程指南】
大家好,今天借助本文,从实践
、避坑
和实现原理
三个角度分析下C++中的智能指针。
本文主要内容如下图所示:
智能指针的由来
auto_ptr为什么被废弃
unique_ptr的使用、特点以及实现
shared_ptr的使用、特点以及实现
weak_ptr的使用、特点以及实现
介绍笔者在工作中遇到的一些职能指针相关的坑,并给出一些建议
背景
内存的分配与回收都是由开发人员在编写代码时主动完成的,好处是内存管理的开销较小,程序拥有更高的执行效率;弊端是依赖于开发者的水平,随着代码规模的扩大,极容易遗漏释放内存的步骤,或者一些不规范的编程可能会使程序具有安全隐患。如果对内存管理不当,可能导致程序中存在内存缺陷,甚至会在运行时产生内存故障错误。换句话说,开发者自己管理内存,最容易发生下面两种情况:
申请了内存却没有释放,造成内存泄漏
使用已经释放的内存,造成
segment fault
所以,为了在保证性能的前提下,又能使得开发者不需要关心内存的释放,进而使得开发者能够将更多的精力投入到业务上,自C++11开始,STL正式引入了智能指针。
所有权
智能指针一个很关键的一个点就是是否拥有一个对象的所有权
,当我们通过std::make_xxx或者new一个对象,那么就拥有了这个对象的所有权。
所有权分为独占所有权
、共享所有权
以及弱共享所有权
三种。
独占所有权
顾名思义,独占该对象。独占的意思就是不共享
,所有权可以转移,但是转移之后,所有权也是独占。auto_ptr和unique_ptr
就是一种独占所有权方式的智能指针。
假设有个Object对象,如果A拥有该对象的话,就需要保证其在不使用该对象的时候,将该对象释放;而此时如果B也想拥有Object对象,那么就必须先让A放弃该对象所有权,然后B独享该对象,那么该对象的使用和释放就只归B所有,跟A没有关系了。
独占所有权具有以下几个特点:
如果创建或者复制了某个对象,就拥有了该对象
如果没有创建对象,而是将对象保留使用,同样拥有该对象的所有权
如果你拥有了某个对象的所有权,在不需要某一个对象时,需要释放它们
共享所有权
共享所有权,与独占所有权正好相反,对某个对象的所有权可以共享。shared_ptr
就是一种共享所有权方式的智能指针。
假设此时A拥有对象Object,在没有其它拥有该对对象的情况下,对象的释放由A来负责;如果此时B也想拥有该对象,那么对象的释放由最后一个拥有它的来负责。
举一个我们经常遇到的例子,socket连接,多个发送端(sender)可以使用其发送和接收数据。
弱共享所有权
弱共享所有权,指的是可以使用该对象,但是没有所有权,由真正拥有其所有权的来负责释放。weak_ptr
就是一种弱共享所有权方式的智能指针。
分类
在C++11中,有unique_ptr
、shared_ptr
以及weak_ptr
三种,auto_ptr因为自身转移所有权
的原因,在C++11中被废弃
(本节最后,将简单说下被废弃的原因)。
unique_ptr
使用上限制最多的一种智能指针,被用来取代之前的auto_ptr,一个对象只能被一个unique_ptr所拥有,而不能被共享,如果需要将其所拥有的对象转移给其他unique_ptr,则需要使用move语义
shared_ptr
与unique_ptr不同的是,unique_ptr是
独占管理权
,而shared_ptr则是共享管理权
,即多个shared_ptr可以共用同一块关联对象,其内部采用的是引用计数,在拷贝的时候,引用计数+1,而在某个对象退出作用域或者释放的时候,引用计数-1,当引用计数为0的时候,会自动释放其管理的对象。
weak_ptr
weak_ptr的出现,主要是为了解决shared_ptr的
循环引用
,其主要是与shared_ptr一起来使用。和shared_ptr不同的地方在于,其并不会拥有资源,也就是说不能访问对象所提供的成员函数,不过,可以通过weak_ptr.lock()来产生一个拥有访问权限的shared_ptr。
auto_ptr
auto_ptr自C++98被引入,因为其存在较多问题,所以在c++11中被废弃,自C++17开始正式从STL中移除。
首先我们看下auto_ptr的简单实现(为了方便阅读,进行了修改,基本功能类似于std::auto_ptr):
template<class T>
class auto_ptr
{
T* p;
public:
auto_ptr(T* s) :p(s) {}
~auto_ptr() { delete p; }
auto_ptr(auto_ptr& a) {
p = a.p;
a.p = NULL;
}
auto_ptr& operator=(auto_ptr& a) {
delete p;
p=a.p;
a.p = NULL;
return *this;
}
T& operator*() const { return *p; }
T* operator->() const { return p; }
};
从上面代码可以看出,auto_ptr采用copy
语义来转移所有权,转移之后,其关联的资源指针设置为NULL,而这跟我们理解上copy行为不一致。
在<< Effective STL >>第8条,作者提出永不建立auto_ptr的容器
,并以一个例子来说明原因,感兴趣的可以去看看这本书,还是不错的。
实际上,auto_ptr被废弃的直接原因是拷贝造成所有权转移
,如下代码:
auto_ptr<ClassA> a(new ClassA);
auto_ptr<ClassA> b = a;
a->Method();
在上述代码中,因为b = a
导致所有权被转移,即a关联的对象为NULL,如果再调用a的成员函数,显然会造成coredump。
正是因为拷贝导致所有权被转移
,所以auto_ptr使用上有很多限制:
不能在STL容器中使用,因为复制将导致数据无效
一些STL算法也可能导致auto_ptr失效,比如std::sort算法
不能作为函数参数,因为这会导致复制,并且在调用后,导致原数据无效
如果作为类的成员变量,需要注意在类拷贝时候导致的数据无效
正是因为auto_ptr的诸多限制,所以自C++11起,废弃了auto_ptr,引入unique_ptr。
unique_ptr
unique_ptr是C++11提供的用于防止内存泄漏的智能指针中的一种实现(用来替代auto_ptr),独享被管理对象指针所有权的智能指针。
unique_ptr对象包装一个原始指针,并负责其生命周期。当该对象被销毁时,会在其析构函数中删除关联的原始指针。具有->和*运算符重载符,因此它可以像普通指针一样使用。
分类
unique_ptr分为以下两种:
指向单个对象
std::unique_ptr<Type> p1; // p1关联Type对象
指向一个数组
unique_ptr<Type[]> p2; // p2关联Type对象数组
特点
在前面的内容中,我们已经提到了unique_ptr的特点,主要具有以下:
独享所有权,在作用域结束时候,自动释放所关联的对象
void fun() {
unique_ptr<int> a(new int(1));
}
无法进行拷贝与赋值操作
unique_ptr<int> ptr(new int(1));
unique_ptr<int> ptr1(ptr) ; // error
unique_ptr<int> ptr2 = ptr; //error
显示的所有权转移(通过move语义)
unique_ptr<int> ptr(new int(1));
unique_ptr<int> ptr1 = std::move(ptr) ; // ok
作为容器元素存储在容器中
unique_ptr<int> ptr(new int(1));
std::vector<unique_ptr<int>> v;
v.push_back(ptr); // error
v.push_back(std::move(ptr)); // ok
std::cout << *ptr << std::endl;// error
需要注意的是,自c++14起,可以使用下面的方式对unique_ptr进行初始化:
auto p1 = std::make_unique<double>(3.14);
auto p2 = std::make_unique<double[]>(n);
如果在c++11中使用上述方法进行初始化,会得到下面的错误提示:
error: ‘make_unique’ is not a member of ‘std’
因此,如果为了使得c++11也可以使用std::make_unique,我们可以自己进行封装,如下:
namespace details {
#if __cplusplus >= 201402L // C++14及以后使用STL实现的
using std::make_unique;
#else
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args &&... args)
{
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
#endif
} // namespace details
使用
为了尽可能了解unique_ptr的使用姿势,我们使用以下代码为例:
#include <memory>
#include <utility> // std::move
void fun1(double *);
void fun2(std::unique<double> *);
void fun3(std::unique<double> &);
void fun4(std::unique<double> );
int main() {
std::unique_ptr<double> p(new double(3.14));
fun1(p.get());
fun2(&p);
fun3(p);
if (p) {
std::cout << "is valid" << std::endl;
}
auto p2(p.release()); // 转移所有权
auto p2.reset(new double(1.0));
fun4(std::move(p2));
return 0;
}
上述代码,基本覆盖了常见的unique_ptr用法:
第10行,通过new
创建
一个unique_ptr对象第11行,通过get()函数获取其关联的
原生指针
第12行,通过unique_ptr对象的
指针
进行访问第13行,通过unique_ptr对象的
引用
进行访问第16行,通过if(p)来判断其是否
有效
第18行,通过release函数
释放所有权
,并将所有权进行转移
第19行,通过reset
释放
之前的原生指针,并重新关联
一个新的指针第20行,通过std::move
转移
所有权
简单实现
本部分只是基于源码的一些思路,便于理解,实现的一个简单方案,如果想要阅读源码,请点击unique_ptr查看。
基本代码如下:
template<class T>
class unique_ptr
{
T* p;
public:
unique_ptr() :p() {}
unique_ptr(T* s) :p(s) {}
~unique_ptr() { delete p; }
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
unique_ptr(unique_ptr&& s) :p(s.p) { s.p = nullptr }
unique_ptr& operator=(unique_ptr s)
{ delete p; p = s.p; s.p=nullptr; return *this; }
T* operator->() const { return p; }
T& operator*() const { return *p; }
};
从上面代码基本可以看出,unique_ptr的控制权转移是通过move语义来实现的,相比于auto_ptr的拷贝语义转移所有权,更为合理。
shared_ptr
unique_ptr因为其局限性(独享所有权),一般很少用于多线程操作。在多线程操作的时候,既可以共享资源,又可以自动释放资源,这就引入了shared_ptr。
shared_ptr为了支持跨线程访问,其内部有一个引用计数(线程安全),用来记录当前使用该资源的shared_ptr个数,在结束使用的时候,引用计数为-1,当引用计数为0时,会自动释放其关联的资源。
特点
相对于unique_ptr的独享所有权,shared_ptr可以共享所有权。其内部有一个引用计数,用来记录共享该资源的shared_ptr个数,当共享数为0的时候,会自动释放其关联的资源。
shared_ptr不支持数组,所以,如果用shared_ptr指向一个数组的话,需要自己手动实现deleter,如下所示:
std::shared_ptr<int> p(new int[8], [](int *ptr){delete []ptr;});
使用
仍然以一段代码来说明,毕竟代码更有说服力。
#include <iostream>
#include <memory>
int main() {
// 创建shared_ptr对象
std::shared_ptr<int> p1 = std::make_shared<int>();
*p1 = 78;
std::cout << "p1 = " << *p1 << std::endl;
// 打印引用计数
std::cout << "p1 Reference count = " << p1.use_count() << std::endl;
std::shared_ptr<int> p2(p1);
// 打印引用计数
std::cout << "p2 Reference count = " << p2.use_count() << std::endl;
std::cout << "p1 Reference count = " << p1.use_count() << std::endl;
if (p1 == p2)
{
std::cout << "p1 and p2 are pointing to same pointer\n";
}
std::cout<<"Reset p1 "<<std::endl;
// 引用计数-1
p1.reset();
std::cout << "p1 Reference Count = " << p1.use_count() << std::endl;
// 重置
p1.reset(new int(11));
std::cout << "p1 Reference Count = " << p1.use_count() << std::endl;
p1 = nullptr;
std::cout << "p1 Reference Count = " << p1.use_count() << std::endl;
if (!p1) // 通过此种方式来判断关联的资源是否为空
{
std::cout << "p1 is NULL" << std::endl;
}
return 0;
}
输出如下:
p1 = 78
p1 Reference count = 1
p2 Reference count = 2
p1 Reference count = 2
p1 and p2 are pointing to same pointer
Reset p1
p1 Reference Count = 0
p1 Reference Count = 1
p1 Reference Count = 0
p1 is NULL
上面代码基本罗列了shared_ptr的常用方法,对于其他方法,可以参考源码或者官网。
线程安全
可能很多人都对shared_ptr是否线程安全存在疑惑,借助本节,对线程安全方面的问题进行分析和解释。
shared_ptr的线程安全问题主要有以下两种:
引用计数
的加减操作是否线程安全shared_ptr修改指向时是否线程安全
引用计数
shared_ptr中有两个指针,一个指向所管理数据的地址
,另一个一个指向执行控制块的地址
。
执行控制块包括对关联资源的引用计数
以及弱引用计数
等。在前面我们提到shared_ptr支持跨线程操作,引用计数变量是存储在堆上的,那么在多线程的情况下,指向同一数据的多个shared_ptr在进行计数的++或--时是否线程安全呢?
引用计数在STL中的定义如下:
_Atomic_word _M_use_count; // #shared
_Atomic_word _M_weak_count; // #weak + (#shared != 0)
当对shared_ptr进行拷贝时,引入计数增加,实现如下:
template<>
inline void
_Sp_counted_base<_S_atomic>::
_M_add_ref_lock() {
// Perform lock-free add-if-not-zero operation.
_Atomic_word __count;
do
{
__count = _M_use_count;
if (__count == 0)
__throw_bad_weak_ptr();
}
while (!__sync_bool_compare_and_swap(&_M_use_count, __count,
__count + 1));
}
最终,计数的增加,是调用__sync_bool_compare_and_swap
实现的,而该函数是线程安全的,因此我们可以得出结论:在多线程环境下,管理同一个数据的shared_ptr在进行计数的增加或减少的时候是线程安全的,这是一波原子操作
。
修改指向
修改指向分为操作同一个对象
和操作不同对象两种。
同一对象
以下面代码为例:
void fun(shared_ptr<Type> &p) {
if (...) {
p = p1;
} else {
p = p2;
}
}
当在多线程场景下调用该函数时候,p之前的引用计数要进行-1操作,而p1对象的引用计数要进行+1操作,虽然这俩的引用计数操作都是线程安全的,但是对这俩对象的引用计数的操作在一起时候却不是线程安全的
。这是因为当对p1的引用计数进行+1时候,恰恰前一时刻,p1的对象被释放,后面再进行+1操作,会导致segment fault
。
不同对象
代码如下:
void fun1(std::shared_ptr<Type> &p) {
p = p1;
}
void fun2(std::shared_ptr<Type> &p) {
p = p2;
}
int main() {
std::shared_ptr<Type> p = std::make_shared<Type>();
auto p1 = p;
auto p2 = p;
std::thread t1(fun1, p1);
std::thread t2(fun2, p2);
t1.join();
t2.join();
return 0;
}
在上述代码中,p、p1、p2指向同一个资源,分别有两个线程操作不同的shared_ptr对象(虽然关联的底层资源是同一个),这样在多线程下,只对p1和p2的引用计数进行操作,不会引起segment fault,所以是线程安全的。
*同一个shared_ptr被多个线程
同时读
是安全的同一个shared_ptr被多个线程同时
*读写
是不安全的
简单实现
本部分只是基于源码的一些思路,便于理解,实现的一个简单方案,如果想要阅读源码,请点击shared_ptr查看。
记得之前看过一个问题为什么引用计数要new
,这个问题我在面试的时候也问过,很少有人能够回答出来,其实,很简单,因为要支持多线程
访问,所以只能要new呀。
代码如下:
template <class T>
class weak_ptr;
class Counter {
public:
Counter() = default;
int s_ = 0; // shared_ptr的计数
int w_ = 0; // weak_ptr的计数
};
template <class T>
class shared_ptr {
public:
shared_ptr(T *p = 0) : ptr_(p) {
cnt_ = new Counter();
if (p) {
cnt_->s_ = 1;
}
}
~shared_ptr() {
release();
}
shared_ptr(shared_ptr<T> const &s) {
ptr_ = s.ptr_;
(s.cnt)->s_++;
cnt_ = s.cnt_;
}
shared_ptr(weakptr_<T> const &w) {
ptr_ = w.ptr_;
(w.cnt_)->s_++;
cnt_ = w.cnt_;
}
shared_ptr<T> &operator=(shared_ptr<T> &s) {
if (this != &s) {
release();
(s.cnt_)->s_++;
cnt_ = s.cnt_;
ptr_ = s.ptr_;
}
return *this;
}
T &operator*() {
return *ptr_;
}
T *operator->() {
return ptr_;
}
friend class weak_ptr<T>;
protected:
void release() {
cnt_->s_--;
if (cnt_->s_ < 1)
{
delete ptr_;
if (cnt_->w_ < 1)
{
delete cnt_;
cnt_ = NULL;
}
}
}
private:
T *ptr_;
Counter *cnt_;
};
weak_ptr
在三个智能指针中,weak_ptr是存在感最低的一个,也是最容易被大家忽略的一个智能指针。它的引入是为了解决shared_ptr存在的一个问题循环引用
。
特点
不具有普通指针的行为,没有重载operator*和operator->
没有共享资源,它的构造不会引起引用计数增加
用于协助shared_ptr来解决循环引用问题
可以从一个shared_ptr或者另外一个weak_ptr对象构造,进而可以间接获取资源的弱共享权。
使用
int main() {
std::shared_ptr<int> p1 = std::make_shared<Entity>(14);
{
std::weak_ptr<int> weak = p1;
std::shared_ptr<Entity> new_shared = weak.lock();
shared_e1 = nullptr;
new_shared = nullptr;
if (weak.expired()) {
std::cout << "weak pointer is expired" << std::endl;
}
new_shared = weak.lock();
std::cout << new_shared << std::endl;
}
return 0;
}
上述代码输出如下:
weak pointer is expired
0
使用成员函数use_count()和expired()来获取资源的引用计数,如果返回为0或者false,则表示关联的资源不存在
使用lock()成员函数获得一个可用的shared_ptr对象,进而操作资源
当expired()为true的时候,lock()函数将返回一个空的shared_ptr
简单实现
template <class T>
class weak_ptr
{
public:
weak_ptr() = default;
weak_ptr(shared_ptr<T> &s) : ptr_(s.ptr_), cnt(s.cnt_) {
cnt_->w_++;
}
weak_ptr(weak_ptr<T> &w) : ptr_(w.ptr_), cnt_(w.cnt_) {
cnt_->w_++;
}
~weak_ptr() {
release();
}
weak_ptr<T> &operator=(weak_ptr<T> &w) {
if (this != &w) {
release();
cnt_ = w.cnt_;
cnt_->w_++;
ptr_ = w.ptr_;
}
return *this;
}
weak_ptr<T> &operator=(shared_ptr<T> &s)
{
release();
cnt_ = s.cnt_;
cnt_->w_++;
ptr_ = s.ptr_;
return *this;
}
shared_ptr<T> lock() {
return shared_ptr<T>(*this);
}
bool expired() {
if (cnt) {
if (cnt->s_ > 0) {
return false;
}
}
return true;
}
friend class shared_ptr<T>;
protected:
void release() {
if (cnt_) {
cnt_->w_--;
if (cnt_->w_ < 1 && cnt_->s_ < 1) {
cnt_ = nullptr;
}
}
}
private:
T *ptr_ = nullptr;
Counter *cnt_ = nullptr;
};
循环引用
在之前的文章内存泄漏-原因、避免以及定位中,我们讲到使用weak_ptr来配合shared_ptr使用来解决循环引用的问题,借助本文,我们深入说明下如何来解决循环引用的问题。
代码如下:
class Controller {
public:
Controller() = default;
~Controller() {
std::cout << "in ~Controller" << std::endl;
}
class SubController {
public:
SubController() = default;
~SubController() {
std::cout << "in ~SubController" << std::endl;
}
std::shared_ptr<Controller> controller_;
};
std::shared_ptr<SubController> sub_controller_;
};
在上述代码中,因为controller和sub_controller之间都有一个指向对方的shared_ptr,这样就导致任意一个都因为对方有一个指向自己的对象,进而引用计数不能为0。
为了解决std::shared_ptr循环引用导致的内存泄漏,我们可以使用std::weak_ptr来单面去除上图中的循环。
class Controller {
public:
Controller() = default;
~Controller() {
std::cout << "in ~Controller" << std::endl;
}
class SubController {
public:
SubController() = default;
~SubController() {
std::cout << "in ~SubController" << std::endl;
}
std::weak_ptr<Controller> controller_;
};
std::shared_ptr<SubController> sub_controller_;
};
在上述代码中,我们将SubController类中controller_的类型从std::shared_ptr变成std::weak_ptr。
那么,为什么将SubController中的shared_ptr换成weak_ptr就能解决这个问题呢?我们看下源码:
template<typename _Tp1>
__weak_ptr&
operator=(const __shared_ptr<_Tp1, _Lp>& __r) // never throws
{
_M_ptr = __r._M_ptr;
_M_refcount = __r._M_refcount;
return *this;
}
在上面代码中,我们可以看到,将一个shared_ptr赋值给weak_ptr的时候,其引用计数并没有+1,所以也就解决了循环引用的问题。
那么,如果我们想要使用shared_ptr关联的对象进行操作时候,该怎么做呢?使用weak_ptr::lock()函数来实现,源码如下:
__shared_ptr<_Tp, _Lp>
lock() const {
return expired() ? __shared_ptr<element_type, _Lp>() : __shared_ptr<element_type, _Lp>(*this);
}
从上面代码可看出,使用lock()函数生成一个shared_ptr供使用,如果之前的shared_ptr已经被释放,那么就返回一个空shared_ptr对象,否则生成shared_ptr对象的拷贝(这样即使之前的释放也不会存在问题)。
经验之谈
不要混用
指针之间的混用,有时候会造成不可预知的错误,所以建议尽量不要混用。包括裸指针和智能指针
以及智能指针
之间的混用
裸指针和智能指针混用
代码如下:
void fun() {
auto ptr = new Type;
std::shared_ptr<Type> t(ptr);
delete ptr;
}
在上述代码中,将ptr所有权归shared_ptr所拥有,所以在出fun()函数作用域的时候,会自动释放ptr指针,而在函数末尾又主动调用delete来释放,这就会造成double delete,会造成segment fault
。
智能指针混用
代码如下:
void fun() {
std::unique_ptr<Type> t(new Type);
std::shared_ptr<Type> t1(t.get());
}
在上述代码中,将t关联的对象又给了t1,也就是说同一个对象被两个智能指针所拥有,所以在出fun()函数作用域的时候,二者都会释放其关联的对象,这就会造成double delete,会造成segment fault
。
需要注意的是,下面代码在STL中是支持的:
void fun() {
std::unique_ptr<Type> t(new Type);
std::shared_ptr<Type> t1(std::move(t));
}
不要管理同一个裸指针
代码如下:
void fun() {
auto ptr = new Type;
std::unique_ptr<Type> t(ptr);
std::shared_ptr<Type> t1(ptr);
}
在上述代码中,ptr所有权同时给了t和t1,也就是说同一个对象被两个智能指针所拥有,所以在出fun()函数作用域的时候,二者都会释放其关联的对象,这就会造成double delete,会造成segment fault
。
避免使用get()获取原生指针
void fun(){
auto ptr = std::make_shared<Type>();
auto a= ptr.get();
std::shared_ptr<Type> t(a);
delete a;
}
一般情况下,生成的指针都要显式调用delete来进行释放,而上述这种,很容易稍不注意就调用delete;非必要不要使用get()获取原生指针
。
不要管理this指针
class Type {
private:
void fun() {
std::shared_ptr<Type> t(this);
}
};
在上述代码中,如果Type在栈上,则会导致segment fault
,堆上视实际情况(如果在对象在堆上生成,那么使用合理的话,是允许的)。
只管理堆上的对象
void fun() {
Type t;
std::shared_ptr<Type> ptr(&t);
};
在上述代码中,t在栈上进行分配,在出作用域的时候,会自动释放。而ptr在出作用域的时候,也会调用delete释放t,而t本身在栈上,delete一个栈上的地址,会造成segment fault
。
优先使用unique_ptr
根据业务场景,如果需要资源独占,那么建议使用unique_ptr而不是shared_ptr,原因如下:
性能优于shared_ptr
因为shared_ptr在拷贝或者释放时候,都需要操作引用计数
内存占用上小于shared_ptr
shared_ptr需要维护它指向的对象的线程安全引用计数和一个控制块,这使得它比unique_ptr更重量级
使用make_shared初始化
我们看下常用的初始化shared_ptr两种方式,代码如下:
std::shared_ptr<Type> p1 = new Type;
std::shared_ptr<Type> p2 = std::make_shared<Type>();
那么,上述两种方法孰优孰劣呢?我们且从源码的角度进行分析。
第一种初始化方法,有两次内存分配:
new Type分配对象
为p1分配控制块(control block),控制块用于存放引用计数等信息
我们再看下make_shared源码:
template<class _Ty,
class... _Types> inline
shared_ptr<_Ty> make_shared(_Types&&... _Args)
{ // make a shared_ptr
_Ref_count_obj<_Ty> *_Rx =
new _Ref_count_obj<_Ty>(_STD forward<_Types>(_Args)...);
shared_ptr<_Ty> _Ret;
_Ret._Resetp0(_Rx->_Getptr(), _Rx);
return (_Ret);
}
这里的_Ref_count_obj
类包含成员变量:
控制块
一个内存块,用于存放智能指针管理的资源对象
再看看_Ref_count_obj的构造函数:
template<class... _Types>
_Ref_count_obj(_Types&&... _Args)
: _Ref_count_base()
{ // construct from argument list
::new ((void *)&_Storage) _Ty(_STD forward<_Types>(_Args)...);
}
此处虽然也有一个new操作,但是此处是placement new
,所以不存在内存申请。
从上面分析我们可以看出,第一种初始化方式(new方式)共有两次内存分配操作,而第二种初始化方式(make_shared)只有一次内存申请,所以建议使用make_shared
方式进行初始化。
结语
智能指针的出现,能够使得开发者不需要关心内存的释放,进而使得开发者能够将更多的精力投入到业务上。但是,因为智能指针本身也有其局限性,如果使用不当,会造成意想不到的后果,所以,在使用之前,需要做一些必要的检查,为了更好地用好智能指针,建议看下源码实现,还是比较简单的。
好了,今天的文章就到这,我们下期见。
参考
https://docs.microsoft.com/en-us/previous-versions/visualstudio/visual-studio-2012/hh279676(v=vs.110)?redirectedfrom=MSDN
https://rufflewind.com/2016-03-05/unique-ptr
https://www.nextptr.com/tutorial/ta1450413058/unique_ptr-shared_ptr-weak_ptr-or-reference_wrapper-for-class-relationships
https://gcc.gnu.org/onlinedocs/gcc-4.6.3/libstdc++/api/a01099_source.html
https://gcc.gnu.org/onlinedocs/libstdc++/libstdc++-html-USERS-4.4/a01327.html
https://www.nextptr.com/tutorial/ta1358374985/shared_ptr-basics-and-internals-with-examples
边栏推荐
猜你喜欢
随机推荐
Detailed explanation of stored procedures
RestTemplate 使用:设置请求头、请求体
Fabric.js 动态设置字号大小
面试SQL语句,学会这些就够了!!!
自动生成代码器推荐-code-gen
Win11怎么修改关机界面颜色?Win11修改关机界面颜色的方法
为什么IDEA连接mysql Unable to resolve table 编译报错但是可以运行
SQL函数 UNIX_TIMESTAMP
Word | 关于删除分节符(下一页)前面的版式就乱了解决方案
删除链表的节点
移动端适配,华为浏览器底色无法正常显示
Markdown怎么加入emoji
RISC-V instruction format and 6 basic integer instructions
Oracle数据库的闪回技术
【C语言】函数哪些事儿,你真的get到了吗?(2)
二叉树的类型、构建、遍历、操作
劲爆!阿里巴巴面试参考指南(嵩山版)开源分享,程序员面试必刷
Cannot determine loading status from target frame detached when selenium chrome driver is running
How to create short images and short videos from the media?How to make the click volume reach 10W?
[typescript] Use the RangePicker component in antd to implement time limit the previous year (365 days) of the current time