《重学C++》5. 彻底学会 C++ 指针,引用(三)智能指针

1. 智能指针——指针更安全的解决方案(一)

指针的功能比较强大,但单独使用指针存在一些问题,可能会导致野指针、内存泄露。
因此应该尽量使用两种方法,更加安全地使用指针:

  1. 使用更安全的指针——智能指针
  2. 不使用指针,使用更安全的方式——引用

智能指针需要使用头文件memory。

空指针的表示方法:原始版本中,用指针值NULL代表空指针。但是由于NULL不仅可以赋值给指针,还可以赋值给int等类型的元素,因此存在歧义。后来引入nullptr,用于代表空指针。


(1) auto_ptr(C++ 17中移除)

auto_ptr:一种拥有严格对象所有权语义的智能指针。
特点:

  1. 指针超出作用域/生命周期结束之后,指向的空间会被自动释放(防止内存泄露);
  2. 对其他元素赋值/拷贝时,会发生所有权转移,剥夺原指针的控制权(变为nullptr)。

auto_ptr原理:在拷贝 / 赋值过程中,直接剥夺原对象对内存的控制权,转交给新对象,然后再将原对象指针置为nullptr(早期:NULL)。这种做法也叫管理权转移。
他的缺点不言而喻,当我们再次去访问原对象时,程序就会报错,所以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
int main()
{
{// 特点一:确定auto_ptr失效的范围,该括号外pl失效,同时指向的空间也被回收
// 对int使用
auto_ptr<int> pI(new int(10));
cout << *pI << endl; // 10


auto_ptr<string> languages[5] = {
auto_ptr<string>(new string("C")),
auto_ptr<string>(new string("Java")),
auto_ptr<string>(new string("C++")),
auto_ptr<string>(new string("Python")),
auto_ptr<string>(new string("Rust"))
};
cout << "There are some computer languages here first time: \n";
for (int i = 0; i < 5; ++i)
{
cout << *languages[i] << endl;
}
auto_ptr<string> pC;
pC = languages[2]; // 特点二:languges[2] loses ownership. 将所有权从languges[2]转让给pC,
//特点二:此时languges[2]不再引用该字符串从而变成空指针
cout << "There are some computer languages here second time: \n";
for (int i = 0; i < 2; ++i)
{
cout << *languages[i] << endl;
}
cout << "The winner is " << *pC << endl;
//cout << "There are some computer languages here third time: \n";
//for (int i = 0; i < 5; ++i)
//{
// cout << *languages[i] << endl;
//}
}
return 0;
}

(2) unique_ptr

特点:

  1. 指针超出作用域/生命周期结束之后,指向的空间会被自动释放(防止内存泄露)
  2. unique_ptr是一块内存的专属指针,一般不允许被赋值和复制
  3. 可以通过move函数进行所有权转移,解决其他指针需要访问的问题。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    int main()
    {
    // 特点一:在这个范围之外,unique_ptr指向的空间被自动释放
    {
    auto i = unique_ptr<int>(new int(10));
    cout << *i << endl;
    }

    // unique_ptr
    auto w = std::make_unique<int>(10);
    cout << *(w.get()) << endl; // 10
    //auto w2 = w; // 特点二:编译错误,如果想要把 w 复制给 w2, 是不可以的。
    // 因为复制从语义上来说,两个对象将共享同一块内存。

    // 特点三:unique_ptr只 支持移动语义, 即如下
    auto w2 = std::move(w); // w2 获得内存所有权,w 此时等于 nullptr
    cout << ((w.get() != nullptr) ? (*w.get()) : -1) << endl; // -1
    cout << ((w2.get() != nullptr) ? (*w2.get()) : -1) << endl; // 10
    }

所有智能指针的创建方法都有两种,以unique_ptr为例,下面两种方法都可以创建unique_ptr指针:

auto upw1(std::make_unique<Widget>());    
std::unique_ptr<Widget> upw2(new Widget); 

c++ 之智能指针:尽量使用std::make_unique和std::make_shared而不直接使用new
https://blog.csdn.net/p942005405/article/details/84635673


(3) shared_ptr和weak_ptr

为了解决auto_ptr在对象所有权上的局限性,在使用引用计数的机制上提供了可以共享所有权的智能指针shared_ptr(当然这需要额外的开销)。

shared_ptr特点:

  1. 使用引用计数表示访问空间的指针数量。
  2. 通过引用计数和对象共享所有权(引用计数会记录共享的数量)。
  3. 当引用计数=0,表示没有指针使用,开始析构并释放空间。

此外,shared_ptr也支持move函数来转移所有权、也会在超出作用域/生命周期结束之后,自动释放指向的空间

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
int main()
{
// shared_ptr 的生命范围,超出后自动释放空间
{
//shared_ptr 代表的是共享所有权,即多个 shared_ptr 可以共享同一块内存。
auto wA = shared_ptr<int>(new int(20));
{
auto wA2 = wA; //wA2也是shared_ptr,和wA共享使用权
cout << ((wA2.get() != nullptr) ? (*wA2.get()) : -1) << endl;// 20
cout << ((wA.get() != nullptr) ? (*wA.get()) : -1) << endl; // 20
cout << wA2.use_count() << endl; // 引用计数,2
cout << wA.use_count() << endl; // 引用计数,2
}

//cout << wA2.use_count() << endl; //wA2的声明周期已结束,指向的空间也已释放
cout << wA.use_count() << endl; // 1
cout << ((wA.get() != nullptr) ? (*wA.get()) : -1) << endl; // 20
//shared_ptr 内部是利用引用计数来实现内存的自动管理,每当复制一个 shared_ptr,
// 引用计数会 + 1。当一个 shared_ptr 离开作用域时,引用计数会 - 1。
// 当引用计数为 0 的时候,则 delete 内存。
}


// shared_ptr的move 语法
auto wAA = std::make_shared<int>(30);
auto wAA2 = std::move(wAA); // 此时 wAA 等于 nullptr,wAA2.use_count() 等于 1
cout << ((wAA.get() != nullptr) ? (*wAA.get()) : -1) << endl; // -1
cout << ((wAA2.get() != nullptr) ? (*wAA2.get()) : -1) << endl; // 30
cout << wAA.use_count() << endl; // 0
cout << wAA2.use_count() << endl; // 1
//将 wAA 对象 move 给 wAA2,意味着 wAA 放弃了对内存的所有权和管理,此时 wAA对象等于 nullptr。
//而 wAA2 获得了对象所有权,但因为此时 wAA 已不再持有对象,因此 wAA2 的引用计数为 1。

return 0;
}

但shared_ptr同时产生了问题:循环引用

共享指针循环引用

于是引入指针weak_ptr,依附于shared_ptr,和它一起协同工作,采用一种观察者的模式(不会对shared_ptr造成直接影响),两者地位并不对等

weak_ptr特点:

  1. 像“粉丝”一样只进行观察,不对shared_ptr造成直接影响;
  2. 只能对shared_ptr进行引用;
  3. 当shared_ptr失效后,对应的weak_ptr也失效。

weak_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
57
58
59
60
61
62
63

struct B; //先声明,这样struct A中才可以定义该类型成员
struct A {
shared_ptr<B> pb;
~A(){cout << "~A()" << endl;}
};
struct B {
shared_ptr<A> pa;
~B(){cout << "~B()" << endl;}
};

// pa 和 pb 存在着循环引用,根据 shared_ptr 引用计数的原理,pa 和 pb 都无法被正常的释放。
// weak_ptr 是为了解决 shared_ptr 双向引用的问题。
struct BW; //先声明,这样struct AW中才可以定义该类型成员
struct AW
{
shared_ptr<BW> pb;
~AW(){cout << "~AW()" << endl;}
};
struct BW
{
weak_ptr<AW> pa;
~BW(){cout << "~BW()" << endl;}
};


//产生循环引用
void Test()
{
cout << "Test shared_ptr and shared_ptr: " << endl;
shared_ptr<A> tA(new A()); //生成一个指针指向结构体A,结构体内部有一个B的指针
shared_ptr<B> tB(new B()); //生成一个指针指向结构体B,结构体内部有一个A的指针
cout << tA.use_count() << endl; // 1
cout << tB.use_count() << endl; // 1
tA->pb = tB; //tA指向结构体A,A内部的指针指向B
tB->pa = tA; //tB指向结构体B,B内部的指针指向A
cout << tA.use_count() << endl; // 2 除了tA外,tB->pa也指向A
cout << tB.use_count() << endl; // 2 除了tB外,tA->pb也指向B
}


//解决了循环引用
void Test2()
{
cout << "Test weak_ptr and shared_ptr: " << endl;
shared_ptr<AW> tA(new AW());
shared_ptr<BW> tB(new BW());
cout << tA.use_count() << endl; // 1
cout << tB.use_count() << endl; // 1
tA->pb = tB;
tB->pa = tA;
cout << tA.use_count() << endl; // 1,因为weak_ptr不被计数
cout << tB.use_count() << endl; // 2
}

int main()
{
Test();
Test2();


return 0;
}

最终会看到,只有结构体AW、BW执行了析构函数,因为循环引用导致了无法自动释放空间。


2. 引用——指针更安全的解决方案(二)

引用本质是一种不允许修改指向的指针,变量的引用可以认为是变量的别名。
Java中貌似没有指针,但实际上如果没有指针,它的功能会大大削弱。Java实际上是通过引用来间接使用指针的。从某种角度上来说,Java是介于C和C++之间的语言。

使用指针有哪些坑:

  • 空指针
  • 野指针
  • 不知不觉改变了指针的值,却继续使用;

使用引用,则可以:

  • 不存在空引用;
  • 必须初始化;
  • 一个引用永远指向它初始化的那个对象;

有了指针为什么还需要引用?
Bjarne Stroustrup(C++之父)的解释:
为了支持函数运算符重载;

有了引用为什么还需要指针?
Bjarne Stroustrup(C++之父)的解释:
为了兼容C语言;
注意:C语言完全用指针,JAVA语言完全用引用。

补充

关于函数传递参数类型的说明:

  • 内置基础类型(如int,double等)而言,在函数中传递pass by value更高效;
  • 对OO面向对象中自定义类型而言,在函数中传递时pass by reference to const更高效。