0%

CPP使用Smart Pointer

CPP 引入 Smart Pointer 的原因,以及最佳实践

Smart Pointer 引入原因

使用 Raw Pointer 管理动态内存时,经常会遇到这样的问题:

  • 忘记delete内存,造成内存泄露
  • 出现异常时,不会执行delete,造成内存泄露

下面的代码解释了,当一个操作发生异常时,会导致delete不会被执行:

1
2
3
4
5
6
7
void func() {
auto ptr = new Widget;
// 执行一个会抛出异常的操作
func_throw_exception();

delete ptr;
}

在 C++98 中我们需要用一种笨拙的方式,写出异常安全的代码:

1
2
3
4
5
6
7
8
9
10
11
void func() {
auto ptr = new Widget;
try {
func_throw_exception();
}
catch(...) {
delete ptr;
throw;
}
delete ptr;
}

C++11 使用 Smart Pointer 能轻易写出异常安全的代码,因为当对象退出作用域时, Smart Pointer 将自动调用对象的析构函数,避免内存泄露:

1
2
3
4
void func() {
std::unique_ptr<Widget> ptr{ new Widget };
func_throw_exception();
}

Smart Pointer 使用方法

Smart Pointer 在 头文件的 std 名称空间中定义。 它们对于RAII或资源获取初始化编程惯用语至关重要。 这个习惯用法的主要目标是确保资源获取在对象被初始化的同时进行,以便在一行代码中创建和准备对象的所有资源。 实际上,RAII的主要原则是将任何堆分配的资源(例如,动态分配的内存或系统对象句柄)归属给一个堆栈分配的对象,该对象的析构函数包含删除或释放资源的代码, 还有任何相关的清理代码。

在大多数情况下,初始化 Raw Pointer 或资源句柄以指向实际资源时,请立即将指针传递给Smart Pointer。在现代C++中,Raw Pointer 仅用于有限范围,循环或辅助函数的小代码块,其中性能至关重要,并且不存在对所有权混淆的可能性。

以下示例将 Raw Pointer 声明与 Smart Pointer 声明进行比较。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 使用 Raw Pointer
void UseRawPointer() {
// 使用 Raw Pointer -- 不推荐
Song* pSong = new Song(L"Nothing on You", L"Bruno Mars");

// Use pSong...

// 使用完成后,不要忘记释放申请的空间
delete pSong;
}

// 使用 Smart Pointer
void UseSmartPointer() {
// 在堆栈上声明一个 Smart Pointer 并将其传递给 Raw Pointer -- 推荐
unique_ptr<Song> song2(new Song(L"Nothing on You", L"Bruno Mars"));

// Use song2...
wstring s = song2->duration_;
//...

} // song2 该程序块执行完后自动删除

如示例中所示, Smart Pointer 是您在堆栈中声明的类模板,并使用指向堆分配对象的 Raw Pointer 进行初始化。 Smart Pointer 初始化后,它拥有 Raw Pointer 。这意味着 Smart Pointer 负责删除 Raw Pointer 指定的内存。 Smart Pointer 析构函数包含对删除的调用,并且因为 Smart Pointer 在堆栈中声明,所以当 Smart Pointer 超出作用域时,即使在堆栈之后的某处抛出异常,也会调用析构函数。

通过使用熟悉的指针操作符 -> 和 * 来访问封装的指针, Smart Pointer 类重载该指针来返回封装的 Raw Pointer 。

C++ Smart Pointer 的对象创建:您创建对象,然后让系统在正确的时间删除它。不同之处在于没有单独的垃圾收集器在后台运行; 内存通过标准C++范围规则进行管理,以便运行时环境更快,更高效。

总是在单独的代码行上创建Smart Pointer,而不是在参数列表中创建,以便由于某些参数列表分配规则而不会发生细微的资源泄漏。

以下示例显示了如何使用标准模板库中的unique_ptrSmart Pointer类型来封装指向大对象的指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class LargeObject {
public:
void DoSomething(){}
};

void ProcessLargeObject(const LargeObject& lo){}
void SmartPointerDemo() {
// Create the object and pass it to a smart pointer
std::unique_ptr<LargeObject> pLarge(new LargeObject());

//Call a method on the object
pLarge->DoSomething();

// Pass a reference to a method.
ProcessLargeObject(*pLarge);

} //pLarge is deleted automatically when function block goes out of scope.

该示例演示了使用 Smart Pointer 的以下基本步骤:

  • 将Smart Pointer声明为自动(本地)变量
    – 不要在Smart Pointer本身上使用new 或 malloc 表达式
  • 在类型参数中,指定封装指针的指向类型
  • 将一个Raw Pointer传递给Smart Pointer构造函数中的新对象
    – Some utility functions or smart pointer constructors do this for you
  • 使用重载的 -> 和 * 运算符来访问该对象
  • 让 Smart Pointer 删除对象

Smart Pointer 的设计在内存和性能方面尽可能高效。 例如,unique_ptr中唯一的数据成员是封装的指针。 这意味着 unique_ptr 与该指针的大小完全相同,可以是四个字节,也可以是八个字节。 通过使用Smart Pointer重载的 * 和 -> 操作符访问封装的指针不会比直接访问 Raw Pointer 慢得多。

Smart Pointer有自己的成员函数,可以用“.”符号来访问。 例如,一些STL Smart Pointer有一个 reset member function ,释放指针的所有权。 如果您想在 Smart Pointer 超出范围之前释放 Smart Pointer 所拥有的内存,这非常有用,如下例所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
void SmartPointerDemo2() {
// Create the object and pass it to a smart pointer
std::unique_ptr<LargeObject> pLarge(new LargeObject());

//Call a method on the object
pLarge->DoSomething();

// Free the memory before we exit function block.
pLarge.reset();

// Do some other work...

}

Smart Pointer 类型

  • ** unique_ptr **
    • 仅允许一个底层指针的所有者
    • 可以移动到新的所有者,但不能复制或共享
    • unique_ptr小而高效, 大小是一个指针
    • 它支持右值引用,以便从STL集合中快速插入和检索
    • 头文件:<memory>
  • ** shared_ptr **
    • 引用计数的Smart Pointer
    • 当您想要将一个Raw Pointer分配给多个所有者时使用,例如,当您从容器中返回指针的副本但想保留Raw Pointer时。 直到所有shared_ptr所有者超出范围或放弃所有权之后,才会删除Raw Pointer
    • 大小是两个指针, 一个用于对象,另一个用于包含引用计数的共享控制块
    • 头文件:<memory>
  • ** weak_ptr **
    • 与shared_ptr结合使用的特例Smart Pointer
    • weak_ptr 提供对一个或多个shared_ptr实例拥有的对象的访问权限,但不参与引用计数
    • 当你想观察一个物体时使用,但不要求它保持活着。在某些情况下需要中断shared_ptr实例之间的循环引用。
    • 头文件:<memory>

创建和使用unique_ptr实例

A unique_ptr不会共享它的指针。 无法将它复制到另一个unique_ptr,(除非它是可修改rvalue) 通过值传递给函数,或需要对其进行复制的任何标准模板库 (STL) 算法中使用。 A unique_ptr只能移动。 这意味着内存资源的所有权将转移到新的unique_ptr和原始unique_ptr不再拥有它。 我们建议您将一个对象限制为一个所有者,因为拥有多个程序逻辑增加复杂性。 因此,当您需要为普通的 C++ 对象的 Smart Pointer ,使用unique_ptr。

下图说明了两个转让所有权unique_ptr实例。

移动 unique_ptr 的所有权, unique_ptr在中定义 STL 中的标头。 它是完全有效与 Raw Pointer ,可以使用 STL 容器中。 添加unique_ptr是有效的实例的 STL 容器因为移动构造函数的unique_ptr不需要复制操作。

** 示例 **
下面的示例演示如何创建unique_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
// 
// Created by Eter J on 2017/12/31.
//
#include <iostream>
#include <memory>

struct Foo {
Foo() { std::cout << "Foo::Foo\n"; }
~Foo() { std::cout << "Foo::~Foo\n"; }
void bar() { std::cout << "Foo::bar\n"; }
};

void f(const Foo &foo)
{
std::cout << "f(const Foo&)\n";
}

int main()
{
std::unique_ptr<Foo> p1(new Foo); // p1 owns Foo
if (p1) p1->bar();

{
std::unique_ptr<Foo> p2(std::move(p1)); // now p2 owns Foo
f(*p2);

p1 = std::move(p2); // ownership returns to p1
std::cout << "destroying p2...\n";
}

if (p1) p1->bar();

// Foo instance is destroyed when p1 goes out of scope
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
unique_ptr<Song> SongFactory(std::wstring artist, std::wstring title) {
// Implicit move operation into the variable that stores the result.
return unique_ptr<Song>(new Song(artist, title));
}

void MakeSongs() {
// Create a new unique_ptr with a new object.
unique_ptr<Song> pSong = unique_ptr<Song>(new Song(L"Mr. Children", L"Namonaki Uta"));

// Use the unique_ptr
vector<wstring> titles;
titles.push_back(pSong->title);

// Move raw pointer from one unique_ptr to another.
unique_ptr<Song> pSong2 = std::move(pSong);

// Obtain unique_ptr from function that returns rvalue reference.
auto pSong3 = SongFactory(L"Michael Jackson", L"Beat It");
}

这些例子演示了unique_ptr的基本特征:它可以被移动,但不能被复制。 “移动”将所有权转移到新的unique_ptr并重置旧的unique_ptr。

以下示例显示如何创建unique_ptr实例并在矢量中使用它们

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void SongVector() {
vector<unique_ptr<Song>> v;

// Create a few new unique_ptr<Song> instances
// and add them to vector using implicit move semantics.
v.push_back(unique_ptr<Song>(new Song(L"B'z", L"Juice")));
v.push_back(unique_ptr<Song>(new Song(L"Namie Amuro", L"Funky Town")));
v.push_back(unique_ptr<Song>(new Song(L"Kome Kome Club", L"Kimi ga Iru Dake de")));
v.push_back(unique_ptr<Song>(new Song(L"Ayumi Hamasaki", L"Poker Face")));

// Pass by reference to lambda body.
for_each(v.begin(), v.end(), [] (const unique_ptr<Song>& p)
{
wcout << L"Artist: " << p->artist << L"Title: " << p->title << endl;
});
}

在for_each循环中,请注意unique_ptr是在lambda表达式中通过引用传递的。 如果你尝试在这里传值,编译器会抛出一个错误,因为unique_ptr拷贝构造函数被禁用。

以下示例显示如何初始化一个类成员unique_ptr。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyClass
{
private:
// MyClass owns the unique_ptr.
unique_ptr<ClassFactory> factory;
public:

// Initialize by invoking the unique_ptr move constructor.
MyClass() : factory ( unique_ptr<ClassFactory>(new ClassFactory()))
{

}

void MakeClass()
{
factory->DoSomething();
}
};
  • 仅允许一个底层指针的所有者
  • 可以移动到新的所有者,但不能复制或共享

创建和使用shared_ptr实例

shared_ptr 类型是 Smart Pointer 在为方案设计多个所有者可能必须管理对象生存期内存中的 C++ 标准库中。 在初始化可以将它复制的 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
21
22
23
//
// Created by Eter J on 2017/12/31.
//
#include <iostream>
#include <memory>

using namespace std;

class C {};

int main() {
shared_ptr<C> gp;
{
C* ptr = new C;
shared_ptr<C> sp(ptr);
gp = sp ;
cout << "sp.use_count(): " << sp.use_count() << endl;
cout << "gp.use_count(): " << gp.use_count() << endl;
}
cout << "gp.use_count(): " << gp.use_count() << endl;

return 0;
}

OUTPUT:

sp.use_count(): 1
gp.use_count(): 2
gp.use_count(): 1

创建和使用weak_ptr实例

有时一个对象必须存储一个访问shared_ptr的底层对象的方法,而不会导致引用计数增加。通常情况下,如果在shared_ptr实例之间有循环引用,则会发生这种情况。

最好的设计是尽可能避免指针的共享所有权。但是,如果您必须共享shared_ptr实例的所有权,请避免它们之间的循环引用。当循环引用是不可避免的,或者由于某种原因更可取的时候,使用weak_ptr来给一个或多个所有者一个弱引用给另一个shared_ptr。通过使用weak_ptr,您可以创建一个shared_ptr,它将连接到现有的一组相关实例,但前提是基础内存资源仍然有效。 weak_ptr本身不参与引用计数,因此它不能阻止引用计数变为零。但是,可以使用weak_ptr尝试获取初始化的shared_ptr的新副本。如果内存已被删除,则抛出bad_weak_ptr异常。如果内存仍然有效,那么只要shared_ptr变量保持在作用域内,新的共享指针就会增加引用计数并保证内存有效。

下面的代码示例演示了weak_ptr用于确保正确删除具有循环依赖关系的对象的情况。当你检查这个例子时,假设它只是在考虑了其他解决方案之后才创建的。 Controller对象表示机器进程的某些方面,它们独立运行。每个控制器必须能够随时查询其他控制器的状态,并且每个控制器都包含一个专用矢量<weak_ptr <Controller>>。每个向量包含一个循环引用,因此,使用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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
//
// Created by Eter J on 2018/1/1.
//

#include <iostream>
#include <memory>
#include <string>
#include <vector>
#include <algorithm>

using namespace std;

class Controller
{
public:
int Num;
wstring Status;
vector<weak_ptr<Controller>> others;
explicit Controller(int i) : Num(i) , Status(L"On")
{
wcout << L"Creating Controller" << Num << endl;
}

~Controller()
{
wcout << L"Destroying Controller" << Num << endl;
}

// Demonstrates how to test whether the
// pointed-to memory still exists or not.
void CheckStatuses() const
{
for_each(others.begin(), others.end(), [] (weak_ptr<Controller> wp)
{
try
{
auto p = wp.lock();
wcout << L"Status of " << p->Num << " = " << p->Status << endl;
}

catch (bad_weak_ptr b)
{
wcout << L"Null object" << endl;
}
});
}
};

void RunTest()
{
vector<shared_ptr<Controller>> v;

v.push_back(shared_ptr<Controller>(new Controller(0)));
v.push_back(shared_ptr<Controller>(new Controller(1)));
v.push_back(shared_ptr<Controller>(new Controller(2)));
v.push_back(shared_ptr<Controller>(new Controller(3)));
v.push_back(shared_ptr<Controller>(new Controller(4)));

// Each controller depends on all others not being deleted.
// Give each controller a pointer to all the others.
for (int i = 0 ; i < v.size(); ++i)
{
for_each(v.begin(), v.end(), [v,i] (shared_ptr<Controller> p)
{
if(p->Num != i)
{
v[i]->others.push_back(weak_ptr<Controller>(p));
wcout << L"push_back to v[" << i << "]: " << p->Num << endl;
}
});
}

for_each(v.begin(), v.end(), [](shared_ptr<Controller>& p)
{
wcout << L"use_count = " << p.use_count() << endl;
p->CheckStatuses();
});
}

int main()
{
RunTest();
wcout << L"Press any key" << endl;
char ch;
cin.getline(&ch, 1);
}

OUTPUT

Creating Controller0
Creating Controller1
Creating Controller2
Creating Controller3
Creating Controller4
push_back to v[0]: 1
push_back to v[0]: 2
push_back to v[0]: 3
push_back to v[0]: 4
push_back to v[1]: 0
push_back to v[1]: 2
push_back to v[1]: 3
push_back to v[1]: 4
push_back to v[2]: 0
push_back to v[2]: 1
push_back to v[2]: 3
push_back to v[2]: 4
push_back to v[3]: 0
push_back to v[3]: 1
push_back to v[3]: 2
push_back to v[3]: 4
push_back to v[4]: 0
push_back to v[4]: 1
push_back to v[4]: 2
push_back to v[4]: 3
use_count = 1
Status of 1 = On
Status of 2 = On
Status of 3 = On
Status of 4 = On
use_count = 1
Status of 0 = On
Status of 2 = On
Status of 3 = On
Status of 4 = On
use_count = 1
Status of 0 = On
Status of 1 = On
Status of 3 = On
Status of 4 = On
use_count = 1
Status of 0 = On
Status of 1 = On
Status of 2 = On
Status of 4 = On
use_count = 1
Status of 0 = On
Status of 1 = On
Status of 2 = On
Status of 3 = On
Destroying Controller4
Destroying Controller3
Destroying Controller2
Destroying Controller1
Destroying Controller0
Press any key