CPP智能指针浅探

本文最后更新于 2024年11月8日 下午

智能指针

智能指针是c++11引入的新特性,其本质是封装了一个原始c++指针的类模板,为了确保动态内存的安全性而产生的。实现原理是通过一个对象存储需要被自动释放的资源,然后依靠对象的析构函数来释放资源。其头文件是#include <memory>

unique_ptr

1、介绍

  • unique_ptr 不共享它的指针。它无法复制到其他 unique_ptr,无法通过值传递到函数,也无法用于需要副本的任何标准模板库 (STL) 算法。只能移动unique_ptr。当需要智能指针用于纯 C++ 对象时,可使用 unique_ptr,而当构造 unique_ptr 时,可使用make_unique 函数。
  • std::unique_ptr 实现了独享所有权的语义。转移一个 std::unique_ptr 将会把所有权也从源指针转移给目标指针(源指针被置空)。拷贝一个 std::unique_ptr 将不被允许,因为如果你拷贝一个 std::unique_ptr ,那么拷贝结束后,这两个 std::unique_ptr 都会指向相同的资源,它们都认为自己拥有这块资源(所以都会企图释放)。因此 std::unique_ptr 是一个仅能移动(move_only)的类型。

2、创建unique_ptr

  • unique_ptr 不像 shared_ptr 一样拥有标准库函数 make_shared 来创建一个 shared_ptr 实例。要想创建一个 unique_ptr,我们需要将一个new 操作符返回的指针传递给 unique_ptr 的构造函数。
  • std::make_unique是C++14才有的特性。
1
2
3
4
5
int main()
{
std::unique_ptr<int> p(new int(5));
cout << *p;
}

3、无法进行复制构造和赋值操作

  • unique_ptr没有copy构造函数,不支持拷贝和赋值操作。
1
2
3
4
5
6
int main()
{
std::unique_ptr<int> p(new int(5));
std::unique_ptr<int> p2(p); // 报错
std::unique_ptr<int> p3 = p; // 报错
}

4、可以进行移动构造和移动赋值操作

  • 可以移交所有权,使用std::move()函数。
1
2
3
4
5
6
7
8
int main()
{
std::unique_ptr<int> p(new int(5));
std::unique_ptr<int> p2 = std::move(p); // 转移所有权
// std::cout << *p << endl; // 报错,p为空
std::cout << *p2 << endl;
unique_ptr<int> p(std::move(p2));
}

5、可以返回unique_ptr

1
2
3
4
5
6
7
8
9
10
11
12
unique_ptr<int> clone(int p)
{
unique_ptr<int> pI(new int(5));
return pI;
}

int main()
{
int p = 5;
unique_ptr<int> ret = clone(p);
cout << *ret << endl;
}

6、unique_ptr一些使用场景

  • 在容器中保存指针
1
2
3
4
5
6
int main()
{
vector<unique_ptr<int>> vec;
unique_ptr<int> p(new int(5));
vec.push_back(std::move(p));
}
  • 管理动态数组
1
2
3
4
5
int main()
{
unique_ptr<int[]> p(new int[5]{1, 2, 3, 4, 5});
p[0] = 0;
}
  • unique_ptr可以不占用对象,即为空。可以通过reset或者赋值nullptr释放管理对象
  • 标准库早期版本中定了auto_ptr,它具有unique_ptr的部分特征,但不是全部。例如不能在容器中保存auto_ptr,不能从函数中返回auto_ptr等等,这也是unique_ptr主要的使用场景。

shared_ptr

1、介绍

shared_ptr是存储动态创建对象的指针,其主要功能是管理动态创建对象的销毁,帮助消除内存泄漏和悬空指针的问题。

其原理是记录对象被引用次数,当引用次数为0时,也就是最后一个指向该对象的共享指针析构的时候,共享指针的析构函数就把指向的内存区域释放掉。

共享指针内存:每个shared_ptr对象在内部指向两个内存位置。

  • 指向对象的指针;
  • 用于控制引用计数的指针。

2、shared_ptr的创建

构建空的智能指针:

1
2
3
std::shared_ptr<int> p1; // 不传入任何实参
std::shared_ptr<int> p2(nullptr); // 传入空指针nullptr
// 空的shared_ptr指针,其初始引用计数为0,而不是1。

明确其指向。

1
2
3
std::shared_ptr<int> p(new int(10));

std::shared_ptr<int> p3 = std::make_shared<int>(10);

还可以使用相应的拷贝构造函数和移动构造函数。

1
2
3
4
// 调用拷贝构造函数
std::shared_ptr<int> p4(p3); // 或者std::shared_ptr<int> p4 = p3;
// 调用移动构造函数
std::shared_ptr<int> p5(std::move(p4)); // 或者std::shared_ptr<int> p5 = std::move(p4);

在初始化shared_ptr智能指针时,还可以自定义所指堆内存的释放规则,这样当堆内存的引用计数为0时,会优先调用自定义的释放规则。

对申请的动态数组,释放规则可以用c++11标准中提供的default_delete模板类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 指定default_delete作为释放规则
std::shared_ptr<int> p6(new int[10], std::default_delete<int[]>());

// 自定义释放规则
void deleteInt(int* p)
{
delete[] p;
}

// 初始化智能指针,并自定义释放规则
std::shared_ptr<int> p7(new int[10], deleteInt);

// 实际上借助lambda表达式,我们还可以像如下这样初始化p7,是完全相同的
std::shared_ptr<int> p7(new int[10], [](int* p) {delete[] p;});

3、share_ptr常用函数

  • get()函数,返回当前存储的指针
1
2
std::shared_ptr<T> p(new T());
T* p1 = p.get(); // 获得传统c指针
  • use_count()函数,当前引用计数
1
2
std::shared_ptr<T> a(new T());
a.use_count(); // 获取当前的引用计数
  • reset()函数,表示重置当前存储的指针
1
2
std::shared_ptr<T> a(new T());
a.reset(); // 此后 a 原先所指的对象会被销毁,并且 a 会变成 NULL
  • operator,表示返回对存储指针指向的对象的引用。它相当于: get()。
  • operator->,表示返回指向存储指针所指向的对象的指针,以便访问其中一个成员。跟get函数一样的效果。

weak_ptr

1、介绍

weak_ptr主要针对shared_ptr的空悬指针和循环引用问题而提出:

  • 空悬指针问题:有两个指针p1和p2,指向堆上的同一个对象Object,p1和p2位于不同的线程中。假设线程A通过p1指针将对象销毁了(尽管把p1置为了NULL),那p2就成了空悬指针。weak_ptr不控制对象的生命期,但是它知道对象是否还活着。如果对象还活着,那么它可以提升为有效的shared_ptr(提升操作通过lock()函数获取所管理对象的强引用指针);如果对象已经死了,提升会失败,返回一个空的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
34
35
36
37
38
39
40
41
42
43
44
45
#include <iostream>
#include <memory>

using namespace std;

class A;
class B;

typedef shared_ptr<A> A_ptr;
typedef shared_ptr<B> B_ptr;

class A
{
public:
~A() {
cout << "~A()" << endl;
}
public:
B_ptr b;
};

class B
{
public:
~B() {
cout << "~B()" << endl;
}
public:
A_ptr a;
};

int main()
{
A_ptr ap(new A);
B_ptr bp(new B);

// 父子互相引用
ap->b = bp;
bp->a = ap;

cout << ap.use_count() << endl; // 引用计数为2
cout << bp.use_count() << endl; // 引用计数为2

return 0;
}

如上代码,将在程序退出前,ap的引用计数为2,bp的计数也为2,退出时,shared_ptr所作操作就是简单的将计数减1,如果为0则释放,显然,这个情况下,引用计数不为0,于是造成ap和bp所指向的内存得不到释放,导致内存泄露。

使用weak_ptr可以打破这样的循环引用。由于弱引用不更改引用计数,类似普通指针,只要把循环引用的一方使用弱引用,即可解除循环引用。以上述代码为例,只要把B类的代码修改为如下即可:

1
2
3
4
5
6
7
8
9
class B
{
public:
~B() {
cout << "~B()" << endl;
}
public:
std::weak_ptr<A> a;
};

最后值得一提的是,虽然通过弱引用指针可以有效的解除循环引用,但这种方式必须在能预见会出现循环引用的情况下才能使用,即这个仅仅是一种编译期的解决方案,如果程序在运行过程中出现了循环引用,还是会造成内存泄漏的。因此,不要认为只要使用了智能指针便能杜绝内存泄漏。

weak_ptr创建

创建一个空weak_ptr

1
2
std::weak_ptr<int> wp;
std::weak_ptr<int> wp1(wp); // 使用已有weak_ptr创建新的ptr

weak_ptr 指针更常用于指向某一 shared_ptr 指针拥有的堆内存,因为在构建 weak_ptr 指针对象时,可以利用已有的 shared_ptr 指针为其初始化。例如:

1
2
std::shared_ptr<int> sp(new int[5]);
std::shared_ptr<int> wp3(sp);

weak_ptr模板类提供的成员方法

  • operator=():重载 = 赋值运算符,是的 weak_ptr 指针可以直接被 weak_ptr 或者 shared_ptr 类型指针赋值。
  • swap(x):其中 x 表示一个同类型的 weak_ptr 类型指针,该函数可以互换 2 个同类型 weak_ptr 指针的内容。
  • reset():将当前 weak_ptr 指针置为空指针。
  • use_count():查看指向和当前 weak_ptr 指针相同的 shared_ptr 指针的数量。
  • expired():判断当前 weak_ptr 指针为否过期(指针为空,或者指向的堆内存已经被释放)。
  • lock():如果当前 weak_ptr 已经过期,则该函数会返回一个空的 shared_ptr 指针;反之,该函数返回一个和当前 weak_ptr 指向相同的 shared_ptr 指针。

CPP智能指针浅探
https://sunz123.github.io/2024/11/05/CPP智能指针浅探/
作者
Leo Sun
发布于
2024年11月5日
更新于
2024年11月8日
许可协议