“指针问题”
C语言中的指针语法是很多初学者的噩梦,但是由于指针能够便捷的管理内存,后进者C++并没有抛弃指针语法。不过,由于指针过于灵活,很容易为程序带来内存溢出、内存泄漏等问题,即使是经验老道的程序员也不敢说能够完全避免这些问题,所以C++提供了引用
语法,以期解决指针带来的问题。
可惜的是,近些年的实践证明C++是离不开指针(这一点我们以后再谈)的,因此早期C语言程序员使用指针面临的问题,C++程序员也不得不考虑。C++的语法比C语言的语法复杂得多,若是不能提供一种缓解“指针问题”的方案,那真的有些说不过去了,所以“智能指针”就被设计出来了。
本文将讨论C++11中的
std::shared_ptr
智能指针。
C++中的指针的一个典型用法就是管理一段内存。常规做法是通过类似于 malloc() 的内存管理函数,或者new
关键字等方法分配一段内存,并且定义一个指针指向这块内存,之后便可通过指针访问这块内存。
不过一般来说,malloc() 或者new
关键字分配的内存不会被系统自动回收,这意味着即使分配的内存不再被使用,但若是程序员不主动释放分配的内存,这块内存将永远不能再被别的逻辑使用,这就是所谓的“内存泄漏”。
可能有读者会想,保证 new
/malloc() 和 delete
/free() 的配对使用不就好了吗?这有什么难的!的确,在简单的项目里比较容易保证C++程序不会发生“内存泄漏”,但若是项目稍稍复杂一些,可能要使用别人提供的接口函数,这时再去确定内存的生命周期就稍显麻烦了,特别是有的同事懒得写文档,而他的接口函数源代码不可得的情况下。
简而言之,C++中类C语言的普通指针管理内存的确有着不方便的地方——程序员必须非常清楚整个架构,才能知道某段已分配的内存是否仍在被使用中,进而确保安全的释放已经不再使用的内存,这并非易事。
std::shared_ptr<> 是什么?
std::shared_ptr<>
是C++11标准中的智能指针类,它很聪明,能够知道自己管理的对象是否还有人使用,若是没有人再使用自己管理的对象,就会自动的删除该对象。所以,shared_ptr
能够在最大程度上帮助C++程序员避免内存泄漏问题,也能够避免“悬空指针”的出现。
正如shared_ptr
的字面含义,“shared”意味着共享,即不同的shared_ptr
可以与同一个指针建立联系,其内部通过“引用计数机制”实现自动管理指针。
通常来说,每一个shared_ptr
对象在其内部都管理两部分内容:
- shared_ptr 对象本身
- shared_ptr 对象管理的内容
“引用计数机制”
假设计划使用指针 p 指向一块分配的内存,现在使用C++的智能指针类 shared_ptr 自动管理这块内存。
- 当 shared_ptr 类实例化一个对象与指针 p 绑定时,其内部的构造函数会将对应指针 p 的计数加 1。
- 当 shared_ptr 对象完成自己的生命周期,它的析构函数会将指针 p 对应的计数减 1。
引用计数减少到 0,就意味着没有 shared_ptr 对象还与指针 p 管理的内存绑定,也即没有人再使用这块内存了,于是 shared_ptr 的析构函数调用“delete”方法释放这块内存。
创建一个shared_ptr对象
将一个裸指针绑定到 shared_ptr 对象上是简单的,例如:
std::shared_ptr<int> p1(new int());
上面这行C++代码在堆上分配了两块内存,一块用于存储new
出来的 int 值,一块用于存储 shared_ptr 对象本身。正如前面讨论的,shared_ptr 对象在其内部使用“引用计数”机制管理“new int()”,这里“计数”的初始值显然是 1,因为暂时只有一个 shared_ptr 对象指向“new int()”上。
C++智能指针类 shared_ptr 提供了成员函数 use_count() 用于检查实际的“计数值”,请参考稍后的实例。
怎样将指针“赋值”给shared_ptr?
因为 shared_ptr 类的构造函数是 Explicit 的,所以像下面这样的C++代码是非法的:
// 非法
std::shared_ptr<int> p1 = new int();
不过,除了前文提到的那样通过shared_ptr构造函数绑定指针,还有一种推荐的方法用于绑定指针:
std::shared_ptr<int> p1 = std::make_shared<int>();
std::make_shared
执行了类似于 shared_ptr 构造函数类似的工作:在堆上分配两块内存,一块用于存储 int 值,一块用于存储 shared_ptr 对象本身。
“解绑”
现在我们知道了怎样将普通的裸指针与 shared_ptr 对象绑定,那么怎样才能“解绑”呢?使用 shared_ptr 类提供的 reset() 成员函数即可:
p1.reset();
reset() 函数会将绑定指针的计数减 1,如果计数减小到 0,那么它将删除绑定的指针。reset() 函数也可以接收一个参数,例如:
p1.reset(new int(32));
这种情况下,它将与一个新指针“new int(32)”绑定,因此内部的计数将重新变为 1。
如果想直接解绑 p1 绑定的指针,还可以使用 nullptr
,例如:
p1 = nullptr;
shared_ptr 并不是严格意义的“指针”
C++中的 shared_ptr 对象在某种程度上虽然表现的很像传统C语言中的指针,例如可以使用 *
,->
运算符,但是它并不是严格意义上的“指针”,这一点应该始终明白。
实例
下面将使用一个较为完整的智能指针类 shared_ptr 使用实例结束本文:
#include <iostream>
#include <memory> // 使用shared_ptr需要包含此头文件
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对象绑定到p1
// 引用计数将会加 1
std::shared_ptr<int> p2(p1);
// 打印当前引用计数
std::cout << "p2 Reference count = " << p2.use_count() << std::endl;
std::cout << "p1 Reference count = " << p1.use_count() << std::endl;
// 对比智能指针
if (p1 == p2) {
std::cout << "p1 and p2 are pointing to same pointer\n";
}
std::cout<<"Reset p1 "<<std::endl;
p1.reset();
// 调用reset(),p1将与指针解绑
// 所以 p1 的引用计数将为 0
std::cout << "p1 Reference Count = " << p1.use_count() << std::endl;
// 调用带参数的reset()
// p1与新指针绑定,引用计数变为 1
p1.reset(new int(11));
std::cout << "p1 Reference Count = " << p1.use_count() << std::endl;
// 通过 nullptr 清空 p1
p1 = nullptr;
std::cout << "p1 Reference Count = " << p1.use_count() << std::endl;
if (!p1) {
std::cout << "p1 is NULL" << std::endl;
}
return 0;
}
编译这段C++代码时,记得添加C++11标准选项,最终得到如下输出:
# g++ t.cpp -std=c++11
# ./a.out
p1 = 78
p1 Reference count = 1
p2 Reference count = 2
p1 Reference count = 2
p1 and p2 are pointing to same pointer
Reset p1
p1 Reference Count = 0
p1 Reference Count = 1
p1 Reference Count = 0
p1 is NULL