概述
场景一: 希望指向多个指针管理一片空间
unique_ptr它是不允许两个智能指针管理一片空间的,所以其禁止直接拷贝和赋值(转化为右值可以)。
auto_ptr虽然其允许我们多个智能指针管理一片空间,但是这样的操作对于auto_ptr来说是不安全的,因为一个智能指针释放空间时,是不管别的指针的。
但是,我们有时候是希望,使用多个指针来指向同一片空间的,这样unique_ptr无法实现,auto_ptr又不安全,所以c++11又增加了shared_ptr,它是允许我们使用多个智能指针管理一片空间的。
场景二: 作为容器的类型前面说到unique_ptr和auto_ptr作为容器的类型的时候,是无法满足容器的特性的(就是两个元素之间赋值),但是shared_ptr是支持的。
shared_ptr原理
智能指针中使用引用计数的方式来判断是否需要释放智能指针指向的动态的空间。
shared_ptr内部使用引用计数的方式,每一片空间都有一个引用计数,每有一个指针指向这片空间,那么其引用计数就会加1,有指针不指向它了,其引用计数就会减1。
当智能指针管理的空间的引用计数为0的时候,就会释放这块内存。所以使用shared_ptr指向同一片空间,是不会出现一个释放影响别的指针使用的,因为一个指针不使用,其只会影响引用计数,只有当智能指针释放的时候,空间的引用计数也减为0了,那么就会释放这块空间。(也就是只有一个智能指针管理这块空间时,当智能指针释放,这块空间也就释放了)
注意事项(适用于所有智能指针)
我们不能随便将一块地址让智能指针进行管理,只能将动态开辟的空间让它去管理。
比如: int a = 10;auto_ptr<int> p1(&a); // error;
千万注意上面这样的代码,因为我们智能指针的析构函数是使用delete来释放空间的,也就是说其管理的应该是动态开辟的空间,如果像上面那样写,会导致delete释放非堆内存,这显然不对。
1.1 定义shared_ptr的对象
- unique_ptr<类型> p1(new 空间); // 类型: 智能指针指向的类型,空间:智能指针管理的空间
- std::shared_ptr<int> p1;
std::shared_ptr<int> p1(NULL); // 创建指向空的智能指针,注意,此时其引用计数为 0(因为没有指向空间)- std::shared_ptr<int> p1(new int()); // p1直系那个一片空间,引用计数+1
- 使用auto_ptr,unique_ptr,shared_ptr的临时对象初始化
std::shared_ptr<int> p1(std::unique_ptr<int> (new int)); // 执行完这行unique_ptr的指针 就释放了
std::shared_ptr<int> p1(std::shared_ptr<int> (new int));
std::shared_ptr<int> p1(std::auto_ptr<int> (new int));- int a = new int();
std::shared_ptr<int> p1(&a);
1.2 使用make_shared()构造智能指针
1)使用make_shared的好处
- make_shared是一个函数模板,可以帮助我们构建共享智能指针(shared_ptr),并且使用make_shared()构建智能指针的效率要比1.1中的方式(使用new去开辟空间)快。(因为使用make_shared不需要多次拷贝)
- 所以,在可以使用make_shared的情况下,建议使用shared_ptr来构建共享智能指针。
- 因为shared_ptr中通过引用计数机制来管理空间的释放,所以其相比unique_ptr而言需要多开辟一块空间来保存引用计数。
当你使用std::shared_ptr<int> p1(new int); 创建指针的时候,其先会开辟存放数据的空间,又会开辟存放引用计数的空间,会涉及到两次开辟。
如果我们使用make_shared()开辟空间的话,其会将存放数据的空间和引用计数的空间一起开辟,只会开辟一次。
所以使用make_shared的目的就是可以提高创建shared_ptr效率。(注意和make_unique创建unique_ptr原因不同,因为shared_ptr可以在后续赋值)
2)如何使用make_shared
- 模板<T类,类...Args >
shared_ptr<T> make_shared( Args&&...参数 );make_shared是一个函数模板,其返回一个共享指针对象。
代码:
std::shared_ptr<int> p1 = std::make_shared<int>(5);
std::cout << *p1 << std::endl; // 输出5
std::shared_ptr<std::string> p2 = std::make_shared<std::string>("Hello World!!!");
std::cout << *p2 << std::endl; // 输出Hello World!!!
make_shared使用时注意事项
- 因为是函数模板,所以在使用的时候需要传入类型实例化类型参数,当然这个类型要和智能指针指向的数据类型是一致的。
- make_shared的()中的参数,我们直接写指针指向空间内部需要存储的数据(其实和emplace使用是类似的)。
- 在c++20之前,make_shared的类型参数是不能为数组类型的,也就是说下面的代码是错误的。
std::shared_ptr<int []> p1 = std::make_shared<int []>(size_t s); // error
(注意在c++20之后使用参数中应该写参数: 要开辟现需空间的大小)
虽然c++11中shared_ptr是支持指向数组类型的,但是使用make_shared是不可以构造指向数组类型的智能指针的(c++20之前)。
所以,c++20之前想要共享指针指向数组类型,还是使用new方式。- 并且使用make_shared构造对象引用计数会正常进行。
std::shared_ptr<int> p1 = std::make_shared<int>(5); // 引用计数为1
2. shared_ptr也是支持指向数组类型的 (c++11支持)
代码:
std::shared_ptr<int []> p1 (new int[5]);
std::shared_ptr<int[]> p1(new int[5] {1,2,3,4,5}); // 使用初始化列表初始化开辟空间
所以,构造共享智能指针可以指向数组类型。 -- 注意: 在c++17之后才支持
- std::shared_ptr<int []> p1 (new int[5]);
原因: 因为其内部有delete和delete[
3. 多个指针指向同一块空间
- 拷贝构造
std::shared_ptr<int> p1(new int());
std::shared_ptr<int> p2(p1);
- 赋值 -- 所以其可以作为容器的类型,其更加符合容器的要求(一个元素可以赋值给另外一个元素)
std::shared_ptr<int> p1(new int());
std::shared_ptr<int> p2;
p2 = p1;
例子:std::shared_ptr<int> p1(new int(10)); // p1指向空间,引用计数+1(为1)
{
std::shared_ptr<int> p2;
p2 = p1; // 指针p2和p1指向了同一片空间(引用计数加1(为2)),在大括号结束之后,p2被 释放,引用计数减1(为1)。
}
std::cout << *p1 << std::endl; // 由于p2释放后引用计数为1,所以空间没有释放,所以此时 访问空间数据没有问题。
当然,当p1析构,那么引用计数就变成0了,那么就释放掉此空间。
4. 指定shared_ptr的删除器
与unique_ptr一样,我们使用shared_ptr也可以指定删除器,不同的是,unique_ptr是通过类型传递的(类型参数),shared_ptr是作为函数参数传递的。
看代码:
代码:
class deleter {
public:
void operator()(int* p) {
std::cout << "deleting" << std::endl;
delete p;
}
};
int main(void) {
{
std::shared_ptr<int> p1(new int, deleter());
}
std::cin.get();
}
在构造p1的时候,我们在()传入了动态开辟的空间,并且传入一个我们定义的函数对象类的临时对象,因它指定删除规则。
我们代码中在析构p1时,会调用我们定义的函数对象释放动态空间。
当然和unique_ptr一样,我们不指定删除器的时候,会使用默认的,如果我们希望在释放动态空间的时候做些什么,就可以自己设计一个删除器(函数对象)。
5. shared_ptr的函数
1)use_count()函数
- long use_count();
我们前面说到,共享指针基于引用计数来实现多个指针管理一片空间的,那我们如何知道当前的指针指向的空间被多少的指针指向。
我们可以使用use_count(),这个函数返回当前指针指向空间的引用计数是多少。
代码:
int main(void) {
std::shared_ptr<int> p1(new int(10));
{
std::shared_ptr<int> p2(p1);
std::cout << p2.use_count() << std::endl; // 输出2
}
std::cout << p1.use_count() << std::endl; // 输出1
std::cin.get();
}
2) unique()函数
- bool unique(); // 用来判断,当前指针指向其管理空间的是否只有它(引用计数是否为1),如果是返回true,如果不是返回false。
代码:
std::shared_ptr<int> p1(new int(10));
if (p1.unique()) { // 返回true
std::cout << "引用计数为1" << std::endl; // 正常输出
}
3)swap(),get(),release()函数
- T* get(p); // 返回智能指针指向的空间地址
- void p.reset(地址); // 重新设置智能指针指向的空间地址
- T* release(p); // 将管理空间的权限重到我们手中
当然这些函数和auto_ptr的用法一样,那里我们进行了详细的描述。
6. 内置类型element_type
表示当前智能指针指向的数据的类型,我们在使用智能指针数据的时候,可以直接使用此类型来统一表示,并不用去关心其具体指向什么类型。
std::shared_ptr<int>::element_type t = 10;
7. 手动将智能指针置空
ptr = NULL 或者 nullptr(c++11后支持)
共享指针的引用计-1。
8. 注意 :开辟连续空间
无论使用make_shared,还是shared_ptr的构造函数,要想开辟一块连续的空间必须在<>传入T [],才可以。
而且只有传入T[] 创建的智能指针才能使用[]运算符重载。
9. 关于shared_ptr线程不安全的情况
shared_ptr
本身并不是严格意义上的线程不安全。
shared_ptr
在正常的使用场景下,如进行基本的创建、共享所有权操作等,通常不会出现线程安全问题。然而,在一些特定的并发场景中,如果多个线程同时对同一个
shared_ptr
进行非常复杂且交织的操作,比如同时修改引用计数、同时进行一些自定义的与共享状态相关的复杂逻辑等,可能会出现潜在的竞争条件和不一致性,但这并不是shared_ptr
本身的固有缺陷,而是极端并发情况下复杂操作的结果。一般来说,在合理的编程实践和常见的使用模式下,
shared_ptr
可以在多线程环境中安全使用。