C++智能指针

文章目录

为了解决指针资源忘记或没有删除导致的内存泄露问题,C++就出现了智能指针的机制,可以在使用的时候初始化,在离开作用域之后就自动析构,删除资源

C++98的智能指针

auto_ptr的模拟实现

auto_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
46
47
48
49
50
51
52
53
54
55
56
namespace xzw
{
template <class T>
class auto_ptr
{
private:
T *_ptr;

public:
//在构造函数
auto_ptr(T *ptr)
: _ptr(ptr) //原生指针支持拷贝
{
}
~auto_ptr()
{
if (_ptr)
{
cout << "delete" << endl;
delete _ptr; //析构函数把他清理
}
}

//如何解决拷贝问题,管理权转移,只有一个人析构它
auto_ptr(auto_ptr<T> &sp)
: _ptr(sp._ptr)
{
//资源转给你,
//把管理权转移
sp._ptr = nullptr; //自己置成空,自己没了,
}

T &operator*()
{
return *_ptr;
}
T *operator->() //返回原生指针即可
{
return _ptr;
}
};
};
void demo1()
{
int *p1 = new int;
xzw::auto_ptr<int> sp1(p1); //拷贝构造,出了作用域就调用他们的析构函数,抛异常的话也会出作用域,也就自动调用析构函数,
int *p2 = new int;
xzw::auto_ptr<int> sp2(sp1); //拷贝构造
xzw::auto_ptr<int> sp3(new int); //直接用new出来的资源给它
*sp3 = 10;
cout << __LINE__ << *sp3 << endl;
cout << *sp1 << endl; //出现空指针问题
//希望能够像指针一样使用,重载以下operator*

//结论就是auto_ptr是一个失败的设计.很多公司明确要求不能使用auto_ptr
}

实际工作中绝对不能使用auto_ptr

C++11的智能指针

定制删除器

默认情况下,智能指针底层的删除器都是用delete
但是不同的资源销毁的方式不同,直接用delete十分暴力,不合理,所以就有了定制删除器

比如:

  • malloc -> free
  • open -> close
  • fopen -> fclose
  • new[] -> delete[]

我们在下文会详细解释

unique_ptr的模拟实现

unique_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
46
47
48
49
50
namespace Uni_Ptr
{

template <class T>
class defult_delete
{
public:
void operator()(const T* ptr)
{
cout<<__LINE__<<endl;
cout<<"delete"<<endl;
delete ptr;
}
};
template <class T, class D=default_delete<T>>//默认释放这个类型,在模板里面调用的不是仿函数,而是对应的类型

//原理简单粗暴,——防拷贝,直接不让你拷贝
class unique_ptr
{
private:
T *_ptr;
unique_ptr(const unique_ptr<T> &sp) = delete; //直接把它拷贝给弄掉
public:
//在构造函数
unique_ptr(T *ptr)
: _ptr(ptr) //原生指针支持拷贝
{
}
~unique_ptr()
{
if (_ptr)
{
// cout << "delete" << endl;
// delete _ptr; //析构函数把他清理
D del;
del(_ptr);//默认的情况就是用default_delete,
}
}

T &operator*()
{
return *_ptr;
}
T *operator->() //返回原生指针即可
{
return _ptr;
}
};
};

1
2
3
4
5
6
void demo2()
{
Uni_Ptr::unique_ptr<int> sp1(new int);
// Uni_Ptr::unique_ptr<int> sp2(sp1);
// std::unique_ptr<int> sp(sp1);//不支持拷贝构造
}

unique_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
template <class T>
struct DeleteArray
{
void operator()(const T *ptr)
{
cout << "delete[]" << endl;
delete[] ptr;
}
};

#include<cstdio>
#include<stdio.h>
struct DeleteFile
{
void operator()(FILE *ptr)
{
cout << "fclose:" << endl;
fclose(ptr);
}
};


void demo6()
{
//定制删除器
//默认情况下,智能指针在底层都是用delete
//那么如果不是new 出来,如new[],malloc,fopen
//unque_ptr是在类的模板参数里面(类型)
Uni_Ptr::unique_ptr<Date> s(new Date);
Uni_Ptr::unique_ptr<Date, DeleteArray<Date>> s1(new Date[10]); //我们可以显示定制删除器
Uni_Ptr::unique_ptr<FILE,DeleteFile> s2(fopen("1.txt","w"));//我们这里用fopen,要自己特制一个删除器

}

shared_ptr的模拟实现

shared_ptr是为了解决unique_ptr无法实现拷贝构造
新增加了一个引用计数的机制:

同一个对象只能有一个引用计数,当调用构造函数的时候,第一个引用计数出现为1,后续如果有发生拷贝构造,引用计数就+1,当析构的时候,引用计数就-1,直到引用计数为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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
namespace Shared_Ptr
{
//我们要实现定制删除器

template <class T>
class defult_delete
{
public:
void operator()(const T* ptr)
{
cout<<__LINE__<<endl;
delete ptr;
}
};
template <class T, class D=default_delete<T>>//默认释放这个类型,在模板里面调用的不是仿函数,而是对应的类型
class shared_ptr
{
private:
T *_ptr;
// static int _refcout; //这样就只有一个了,属于所有对象共享,但是要在类外面初始化,所以我们应该一个资源配一个引用计数,因为static是属于类的
int *_pRefCount; //这个就是引用计数,凡是用浅拷贝都是要用引用计数,才能搞定这个东西
//一个引用计数就要管理一个锁
mutex *_mtx; // 这样就可以访问同一个锁
public:
//在构造函数,调用一次构造函数引用计数就加1
shared_ptr(T *ptr)
: _ptr(ptr), _pRefCount(new int(1)), _mtx(new mutex) //原生指针支持拷贝,用指针,不同的人就可以指向同一个资源

{
//只有第一个人会调用构造函数
// _refcout=1;//就为1,管理不同的资源就会把别人的给修改了,
}

shared_ptr(const shared_ptr<T> &sp) //调用拷贝构造
: _ptr(sp._ptr), _pRefCount(sp._pRefCount), _mtx(sp._mtx) //引用计数也拷贝过去
{
// ++_refcout;
// ++(*_pRefCount); //因为是同一个资源,所以就可以对它进行++,
AddRef();
}
~shared_ptr()
{
Release();
}
T *get() const
{
return _ptr;
}
//如何解决拷贝问题,管理权转移,只有一个人析构它

T &operator*()
{
return *_ptr;
}
T *operator->() //返回原生指针即可
{
return _ptr;
}
void Release()
{
_mtx->lock();
//管理临界区
(*_pRefCount)--; //析构的时候,把引用计数--
bool flag = false;
if (!(*_pRefCount) && _ptr)
{
cout << "delete" << endl;
delete _ptr; //析构函数把他清理
delete _pRefCount;
_ptr = nullptr;
_pRefCount = nullptr;
flag = true;
// delete是把原来对应的空间给释放掉,但是对应的指针还在堆区里面,所以还能用
}
_mtx->unlock();
if (flag)
{
delete _mtx;
_mtx = nullptr;
}
}
void AddRef()
{
_mtx->lock();
(*_pRefCount)++;
_mtx->unlock();
}
shared_ptr<T> &operator=(const shared_ptr<T> &sp) //这个就牵扯到两个资源,
{
if (_ptr != sp._ptr) //自己给自己赋值
{
//原来的引用计数减一
Release();
_ptr = sp._ptr; // delete之后指针还在,指向不同的空间就行了
_pRefCount = sp._pRefCount; //现在引用计数也要和其相等
_mtx = sp._mtx;
AddRef();
}
return *this;
}
};
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void demo3()
{
Shared_Ptr::shared_ptr<int> sp(new int);
Shared_Ptr::shared_ptr<int> sp1(sp);
Shared_Ptr::shared_ptr<int> sp2(sp);
Shared_Ptr::shared_ptr<int> sp3(sp);
Shared_Ptr::shared_ptr<int> sp4(new int(3)); //这样就可以做到构造的对象就只析构一次
sp1 = sp4;
sp2 = sp4;
sp = sp4;
// sp3=sp4;

//指向的堆上资源的线程安全的问题是访问的人处理的,智能指针不管
//引用计数的线程安全问题是智能指针要处理的

*sp4 = 3;
*sp = 4;

cout << (*sp3) << endl;
cout << __LINE__ << endl;
}
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
struct Date
{
int _year = 1;
int _month = 1;
int _day = 1;
};

void SharePtreFunc(Shared_Ptr::shared_ptr<Date> &sp, size_t n, mutex &mtx)
{
for (int i = 0; i < n; i++)
{
Shared_Ptr::shared_ptr<Date> copy(sp);
//访问临界资源,加锁

{
//这里我们可以括一个匿名域
unique_lock<mutex> lock(mtx); //这个支持中间解锁

copy->_day++;
copy->_month++;
copy->_year++;
//或者unlock
lock.unlock();
}

cout << "hello" << endl; //这个我不想管它的线程安全
}
}

//智能指针是线程安全的
//因为引用计数的加减都是加锁保护的,但是指向的资源不是线程安全的,要我们自己手动处理

void demo4()
{
Shared_Ptr::shared_ptr<Date> p(new Date);
const size_t n = 10000000;
mutex mtx;
thread t1(SharePtreFunc, std::ref(p), n, std::ref(mtx)); //这样就有线程安全问题,两个线程同时去++,就会有问题,
thread t2(SharePtreFunc, std::ref(p), n, std::ref(mtx));

t1.join();
t2.join();
cout << p->_day << endl;
cout << p->_month << endl;
cout << p->_year << endl;
}

shared_ptr对于定制删除器的使用:就是在构造函数的时候加上,该可调用对象(函数指针,仿函数,lambda表达式)
使用lambda表达式最方便

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
template <class T>
struct DeleteArray
{
void operator()(const T *ptr)
{
cout << "delete[]" << endl;
delete[] ptr;
}
};

#include<cstdio>
#include<stdio.h>
struct DeleteFile
{
void operator()(FILE *ptr)
{
cout << "fclose:" << endl;
fclose(ptr);
}
};


void demo6()
{


//删除器在构造函数里面给对象
std::shared_ptr<Date> srp(new Date);
std::shared_ptr<Date> srp4(new Date[10],DeleteArray<Date>());

//使用lambda表达式就更加方便了

std::shared_ptr<Date> srp3(new Date[10],[](Date* ptr){cout<<"delete []"<<endl;
delete[] ptr;});

std::shared_ptr<FILE> srp1(fopen("1.txt","w"),DeleteFile());//在构造函数里面传删除器
std::shared_ptr<FILE> srp2(fopen("1.txt","w"),[](FILE* ptr){fclose(ptr);});//在构造函数里面传删除器

}

weak_ptr

shared_ptr会出现循环引用的问题
一个指针,你里面有一个智能指针,指向我,我里面有一个智能指针指向你,这就是循环引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct ListNode
{
int _val;

shared_ptr<ListNode> _prev;
shared_ptr<ListNode> _next;
};

void demo5()
{
shared_ptr<ListNode> n1(new ListNode);
shared_ptr<ListNode> n2(new ListNode);
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
//这样就能解决循环引用的问题,一个指针,你里面有一个智能指针,指向我,我里面有一个智能指针指向你,这就是循环引用

n1->_prev=n2;//这样子会增加引用计数
n2->_next=n1;
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
}

这样子会出现问题

weak_ptr是一个弱指针,没有引用计数的机制,可以支持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
  template <class T>
class weak_ptr
{
private:
T *_ptr;

public:
weak_ptr()
: _ptr(nullptr)
{
}
weak_ptr(const shared_ptr<T> &sp)
: _ptr(sp.get())
{
}
weak_ptr<T> &operator=(const shared_ptr<T> &sp)
{
_ptr = sp.get();
return *this;
}
};

struct ListNode
{
int _val;
Shared_Ptr::weak_ptr<ListNode> _prev; //弱指针,不增加引用计数,只是可以访问指向的节点资源,但是不参与节点资源的释放管理,
Shared_Ptr::weak_ptr<ListNode> _next;

};


void demo5()
{
shared_ptr<ListNode> n1(new ListNode);
shared_ptr<ListNode> n2(new ListNode);
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
//这样就能解决循环引用的问题,一个指针,你里面有一个智能指针,指向我,我里面有一个智能指针指向你,这就是循环引用

n1->_prev=n2;
n2->_next=n1;
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
}

总结一点:如果使用对象的话,就要用强指针,如果要引用一个对象的话,就要用弱指针
即weak_ptr不会参与空间资源的管理,只是作为一个解决循环引用的工具


C++智能指针
http://example.com/2022/08/23/C++智能指针/
作者
Zevin
发布于
2022年8月23日
许可协议