老规矩,在研究一些东西之前,我们得先明白问题的起源和过程。
PS:本文的目的是学会使用智能指针,可能会插播一点理论。
起源
在由 C++ 开发的大型项目中,使用原生指针(C 中的指针)进行开发工作是一件让人心力交瘁的事情,时不时出现的内存问题让人备受折磨。于是为了减轻在内存管理方面的工作量,提高内存安全性,智能指针就出现了。
智能指针(smart pointer)最早出现于 C++98 中,是 C++ 标准委员会对 C++ 资源管理的改进。当时提出的智能指针叫做auto_ptr
,本质是一个类模板,其通过 RAII(Resource Acquisition Is Initialization)机制实现资源自动释放,但存在一些使用上的问题。
后来 boost 库改进了这个方案,实现了其他种类的智能指针。
再之后 C++11 标准采纳了 boost 库的改进方案,将智能指针具体地分为了三种类型:unique_ptr
、shared_ptr
和weak_ptr
,并沿用至今。
好了,闲话就说这么多。
分类
如前面所说,我们可以把出现过的智能指针全部列举出来,并整理成下面的表格:
名称 | 引入版本 | 所有权 | 备注 |
---|---|---|---|
auto_ptr |
C++98 | 独占 | C++11 弃用,C++17 移除 |
shared_ptr |
C++11 | 共享 | 无 |
unique_ptr |
C++11 | 独占 | 无 |
weak_ptr |
C++11 | 无所有权 | 无 |
下面我们逐个来总结。
因为auto_ptr
在 C++11 中已经被弃用了,所以本文也就不再记录了。另外,记得使用智能指针时需要引入头文件memory
。
shared_ptr
shared_ptr
是一种多个智能指针可以同时指向同一个对象的智能指针,使用的方式与原生指针类似。前面我们已经提到了,所有的智能指针都是类模板。所以,使用它们的方法也与使用vector
、stack
等 STL 容器类似,比如:1
2
3
4
5
6
7
8
9
10
11// 定义一个指向内置类型 int 的智能指针 p1,但并未初始化指向哪个对象
std::shared_ptr<int> p1;
// 定义一个指向 vector<int> 容器的智能指针 p2,但并未初始化指向哪个对象
std::shared_ptr<std::vector<int>> p2;
// 初始化一个指向 vector<int> 容器的智能指针 p3
std::shared_ptr<std::vector<int>> p3(new vector<int>);
// 初始化一个指向 vector<int> 容器的智能指针 p4
std::shared_ptr<std::vector<int>> p4(std::make_shared<std::vector<int>>());
// 初始化一个指向 string 容器的智能指针 p5,同时提供初始化 string 对象需要的参数
// 此时编译器会自动调用 string 的构造函数
std::shared_ptr<std::string> p5 = std::make_shared<std::string>(10, '1');
习惯使用 STL 容器的同学对上面的写法应该没有任何压力,推荐直接初始化一个智能指针,而不是仅仅定义就结束了,千万不要留下野指针。
使用的方法与原生指针类似,shared_ptr
也可以使用*
和->
运算符,比如:1
2
3std::shared_ptr<std::vector<int>> p6(new vector<int>);
p6->push_back(1);
(*p6).push_back(2);
另外,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
int main() {
std::shared_ptr<std::string> p6(std::make_shared<std::string>(10, '1'));
std::shared_ptr<std::string> p7(p6);
// 若是不初始化 p7,使用下面的赋值语句,也会使 shared_ptr 的内置计数器加 1
// p7 = p6;
std::cout << p7.use_count() << std::endl;
p6.reset();
std::cout << p7.use_count() << std::endl;
const std::string* s = p7.get();
std::cout << *s << std::endl;
return 0;
}
/*
2
1
1111111111
*/
同时,使用use_count()
函数可以获取指向该对象的shared_ptr
的总数,使用reset()
函数可以使智能指针释放指向的对象,使用get()
函数可以获取指向源对象的原生指针。
unique_ptr
一个unique_ptr
“独占”它所指向的对象,这意味着在某个时刻,只能有一个unique_ptr
指向一个给定对象。对应的用法与shared_ptr
有些类似:1
2
3
4
5
6
7
8
9
10
11// 定义一个指向 vector<int> 容器的 unique_ptr 指针 p1,但并未初始化指向哪个对象
std::unique_ptr<std::vector<int>> p1;
// 初始化一个指向 vector<int> 容器的 unique_ptr 指针 p2
std::unique_ptr<std::vector<int>> p2(new std::vector<int>);
// 初始化一个指向 vector<int> 容器的 unique_ptr 指针 p3
std::unique_ptr<std::vector<int>> p3(std::make_unique<std::vector<int>>());
// 初始化一个指向 vector<int> 容器的 unique_ptr 指针 p4
std::unique_ptr<std::vector<int>> p4 = std::make_unique<std::vector<int>>();
// 初始化一个指向 string 容器的 unique_ptr p5,同时提供初始化 string 对象需要的参数
// 此时编译器会自动调用 string 的构造函数
std::unique_ptr<std::string> p5 = std::make_unique<std::string>(10, '1');
需要注意的是,make_unique
函数在 C++14 才被引入标准库。unique_ptr
也可以当作原生指针进行使用:1
2
3std::unique_ptr<std::string> p5 = std::make_unique<std::string>(10, '1');
std::cout << *p5 << std::endl;
std::cout << p5->length() << std::endl;
对比shared_ptr
,unique_ptr
没有use_count()
函数,因为它只允许独占一个对象。但它依然可以使用get()
函数获取原生指针,同时它还支持reset()
和release()
函数,对应的用法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main() {
std::unique_ptr<std::string> p = std::make_unique<std::string>(10, 'x');
// 依然可以使用原生指针的运算符
std::cout << *p << " " << p->length() << std::endl;
// 调用 release 函数会让 p 置空,并返回该对象的原生指针
std::string* ps = p.release();
// 调用 reset 函数时提供了对象指针,那么 p 会指向这个对象
p.reset(new std::string("xx"));
// 调用不提供参数的 reset 函数,会将 p 置为空并释放其所指对象,该对象也会被自动析构掉
p.reset();
return 0;
}
/*
xxxxxxxxxx 10
*/
weak_ptr
在前面的表格中,我们指出了weak_ptr
是无占有权的,这意味着它可以指向一个对象,但是无法控制该对象的生命周期。
具体而言,weak_ptr
它指向由一个shared_ptr
管理的对象。将一个weak_ptr
绑定到一个shared_ptr
上不会改变shared_ptr
的引用计数。因此,当最后一个指向对象的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
int main() {
std::shared_ptr<std::vector<int>> p(new std::vector<int>{1, 2, 3});
// 初始化一个指向 vector<int> 容器的 weak_ptr 指针 wp
std::weak_ptr<std::vector<int>> wp(p);
// weak_ptr 只提供不带参数的 reset 函数,此处功能是将 wp 置空
wp.reset();
// weak_ptr 支持赋值操作
wp = p;
// weak_ptr 也提供 use_count 函数,此处会返回与 wp 共享对象的 shared_ptr 的数量
std::cout << wp.use_count() << std::endl;
// 若 use_count 返回为 0,返回 true,否则返回 false
std::cout << wp.expired() << std::endl;
// 若 expired 返回为 true,返回一个空的 shared_ptr,否则返回一个与 wp 指向相同的 shared_ptr
auto p2 = wp.lock();
return 0;
}
/*
1
0
*/
另外,weak_ptr
不支持*
和->
运算符,一般会配合shared_ptr
一起使用。
关于 auto_ptr
想了想,还是提一下auto_ptr
。
它的特性有点类似weak_ptr
,但不是全部。为了兼容老版本的代码,在 STL 中还保存了有关auto_ptr
的代码。如果强行使用,其实也是可以使用的,但 C++11 已经明确弃用auto_ptr
了,C++17 更是直接移除了。
尽管说了很多遍会弃用auto_ptr
,但并没有说为什么要弃用。
原因实际上很简单,就是存在设计缺陷。当我们使用一个auto_ptr
拷贝构造和赋值构造另一个auto_ptr
时,源auto_ptr
会失去对资源的所有权,并将其转移给目标auto_ptr
,同时源auto_ptr
会被置为nullptr
。这样就会导致一些问题,比如:
资源被提前释放,类似下面的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void func(std::auto_ptr<int> pp) {
}
int main() {
std::auto_ptr<int> p(new int(10));
func(p);
// 函数 func 执行完成后,函数形参 pp 就会被析构掉,对应其指向的对象也会被析构掉
// 同时 pp 指向的对象与 p 指向的对象是相同的,此时对 p 解引用程序就会异常终止
std::cout << *p << std::endl;
return 0;
}没办法与标准库的容器兼容,比如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main() {
std::vector<std::auto_ptr<int>> v1;
std::vector<std::auto_ptr<int>> v2;
// 指向下面的代码后,会导致 v1 内的所有 auto_ptr 的指向为空
// 这与常规的拷贝操作不符
v2 = v1;
return 0;
}auto_ptr
使用delete
释放资源,所以只能释放单个对象,无法管理对象数组,因为数组需要delete[]
释放。
从语义的角度来理解就是,auto_ptr
将原本的值语义(value sematics)改变成了移动语义(move sematics)。
总结
本文介绍了shared_ptr
、unique_ptr
和weak_ptr
的基本使用方法,并解释了auto_ptr
存在的设计缺陷。作为学会使用智能指针的第一步,应该算是合格了。
最后,我们说说为什么智能指针可以更安全的管理内存,其实也很简单,可以按照下面的步骤来进行理解:
- 首先应该知道,所谓的智能指针其实并不是一个“指针”,至少不是原生指针(C 中的指针)。那它是什么?其实就是一种对象,只是这个对象它具备了原生指针的性质(解引用、保存了指向对象的地址等)。
其次应该明白,直接定义的对象或内置类型的内存,是编译器负责回收的;而整个程序执行完成后,其所占用的内存是由操作系统回收的。比如下面这段代码:
1
2
3
4
5
6
7
8
9
10
11
void func() {
int a = 10;
}
int main() {
func();
// func 函数执行完成后,其内部定义的局部变量 a 的内存会被编译器自动回收掉
return 0;
}现在回忆一下智能指针的定义方式,本质上与直接定义的变量有区别吗?答案是没有,所以我们定义的智能指针对象所占用的内存也会被编译器或操作系统回收掉。而这个时候,会调用对应的析构函数来析构智能指针对象。如果我们自定义析构函数,并在这个析构函数中使用
delete
(或delete[]
)释放掉所指向的对象,就可以完成对象内存的自动回收了。
上面的过程,我们总结一下就是:智能指针是通过将动态分配对象的生命周期与智能指针对象绑定在一起来完成自动管理内存资源的。或者再说的更理论化一点就是:智能指针是利用栈对象的确定性析构来管理堆上分配的内存资源(最典型的就是动态分配的内存对象)。这种在对象构造时初始化获取资源,并在对象生命周期结束析构时释放资源的机制,叫做 RAII(Resource Acquisition Is Initialization),也称为“资源获取就是初始化”,由 C++ 之父 Bjarne Stroustrup 提出。
好了,暂时就这样吧。