[TOC]

文章参考:https://www.jianshu.com/p/03eea8262c11

文章参考:http://c.biancheng.net/view/7898.html

概述

在实际的 C++ 开发中,我们经常会遇到诸如程序运行中突然崩溃、程序运行所用内存越来越多最终不得不重启等问题,这些问题往往都是内存资源管理不当造成的。比如:

  • 有些内存资源已经被释放,但指向它的指针并没有改变指向(成为了野指针),并且后续还在使用;
  • 有些内存资源已经被释放,后期又试图再释放一次(重复释放同一块内存会导致程序运行崩溃);
  • 没有及时释放不再使用的内存资源,造成内存泄漏,程序占用的内存资源越来越多。

针对以上这些情况,很多程序员认为 C++ 语言应该提供更友好的内存管理机制,这样就可以将精力集中于开发项目的各个功能上。

C++11 中引入了智能指针, 同时还有一个模板函数 std::make_shared 可以返回一个指定类型的 std::shared_ptr, 那与 std::shared_ptr 的构造函数相比它能给我们带来什么好处呢 ?

什么是std::shared_ptr<>?

std::shared_ptr<>是c++11中引入的一种智能指针,它足够聪明,如果指针不在任何地方使用,就会自动删除指针。这可以帮助我们彻底消除内存泄露和悬挂指针的问题。

所谓智能指针,可以从字面上理解为“智能”的指针。具体来讲,智能指针和普通指针的用法是相似的,不同之处在于,智能指针可以在适当时机自动释放分配的内存。也就是说,使用智能指针可以很好地避免“忘记释放内存而导致内存泄漏”问题出现。由此可见,C++ 也逐渐开始支持垃圾回收机制了,尽管目前支持程度还有限。

C++ 智能指针底层是采用引用计数的方式实现的。简单的理解,智能指针在申请堆内存空间的同时,会为其配备一个整形值(初始值为 1),每当有新对象使用此堆内存时,该整形值 +1;反之,每当使用此堆内存的对象被释放时,该整形值减 1。当堆空间对应的整形值为 0 时,即表明不再有对象使用它,该堆空间就会被释放掉。

下面我们总结一下他的优点:

2、shared_ptr和共享所有权

它遵循共享所有权的概念,即不同的shared_ptr对象可以与相同的指针相关联,并且在内部使用引用计数机制来实现。

每个shared_ptr对象内部指向两块内存区域

1)指向对象
2)指向用于引用计数的控制数据

共享所有权怎样在引用计数的帮助下工作

当一个新的shared_ptr对象与一个指针相关联时,在它的构造函数中,它将与这个指针相关的引用计数增加1.
·当任何shared_ptr对象超出作用域时,则在其析构函数中将相关指针的引用计数递减1.当引用计数变为0时,意味着没有任何shared_ptr对象与这块内存关联,在这种情况下,它使用“删除”功能删除这块内存

shared_ptr创建

实际上,每种智能指针都是以类模板的方式实现的,shared_ptr 也不例外。shared_ptr<T>(其中 T 表示指针指向的具体数据类型)的定义位于<memory>头文件,并位于 std 命名空间中,因此在使用该类型指针时,程序中应包含如下 2 行代码:

1
2
#include <memory>
using namespace std; //非必须

值得一提的是,和 unique_ptr、weak_ptr 不同之处在于,多个 shared_ptr 智能指针可以共同使用同一块堆内存。并且,由于该类型智能指针在实现上采用的是引用计数机制,即便有一个 shared_ptr 指针放弃了堆内存的“使用权”(引用计数减 1),也不会影响其他指向同一堆内存的 shared_ptr 指针(只有引用计数为 0 时,堆内存才会被自动释放)。

shared_ptr<T> 类模板中,提供了多种实用的构造函数,这里给读者列举了几个常用的构造函数(以构建指向 int 类型数据的智能指针为例)。

创建空指针

1) 通过如下 2 种方式,可以构造出 shared_ptr<T> 类型的空智能指针:

1
2
std::shared_ptr<int> p1;             //不传入任何实参
std::shared_ptr<int> p2(nullptr); //传入空指针 nullptr

注意,空的 shared_ptr 指针,其初始引用计数为 0,而不是 1。

构造函数

在构建 shared_ptr 智能指针,也可以明确其指向。例如:

1
std::shared_ptr<int> p3(new int(10));

由此,我们就成功构建了一个 shared_ptr 智能指针,其指向一块存有 10 这个 int 类型数据的堆内存空间。

make_shared创建

同时,C++11 标准中还提供了 std::make_shared<T> 模板函数,其可以用于初始化 shared_ptr 智能指针,例如:

1
std::shared_ptr<int> p3 = std::make_shared<int>(10);

以上 2 种方式创建的 p3 是完全相同。

拷贝构造函数和移动构造函数

除此之外,shared_ptr<T> 模板还提供有相应的拷贝构造函数和移动构造函数,例如:

1
2
3
4
//调用拷贝构造函数
std::shared_ptr<int> p4(p3);//或者 std::shared_ptr<int> p4 = p3;
//调用移动构造函数
std::shared_ptr<int> p5(std::move(p4)); //或者 std::shared_ptr<int> p5 = std::move(p4);

如上所示,p3 和 p4 都是 shared_ptr 类型的智能指针,因此可以用 p3 来初始化 p4,由于 p3 是左值,因此会调用拷贝构造函数。需要注意的是,如果 p3 为空智能指针,则 p4 也为空智能指针,其引用计数初始值为 0;反之,则表明 p4 和 p3 指向同一块堆内存,同时该堆空间的引用计数会加 1。

而对于 std::move(p4) 来说,该函数会强制将 p4 转换成对应的右值,因此初始化 p5 调用的是移动构造函数。另外和调用拷贝构造函数不同,用 std::move(p4) 初始化 p5,会使得 p5 拥有了 p4 的堆内存,而 p4 则变成了空智能指针。

注意,同一普通指针不能同时为多个 shared_ptr 对象赋值,否则会导致程序发生异常。例如:

1
2
3
int* ptr = new int;
std::shared_ptr<int> p1(ptr);
std::shared_ptr<int> p2(ptr);//错误

shared_ptr的释放

在初始化 shared_ptr 智能指针时,还可以自定义所指堆内存的释放规则,这样当堆内存的引用计数为 0 时,会优先调用我们自定义的释放规则。

在某些场景中,自定义释放规则是很有必要的。比如,对于申请的动态数组来说,shared_ptr 指针默认的释放规则是不支持释放数组的,只能自定义对应的释放规则,才能正确地释放申请的堆内存。

对于申请的动态数组,释放规则可以使用 C++11 标准中提供的 default_delete<T> 模板类,我们也可以自定义释放规则:

1
2
3
4
5
6
7
8
9
10
11
//指定 default_delete 作为释放规则
std::shared_ptr<int> p6(new int[10], std::default_delete<int[]>());
//自定义释放规则
void deleteInt(int*p) {
delete []p;
}
//初始化智能指针,并自定义释放规则
std::shared_ptr<int> p7(new int[10], deleteInt);

// 借助 lambda 表达式,我们还可以像如下这样初始化 p7,它们是完全相同的:
std::shared_ptr<int> p7(new int[10], [](int* p) {delete[]p; });

shared_ptr成员方法

成员方法名 功 能
operator=() 重载赋值号,使得同一类型的 shared_ptr 智能指针可以相互赋值。
operator*() 重载 * 号,获取当前 shared_ptr 智能指针对象指向的数据。
operator->() 重载 -> 号,当智能指针指向的数据类型为自定义的结构体时,通过 -> 运算符可以获取其内部的指定成员。
swap() 交换 2 个相同类型 shared_ptr 智能指针的内容。
reset() 当函数没有实参时,该函数会使当前 shared_ptr 所指堆内存的引用计数减 1,同时将当前对象重置为一个空指针;当为函数传递一个新申请的堆内存时,则调用该函数的 shared_ptr 对象会获得该存储空间的所有权,并且引用计数的初始值为 1。
get() 获得 shared_ptr 对象内部包含的普通指针。
use_count() 返回同当前 shared_ptr 对象(包括它)指向相同的所有 shared_ptr 对象的数量。
unique() 判断当前 shared_ptr 对象指向的堆内存,是否不再有其它 shared_ptr 对象再指向它。
operator bool() 判断当前 shared_ptr 对象是否为空智能指针,如果是空指针,返回 false;反之,返回 true。

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
45
46
47
48
49
#include <iostream>
#include <memory> //使用shared_ptr需要include它

int main() {
//通过make_shared创建shared_ptr
std::shared_ptr<int> p1 = std::make_shared<int>();
// 给这个指针数据进行赋值
*p1 = 78;

std::cout << "p1 = " << *p1 << std::endl;

//查看引用计数
std::cout << "p1 Reference count = " << p1.use_count() << std::endl;

//第二个shared_ptr也将在内部指向相同的指针,这将会使引用计数变为2
std::shared_ptr<int> p2(p1);

//查看引用计数
// p2 Reference count = 2
// p1 Reference count = 2
std::cout << "p2 Reference count = " << p2.use_count() << std::endl;
std::cout << "p1 Reference count = " << p1.use_count() << std::endl;

//比较智能指针。我们可以看到p1和p2其实是同一个指针
if (p1 == p2) {
std::cout << "p1 and p2 are pointing to same pointer\n";
}

std::cout << "Reset p1" << std::endl;
//重置shared_ptr,在这种情况下,其内部不会指向内部的任何指针
//因此其引用计数将会变为0
p1.reset();
std::cout << "p1 Reference Count = " << p1.use_count() << std::endl;

//重置shared_ptr,在这种情况下,其内部将会指向一个新的指针
//因此其引用计数将会变为1
p1.reset(new int(11));
std::cout << "p1 Reference Count = " << p1.use_count() << std::endl;

//分配nullptr将取消关联指针并使其指向空值
p1 = nullptr;
std::cout << "p1 Reference Count = " << p1.use_count() << std::endl;

if (!p1) {
std::cout << "p1 is NULL" << std::endl;
}

return 0;
}

shared_ptr优点

提高性能

shared_ptr 需要维护引用计数的信息,

  • 强引用, 用来记录当前有多少个存活的 shared_ptrs 正持有该对象. 共享的对象会在最后一个强引用离开的时候销毁( 也可能释放).
  • 弱引用, 用来记录当前有多少个正在观察该对象的 weak_ptrs. 当最后一个弱引用离开的时候, 共享的内部信息控制块会被销毁和释放 (共享的对象也会被释放, 如果还没有释放的话).

如果你通过使用原始的 new 表达式分配对象, 然后传递给 shared_ptr (也就是使用 shared_ptr 的构造函数) 的话, shared_ptr 的实现没有办法选择, 而只能单独的分配控制块:

如果选择使用 make_shared 的话, 情况就会变成下面这样:

std::make_shared(比起直接使用new)的一个特性是能提升效率。使用std::make_shared允许编译器产生更小,更快的代码,产生的代码使用更简洁的数据结构。考虑下面直接使用new的代码:

1
std::shared_ptr<Widget> spw(new Widget);

很明显这段代码需要分配内存,但是它实际上要分配两次。每个std::shared_ptr都指向一个控制块,控制块包含被指向对象的引用计数以及其他东西。这个控制块的内存是在std::shared_ptr的构造函数中分配的。因此直接使用new,需要一块内存分配给Widget,还要一块内存分配给控制块。

如果使用std::make_shared来替换

1
auto spw = std::make_shared<Widget>();

一次分配就足够了。这是因为std::make_shared申请一个单独的内存块来同时存放Widget对象和控制块。这个优化减少了程序的静态大小,因为代码只包含一次内存分配的调用,并且这会加快代码的执行速度,因为内存只分配了一次。另外,使用std::make_shared消除了一些控制块需要记录的信息,这样潜在地减少了程序的总内存占用。

对std::make_shared的效率分析可以同样地应用在std::allocate_shared上,所以std::make_shared的性能优点也可以扩展到这个函数上。

异常安全

我们在调用processWidget的时候使用computePriority(),并且用new而不是std::make_shared:

1
2
processWidget(std::shared_ptr<Widget>(new Widget),  //潜在的资源泄露 
computePriority());

就像注释指示的那样,上面的代码会导致new创造出来的Widget发生泄露。那么到底是怎么泄露的呢?调用代码和被调用函数都用到了std::shared_ptr,并且std::shared_ptr就是被设计来阻止资源泄露的。当最后一个指向这儿的std::shared_ptr消失时,它们会自动销毁它们指向的资源。如果每个人在每个地方都使用std::shared_ptr,那么这段代码是怎么导致资源泄露的呢?

答案和编译器的翻译有关,编译器把源代码翻译到目标代码,在运行期,函数的参数必须在函数被调用前被估值,所以在调用processWidget时,下面的事情肯定发生在processWidget能开始执行之前:

表达式“new Widget”必须被估值,也就是,一个Widget必须被创建在堆上。
std::shared_ptr(负责管理由new创建的指针)的构造函数必须被执行。
computePriority必须跑完。

shared_ptr缺点

构造函数是保护或私有时,无法使用 make_shared

make_shared 虽好, 但也存在一些问题, 比如, 当我想要创建的对象没有公有的构造函数时, make_shared 就无法使用了。

解决方法:https://stackoverflow.com/questions/8147027/how-do-i-call-stdmake-shared-on-a-class-with-only-protected-or-private-const?rq=1

对象的内存可能无法及时回收

make_shared 只分配一次内存, 这看起来很好. 减少了内存分配的开销. 问题来了, weak_ptr 会保持控制块(强引用, 以及弱引用的信息)的生命周期, 而因此连带着保持了对象分配的内存, 只有最后一个 weak_ptr 离开作用域时, 内存才会被释放. 原本强引用减为 0 时就可以释放的内存, 现在变为了强引用, 若引用都减为 0 时才能释放, 意外的延迟了内存释放的时间. 这对于内存要求高的场景来说, 是一个需要注意的问题。