整理下 C++ 智能指针的用法...
老规矩,在研究一些东西之前,我们得先明白问题的起源和过程。
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 | // 定义一个指向内置类型 int 的智能指针 p1,但并未初始化指向哪个对象 |
习惯使用 STL 容器的同学对上面的写法应该没有任何压力,推荐直接初始化一个智能指针,而不是仅仅定义就结束了,千万不要留下野指针。
使用的方法与原生指针类似,shared_ptr也可以使用*和->运算符,比如:
1 | std::shared_ptr<std::vector<int>> p6(new vector<int>); |
另外,shared_ptr还可以多个智能指针指向同一个对象,比如:
1 |
|
同时,使用use_count()函数可以获取指向该对象的shared_ptr的总数,使用reset()函数可以使智能指针释放指向的对象,使用get()函数可以获取指向源对象的原生指针。
unique_ptr
一个unique_ptr“独占”它所指向的对象,这意味着在某个时刻,只能有一个unique_ptr指向一个给定对象。对应的用法与shared_ptr有些类似:
1 | // 定义一个指向 vector<int> 容器的 unique_ptr 指针 p1,但并未初始化指向哪个对象 |
需要注意的是,make_unique函数在 C++14 才被引入标准库。unique_ptr也可以当作原生指针进行使用:
1 | std::unique_ptr<std::string> p5 = std::make_unique<std::string>(10, '1'); |
对比shared_ptr,unique_ptr没有use_count()函数,因为它只允许独占一个对象。但它依然可以使用get()函数获取原生指针,同时它还支持reset()和release()函数,对应的用法:
1 |
|
weak_ptr
在前面的表格中,我们指出了weak_ptr是无占有权的,这意味着它可以指向一个对象,但是无法控制该对象的生命周期。
具体而言,weak_ptr它指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr上不会改变shared_ptr的引用计数。因此,当最后一个指向对象的shared_ptr被销毁时,对象就会被自动释放(析构)掉了。此时,如果强行使用weak_ptr指针,就会产生未定义行为了。weak_ptr提供的方法要少一点(功能相对单一),对应的用法:
1 |
|
另外,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 |
|
- 没办法与标准库的容器兼容,比如:
1 |
|
auto_ptr使用delete释放资源,所以只能释放单个对象,无法管理对象数组,因为数组需要delete[]释放。
从语义的角度来理解就是,auto_ptr将原本的值语义(value sematics)改变成了移动语义(move sematics)。
总结
本文介绍了shared_ptr、unique_ptr和weak_ptr的基本使用方法,并解释了auto_ptr存在的设计缺陷。作为学会使用智能指针的第一步,应该算是合格了。
最后,我们说说为什么智能指针可以更安全的管理内存,其实也很简单,可以按照下面的步骤来进行理解:
- 首先应该知道,所谓的智能指针其实并不是一个“指针”,至少不是原生指针(C 中的指针)。那它是什么?其实就是一种对象,只是这个对象它具备了原生指针的性质(解引用、保存了指向对象的地址等)。
- 其次应该明白,直接定义的对象或内置类型的内存,是编译器负责回收的;而整个程序执行完成后,其所占用的内存是由操作系统回收的。比如下面这段代码:
1 |
|
- 现在回忆一下智能指针的定义方式,本质上与直接定义的变量有区别吗?答案是没有,所以我们定义的智能指针对象所占用的内存也会被编译器或操作系统回收掉。而这个时候,会调用对应的析构函数来析构智能指针对象。如果我们自定义析构函数,并在这个析构函数中使用
delete(或delete[])释放掉所指向的对象,就可以完成对象内存的自动回收了。
上面的过程,我们总结一下就是:智能指针是通过将动态分配对象的生命周期与智能指针对象绑定在一起来完成自动管理内存资源的。或者再说的更理论化一点就是:智能指针是利用栈对象的确定性析构来管理堆上分配的内存资源(最典型的就是动态分配的内存对象)。这种在对象构造时初始化获取资源,并在对象生命周期结束析构时释放资源的机制,叫做 RAII(Resource Acquisition Is Initialization),也称为“资源获取就是初始化”,由 C++ 之父 Bjarne Stroustrup 提出。
好了,暂时就这样吧。