Featured image of post 【C++11】共享智能指针

【C++11】共享智能指针

C++ 11 特性学习

|
3954 字
|

共享智能指针

个人学习笔记,参照大丙老师的博客:爱编程的大丙

在C++中没有垃圾回收机制,必须自己释放分配的内存,否则就会造成内存泄露。解决这个问题最有效的方法是使用智能指针(smart pointer)。智能指针是存储指向动态分配(堆)对象指针的类,用于生存期的控制,能够确保在离开指针所在作用域时,自动地销毁动态分配的对象,防止内存泄露。智能指针的核心实现技术是引用计数,每使用它一次,内部引用计数加1,每析构一次内部的引用计数减1,减为0时,删除所指向的堆内存。

C++11中提供了三种智能指针,使用这些智能指针时需要引用头文件 <memory>

  • std::shared_ptr:共享的智能指针
  • std::unique_ptr:独占的智能指针
  • std::weak_ptr:弱引用的智能指针,它不共享指针,不能操作资源,是用来监视 shared_ptr的。

共享智能指针是指多个智能指针可以同时管理同一块有效的内存,共享智能指针 shared_ptr 是一个模板类,如果要进行初始化有三种方式:通过构造函数、std::make_shared辅助函数以及 reset方法。 共享智能指针对象初始化完毕之后就指向了要管理的那块堆内存,如果想要查看当前有多少个智能指针同时管理着这块内存可以使用共享智能指针提供的一个成员函数 use_count,函数原型如下:

1
long use_count() const noexcept;

1. 通过构造函数初始化

1
std::shared_ptr<T> 智能指针名字(创建堆内存);

测试代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <memory>
using namespace std;

int main()
{
    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;
}

测试代码输出的结果如下:

1
2
3
4
ptr1管理的内存引用计数: 1
ptr2管理的内存引用计数: 1
ptr3管理的内存引用计数: 0
ptr4管理的内存引用计数: 0

如果智能指针被初始化了一块有效内存,那么这块内存的引用计数+1,如果智能指针没有被初始化或者被初始化为nullptr空指针,引用计数不会+1。

另外,不要使用一个原始指针初始化多个 shared_ptr

1
2
3
int *p = new int;
shared_ptr<int> p1(p);
shared_ptr<int> p2(p);

2. 通过拷贝和移动构造函数初始化

当一个智能指针被初始化之后,就可以通过这个智能指针初始化其他新对象。在创建新对象的时候,对应的拷贝构造函数或者移动构造函数就被自动调用了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <memory>
using namespace std;

int main()
{
    // 1. 通过构造函数初始化
    shared_ptr<int> ptr1(new int(520));
    cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl;
  
    // 2. 通过拷贝构造函数初始化
    shared_ptr<int> ptr2(ptr1);
    cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl;
    // 3. 通过拷贝赋值运算符初始化
    shared_ptr<int> ptr3 = ptr1;
    cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl;
  
    // 4. 通过移动构造函数初始化
    shared_ptr<int> ptr4(std::move(ptr1));
    cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl;
    // 5. 通过移动赋值运算符初始化
    std::shared_ptr<int> ptr5 = std::move(ptr2);
    cout << "ptr5管理的内存引用计数: " << ptr5.use_count() << endl;

    return 0;
}

测试程序输入的结果:

1
2
3
4
5
ptr1管理的内存引用计数: 1
ptr2管理的内存引用计数: 2
ptr3管理的内存引用计数: 3
ptr4管理的内存引用计数: 3
ptr5管理的内存引用计数: 3

如果使用拷贝的方式初始化共享智能指针对象,这两个对象会同时管理同一块堆内存,堆内存对应的引用计数也会增加;如果使用移动的方式初始智能指针对象,只是转让了内存的所有权,管理内存的对象并不会增加,因此内存的引用计数不会变化。

3. 通过 make_shared () 初始化

通过C++提供的 std::make_shared() 就可以完成内存对象的创建并将其初始化给智能指针,函数原型如下:

1
2
template< class T, class... Args >
shared_ptr<T> make_shared( Args&&... args );
  • T:模板参数的数据类型
  • Args&&... args :要初始化的数据,如果是通过make_shared创建对象,需按照构造函数的参数列表指定

测试代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#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()
{
    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;
}

输出:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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()模板函数可以完成内存地址的创建,并将最终得到的内存地址传递给共享智能指针对象管理。如果申请的内存是普通类型,通过函数的()可完成地址的初始化,如果要创建一个类对象,函数的()内部需要指定构造对象需要的参数,也就是类构造函数的参数。

4. 通过 reset 方法初始化

共享智能指针类提供的std::shared_ptr::reset方法函数原型如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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:指向要取得所有权的对象的指针
  • aloc:内部存储所用的分配器

测试代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
#include <string>
#include <memory>
using namespace std;

int main()
{
    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;
    cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl;
    cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl;
    cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl;

    ptr4.reset();
    cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl;
    cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl;
    cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl;
    cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl;

    shared_ptr<int> ptr5;
    ptr5.reset(new int(250));
    cout << "ptr5管理的内存引用计数: " << ptr5.use_count() << endl;

    return 0;
}

测试代码输入的结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
ptr1管理的内存引用计数: 4
ptr2管理的内存引用计数: 4
ptr3管理的内存引用计数: 4
ptr4管理的内存引用计数: 4
  
ptr1管理的内存引用计数: 3
ptr2管理的内存引用计数: 3
ptr3管理的内存引用计数: 3
ptr4管理的内存引用计数: 0
  
ptr5管理的内存引用计数: 1

如果使用reset方法,就可以将智能指针重新指向一块新的内存,同时这块新的内存的引用计数也会+1。如果reset方法不带参数,那么智能指针就会指向空指针,同时原来指向的内存的引用计数也会-1。
对于一个未初始化的共享智能指针,可以通过reset方法来初始化,

5. 获取原始指针

通过智能指针可以管理一个普通变量或者对象的地址,此时原始地址就不可见了。当我们想要修改变量或者对象中的值的时候,就需要从智能指针对象中先取出数据的原始内存的地址再操作,解决方案是调用共享智能指针类提供的 get()方法,其函数原型如下:

1
T* get() const noexcept;

测试代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#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();
    memset(add, 0, len);
    strcpy(add, "我是要成为海贼王的男人!!!");
    cout << "string: " << add << endl;
  
    shared_ptr<int> p(new int);
    *p = 100;
    cout << *p.get() << "  " << *p << endl;
  
    return 0;
}

memset 是 C/C++ 中的一个标准库函数,用于将内存块的每个字节设置为特定的值。它的函数原型如下:

1
void *memset(void *ptr, int value, size_t num);

参数说明:

  • ptr :指向要填充的内存块的指针
  • value :要设置的值(以 int 形式传递,但实际会转换为 unsigned char)
  • num :要填充的字节数

如果value设置为0,即清空内存块缓冲区

6. 指定删除器

当智能指针管理的内存对应的引用计数变为0的时候,这块内存就会被智能指针析构掉了。另外,我们在初始化智能指针的时候也可以自己指定删除动作,这个删除操作对应的函数被称之为删除器,这个删除器函数本质是一个回调函数,我们只需要进行实现,其调用是由智能指针完成的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <iostream>
#include <memory>
using namespace std;

void deleteIntPtr(int* p)
{
    delete p;
    cout << "int 型内存被释放了...";
}

int main()
{
    shared_ptr<int> ptr(new int(250), deleteIntPtr);
    return 0;
}

删除器函数也可以是lambda表达式,因此代码也可以写成下面这样:

1
2
3
4
5
int main()
{
    shared_ptr<int> ptr(new int(250), [](int* p) {delete p; });
    return 0;
}

在上面的代码中,lambda表达式的参数就是智能指针管理的内存的地址,有了这个地址之后函数体内部就可以完成删除操作了。

在C++11中使用shared_ptr管理动态数组时,需要指定删除器,因为 std::shared_ptr的默认删除器不支持数组对象,具体的处理代码如下:

1
2
3
4
5
int main()
{
    shared_ptr<int> ptr(new int[10], [](int* p) {delete[]p; });
    return 0;
}

在删除数组内存时,除了自己编写删除器,也可以使用C++提供的 std::default_delete<T>()函数作为删除器,这个函数内部的删除功能也是通过调用 delete来实现的,要释放什么类型的内存就将模板类型T指定为什么类型即可。具体处理代码如下:

1
2
3
4
5
int main()
{
    shared_ptr<int> ptr(new int[10], default_delete<int[]>());
    return 0;
}

另外,我们还可以自己封装一个make_shared_array方法来让shared_ptr支持数组,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#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()
{
    // 创建并操作int数组
    shared_ptr<int> ptr1 = make_share_array<int>(5);
    for(int i = 0; i < 5; ++i) {
        ptr1.get()[i] = i * 10;  // 使用get()获取原始指针并操作数组
        cout << ptr1.get()[i] << " ";
    }
    cout << endl;

    // 创建并操作char数组(字符串)
    shared_ptr<char> ptr2 = make_share_array<char>(20);
    strcpy(ptr2.get(), "Hello, shared_ptr!");
    cout << ptr2.get() << endl;

    return 0;
}
使用 Hugo 构建
主题 StackJimmy 设计