C++之智能指针
导读
在《C++之指针扫盲》一文中我们对指针进行了讲解,虽然原始指针是几乎无所不能,的确是一把利器,但就是这样的一把利器让多少人既爱又恨,一不小心就杀敌一千,自损八百,无论你是 多么的严谨,总是很难从根本上避免内存泄漏。
有没有好的方式去用好这把利刃而又不伤手呢?带着手套不就行了么。。。
RAII
在C程序中有一条行规是:
谁开发谁保护,谁污染谁治理
所以我们在很多库的API中经常发现一些传递二级指针的alloc函数和一些对应的xxx_free函数,这就是遵循谁开发谁保护,谁污染谁治理的原则。
在进入智能指针话题之前我们先来了解下RAII
。
RAII(Resource Acquisition Is Initialization)是由c++之父Bjarne Stroustrup提出的,中文翻译为资源获取即初始化,他说:使用局部对象来管理资源的技术称为资源获取即初始化;这里的资源主要是指操作系统中有限的东西如内存、网络套接字等等,局部对象是指存储在栈的对象,它的生命周期是由操作系统来管理的,无需人工介入。
堆指针在C/C++实在是太灵活,但是每次使用完毕后都需要程序员手动地去释放它,但是程序员们往往会忘记释放它,又或者是命名写了释放的代码,但是因为各种执行的异常情况,导致到释放资源的代码根本没有执行到,特别是引入了异常机制后的C++更是如此。因此为了解决这些问题,c++之父给出了解决问题的方案:
使用RAII,它充分地利用了C++语言局部对象自动销毁的特性来控制资源的生命周期。
一句话总结起来就是在构造函数中去申请资源,在析构函数中去释放资源。
以下展示了一个简单的RAII的例子:
class Student{
public:
Student():name(new string("张三")){
}
~Student(){
// 声明周期结束自动释放指针
delete name;
name = nullptr;
}
public:
const string *name;
};
int main() {
Student student;
// delete student.name; // 不需要手动释放内部的指针
return 0;
}
咋一看,这RAII确实是个好东西呀,既然有了RAII还需要智能指针干嘛呢?难道C++造物主也要扛KPI???
智能指针
C++11推出了三种智能指针,它们分别是unique_ptr、shared_ptr和weak_ptr,同时也将auto_ptr废弃掉。既然auto_ptr已经废弃掉了,这里就不再讨论了, 感兴趣的童鞋可以自行查阅auto_ptr的相关隐患来了解下它为什么会被替代掉。
智能指针,既然是智能的,为什么需要三种呢?一种万能的不就可以了吗?存在即合理,unique_ptr、shared_ptr和weak_ptr三种智能指针在不同的场合区分使用更能提升我们程序的强壮性。
当需要使用智能指针的时候包含头文件<memory>
即可。
1、unique_ptr
std::unique_ptr
是一种独占的智能指针,它禁止其他智能指针与其共享同一个对象,也就是拷贝构造函数,赋值运算符这些相对于std::unique_ptr
来说是不可用的了。如下例子可以验证这点:
int main() {
unique_ptr<int> p1(new int(1));
unique_ptr<int> p2 = p1; // 错误,unique_ptr不允许共享内部的原始指针
unique_ptr<int> p3(new int(3));
p3 = p1; //错误,unique_ptr不允许共享内部的原始指针
unique_ptr<int> p4 = std::move(p1);// 可以,但是此时p1是不可用的,p1中的指针已经转移到了p4
std::cout << "p1:" << *p1 << std::endl; // 获取不到值
return 0;
}
** 注意,在C++11 没有提供std::make_unique
,它是在C++14之后才提供的。**
2、shared_ptr
显然std::unique_ptr
智能指针的独占性在在一些场合是无法满足开发者们的需求的,此时就需要shared_ptr
登场了,shared_ptr
是基于引用计数的一种智能指针,shared_ptr
内部维护着一个原始指针和一个引用计数的指针,当shared_ptr
发生析构销毁的时候,shared_ptr
会将引用计数减去1,如果引用计数不为0则不会销毁原始指针,直到引用计数
为0才会销毁这个原始指针。
相对于std::unique_ptr
来说,shared_ptr
的拷贝构造函数和赋值运算符是可以正常使用的,同时在C++11中就提供了make_shared
函数,至于std::make_unique
为什么到了C++14才提供,
俺也不知道,俺也不敢问呀....
shared_ptr
可以通过use_count
获取内部的原始指针的引用计数。
shared_ptr
可以通过get
函数获取到内部的原始指针,但是这是一件很疯狂的事情,意味着你需要对你自己做的事情负责....
int main() {
shared_ptr<int> p1 = make_shared<int>(1);
shared_ptr<int> p2 = p1; // 可以
shared_ptr<int> p3(p1);
std::cout << "p1 use_count:" << p1.use_count() << std::endl; // 3
std::cout << "p2 use_count:" << p2.use_count() << std::endl; // 3
std::cout << "p3 use_count:" << p3.use_count() << std::endl; // 3
p1.reset(); // p1重置
std::cout << "#######################" << std::endl;
std::cout << "p1 use_count:" << p1.use_count() << std::endl; // 0
std::cout << "p2 use_count:" << p2.use_count() << std::endl; // 2
std::cout << "p3 use_count:" << p3.use_count() << std::endl; // 2
std::cout << "*p1:" << *p1<< std::endl; // 无法获取到值
return 0;
}
当然shared_ptr
除了上面例子中用到的成员函数,还有其他的成员函数,大家需要多动手敲敲才能实践出真知。
与unique_ptr不同,shared_ptr不直接支持管理动态数组。如果希望使用shared_ptr管理一个动态数组,必须提供自己定义的删除器:
int main() {
unique_ptr<int[]> up(new int[10]); // 正确
shared_ptr<int[]> sharedPtr1(new int[10]); // 错误
shared_ptr<int> sharedPtr(new int[10], [](int *p) {
std::cout << "释放指针" << std::endl;
delete[] p;
}); // 正确,要许自定义指针释放函数
return 0;
}
3、weak_ptr
有了shared_ptr
和unique_ptr
看来和内存泄漏说再见真是指日可待呀。
我们看看以下的例子,为什么他们的构造函数没有被调用?
main.cpp
using namespace std;
class A;
class B;
class A{
public:
A(){
}
~A(){
std::cout << "A被析构" << std::endl;
}
public:
shared_ptr<B> ptrB;
};
class B{
public:
B(){
}
~B(){
std::cout << "B被析构" << std::endl;
}
public:
shared_ptr<A> ptrA;
};
int main() {
shared_ptr<A> pa = make_shared<A>();
shared_ptr<B> pb = make_shared<B>();
pa->ptrB = pb;
pb->ptrA = pa;
std::cout << "pa.use_count:" << pa.use_count() << std::endl;
std::cout << "pb.use_count:" << pb.use_count() << std::endl;
return 0;
}
很明显,已经发生了循环引用了,所以导致main函数执行完毕之后智能指针pa
和pb
依然保持着1个引用计数,所以导致A和B都没有执行析构函数。
这是就需要weak_ptr
来解决循环引用的问题了,weak_ptr
是一种若引用的指针,它一般是搭配shared_ptr
使用,但是它不会增加shared_ptr
的引用计数,也就是说虽然我拥有你,但是我并限制你。。。
但也就是因为weak_ptr
没有强引用,所以有可能在weak_ptr
需要使用原始指针的时候,原始指针已经被别人释放掉了,所以在使用weak_ptr
获取原始值之前需要使用lock
校验一下。
weak_ptr
一般不会单独使用,它一般都会使用一个shared_ptr
来初始化它。
针对以上程序我们使用weak_ptr
修复一下即可:
using namespace std;
class A;
class B;
class A{
public:
A(){
}
~A(){
std::cout << "A被析构" << std::endl;
}
public:
weak_ptr<B> ptrB;
};
class B{
public:
B(){
}
~B(){
std::cout << "B被析构" << std::endl;
}
public:
weak_ptr<A> ptrA;
};
int main() {
shared_ptr<A> pa = make_shared<A>();
shared_ptr<B> pb = make_shared<B>();
pa->ptrB = pb;
pb->ptrA = pa;
std::cout << "pa.use_count:" << pa.use_count() << std::endl;
std::cout << "pb.use_count:" << pb.use_count() << std::endl;
shared_ptr<B> lock = pa->ptrB.lock(); // 如果已经被释放weak_ptr lock会返回空
if(nullptr != lock){
}
return 0;
}
4、返回自己的智能指针
假如你希望通过智能指针来管理你的资源,在书写一个类的成员函数时你希望不返回原始的指向自己的指针,你希望是返回一个shared_ptr
那么请看以下写法是否正确?
class A{
public:
shared_ptr<A> getPtr(){
return make_shared<A>();
}
};
int main() {
shared_ptr<A> pa = make_shared<A>();
shared_ptr<A> ptr = pa->getPtr();
std::cout << "pa.use_count" << pa.use_count() << std::endl; // 打印1
std::cout << "ptr.use_count" << ptr.use_count() << std::endl; // 打印1
return 0;
}
上面的写法会有什么问题呢?智能指针pa和智能指针ptr的引用计数都是1,但是他们却引用了同一个对象,这是很危险的,既然智能指针pa和智能指针ptr是相互独立的,并没有实现真正意义上的共享, 一旦他们当中的其中一个被析构,另外一个再去获取对象时就会发生意外。
如果想要在类的成员函数内返回自己的共享智能指针的话需要继承std::enable_shared_from_this<T>
:
class A:public std::enable_shared_from_this<A>{
public:
shared_ptr<A> getPtr(){
return shared_from_this();
}
};
int main() {
shared_ptr<A> pa = make_shared<A>();
shared_ptr<A> ptr = pa->getPtr();
std::cout << "pa.use_count" << pa.use_count() << std::endl; // 正常,打印2
std::cout << "ptr.use_count" << ptr.use_count() << std::endl; // 正常,打印2
return 0;
}
经过修改后我们发现智能指针pa和智能指针ptr实现了共享,他们的引用计数变成了2。
5、shared_ptr是线程安全的吗
这是一个面试官经常喜欢问的一道面试题。shared_ptr
内部的引用计数是线程安全的,但是shared_ptr
的指针不是线程安全的。大家可以想象下为什么shared_ptr
引用计数要设计成线程安全的呢?
智能指针使用的建议
1、在使用指针指针时尽量使用标准库提供的初始化方法,不到万不得已,不要使用new的方式产生智能指针。这是因为 只有当一切构造动作都完成了,析构函数才有可能被调用。如果智能指针构造失败,那new方式传递进去的指针就不会被析构函数释放。
这条在《C++ Primer》一书中有说:
最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。
2、尽量不要获取智能指针内部的原始指针,在《C++ Primer》一书中有提到:
使用一个内置指针来访问一个智能指针所负责的对象是很危险的,因为我们无法知道对象何时会被销毁。
3、智能指针作为函数参数,能传引用则传递引用,否则使用值传递,虽然之间传递原始指针可能会损耗更加小的内存,但是这样会加大犯错的概率, 不太建议。
4、优先使用unique_ptr指针指针,如果确实需要共享的才使用shared_ptr,如果存在这循环引用的对象则shared_ptr搭配weak_ptr使用。
5、更多关于智能指针的资料,笔者建议童鞋们看看《Effective Modern C++》这本书的第四章内容。
关注我,一起进步,人生不止coding!!!