文章目录
- 一、初始化shared_ptr
-
- 1.1常规初始化(share_ptr和new配合使用)
- [1.2 make_shared 函数](#1.2 make_shared 函数)
- 二、两者的区别
- [三、 使用限制](#三、 使用限制)
-
- [3.1 指定删除器](#3.1 指定删除器)
一、初始化shared_ptr
shared_ptr的初始化有两种方法
1.1常规初始化(share_ptr和new配合使用)
cpp
shared_ptr<int> ptr (new int(100)); //ptr 指向一个值为100的int类型数据
shared_ptr<int> ptr= new int(100);//错误,智能指针是explicit,不能进行隐式类型转换,必须用直接初始化形式。
shared_ptr<int> makes(value){
retrun new int(value);//不可以
}
shared_ptr<int> makes(value){
retrun shared_ptr<int>(new int(value));//可以
}
虽然裸指针可以初始化shared_ptr,但是并不推荐使用,使用裸指针与智能指针穿插使用的方式,一不小心就会出问题。譬如:
cpp
void fun(shared_ptr<int> ptr){
retrun;
}
int main(){
int *p =new int(100);
//fun(p)//不可以,不能隐式转换。
fun(shared_pre<int>(p));
*p=45;//此时会出现不可预料的结果,应为p指向的内存已经被释放了。
//修改
shared_ptr<int>p(new int(100));
fun(p);
*p=45;
}
另外不要用裸指针初始化多个共享指针:
cpp
int *p =new int(100);
stared_ptr<int>ptr1(p);
shared_ptr<int>ptr2(p);
//此时ptr1与ptr2的引用计数都是1,ptr1与ptr2两者之间并无关联,势必会造成释放两次内存的结果。
1.2 make_shared 函数
cpp
shared_ptr<int>p2 = make_shared<int>(100);
shared_ptr<string>p3 = make_shared<string>(5,'a');
shared_ptr<int>p4 = make_shared<int>();//初始值为0
p4 =make_shared<int>(400);//p4释放刚才对象,重新指向新对象
auto p5 = make_shared<string>(5,'b'); //auto 更加方便
二、两者的区别
2.1.make_shared效率更高
对于使用裸指针创建共享指针的方式,例如:
cpp
shared_ptr<int>p(new int(100));
首先会在堆上为int(100)分配内存并初始化,返回一个原始指针。接着,共享指针的构造函数为这个原始指针创建控制块,又进行了一次内存分配 。此时会创建一个指针指向控制块中存在的强引用与弱引用。而当使用make_shared初始化时编译器会在堆上分配一块内存,这块内存既存放int类型的对象(值为 100),又存放控制块,控制块里包含引用计数等管理信息 。
我们通过单步调试看一下make_shared到底怎样完成的这项操作。首先我们先打上断点:然后按"F5"进行调试。

程序运行到此处时开始逐语句进行调试,我们进入make_shared内部。

通过上图我们可以发现程序首先new了一个_Ref_count_obj2的对象_Rx,然后创建了一个shared_ptr<_Ty> 对象_Ret,最后调用了_Ret中的函数,而且参数和_Rx相关,我们猜测,在new_Rx对象时已经分配好了内存,嗲用_Set_ptr_rep_and_enable_shared函数只是将shared_ptr类中的两个指针分贝指向这两块内存(内存是连续的)。我们继续往下运行:

进入_Ref_count_obj2类,我们此时发现,_Ref_count_obj2集成了_Ref_count_base类。运行_Ref_count_obj2构造函数之前会去初始化各类构造函数,我们继续运行进入_Ref_count_base类中:

我们可以看到黄色框中醒目的两个变量:_Uses与 _Weaks,这便是强引用与弱引用。继续单步运行会发现_Uses与 _Weaks皆被初始化为1。退出_Ref_count_base类,继续运行_Ref_count_obj2的构造函数,进入"_STD _Construct_in_place(_Storage._Value, _STD forward<_Types>(_Args)...);":

进入函数之后,我们看到了我们殷切期望的关键字::new,而其中的参数addressof(_Obj)也是我们需要关心的,见名知义,这个函数功能应该是给obj也就是_Storage._Value在对内开辟空间,那他应该返回一个地址才对。我们继续运行,验证一下是否是这样:

的确如此。我们可以看到,函数返回一个地址为0x0000021AD1772290,此时只是分配了地址,但并没有初始化内容(红色框中显示)。
运行完"::new (static_cast<void*>(_STD addressof(_Obj))) _Ty(_STD forward<_Types>(_Args)...);"函数之后我们再看0x0000021AD1772290地址发现已经初始化了值,值为 0f =15 ,int类型4个字节。

跳出函数,此时_Storage._Value已被赋值15,继续跳出循环,继续单步运行:

此时可以发现类_Rx的地址为0x0000021ad1772280紧挨着0x0000021AD1772290,是在一块内存中。继续运行,进入shared_ptr类:

类shared_ptr继承了_Ptr_base,初始化之前要先初始化_Ptr_base:

_Ptr_base中这两个指针便是指向数值和控制块的指针,在此时初始化。
单步跳出,运行"_Ret._Set_ptr_rep_and_enable_shared(_STD addressof(_Rx->_Storage._Value), _Rx);"函数:
这个函数参数一个是Storage._Value的地址,一个是类_Rx的地址

分别将这两个地址赋值给_Ptr和_Rep两个指针,大功告成了。最后返回_Ret。
通过上述调试,我们发现使用make_shared初始化一个共享指针时,他的强引用与弱引用计数都会初始化为1(vs2022 windows)。
对于裸指针创建共享指针时不再演示。多一次内存分配,就多一次与内存分配器打交道的开销,并且增加了内存碎片化的风险,在频繁进行内存分配和释放的场景中,这对性能的影响不容小觑 。
2.2.make_shared更安全
c++17之前,c++不保证参数求值顺序,以及内部表达式的求值顺序的。譬如如下代码:
cpp
class AnotherClass {
public:
AnotherClass(int val) {
if (val < 0) {
throw std::runtime_error("Negative value not allowed");
}
}
};
void someFunction(const std::shared_ptr<MyComplexClass>& p1, const std::shared_ptr<AnotherClass>& p2) {
// TODO
}
// 在C++17之前存在风险的调用
try {
someFunction(
std::shared_ptr<MyComplexClass>(new MyComplexClass(-1)),
std::shared_ptr<AnotherClass>(new AnotherClass(2))
);
} catch (const std::exception& e) {
std::cerr << "Exception caught: " << e.what() << std::endl;
}
如果new MyComplexClass(-1)先执行,并且MyComplexClass的构造函数抛出异常,此时new AnotherClass(2)已经分配了内存,但由于std::shared_ptr的构造函数还未执行完,这块内存没有被std::shared_ptr接管,就会导致内存泄漏 。
2.3.make_shared对象内存可能无法及时回收
std::make_shared在内存释放时机上有其独特之处,这与它的内存分配方式紧密相关 。由于std::make_shared将对象和控制块分配在同一块连续内存中,这就导致在引用计数为 0 时,这块内存并不会立即被释放 。因为控制块不仅记录着引用计数,还包含了一些用于管理对象生命周期的其他信息,只有当最后一个指向控制块的std::shared_ptr被销毁时,整个内存块(包括对象和控制块)才会被释放
构造函数传指针的方式在内存释放时机上相对更加灵活 。当对象的引用计数降为 0 时,对象所占用的内存会立即被释放,而控制块的内存则在最后一个指向它的std::shared_ptr被销毁时释放 。因为对象和控制块是分开分配的,它们的生命周期管理相对独立 。
三、 使用限制
1.当构造函数是保护或私有时,无法使用 make_shared。
2.当使用自定义删除器时也无法使用make_shared。
3.1 指定删除器
我们可以自己指定删除器,这样当智能指针需要删除一个它所指向的对象时,不再调用默认的delete而是去调用删除器来删除指定的对象。
cpp
void myDeleter(int * p){
delete p;
}
shared_ptr<int>(new int(100),myDeleter)
//lambda 表达式:
shared_ptr<int>(new int(200),[](int *p){
delete p;
})
p.reset();
当我们使用共享指针去管理动态数组时就必须用删除器:
cpp
shared_ptr<int[]> p (new int[10],[](int *p){
delete []p;
})
p.reset();
先写到这里,后续再补充。
