谈谈 shared_ptr 的那些坑

共享所有权

  一个动态分配的对象可以在多个shared_ptr之间共享,这是因为shared_ptr支持 copy 操作:

 

1

2

 

shared_ptr<string> ptr1{ new string("hello") };

auto ptr2 = ptr1; // copy constructor

原理介绍

  shared_ptr内部包含两个指针,一个指向对象,另一个指向控制块(control block),控制块中包含一个引用计数和其它一些数据。由于这个控制块需要在多个shared_ptr之间共享,所以它也是存在于 heap 中的。shared_ptr对象本身是线程安全的,也就是说shared_ptr的引用计数增加和减少的操作都是原子的。


  通过unique_ptr来构造shared_ptr是可行的:

 

1

2

 

unique_ptr<string> p1{ new string("senlin") };

shared_ptr<string> p2{ std::move(p1) };

shared_ptr 的风险

  你大概觉得使用智能指针就再也高枕无忧了,不再为内存泄露烦恼了。然而梦想总是美好的,使用shared_ptr时,不可避免地会遇到循环引用的情况,这样容易导致内存泄露。循环引用就像下图所示,通过shared_ptr创建的两个对象,同时它们的内部均包含shared_ptr指向对方。
 

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

 

// 一段内存泄露的代码

struct Son;

struct Father {

shared_ptr<Son> son_;

};

struct Son {

shared_ptr<Father> father_;

};

int main()

{

auto father = make_shared<Father>();

auto son = make_shared<Son>();

father->son_ = son;

son->father_ = father;

return 0;

}

显示详细信息

  分析一下main函数是如何退出的,一切就都明了:

  • main函数退出之前,FatherSon对象的引用计数都是2
  • son指针销毁,这时Son对象的引用计数是1
  • father指针销毁,这时Father对象的引用计数是1
  • 由于Father对象和Son对象的引用计数都是1,这两个对象都不会被销毁,从而发生内存泄露。

  为避免循环引用导致的内存泄露,就需要使用weak_ptrweak_ptr并不拥有其指向的对象,也就是说,让weak_ptr指向shared_ptr所指向对象,对象的引用计数并不会增加:

 

1

2

3

 

auto ptr = make_shared<string>("senlin");

weak_ptr<string> wp1{ ptr };

cout << "use count: " << ptr.use_count() << endl;// use count: 1

  使用weak_ptr就能解决前面提到的循环引用的问题,方法很简单,只要让Son或者Father包含的shared_ptr改成weak_ptr就可以了。

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

 

// 修复内存泄露的问题

struct Son;

struct Father {

shared_ptr<Son> son_;

};

struct Son {

weak_ptr<Father> father_;

};

int main()

{

auto father = make_shared<Father>();

auto son = make_shared<Son>();

father->son_ = son;

son->father_ = father;

return 0;

}

显示详细信息

  同样,分析一下main函数退出时发生了什么:

  • main函数退出前,Son对象的引用计数是2,而Father的引用计数是1
  • son指针销毁,Son对象的引用计数变成1
  • father指针销毁,Father对象的引用计数变成0,导致Father对象析构,Father对象的析构会导致它包含的son_指针被销毁,这时Son对象的引用计数变成0,所以Son对象也会被析构。

  然而,weak_ptr并不是完美的,因为weak_ptr不持有对象,所以不能通过weak_ptr去访问对象的成员,例如:

 

1

2

3

4

5

6

7

 

struct Square {

int size = 0;

};

auto sp = make_shared<Square>();

weak_ptr<Square> wp{ sp };

cout << wp->size << endl; // compile-time ERROR

  你可能猜到了,既然shared_ptr可以访问对象成员,那么是否可以通过weak_ptr去构造shared_ptr呢?事实就是这样,实际上weak_ptr只是作为一个转换的桥梁(proxy),通过weak_ptr得到shared_ptr,有两种方式:

  • 调用weak_ptrlock()方法,要是对象已被析构,那么lock()返回一个空的shared_ptr
  • weak_ptr传递给shared_ptr的构造函数,要是对象已被析构,则抛出std::exception异常。

  既然weak_ptr不持有对象,也就是说weak_ptr指向的对象可能析构了,但weak_ptr却不知道。所以需要判断weak_ptr指向的对象是否还存在,有两种方式:

  • weak_ptruse_count()方法,判断引用计数是否为0
  • 调用weak_ptrexpired()方法,若对象已经被析构,则expired()将返回true

  转换过后,就可以通过shared_ptr去访问对象了:

 

1

2

3

4

5

6

7

8

 

auto sp = make_shared<Square>();

weak_ptr<Square> wp{ sp };

if (!wp.expired())

{

auto ptr = wp.lock(); // get shared_ptr

cout << ptr->size << endl;

}

销毁操作

  为shared_ptr指定 deleter 的做法很简单:

 

1

2

3

4

5

6

7

8

9

10

 

shared_ptr<string> ptr( new string("hello"),

[]( string *p ) {

cout << "delete hello" << endl;

});

ptr = nullptr;

cout << "Before the end of main()" << endl;

/**

delete hello

Before the end of main()

**/

  与unique_ptr不同,标准库并不提供shared_ptr<T[]>,因此,使用shared_ptr处理数组时需要显示指定删除行为,例如:

 

1

2

3

4

5

6

 

shared_ptr<string> ptr1( new string[10],

[]( string *p ) {

delete[] p;

});

shared_ptr<string> ptr2( new string[10],

std::default_delete<string[]>() );

  由于不存在shared_ptr<T[]>,我们无法使用[]来访问数组中的元素,实际上无法访问到数组中的元素。也就是说使用shared_ptr来指向数组意义并不大。若想要数组在多个shared_ptr之间共享,可以考虑使用shared_ptr<vector>shared_ptr<array>

更多陷阱

  使用shared_ptr时,注意不能直接通过同一个 raw pointer 指针来构造多个shared_ptr

 

1

2

3

 

int *p = new int{10};

shared_ptr<int> ptr1{ p };

shared_ptr<int> ptr2{ p }; // ERROR

  很明显,每次通过 raw pointer 来构造shared_ptr时候就会分配一个控制块,这时存在两个控制块,也就是说存在两个引用计数。这显然是错误的,因为当这两个shared_ptr被销毁时,对象将会被delete两次。


  考虑到this也是 raw pointer,所以一不小心就会用同一个this去构造多个shared_ptr了,就像这样:

 

1

2

3

4

5

6

7

8

9

10

11

 

class Student

{

public:

Student( const string &name ) : name_( name ) { }

void addToGroup( vector<shared_ptr<Student>> &group ) {

group.push_back( shared_ptr<Student>(this) ); // ERROR

}

private:

string name_;

};

  每次调用addToGroup()时都会创建一个控制块,所以这个对象会对应多个引用计数,最终这个对象就会被delete多次,导致运行出错。解决这个问题很简单,只要让std::enable_shared_from_this<Student>作为Student的基类:

 

1

2

3

4

5

6

7

8

9

10

11

 

class Student : public std::enable_shared_from_this<Student>

{

public:

Student( const string &name ) : name_( name ) { }

void addToGroup( vector<shared_ptr<Student>> &group ) {

group.push_back( shared_from_this() ); // OK

}

private:

string name_;

};

  需要注意,调用shared_from_this()之前需要确保对象被share_ptr所持有,理解这一点是很容易的,因为只有当对象被shared_ptr所持有,使用shared_from_this()所返回的shared_ptr才会指向同一个对象。

 

1

2

3

4

5

6

7

8

9

10

11

 

vector<shared_ptr<Student>> group;

// Good: ensure object be owned by shared_ptr

auto goodStudent = make_shared<Student>( "senlin" );

goodStudent->addToGroup( group );

Student badStudent1( "bad1" );

badStudent1.addToGroup( group ); // ERROR

auto badStudent2 = new Student( "bad2" );

badStudent2->addToGroup( group ); // ERROR

  在调用shared_from_this()之前需要确保对象被shared_ptr所持有,要使得对象被shared_ptr所持有,对象首先要初始化(调用构造函数),所以一定不能Student的构造函数中调用shared_from_this()

 

1

2

3

4

5

6

7

8

9

10

11

12

 

class Student : public std::enable_shared_from_this<Student>

{

public:

Student( const string &name, vector<shared_ptr<Student>> &group )

: name_( name )

{

// ERROR: shared_from_this() can't be call in object's constructor

group.push_back( shared_from_this() );

}

private:

string name_;

};


  好了,那么问题来了:

  • 要怎样才能防止用户以不正确的方式来创建Student对象呢?
  • 同时也要使得,我们可以使用不同的构造方式来创建Student对象。

  可以将Student的构造函数声明为private的,因此,用户无法直接创建Student对象。另一方面,增加create()成员函数,在这个函数里面,我们使用 C++11 的 variadic templates 特性,调用Student的构造函数来构造对象:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

 

class Student : public std::enable_shared_from_this<Student>

{

private:

Student( const string &name ) : name_( name ) { }

Student( const Student &rhs ) : name_( rhs.name_ ) { }

// can have other constructor

public:

template <typename... Args>

static shared_ptr<Student> create( Args&&... args ) {

return shared_ptr<Student>(new Student(std::forward<Args>(args)...));

}

void addToGroup( vector<shared_ptr<Student>> &group ) {

group.push_back( shared_from_this() );

}

private:

string name_;

};

显示详细信息

  通过create()保证了用户创建的对象总是被shared_ptr所持有,可以将create()想象成Student构造函数的别名:

 

1

2

3

4

5

 

vector<shared_ptr<Student>> group;

auto student1 = Student::create("senlin");

student1->addToGroup(group);

cout << "student1.use_count() = " << student1.use_count() << endl;

// student1.use_count() = 2

性能考虑

 

1

 

shared_ptr<std::string> ptr{ new string("hello") };

  使用这种方式创建shared_ptr时,需要执行两次new操作,一次在 heap 上为string("hello")分配内存,另一次在 heap 上为控制块分配内存。使用make_shared来创建shared_ptr会高效,因为make_shared仅使用new操作一次,它的做法是在 heap 上分配一块连续的内存用来容纳string("hello")和控制块。同样,当shared_ptr的被析构时,也只需一次delete操作。

 

1

 

auto ptr = std::make_shared<std::string>("hello");

参考资料

谈谈 shared_ptr 的那些坑 | Senlin's Blog (senlinzhan.github.io)

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值