0%

C++智能指针

C++智能指针

智能指针

智能指针的设计目的 是因为 当 new 一个指针时 如果忘记释放就会带来内存泄漏的问题。因此C++提供了智能指针的方法来“智能地”管理指针所指的内存,自动给你释放。通过new 申请返回的是指向 内存的 指针,智能指针是通过类 来对 这个指针进行管理。当超出了类的作用域,就会自动触发类的析构函数完成该指针指向的内存的释放功能。

综上所述,智能指针是通过类 来 对指针进行管理,当超过类的作用域 类的析构函数自动完成指针所指向的内存的释放功能。而为了使智能指针的表达具有 “指针变量” 类似的形式,需要对 指针变量常用的操作符 例如 “-> * = ” 等重定义。

  • auto_ptr (c++11弃用)
  • share_ptr (c++11)
  • weak_ptr (c++11)
  • unique_ptr (c++11)

auto_ptr

auto_ptr 就是对开头的思想进行了实现,是c98里的东西,由于有很多缺陷,c11中已经被弃用。

  1. 所有权问题。当把 一个auto_ptr 对象 A 通过拷贝构造给了 对象 B 此时A就是个空类,你再试图访问A所指向的空间就会越界。
  2. 不支持组管理 就是 通过 new int[ 500] 这种分配的数组 ,它无法管理,因为这时候释放应该使用 delete [] A而 auto_ptr中只支持 delete A直接释放内存。
  3. 不支持容器
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
class Test
{
public:
Test(int a = 0 ) : m_a(a) { }
~Test( )
{
cout << "Calling destructor" << endl;
}
public: int m_a;
};
void test1( )
{
std::auto_ptr<Test> p( new Test(5) );
cout << p->m_a << endl; //当退出 test 函数时 会自动释放 new 的内存
}
//// 缺陷1 举例
void Fun(auto_ptr<Test> p1 )
{
cout<<p1->m_a<<endl;
}
void test2( )
{
std::auto_ptr<Test> p( new Test(5) );
Fun(p);
cout<<p->m_a<<endl;
// 此时再访问 P 就越界 因为 在调用 Fun 函数时,实际完成了一次拷贝构造 将指针所有权交给了 函数Fun中的局部变量并随着函数的退出而释放。所以 此时 p就是野指针了。
std::auto_ptr<Test> p(new Test[5]); // 不能管理申请的数组指针
}

unique_ptr

unique_ptr也是对auto_ptr的替换unique_ptr遵循着独占语义。在任何时间点,资源只能唯一地被一个unique_ptr占有。当unique_ptr离开作用域,所包含的资源被释放。如果资源被其它资源重写了,之前拥有的资源将被释放。所以它保证了他所关联的资源总是能被释放。但是如果程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做,如:

1
2
3
4
5
6
unique_ptr<string> pu1(new string ("hello world"));
unique_ptr<string> pu2;
pu2 = pu1; // #1 not allowed
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string ("You")); // #2 allowed
pu2 = move(pu1); // #3 allowed

特性

  1. unique_ptr的创建方法和shared_ptr一样,除非创建一个指向数组类型的unique_ptr
  2. unique_ptr提供了创建数组对象的特殊方法,当指针离开作用域时,调用delete[]代替delete。当创建unique_ptr时,这一组对象被视作模板参数的部分。这样,程序员就不需要再提供一个指定的析构方法
  3. unique_ptr拷贝赋值和拷贝构造都不可以(编译报错),只支持移动语义(move semantics).

接口

unique_ptr提供的接口和传统指针差不多,但是不支持指针运算。

unique_ptr提供一个release()的方法,释放所有权。releasereset的区别在于,release仅仅释放所有权但不释放资源,reset也释放资源。

share_ptr

对于上面 auto_ptr 最基本的 当拷贝构造后 指针的所有权被剥离 只剩野指针的问题,share_ptr 通过共享所有权的概念解决。share_ptr通过计数的机制实现 共享。

  • 多个智能指针可以共同拥有/指向同一个 申请的内存/对象,当最后个智能指针离开作用域的时候,内存才会自动释放
  • 包含两种计数 : 强引用计数 弱引用计数

创建

1
2
3
4
5
6
7
8
void main( )
{
shared_ptr<int> sptr1( new int );
shared_ptr<int> sptr2 = make_shared<int>(100); // 使用 make_shared 加速创建过程
shared_ptr<int> sptr3 = sptr1; // 此时 sptr1 和 3 同时指向同一片内存 强引用计数 值 加 1

cout << sptr3.use_count(); // 可以获得 强引用计数值
}

析构

shared_ptr默认调用delete释放关联的资源。如果用户采用一个不一样的析构策略时,他可以自由指定构造这个shared_ptr的策略。应该调用delete[]来销毁这个数组。用户可以通过调用一个函数,例如一个lamda表达式,来指定一个通用的释放步骤

1
2
3
4
5
6
7
8
9
Class Test
...

void main( )
{
shared_ptr<Test> sptr1( new Test[5] ); // 默认析构无法释放 数组
// 正确的方法 指定析构函数
shared_ptr<Test> sptr1( new Test[5], [ ](Test* p) { delete[ ] p; } ); // 参见 lambda 用法
}

接口

除了 指针常用的 操作 符*,-> 之外 它还提供了一些有用的接口

  • get(): 获取shared_ptr绑定的资源
  • swap():交换两个 share_ptr 对象 交换所有权
  • reset(): 释放关联内存块的所有权,如果是最后一个指向该资源的shared_ptr,就释放这块内存
  • unique: 判断是否是唯一指向当前内存的 shared_ptr
  • operator bool : 判断当前的shared_ptr是否指向一个内存块,可以用 if 表达式判断

问题

  1. 对同一个裸指针使用两个 shared_ptr初始化管理。应该尽量避免这种情况。当其中一个智能指针离开作用域后释放该指针 另一个再用就出问题。尽量不要从一个裸指针(naked pointer)创建shared_ptr.
  2. 循环引用问题。如下面的例子,当离开main函数时,sptrBsptrA离开了作用域 (他们俩是在main函数中申请的一个类)就会导致计数值减一,但是不会释放内存,因为此时 new 的 结构 A 和 B 中包含的智能指针还在相互引用呢
image-20210103170722316
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
// 问题1
void main( )
{
int* p = new int;
shared_ptr<int> sptr1( p); // sptr1 和 sptr2 此时属于不同组 但是指向了同一个资源。不同组的智能指针的计数值不共享
shared_ptr<int> sptr2( p); // 尽量不要从一个裸指针`(naked pointer)`创建`shared_ptr`
}

// 问题2
class B;
class A
{
public:
A( ) : m_sptrB(nullptr) { };
~A( )
{
cout<<" A is destroyed"<<endl;
}
shared_ptr<B> m_sptrB;
};
class B
{
public:
B( ) : m_sptrA(nullptr) { };
~B( )
{
cout<<" B is destroyed"<<endl;
}
shared_ptr<A> m_sptrA;
};
//*************当sptrA和sptrB离开作用域时,它们的引用计数都只减少到1,所以它们指向的资源并没有释放!!!!
void main( )
{
shared_ptr<B> sptrB( new B );
shared_ptr<A> sptrA( new A );
sptrB->m_sptrA = sptrA;
sptrA->m_sptrB = sptrB;
}

简单实现

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
template<typename T>  // 注意模板类的使用方法
class SmartPointer {
private:
T* p_;
size_t* count_;
public:
// 构造函数 注意初始值的设置方法
SmartPointer(T* ptr = nullptr) : p_(ptr) {
// 构造函数代表新定义了一个 管理某指针的 类,所以要分配 计数变量并初始化为0 从无到有
if (p_) {
count_ = new size_t(1);
}
else {
count_ = new size_t(0);
}
}
// 拷贝构造函数 就是类似值传递这种场合会使用的情形 所以输入是 const 引用
// 拷贝构造其实是由 多到跟多的过程 多以 计数值加 1 即刻
SmartPointer(const SmartPointer& ptr) {
p_ = ptr.p_;
count_ = ptr.count_;
(*count_)++;
}
// 赋值构造函数重载 注意 = 是有返回值的 返回的是本身 因为有这种用法 int b = 0; c = b = a;连续赋值
SmartPointer& operator=(const SmartPointer& ptr) {
if (p_ == ptr.p_) {
return *this;
}
//赋值构造函数要考虑以下情况:是不是自己给自己赋值,被赋值对象是不是已经有管理的对象了,那么要先释放之前管理的指针
// 最后变更指针指向的地址,计数值增加
if (p_) {
if (--(*count_) == 0) {
delete p_;
delete count_;
}
}
p_ = ptr.p_;
count_ = ptr.count_;
(*count_)++;
return *this;
}
//析构函数 计数值减1后是否到0
~SmartPointer() {
if (--(*count_) == 0) {
delete p_;
delete count_;
}
}

size_t use_count() {
return *count_;
}
};

weak_ptr

根据上面的分析,导致相互引用内存无法释放的主要原因是 两个类内部的share_ptr之间的相互引用。导致管理该类的智能指针无法释放内存。因此提出 通过 weak_ptr来 替代 类内部的 shared_ptrweak_ptr只能由 shared_ptr拷贝初始化,并且只增加 shared_ptr的弱引用计数值,而在释放内存时只考虑强引用计数值,不管弱引用计数值为多少都不影响内存的释放。

基于以上思路,完善一下 weak_ptr 的定义和性质。

  • weak_ptr 只能由 share_ptr指针创建初始化,只增加 弱引用计数。将一个weak_ptr赋给另一个weak_ptr会增加弱引用计数(weak reference count)
  • weak_ptr 没有 * ->操作,它并不包含资源所以也不允许程序员操作资源。但是 在需要访问资源的时候,可以先将它转换为shared_ptr 再访问 如下:
    1. 调用expired()方法 判断weak_ptr是否指向有效资源
    2. 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
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class B;
class A
{
public:
A( ) : m_a(5) { };
~A( )
{
cout<<" A is destroyed"<<endl;
}
void PrintSpB( );
weak_ptr<B> m_sptrB; /// 类中使用了 弱 智能指针 不会强行占用资源 在使用时转换为 shared_ptr才会增加强引用计数,但是此时因为还在调用类的变量,说明这个类正在被使用就不可能是离开了作用域
int m_a;
};
class B
{
public:
B( ) : m_b(10) { };
~B( )
{
cout<<" B is destroyed"<<endl;
}
weak_ptr<A> m_sptrA;
int m_b;
};

void A::PrintSpB( )
{
if( !m_sptrB.expired() )
{
cout<< m_sptrB.lock( )->m_b<<endl;
}
}

void main( )
{
shared_ptr<B> sptrB( new B );
shared_ptr<A> sptrA( new A );
sptrB->m_sptrA = sptrA;
sptrA->m_sptrB = sptrB;
sptrA->PrintSpB( );
}