智能指针


智能指针

注意智能指针管理的是动态分配的内存,即堆区的数据,栈上的会有问题

1
#include <memory>  // 智能指针的公用头文件

shared_ptr 共享

共享指针(是指可以多个 shared_ptr 指向同一块内存,进行管理)

好处:

  • 方便调用,重要的是防止内存泄漏
  • 因为我们自己定义指针,new 出空间,就得手动释放,但是智能指针是一个模板类,封装好了的,当不需要的时候,会在智能指针生命周期结束后自动释放内存
  • 比如定义在函数里,函数结束,它就结束,自动释放内存,还比如它不再指向内存空间了,就自动释放内存,指向空

初始化

默认构造函数

1
2
3
// shared_ptr<T> 类模板中,提供了多种实用的构造函数, 语法格式如下:
std::shared_ptr<T> 智能指针名字(创建堆内存);
shared_ptr<int> ptr1(new int(520));

std::make_shared

1
2
3
4
5
6
7
8
9
10
11
template< class T, class... Args >
shared_ptr<T> make_shared( Args&&... args );
shared_ptr<int> ptr1 = make_shared<int>(520);

class Person{
public:
Person(string name, int age):m_name(name),m_age(age){}
string m_name;
int m_age;
};
shared_ptr<Person> ptr2 = make_shared<Person>("zhangsan", 1);
  • T:模板参数的数据类型
  • Args&&... args :要初始化的数据,如果是通过 make_shared 创建对象,需按照构造函数的参数列表指定

reset

1
2
3
4
5
6
7
8
9
10
11
12
void reset() noexcept;

template<class T>
void reset(T* ptr);

template<class T, class Deleter>
void reset(T* ptr, Deleter d);

template<class T, class Deleter, class Alloc>
void reset(T* ptr, Deleter d, Alloc alloc);

ptr5.reset(new int(250));
  • ptr:指向要取得所有权的对象的指针
  • d:指定删除器,当不再使用这个共享指针的时候,就调用该删除器销毁对象
  • alloc:内部存储所用的分配器

==综合示例==

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
class Test {
public:
Test() {}
Test(int x) {
cout << "construct Test, x = " << x << endl;
}
Test(string str) {
cout << "construct Test, str = " << str << endl;
}
~Test() {}
};

int main()
{
shared_ptr<int> ptr1(new int(520));
//调用拷贝构造函数
shared_ptr<int> ptr2(ptr1);
shared_ptr<int> ptr3 = ptr1;

//调用移动构造函数
shared_ptr<int> ptr4(std::move(ptr1));
shared_ptr<int> ptr5 = std::move(ptr2);

// make_shared 初始化
shared_ptr<int> ptr6 = make_shared<int>(520);
shared_ptr<Test> ptr7 = make_shared<Test>("zhangsan");
shared_ptr<Test> ptr8 = make_shared<Test>(520);

// reset 初始化
ptr4.reset(); // 这样之后这个指针释放空间,不指向任何
ptr5.reset(new int(250));
ptr8.reset(new int(250)); // 错误,类型不一致,一个为 Test 类,一个为 int
}

内部函数

1
2
3
// 管理当前对象的 shared_ptr 实例数量,或若无被管理对象则为 0。
long use_count() const noexcept;
ptr.use_count; // 表示这个内存被多少个 shared_ptr 指针管理
1
2
3
4
5
// 获取原始指针(最开始的指针,指向空间的),也就是在智能指针管理之前的管理内存的指针
T* get() const noexcept;

shared_ptr<int> ptr1(new int(520));
int *p = ptr1.get();
1
2
Test *t = ptr.get();
t->setvalue(100); // 假设 Test 类具有这个 setvalue
  • 上面 reset 和内部函数方法使用 . ,而这里使用 -> 本质是内部重载了这些符号,这是个类模板,这些方法是内部成员函数,使用 .
  • 而指针的使用是->,是为了方便我们把他就当成指针运算,方便记忆和使用,所以内部重载了->,使其和指针一样

注意点

1
2
3
4
5
int *a = new int(10);  
shared_ptr<int>ptr(a);
*ptr = 100;
cout << *a << endl; // 输出 100
cout << ptr.use_count(); // 输出 1
  • 说明这个只是统计智能指针的数量,使用智能指针修改数据,实际也可改变,说明指针确实在管理内存

指定删除器

智能指针不指定删除器,会自动调用内部的删除器,将其内存释放,但也可指定

1
2
3
4
shared_ptr<int> ptr(new int(250), [](int *t){
delete t;
});
// 这样直接结束后,调用析构函数,就会调用这个 lambda 构成的函数

但是数组不具有默认的删除器,因此需指定(C++11 不会,C++11 以后可以了)

1
2
3
shared_ptr<int> ptr(new int[10], [](int* p) {
delete []p;
});
  • 同时,编译器也提供了删除器的模板函数
1
2
3
std::default_delete<T>();

shared_ptr<int> ptr(new int[10], default_delete<int []>());

unique_ptr 独占

只允许一个智能指针管理内存,但是不包括普通指针不能管理

初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
unique_ptr<int> func() {
return unique_ptr<int>(new int(520));
}

int main()
{
// 通过构造函数初始化
unique_ptr<int> ptr1(new int(10));
// 通过转移所有权的方式初始化
unique_ptr<int> ptr2 = std::move(ptr1);
// 即将释放的独占智能指针
unique_ptr<int> ptr3 = func();
// reset
ptr1.reset();
ptr2.reset(new int(250));
}
// 同样支持 get 方法

注意

1
2
3
4
5
6
7
8
int main()  
{
int *a = new int(10);
unique_ptr<int>ptr(a); // 这个定义是正确的
*ptr = 100;
cout << *a << endl; // 输出 100
}
// 说明普通指针也能指向,独占智能指针也在管理内存

指定删除器

语法和共享的类似

展示区别

1
2
3
4
5
6
7
8
9
shared_ptr<int> ptr1(new int(10), [](int*p) {delete p; });	// 正确
unique_ptr<int> ptr1(new int(10), [](int*p) {delete p; }); // 错误

int main()
{
// 这样才正确,需要指定删除器函数类型,这里就是函数指针类型
using func_ptr = void(*)(int*);
unique_ptr<int, func_ptr> ptr1(new int(10), [](int*p) {delete p; });
}
  • 如果上述的 lambda 表达式中 [] 里面传参数,就又不对了,捕捉了外部变量,此时 lambda 就不能看作是函数指针了,此时是一个仿函数
  • lambda 作为仿函数类型 我们不方便指定其类型
  • 使用包装器解决或者不使用 lambda 表达式,就使用一个函数,然后就能指定类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void deleteInt(int* p) { 
delete p;
}

int main()
{
// 错误演示
using func_ptr = void(*)(int*);
unique_ptr<int, func_ptr> ptr1(new int(10), [=](int*p) {delete p; });

// 正确,使用包装器解决
unique_ptr<int, function<void(int*)>> ptr1(new int(10), [&](int*p) {delete p; });
// 正确,使用定义的函数解决
using func_ptr = void(*)(int*);
unique_ptr<int, func_ptr> ptr1(new int(10), deleteInt);
}
// 对捕捉变量后的 lambda 表达式不是函数指针疑惑的,详情见 lambda 表达式章节
1
2
3
// 独占的 C++11 就支持对数组调用默认删除器
// 而共享的 C++11 后支持
unique_ptr<int[]> ptr(new int [3]);

weak_ptr 弱引用

它不共享指针,不能操作资源,是用来监视 shared_ptr

  • 弱引用智能指针 std::weak_ptr 可以看做是 shared_ptr 的助手,它不管理 shared_ptr 内部的指针
  • std::weak_ptr 没有重载操作符 *->,因为它不共享指针,不能操作资源,所以它的构造不会增加引用计数(指向当前内存空间的智能指针数量),析构也不会减少引用计数,它的主要作用就是作为一个旁观者监视 shared_ptr 中管理的资源是否存在

初始化

语法:

1
2
3
4
5
6
7
// 默认构造函数
constexpr weak_ptr() noexcept;
// 拷贝构造
weak_ptr (const weak_ptr& x) noexcept;
template <class T> weak_ptr (const weak_ptr<T>& x) noexcept;
// 通过shared_ptr对象构造
template <class T> weak_ptr (const shared_ptr<T>& x) noexcept;
1
2
3
4
5
6
7
shared_ptr<int> sp(new int (10));

weak_ptr<int> wp1 = new int (11); // 错误,需要在 shared_ptr 基础上创建,监视作用
weak_ptr<int> wp2(sp);
weak_ptr<int> wp3(wp2);
weak_ptr<int> wp4 = sp;
weak_ptr<int> wp5 = wp3;

内部函数

use_count()

1
2
// 函数返回所监测的资源的引用计数
long int use_count() const noexcept;

weak_ptr 只是监测资源,并不管理资源,所以不会导致计数加一

expired()

1
2
// 返回 true 表示 shared_ptr 指向的资源已经被释放, 返回 false 表示资源没有被释放
bool expired() const noexcept;

示例

1
2
3
4
5
6
shared_ptr<int> shared(new int(10));
weak_ptr<int> weak(shared);
cout << "1. weak " << (weak.expired() ? "is" : "is not") << " expired" << endl;

shared.reset();
cout << "2. weak " << (weak.expired() ? "is" : "is not") << " expired" << endl;

执行结果:

1
2
1. weak is not expired
2. weak is expired

lock()

1
2
// 获取所监测资源的 shared_ptr 对象
shared_ptr<element_type> lock() const noexcept;

reset()

1
2
// 清空对象,使其不检测任何资源
void reset() noexcept;

解决 shared_ptr 遗留的问题

不能使用一个原始地址初始化多个共享智能指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Test {  
Test(){};
~Test() {
cout << "class Test is disstruct ..." << endl;
}
};

int main()
{
Test *t = new Test;
shared_ptr<Test> ptr1(t);
cout << "use_count: " << ptr1.use_count() << endl;
shared_ptr<Test> ptr2(t);
cout << "use_count: " << ptr2.use_count() << endl;
return 0;
}

执行结果:

1
2
3
4
use_count: 1
use_count: 1
析构了
析构了
  • 调用了两次析构,一个是 ptr1 ,一个是 ptr2
  • 这就有问题,一块空间释放了两次
  • 这里的错误是使用了原始地址 t 去初始化了多个共享智能指针,正确写法是 shared_ptr<Test> ptr2(ptr1);
  • 还有人可能疑惑第一行 Test *t = new Test 为啥没有析构函数调用
1
2
3
4
5
6
Test *t = new Test;  
delete t;
// 因为是创建在堆区,需要手动释放,写 delete 就会调用析构函数

Test t;
// 这样写,生命周期结束后就会自动调用析构函数

函数不能返回管理了 this 的共享智能指针

需求是通过成员函数返回管理当前对象的共享智能指针

  • 错误实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct Test {
shared_ptr<Test> getSharedPtr() {
return shared_ptr<Test>(this);
}

~Test() {
cout << "析构函数调用" << endl;
}
};

int main()
{
shared_ptr<Test> ptr1(new Test);
cout << "use_count: " << ptr1.use_count() << endl;
shared_ptr<Test> ptr2 = ptr1->getSharedPtr();
cout << "use_count: " << ptr2.use_count() << endl;
return 0;
}
1
2
3
4
use_count: 1
use_count: 1
析构函数调用
析构函数调用
  • 这本质和上面那个 错误一致,都是使用了原始地址初始化了两次智能指针,导致出错
  • 因为返回 shared_ptr ,在成员函数里使用的是 this 初始化 shared_ptr,再赋值给 ptr2,而 this 就是 ptr1 初始化的 new Test,所以就导致 new Test 的地址分配给 ptr1ptr2
  • 就和第一个一样的问题了

解决方案就是通过 weak_ptr 来解决,通过 wek_ptr 返回管理 this 资源的共享智能指针对象 shared_ptr

1
2
std::enable_shared_from_this<T>   // 模板类
shared_from_this() // 所调用的成员方法
  • 它的本质实现是使用了 weak_ptr 里的 lock() 方法实现,返回 shared_ptr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct Test : public enable_shared_from_this<Test> {
shared_ptr<Test> getSharedPtr() {
return shared_from_this();
}

~Test() {
cout << "析构函数调用" << endl;
}
};

int main()
{
shared_ptr<Test> ptr1(new Test);
cout << "use_count: " << ptr1.use_count() << endl;
shared_ptr<Test> ptr2 = ptr1->getSharedPtr();
cout << "use_count: " << ptr2.use_count() << endl;
return 0;
}

执行结果:

1
2
3
use_count: 1
use_count: 2
析构函数调用
  • 在调用 enable_shared_from_this 类的 shared_from_this () 方法之前,必须要先初始化函数内部 weak_ptr 对象,否则该函数无法返回一个有效的 shared_ptr 对象

weak_ptr 是如何隐式创建的?

  • 当你使用 shared_ptr 创建对象并且该对象的类继承了 enable_shared_from_this,在对象创建时,enable_shared_from_this 内部会生成一个指向该对象的 weak_ptr。这个 weak_ptr 用来跟踪当前对象,确保 shared_from_this() 返回的 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
28
29
30
31
32
33
struct TA;
struct TB;

struct TA {
shared_ptr<TB> bptr;
~TA() {
cout << "class TA is disstruct ..." << endl;
}
};

struct TB {
shared_ptr<TA> aptr;
~TB() {
cout << "class TB is disstruct ..." << endl;
}
};

void testPtr()
{
shared_ptr<TA> ap(new TA);
shared_ptr<TB> bp(new TB);
cout << "TA1 use_count: " << ap.use_count() << endl;
cout << "TB1 use_count: " << bp.use_count() << endl;

ap->bptr = bp;
bp->aptr = ap;
cout << "TA2 use_count: " << ap.use_count() << endl;
cout << "TB2 use_count: " << bp.use_count() << endl;
}

int main() {
testPtr();
}
1
2
3
4
TA1 use_count: 1
TB1 use_count: 1
TA2 use_count: 2
TB2 use_count: 2
  • 会发现没有析构函数的调用,但正确的应该有两个
  • 这个原因是循环了,最终的计数都为 2,但是当 testPtr() 函数执行完后,apbp 对象生命周期结束,那么它们所指向的内存空间智能指针计数都减一,最终空间计数都为 1
  • 此时不会析构,因为智能指针析构的判断依据是当前的内存空间指针指针计数为 0,此时就会发生内存泄漏

解决办法就是让类里面的其中一个 shared_ptr 定义为 weak_ptr,这样就打破了环的局面

记住 weak_ptr 不会让智能指针计数增加

1
2
3
4
5
6
7
struct TA {
weak_ptr<TB> bptr;
~TA() {
cout << "class TA is disstruct ..." << endl;
}
};
.... // 其余代码和上面一样
1
2
3
4
5
6
TA1 use_count: 1
TB1 use_count: 1
TA2 use_count: 2
TB2 use_count: 1
class TB is disstruct ...
class TA is disstruct ...

分析这个如何打破的

  • weak_ptr 不会让智能指针计数增加,所以最终结果就是执行结果所示
  • testPtr() 函数执行完毕,apbp 对象生命周期结束,对应的内存空间计数减一,此时 bp 所指向的空间智能指针计数为 0,所以那块空间被释放,调用了析构函数,所以输出了 class TB is disstruct ...
  • 由于 TB 被析构,那么内部成员 aptr 也被析构,那么所指向的 TA 的空间的智能指针计数再减一,此时为 0,就调用了 TA 的析构函数,空间释放,输出 class TA is disstruct ...

可能还有小伙伴考虑为啥不直接删除其中一个 shared_ptr 成员变量,我也考虑到了,hh 还没想明白,但是需要知道 shared_ptr 是有循环引用风险的这里需注意,weak_ptr 是可以打破的 然后还能作为旁观者,应该是有它妙处的,以后遇到就明白了,不深究了


说明:本文是在 https://subingwen.cn/ 学习过程中的总结,这个 up 主 B 站讲得很好 !!