共享智能指针shared_ptr
共享智能指针
在C++中没有垃圾回收机制,必须自己释放分配的内存,否则就会造成内存泄露。解决这个问题最有效的方法是使用智能指针(smart pointer)。智能指针是存储指向动态分配(堆)对象指针的类,用于生存期的控制,能够确保在离开指针所在作用域时,自动地销毁动态分配的对象,防止内存泄露。智能指针的核心实现技术是引用计数,每使用它一次,内部引用计数加1,每析构一次内部的引用计数减1,减为0时,删除所指向的堆内存。
C++11中提供了三种智能指针,使用这些智能指针时需要引用头文件:
- std::shared_ptr:共享的智能指针
- std::unique_ptr:独占的智能指针
- std::weak_ptr:弱引用的智能指针,它不共享指针,不能操作资源,是用来监视shared_ptr的。
1.shared_ptr的初始化
共享智能指针是指多个智能指针可以同时管理同一块有效的内存,共享智能指针shared_ptr 是一个模板类,如果要进行初始化有三种方式:通过构造函数、std::make_shared辅助函数以及reset方法。共享智能指针对象初始化完毕之后就指向了要管理的那块堆内存,如果想要查看当前有多少个智能指针同时管理着这块内存可以使用共享智能指针提供的一个成员函数use_count,函数原型如下:
// 管理当前对象的 shared_ptr 实例数量,或若无被管理对象则为 0。
long use_count() const noexcept;
1.1 通过构造函数初始化
// shared_ptr<T> 类模板中,提供了多种实用的构造函数, 语法格式如下:
std::shared_ptr<T> 智能指针名字(创建堆内存);
测试代码如下:
#include <iostream>
#include <memory>
using namespace std;
int main()
{
// 使用智能指针管理一块 int 型的堆内存
shared_ptr<int> ptr1(new int(520));
cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl;
// 使用智能指针管理一块字符数组对应的堆内存
shared_ptr<char> ptr2(new char[12]);
cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl;
// 创建智能指针对象, 不管理任何内存
shared_ptr<int> ptr3;
cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl;
// 创建智能指针对象, 初始化为空
shared_ptr<int> ptr4(nullptr);
cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl;
return 0;
}
测试代码输出的结果如下:
ptr1管理的内存引用计数: 1
ptr2管理的内存引用计数: 1
ptr3管理的内存引用计数: 0
ptr4管理的内存引用计数: 0
如果智能指针被初始化了一块有效内存,那么这块内存的引用计数+1,如果智能指针没有被初始化或者被初始化为nullptr空指针,引用计数不会+1。另外,不要使用一个原始指针初始化多个shared_ptr。
int *p = new int;
shared_ptr<int> p1(p);
shared_ptr<int> p2(p); // error, 编译不会报错, 运行会出错
1.2 通过拷贝和移动构造函数初始化
当一个智能指针被初始化之后,就可以通过这个智能指针初始化其他新对象。在创建新对象的时候,对应的拷贝构造函数或者移动构造函数就被自动调用了。
void test02() {
// 使用智能指针管理一块 int 型的堆内存, 内部引用计数为 1
shared_ptr<int> ptr1(new int(520));
cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl;
//调用拷贝构造函数
shared_ptr<int> ptr2(ptr1);
cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl;
shared_ptr<int> ptr3 = ptr1;
cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl;
//调用移动构造函数
shared_ptr<int> ptr4(std::move(ptr1));
cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl;
std::shared_ptr<int> ptr5 = std::move(ptr2);
cout << "ptr5管理的内存引用计数: " << ptr5.use_count() << endl;
}
测试程序输入的结果:
ptr1管理的内存引用计数: 1
ptr2管理的内存引用计数: 2
ptr3管理的内存引用计数: 3
ptr4管理的内存引用计数: 3
ptr5管理的内存引用计数: 3
如果使用拷贝的方式初始化共享智能指针对象,这两个对象会同时管理同一块堆内存,堆内存对应的引用计数也会增加;如果使用移动的方式初始智能指针对象,只是转让了内存的所有权,管理内存的对象并不会增加,因此内存的引用计数不会变化。
1.3 通过std::make_shared初始化
通过C++提供的std::make_shared() 就可以完成内存对象的创建并将其初始化给智能指针,函数源码如下:
template<class T, class... Args>
shared_ptr<T> make_shared(Args&&... args) {
return std::allocate_shared<T>(std::allocator<T>(), std::forward<Args>(args)...);
}
这个函数模板使用了可变模板参数(variadic templates
)和完美转发(perfect forwarding
)的特性。它接受一个类型 T
和任意数量的参数(Args),然后调用 std::allocate_shared 来完成共享指针的初始化和对象的构造。
可变模板参数是指函数模板的参数数量是可变的。在std::make_shared
的情况下,它允许我们以任意数量的参数初始化对象。
举例来说,假设有一个类 MyClass:
class MyClass {
public:
MyClass(int a, double b, const std::string& c) {
// 构造函数的实现
}
};
可以使用 std::make_shared
来创建一个 shared_ptr
并调用 MyClass
的构造函数:
#include <memory>
#include <string>
int main() {
auto mySharedPtr = std::make_shared<MyClass>(42, 3.14, "Hello");
// 使用 mySharedPtr
return 0;
}
在这个例子中,MyClass
有一个带有三个参数的构造函数,而 std::make_shared
允许你提供这三个参数,以便在分配内存时调用构造函数进行对象初始化。可变模板参数的使用使得 std::make_shared
能够处理任意数量的构造函数参数。
测试代码如下:
#include <iostream>
#include <string>
#include <memory>
using namespace std;
class Test
{
public:
Test()
{
cout << "construct Test..." << endl;
}
Test(int x)
{
cout << "construct Test, x = " << x << endl;
}
Test(string str)
{
cout << "construct Test, str = " << str << endl;
}
~Test()
{
cout << "destruct Test ..." << endl;
}
};
int main()
{
// 使用智能指针管理一块 int 型的堆内存, 内部引用计数为 1
shared_ptr<int> ptr1 = make_shared<int>(520);
cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl;
shared_ptr<Test> ptr2 = make_shared<Test>();
cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl;
shared_ptr<Test> ptr3 = make_shared<Test>(520);
cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl;
shared_ptr<Test> ptr4 = make_shared<Test>("我是要成为海贼王的男人!!!");
cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl;
return 0;
}
测试代码输出:
ptr1管理的内存引用计数: 1
construct Test...
ptr2管理的内存引用计数: 1
construct Test, x = 520
ptr3管理的内存引用计数: 1
construct Test, str = 我是要成为海贼王的男人!!!
ptr4管理的内存引用计数: 1
destruct Test ...
destruct Test ...
destruct Test ...
使用std::make_shared()模板函数可以完成内存地址的创建,并将最终得到的内存地址传递给共享智能指针对象管理。如果申请的内存是普通类型,通过函数的()可完成地址的初始化,如果要创建一个类对象,函数的()内部需要指定构造对象需要的参数,也就是类构造函数的参数。
1.4 通过reset方法初始化
共享智能指针类提供的std::shared_ptr::reset方法函数原型如下:
void reset() noexcept;
template< class Y >
void reset( Y* ptr );
template< class Y, class Deleter >
void reset( Y* ptr, Deleter d );
template< class Y, class Deleter, class Alloc >
void reset( Y* ptr, Deleter d, Alloc alloc );
- ptr:指向要取得所有权的对象的指针
- d:自定义的删除器(deleter)
- aloc:自定义的分配器
其中最常见的是不带任何参数的形式和带一个指针参数的形式。
不带参数的reset
template<class Y>
void reset();
这个版本的 reset
将 shared_ptr
置为空,即它不再拥有任何对象。如果此时 shared_ptr
是最后一个拥有某个对象的智能指针,它会调用对象的析构函数来释放资源。
std::shared_ptr<int> ptr = std::make_shared<int>(42);
ptr.reset(); // ptr 现在为空,且释放了之前的对象
带指针参数的reset
template<class Y>
void reset(Y* ptr);
这个版本的 reset
会替换 shared_ptr
当前所管理的对象,并且使用参数 ptr
所指向的对象。如果此时 shared_ptr
是最后一个拥有某个对象的智能指针,它会调用原对象的析构函数来释放资源,然后开始管理新的对象。
std::shared_ptr<int> ptr = std::make_shared<int>(42);
ptr.reset(new int(10)); // ptr 不再指向之前的对象,而是指向一个新的 int 对象
带指针和自定义删除器的 reset
:
template<class Y, class Deleter>
void reset(Y* ptr, Deleter deleter);
这个版本的 reset
允许你指定一个自定义的删除器(deleter),该删除器将在 shared_ptr
管理的对象销毁时被调用。这允许你使用 shared_ptr
管理通过非默认方式分配的资源。
std::shared_ptr<int> ptr(new int, [](int* p) { delete p; });
ptr.reset(new int, [](int* p) { delete[] p; });
带指针、自定义删除器和分配器的 reset
:
template<class Y, class Deleter, class Alloc>
void reset(Y* ptr, Deleter deleter, Alloc alloc);
这个版本的 reset
允许你指定自定义的删除器和分配器。通常情况下,std::shared_ptr
使用默认分配器 std::allocator
和默认删除器 delete
。
std::shared_ptr<int> ptr(new int, [](int* p) { delete p; }, std::allocator<int>());
来看一下这个例子:
#include <iostream>
#include <string>
#include <memory>
using namespace std;
int main()
{
// 使用智能指针管理一块 int 型的堆内存, 内部引用计数为 1
shared_ptr<int> ptr1 = make_shared<int>(520);
shared_ptr<int> ptr2 = ptr1;
shared_ptr<int> ptr3 = ptr1;
shared_ptr<int> ptr4 = ptr1;
cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl; // 4
cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl; // 4
cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl; // 4
cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl; // 4
ptr4.reset();
cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl; // 0
ptr4.reset(new int(250));
cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl; // 3
cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl; // 3
cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl; // 3
cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl; // 1
return 0;
}
对于一个未初始化的共享智能指针,可以通过reset方法来初始化,当智能指针中有值的时候,调用reset会使引用计数减1。
1.5 获取原始指针
通过智能指针可以管理一个普通变量或者对象的地址,此时原始地址就不可见了。当我们想要修改变量或者对象中的值的时候,就需要从智能指针对象中先取出数据的原始内存的地址再操作,解决方案是调用共享智能指针类提供的get()方法,其函数原型如下:
T* get() const noexcept;
测试代码如下:
#include <iostream>
#include <string>
#include <memory>
using namespace std;
int main()
{
int len = 128;
shared_ptr<char> ptr(new char[len]);
// 得到指针的原始地址
char* add = ptr.get();
fill(ptr.get(), ptr.get() + len, '\0');
const char* source = "我爱cpp";
copy(source, source + strlen(source) + 1, ptr.get());
cout << "string: " << ptr.get() << endl;
shared_ptr<int> p(new int);
*p = 100;
cout << *p.get() << " " << *p << endl; // 100 100
return 0;
}
2.指定删除器
当智能指针管理的内存对应的引用计数变为0的时候,这块内存就会被智能指针析构掉了。另外,我们在初始化智能指针的时候也可以自己指定删除动作,这个删除操作对应的函数被称之为删除器,这个删除器函数本质是一个回调函数,我们只需要进行实现,其调用是由智能指针完成的。
#include <iostream>
#include <memory>
using namespace std;
// 自定义删除器函数,释放int型内存
void deleteIntPtr(int* p)
{
delete p;
cout << "int 型内存被释放了...";
}
int main()
{
shared_ptr<int> ptr(new int(250), deleteIntPtr);
return 0;
}
删除器函数也可以是lambda表达式,因此代码也可以写成下面这样:
int main()
{
shared_ptr<int> ptr(new int(250), [](int* p) {delete p; });
return 0;
}
在上面的代码中,lambda表达式的参数就是智能指针管理的内存的地址,有了这个地址之后函数体内部就可以完成删除操作了。
管理动态数组
在C++11中使用shared_ptr管理动态数组时,需要指定删除器,因为std::shared_ptr的默认删除器不支持数组对象,具体的处理代码如下:
int main()
{
shared_ptr<int> ptr(new int[10], [](int* p) {delete[]p; });
return 0;
}
在删除数组内存时,除了自己编写删除器,也可以使用C++提供的std::default_delete()函数作为删除器,这个函数内部的删除功能也是通过调用delete来实现的,要释放什么类型的内存就将模板类型T指定为什么类型即可。具体处理代码如下:
int main()
{
shared_ptr<int> ptr(new int[10], default_delete<int[]>());
return 0;
}
另外,我们还可以自己封装一个make_shared_array方法来让shared_ptr支持数组,代码如下:
#include <iostream>
#include <memory>
using namespace std;
template <typename T>
shared_ptr<T> make_share_array(size_t size)
{
// 返回匿名对象
return shared_ptr<T>(new T[size], default_delete<T[]>());
}
int main()
{
shared_ptr<int> ptr1 = make_share_array<int>(10);
cout << ptr1.use_count() << endl;
shared_ptr<char> ptr2 = make_share_array<char>(128);
cout << ptr2.use_count() << endl;
return 0;
}
资源的所有权转移
另一方面: 指定删除器也用于资源的所有权转移
删除器允许你将资源的所有权转移给智能指针。这对于使用自定义释放逻辑的资源管理类很有帮助,因为你可以将资源的释放责任委托给智能指针。
class CustomResource {
public:
CustomResource(int* data) : data_(data) {}
void release() { delete[] data_; }
private:
int* data_;
};
// 将自定义资源类的释放函数作为删除器
std::shared_ptr<CustomResource> customPtr(new CustomResource(new int[10]), [](CustomResource* resource) {
resource->release();
delete resource;
});