C++的智能指针-unique_ptr,shared_ptr,weak_ptr

C++的智能指针-unique_ptr,shared_ptr,weak_ptr

智能指针基本介绍和特点

​ C++11以后出现了智能指针这个新东西,他的出现主要是为了更方便的进行资源内存管理。程序员使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理,例如new一个对象,delete一个对象。

​ 他的底层实现方法就是通过一个class来实现,把指针封装成了一个类。这个类的构造函数中传入一个普通指针,析构函数中释放传入的指针。智能指针的类都是栈上的对象,所以当函数(或程序)结束时会自动被释放,即智能指针的析构函数会delete它所指向的对象从而自动回收内存。

​ 总的来说智能指针有以下几个特点:

  1. 智能指针是利用了一种叫做RAII(资源获取即初始化)的技术对普通的指针用类进行封装,这使得智能指针实质是一个对象,行为表现的却像一个指针。
  2. 智能指针的作用是防止忘记调用delete释放内存和程序异常的进入catch块忘记释放内存。另外指针的释放时机也是非常有考究的, 多次释放同一个指针会造成程序崩溃(注:因为一旦一个内存空间被释放后,如果后续程序没有结束,继续申请内存,很可能申请到之前释放了的内存。那么,这时候如果通过之前没有清除干净的指针进行了删除等的操作,就会对现在本来有用的内存造成影响。),这些都可以通过智能指针来解决。
  3. 智能指针还有一个作用是把值语义转换成引用语义。

​ 什么是值语义?object a; object b = a; a和b是两个对象。

​ 什么是引用语义? object a; object b = a; a和b是同一个对象(Java中是这样的)。

使用方法

unique_ptr,shared_ptr,weak_ptr都包含在头文件<memory>

unique_ptrmake_unique

unique_ptr“唯一地”拥有其所指对象,唯一地占有所拥有地资源,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现),下面是一个小例子:

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

int main() {
{
unique_ptr<int> uptr(new int(10)); //绑定动态对象
//std::unique_ptr<int> uptr2 = uptr; //不能赋值,若赋值成功,则uptr2和uptr都可以对指针指向的同一块地址写东西。
//std::unique_ptr<int> uptr2(uptr); //不能拷贝,同理和上面一样。
unique_ptr<int> uptr2 = move(uptr); //转换所有权
uptr2.release(); //释放所有权
}
//超过uptr的作用域,內存释放
}

同时这里我们提出一个建议:请优先选用std::make_unique和std::make_shared,而非直接new:

​ c++14加入了std::make_unique,它可以取代new并且无需delete pointer,有助于代码管理。同时在《Effective Modern C++》条款21写到:请优先选用std::make_unique和std::make_shared,而非直接new。更多请看下面的文章

《Effective Modern C++》学习笔记之条款二十一:优先选用std::make_unique和std::make_shared,而非直接new - 知乎 https://zhuanlan.zhihu.com/p/355238160

std::make_unique()用法如下:

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
#include <iostream>
#include <memory>
using namespace std;

class ResourceType
{
public:
void fun()
{
cout<<"fun called"<<endl;
}
};

int main() {
{
//绑定对象方法1 : 上面我们一开始用的方法,但我们更建议用下面三种(方法2,3,4)利用make_unique绑定的方法,原因在上面有提到;
unique_ptr<ResourceType> uptr1(new ResourceType);

//绑定对象方法2: 注意这是一个移动构造函数,而并非拷贝构造函数,拷贝构造函数在unique_ptr中已经禁用掉了
unique_ptr<ResourceType> uptr2(make_unique<ResourceType>());
//绑定对象方法3: 注意这是一个移动构造函数,而并非拷贝构造函数,拷贝构造函数在unique_ptr中已经禁用掉了
unique_ptr<ResourceType> uptr3 = make_unique<ResourceType>();
//绑定对象方法4 : 方法2,3中的unique_ptr<int>可以用一个语法糖auto代替,以减少代码量。
auto uptr4(make_unique<ResourceType>());
auto uptr5 = make_unique<ResourceType>();

}
//超过uptr的作用域,內存释放
}

既然unique_ptr中禁用掉了拷贝构造函数,那么怎么把一个unique_ptrA的所有权转交给另一个unique_ptrB,并使得A不再指向原来所指的东西呢?

通过std::move()即把A的内存空间直接交给B,如下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ResourceType
{
public:
void fun()
{
cout<<"fun called"<<endl;
}
};
int main()
{

unique_ptr<ResourceType>uptrA(make_unique<ResourceType>());
unique_ptr<ResourceType>uptrB = move(uptrA);//转换所有权
uptrB.release(); //释放所有权
return 0;
}

同理,如果你想用vector<unique_ptr<ResourceType>>ResourceVec,那么可以使用ResourceVec.push_back(move(...))来向里面塞unique_ptr智能指针。

shared_ptrmake_shared

shared_ptr是多个指针指向相同的对象。shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。

  • 初始化: 智能指针是个模板类,可以指定类型,传入指针通过构造函数初始化。也可以使用make_shared函数初始化。不能将指针直接赋值给一个智能指针,一个是类,一个是指针。例如std::shared_ptr<int> p4 = new int(1);的写法是错误的,但是std::shared_ptr<int> p4(new int(1));是正确的。
  • 拷贝和赋值。拷贝使得对象的引用计数增加1,赋值使得原对象引用计数减1,当计数为0时,自动释放内存。后来指向的对象引用计数加1,指向后来的对象。
  • get()函数获取原始指针

  • use_count()引用计数次数

1
2
3
4
5
6
7
8
9
10
11
12
13
int main()
{

//初始化
int a = 10;
shared_ptr<int> ptra1 = make_shared<int>(a);
shared_ptr<int> ptra2(ptra1); //这个是拷贝构造函数实现的
//use_count()
cout << ptra1.use_count() << ptra2.use_count() << endl;//输出引用计数都是:2 , shared_ptr存着ptra1和ptra2。
//get()
cout << ptra2.get() << " "<< ptra1 << " " << ptra2 <<endl; //输出: 0x1f17a0 0x1f17a0 0x1f17a0,因为这是一个shared_ptr,所以他们指向同一片地址,即指针相同。
return 0;
}
  • reset()函数删除调用者的引用

​ 首先我们要清楚这个是干嘛的,当一个shared_ptr调用这个函数,那么shared_ptr就会引用减去1。代码例子在下面:

image-20220216012011270

查阅cpp reference,可以看出这个shared_ptr类有3个偏特化的模板:

其中提到了第2,3,4种模板在use_count为1时调用reset函数,且函数里含有指针,可以使得:

1.引用计数减1,引用计数为0

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
27
28
class Person
{
public:
Person(int v) {
value = v;
cout << "Cons" <<value<< endl;
}
~Person() {
cout << "Des" <<value<< endl;
}

int value;

};

int main()
{
shared_ptr<Person> p1(new Person(1));// Person(1)的引用计数为1

p1.reset(new Person(3));// 首先生成新对象,然后引用计数减1,引用计数为0,故析构Person(1)
// 最后将新对象的指针交给智能指针

shared_ptr<Person> p3 = p1;//现在p1和p3同时指向Person(3),Person(3)的引用计数为2

p1.reset();//Person(3)的引用计数为1
p3.reset();//Person(3)的引用计数为0,析构Person(3)
return 0;
}

shared_ptr的需要注意的点/缺点:

  • 注意不要用一个原始指针初始化多个shared_ptr,否则会造成二次释放同一内存。这是因为多个shared_ptr互不相关,当一个shared_ptr的引用计数为0时,调用delete使得释放一次内存,如果其他的shared_ptr同样指向了这片内存,当其他的shared_ptr引用次数为0时就会再次调用delete释放内存,导致二次释放同一内存。

    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
    class Person
    {
    public:
    Person(int v) {
    value = v;
    cout << "Cons" <<value<< endl;
    }
    ~Person() {
    cout << "Des" <<value<< endl;
    }

    int value;

    };

    int main()
    {

    Person* a = new Person(1);
    //一个原始指针a初始化多个shared_ptr
    shared_ptr<Person> p1(a);
    shared_ptr<Person> p2(a);
    p1.reset();//释放了a的内存
    p2.reset();//再次释放
    return 0;
    }
  • 注意避免循环引用,shared_ptr的一个最大的陷阱是循环引用,循环引用会导致堆内存无法正确释放,导致内存泄漏。循环引用在weak_ptr中介绍。

weak_ptr

 weak_ptr是为了配合shared_ptr而引入的一种智能指针,因为它不具有普通指针的行为,没有重载operator*->,它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况。weak_ptr可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。使用weak_ptr的成员函数use_count()可以观测资源的引用计数,另一个成员函数expired()的功能等价于use_count()==0,但更快,表示被观测的资源(也就是shared_ptr的管理的资源)已经不复存在。weak_ptr可以使用一个非常重要的成员函数lock()从被观测的shared_ptr获得一个可用的shared_ptr对象, 从而操作资源。但当expired()==true的时候,lock()函数将返回一个存储空指针的shared_ptr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main()
{

shared_ptr<int> sh_ptr = std::make_shared<int>(10);
cout << sh_ptr.use_count() << endl;// 输出: 1

weak_ptr<int> wp(sh_ptr);
cout << wp.use_count() << endl;// 输出: 1

if(!wp.expired())//如果 shared_ptr的引用次数不是0
{
shared_ptr<int> sh_ptr2 = wp.lock(); //从被观测的shared_ptr获得一个可用的shared_ptr对象
*sh_ptr2 = 100;
cout << wp.use_count() << endl;// 输出: 2
}

}

技术/实现细节

这里暂时不谈,不过这里有一个对shared_ptr简单的实现可以看一下,但是它还没有对reset()进行实现:

​ 下面是一个简单智能指针的demo。智能指针类将一个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象共享同一指针。每次创建类的新对象时,初始化指针并将引用计数置为1;当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数;对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)。智能指针就是模拟指针动作的类。所有的智能指针都会重载 -> 和 * 操作符。智能指针还有许多其他功能,比较有用的是自动销毁。这主要是利用栈对象的有限作用域以及临时对象(有限作用域实现)析构函数释放内存。

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#include <iostream>
#include <memory>

template<typename T>
class SmartPointer {
private:
T* _ptr;
size_t* _count;
public:
SmartPointer(T* ptr = nullptr) :
_ptr(ptr) {
if (_ptr) {
_count = new size_t(1);
} else {
_count = new size_t(0);
}
}

SmartPointer(const SmartPointer& ptr) {
if (this != &ptr) {
this->_ptr = ptr._ptr;
this->_count = ptr._count;
(*this->_count)++;
}
}

SmartPointer& operator=(const SmartPointer& ptr) {
if (this->_ptr == ptr._ptr) {
return *this;
}

if (this->_ptr) {
(*this->_count)--;
if (this->_count == 0) {
delete this->_ptr;
delete this->_count;
}
}

this->_ptr = ptr._ptr;
this->_count = ptr._count;
(*this->_count)++;
return *this;
}

T& operator*() {
assert(this->_ptr == nullptr);
return *(this->_ptr);

}

T* operator->() {
assert(this->_ptr == nullptr);
return this->_ptr;
}

~SmartPointer() {
(*this->_count)--;
if (*this->_count == 0) {
delete this->_ptr;
delete this->_count;
}
}

size_t use_count(){
return *this->_count;
}
};

int main() {
{
SmartPointer<int> sp(new int(10));
SmartPointer<int> sp2(sp);
SmartPointer<int> sp3(new int(20));
sp2 = sp3;
std::cout << sp.use_count() << std::endl;
std::cout << sp3.use_count() << std::endl;
}
//delete operator
}