智能指针类

std::auto_ptr

基本用法

1
2
3
4
5
//第一种形式
std::auto_ptr<int> ap1(new int(1));
//第二种形式
std::auto_ptr<int> ap2;
ap2.reset(new int(1));

智能指针对象ap1和ap2均持有一个在堆上分配int对象,这两块堆内存在对象释放时得以释放。

std::auto_ptr 经常误用的是复制操作,当复制auto_ptr对象时(拷贝复制或operator=复制),原对象的堆内存会转移到复制的对象上,原指针就空了。

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
	std::auto_ptr<int> ap1(new int(1));
//拷贝复制
std::auto_ptr<int> ap2(ap1);
if (ap1.get()!= NULL) {
std::cout << "ap1 is not NULL" << std::endl;
}
else {
std::cout << "ap1 is NULL" << std::endl;
}
if (ap2.get() != NULL) {
std::cout << "ap2 is not NULL" << std::endl;
}
else {
std::cout << "ap2 is NULL" << std::endl;
}
//operator=复制
std::auto_ptr<int> ap3(new int(1));
std::auto_ptr<int> ap4 = ap3;
if (ap3.get() != NULL) {
std::cout << "ap3 is not NULL" << std::endl;
}
else {
std::cout << "ap3 is NULL" << std::endl;
}
if (ap4.get() != NULL) {
std::cout << "ap4 is not NULL" << std::endl;
}
else {
std::cout << "ap4 is NULL" << std::endl;
}

//output
ap1 is NULL
ap2 is not NULL
ap3 is NULL
ap4 is not NULL

ap1的堆内存转移到了ap2,ap3的堆内存转移到了ap4

因此在使用中,应尽量避免使用std::auto_ptr(C++11之后 该指针已被弃用,不应该再使用它)

std::unique_ptr

std::unique_ptr对指向的堆内存具有唯一控制权,引用计数永远为1,对象销毁时释放对应的堆内存。

基本使用:

1
2
3
4
5
6
7
//第一种形式
std::unique_ptr<int> up1(new int(1));
//第二种形式
std::unique_ptr<int> up2;
up2.reset(new int(1));
//第三种形式
std::unique_ptr<int> up3 = std::make_unique<int>(1);

std::unique_ptr禁止复制操作

std::unique_ptr类的拷贝构造函数和赋值operator=函数 都被标记为=delete

禁止复制操作存在特例,即可以通过一个函数返回一个std::unique_ptr:

1
2
3
4
5
6
std::unique_ptr<int> func(int val) {
std::unique_ptr<int> up(new int(val));
return up;
}

std::unique_ptr<int> up4 = func(3); //std::move

实现方法:使用移动构造,即std::move

1
2
std::unique_ptr<int> up5(new int(2));
std::unique_ptr<int> up6(std::move(up5));

std::move将up5持有的堆内存转移给了up6,最后up5不再持有堆内存,即是一个空的智能指针对象。

std::move操作是否有意义,取决于是否实现了移动构造(Move Constructor)和移动赋值(operator=)运算符,而unique_ptr正好实现了这两个函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template<typename T, typename Deletor>
class unique_ptr{
public:
unique_ptr(unique_ptr&& rhs){
this->m_pT = ths.m_pT;
rhs.m_pT = nullptr;
}

unique_ptr& operator=(unique_ptr&& rhs){
this->m_pT = rhs.m_pT;
rhs.m_pT = nullptr;
return *this;
}
private:
T* m_pT;
}

std::unique_ptr不仅可以持有一个堆对象,也可以持有一组堆对象,

1
2
3
4
5
6
7
8
9
10
std::unique_ptr<int[]> up7(new int[10]);
for (int i = 0; i < 5; ++i) {
up7[i] = i;
}

for (int i = 0; i < 5; ++i) {
std::cout << up7[i] << std::endl;
}

//std::unique_ptr<int> up8(std::move(up7[2])); //错误

std::shared_ptr和std::weak_ptr也可以持有一组堆对象,用法与std::unique_ptr一致。

自定义 智能指针对象持有的资源的释放函数

默认情况下,智能指针对象在析构时只会释放其持有的堆内存,调用delete或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
27
28
29
30
31
32
33
#include <iostream>
#include <memory>

class Student {
public:
Student() {
name = new char[10];
sprintf(name, "abc");
}
~Student() {}
//这里应该把delete放入析构中,这里只是为了说明智能指针的相关用法才这么写
void release() {
delete[] name;
name = nullptr;
}
const char* getName() {
return name;
}
private:
char* name;
};

int main(){
auto releaseFunc = [](Student* ps){
std::cout << ps->getName() << std::endl;
ps->release();
delete ps;
};
{
std::unique_ptr<Student, void(*)(Student* ps)> pStudent(new Student(), releaseFunc);
}
return 0;
}

自定义std::unique_ptr的资源释放函数规则是:

1
std::unique_ptr<T,DeletorFuncPtr>

其中T是要释放的对象类型,DeletorFuncPtr是自定义函数指针,这里可以使用decltype(releaseFunc)让编译器自行推导releaseFunc的类型

1
2
3
std::unique_ptr<Student, void(*)(Student* ps)> pStudent(new Student(), releaseFunc);
可改为
std::unique_ptr<Student, decltype(releaseFunc))> pStudent(new Student(), releaseFunc);

std::shared_ptr

std::unique_ptr对持有的资源具有独占性,而std::shared_ptr对持有的资源具有共享性,即多个shared_ptr可以同时持有同一个资源对象,每增加一个shared_ptr指向该对象,该对象的引用计数就+1,每析构一个,引用计数-1,当引用计数为0时,该对象被释放回收,多个线程之间,递增或者递减引用计数是安全的,但并不意味着,多个线程之间同时操作引用对象时安全的。std::shared_ptr提供一个use_count()方法来获取当前持有资源的引用计数。除此之外,其余的用法与std::unique_ptr基本相同。

1
2
3
4
5
6
7
//初始化1
std::shared_ptr<int> sp1(new int(1));
//初始化2
std::shared_ptr<int> sp2;
sp2.reset(new int(1));
//初始化3
std::shared_ptr<int> sp3 = std::make_shared<int>(1);

优先使用std::make_shared去初始化std::shared_ptr对象。

std::enable_shared_from_this

在开发中,有时候需要在类中返回包裹当前对象(this)的std::shared_ptr对象给外部使用,有此需求的类只要继承自己的std::enable_shared_from_this模板对象即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class A : public std::enable_shared_from_this<A> {
public:
A(){
std::cout << "A constructor" << std::endl;
}
~A() {
std::cout << "A destructor" << std::endl;
}

std::shared_ptr<A> getSelf() {
return shared_from_this();
}
};
int main(){
std::shared_ptr<A> sp4(new A());
std::shared_ptr<A> sp5 = sp4->getSelf();

std::cout << "use_count:" << sp4.use_count() << std::endl;
return 0;
}

getSelf方法返回自身的std::shared_ptr对象,方法中直接返回shared_from_this()。

陷阱一:不应该共享栈对象的this给智能指针对象
1
2
3
4
5
6
7
8
9
//与上文代码相同
int main(){
A a;
std::shared_ptr<A> sp5 = a.getSelf();
//程序在该行崩溃

std::cout << "use_count:" << sp4.use_count() << std::endl;
return 0;
}

崩溃原因:智能指针是为了管理堆对象的,而a是栈对象,栈对象会在函数调用结束时自行销毁,不能通过shared_from_this交给智能指针管理。

智能指针最初的设计目的就是为了管理不会自动回收的堆内存的。

陷阱二:避免std::enable_shared_from_this的循环引用问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class B : public std::enable_shared_from_this<B> {
public:
B() {
std::cout << "B constructor" << std::endl;
}
~B() {
std::cout << "B destructor" << std::endl;
}

void func() {
m_mySelf = shared_from_this();
}
private:
std::shared_ptr<B> m_mySelf;
};
int main(){
{
std::shared_ptr<B> sp6(new B());
sp6->func();
}
return 0;
}
//output
B constructor

根据输出可以发现对象B没有被回收,发生了内存泄漏,那么这是为什么呢?

sp6创建的时候 对象引用计数为1,之后调用func函数把自身的shared_ptr赋值给了类变量m_mySelf,此时引用计数为2,之后sp6对象被销毁,此时B的引用计数为1,不会被销毁,这时就出现了矛盾,B想要销毁就要引用计数减为0,就需要销毁m_mySelf,那么就必须要销毁B对象,这样B对象就无法销毁了。因此我们实际开发中应该避免出现这样的逻辑。

std::weak_ptr

std::weak_ptr是一个不控制资源生命周期的智能指针,是对对象的一种弱引用,只是提供了对其管理对象的一个访问手段,引入的目的是为了辅助shared_ptr工作。

std::weak_ptr可以从一个std::shared_ptr或另一个std::weak_ptr对象构造,std::shared_ptr可以直接复制给std::weak_ptr,weak_ptr可以通过lock()方法获取shared_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
22
	std::shared_ptr<int> sp1 = std::make_shared<int>(1);
std::cout << "use_count:" << sp1.use_count() << std::endl;

std::weak_ptr<int> wp2(sp1);
std::cout << "use_count:" << sp1.use_count() << std::endl;

std::weak_ptr<int> wp3 = sp1;
std::cout << "use_count:" << sp1.use_count() << std::endl;

std::weak_ptr<int> wp4(wp3);
std::weak_ptr<int> wp5 = wp3;
std::cout << "use_count:" << sp1.use_count() << std::endl;

//通过lock()方法获得shared_ptr会增加引用计数
std::shared_ptr<int> sp6 = wp5.lock();
std::cout << "use_count:" << sp1.use_count() << std::endl;
//output
use count: 1
use count: 1
use count: 1
use count: 1
use count: 2

weak_ptr不管对象的生命周期,当对象被销毁时,通过expired()方法来检测,返回true,表示对象已经不存在了;false表示对象还存在,此时可以通过lock()方法获取shared_ptr继续操作对象。

1
2
3
4
5
6
7
8
//tmpPoint  ---->std::weak_ptr<Point>
if(tmpPoint.expired()){
return;
}
std::shared_ptr<Point> p = tmpPoint.lock();
if(p){
//数据处理
}

weak_ptr没有重写operator->和operator*方法,所以不能直接操作对象,也没有重写operator!操作,所以也不能判断引用的资源是否存在。

weak_ptr不增加引用资源的引用计数来管理资源的生命周期,是因为,即使它实现了以上几个方法,调用他们是不安全的,因为在调用期间,引用的资源可能已经被销毁了。

weak_ptr的正确使用场景是那些资源如果可用就使用,不可用就不用的场景,它不参与资源的生命周期管理。

例如:在网络分层中,Session对象:利用Connection对象提供的服务工作,但是Session对象不管理Connection对象的生命周期,因为网络底层会有各种原因导致Connection对象销毁,如果Session强行持有Connection对象,会与事实矛盾。

经典示例:(订阅者模式 或者 观察者模式)

1
2
3
4
5
6
7
8
9
10
11
12
13
class Subscriber{};
class SubscribeManager{
public:
void publish(){
for(const auto& iter : m_subscirbers){
if(!iter.expired()){
//TODO:Send Message to Subscriber
}
}
}
private:
std::vector<std::weak_ptr<Subscriber>> m_subscirbers;
};

智能指针对象的大小

一个std::unique_ptr对象大小与一个裸指针大小相同,而std::shared_ptr对象的大小是std::unique_ptr大小的一倍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
	std::shared_ptr<int> sp0;
std::shared_ptr<std::string> sp1;
sp1.reset(new std::string());
std::unique_ptr<int> sp2;
std::weak_ptr<int> sp3;

std::cout << "sp0 size: " << sizeof(sp0) << std::endl;
std::cout << "sp1 size: " << sizeof(sp1) << std::endl;
std::cout << "sp2 size: " << sizeof(sp2) << std::endl;
std::cout << "sp3 size: " << sizeof(sp3) << std::endl;
//output
sp0 size: 8
sp1 size: 8
sp2 size: 4
sp3 size: 8

智能指针使用注意事项

  • 一旦一个对象使用智能指针管理后,就不该再使用原始裸指针去操作。
  • 分清楚场合应该使用哪种类型的智能指针。
  • 认真考虑,避免操作某个引用资源已经释放的智能指针。
  • 作为类成员变量时,应该优先使用前置声明(forward declarations)。

本文标题:智能指针类

文章作者:Tokey

发布时间:2020年05月29日 - 10:05

最后更新:2021年06月29日 - 22:06

原始链接:http://TokeyRoad.github.io/2020/05/29/智能指针类/

许可协议: 转载请保留原文链接及作者。

0%