四、智能指针与容器
当把shared_ptr对象放入一个容器中时,会调用shared_ptr的拷贝构造函数并且引用计数+1
int main(int argc, char const *argv[])
{
shared_ptr<test> sp=make_shared<test>();
vector<shared_ptr<test>> vsp;
vsp.push_back(sp);
cout<<sp.use_count()<<endl;
return 0;
}

而当把weak_ptr放入一个容器中时,weak_ptr的引用计数不会增加,而且容器中的weak_ptr有可能是无效的,因为绑定的shared_ptr指向的对象有可能已经被释放
如果存在一个含有unique_ptr的容器,当向容器中添加元素时,要使用move函数转移unique_ptr的控制权,才能添加成功,因为unique_ptr的拷贝构造函数是delete的
五、智能指针与new
除了使用make_unique和make_shared来初始化unique_ptr和shared_ptr外,还可以使用new来初始化,因为new返回的是一个普通指针,而unique_ptr和shared_ptr的用普通指针初始化的构造函数是explicit的,所以,不能将一个普通指针隐式转化为智能指针,必须使用直接初始化的方式
template <class U>
explicit shared_ptr (U* p);
explicit unique_ptr (pointer p) noexcept;
关于explicit,见博客https://blog.youkuaiyun.com/Master_Cui/article/details/106885137
int main(int argc, char const *argv[])
{
unique_ptr<test> up=new test();
return 0;
}

因为用new test()初始化up时,需要将new test()隐式转化为一个unique_ptr<test>对象,需要调用unique_ptr<test>的构造函数,但是unique_ptr<test>的构造函数是explicit,所以转化失败
将第三行代码改为
unique_ptr<test> up(new test());
编译通过
此外,也不要使用new返回的普通指针给一个智能指针赋值,道理同上
同理,如果一个函数的返回值或者形参是一个unique_ptr或shared_ptr,也不要使用new创建的对象作为返回值或者作为参数,道理相同
六、最好不要混合使用智能指针和普通指针
正式因为可以用new返回的内置指针来初始化智能指针,但是如果将new返回的指针绑定到一个智能指针的临时变量上,那么,new的对象就会被无缘无故释放,当再次访问该对象,有可能就会访问到一个空悬指针
示例
void testfunc(shared_ptr<int> sp)
{
cout<<sp.use_count()<<endl;
}
int main(int argc, char const *argv[])
{
int *p=new int(1024);
testfunc(shared_ptr<int>(p));
cout<<*p<<endl;
return 0;
}

上述代码中p创建了一个int数据,值为1024,用p生成一个shared_ptr<int>的临时对象,然后初始化函数形参sp,接着临时变量自动被释放,sp的引用计数变为1,当函数执行结束时,函数的形参sp作用域结束也被自动释放,引用计数为0,所以int数据所占的内存被释放,主函数中的p成了空悬指针,之后解引用该指针(危!!!),数据失效。
虽然用new初始化shared_ptr会出现上述问题,当时如果用一个shared_ptr的临时对象放入一个容器中,可以使得shared_ptr的引用计数不会增加
示例
void rightdelete()
{
vector<shared_ptr<int>> v;
for (int i=0;i<3;++i) {
v.push_back(shared_ptr<int>(new int(10)));
}
for (int i=0;i<v.size();++i) {
cout<<v[i].use_count()<<endl;
}
}

在https://blog.youkuaiyun.com/Master_Cui/article/details/109264151中提到,当把一个shared_ptr对象放入一个容器中时,shared_ptr对象的引用计数会+1,但是如果当把一个shared_ptr的临时对象放入一个容器中,引用技术并不会增加,因为shared_ptr的临时对象会自动释放。所以当上述函数的执行结束后,v中的元素的引用技术减少为0,指向对象的内存会自动被回收,不用手动delete,防止了内存泄漏
所以,综合第五点和第六点,最好不要使用普通指针来初始化一个智能指针,如果使用shared_ptr,请使用make_shared函数来初始化shared_ptr;如果使用unique_ptr,请使用make_unique函数来进行初始化
如果一个容器中存储了智能指针的对象,当想容器中添加元素时,请使用make_shared函数创建一个临时变量并将该临时变量添加到容器中
get函数从shared_ptr对象中返回指向对象的普通指针,用于向不能使用智能指针的程序传递一个普通指针,但是get虽然返回一个普通指针,但是不要delete该指针,因为指向的内容的生命周期由智能指针来决定。如果delete该指针,那么智能指针的引用计数为0时,会再次delete,造成二次delete错误。
int main(int argc, char const *argv[])
{
shared_ptr<int> p(new int(42));
{
shared_ptr<int> p2(p.get());
cout<<p2.use_count()<<endl;
}
cout<<p.use_count()<<endl;
cout<<*p<<endl;
return 0;
}

上述代码中p和p2都是智能指针,且都指向相同的内存,p2通过p.get来初始化p2。但是p和p2的引用计数都是1且p2和p的作用域不同,当p2的作用域结束后,p2的引用计数为0,指向的内存被释放,所以,打印*p时,解引用了空悬指针(危!!!)
所以,不要使用get函数给别的智能指针初始化或者赋值,也不要使用相同的普通指针给多个智能指针初始化或者赋值,因为那样引用计数并不会增加,只有使用智能指针初始化或赋值另一个智能指针,引用计数才会增加
另外,当使用get指针时,应当知道当最后一个智能指针销毁后,get返回的指针就是无效的,所以在使用get函数前,先判断一下引用计数是否为0
八、为什么需要weak_ptr
weak_ptr是shared_ptr的附属品,不会增加shared_ptr的引用计数,weak_ptr中的内容有可能是无效的,所以不能直接使用weak_ptr访问指向的对象,而必须调用weak_ptr的成员函数lock,lock返回的是一个shared_ptr,对lock的返回值进行判空后,才能决定是否能访问指向的对象
weak_ptr因为其不增加引用计数的特点,所以经常用来防止shared_ptr出现循环引用导致的内存泄漏
示例
class node
{
public:
shared_ptr<node> next;
node(int _val):val_(_val) {
cout<<__func__<<endl;
}
~node() {
cout<<__func__<<endl;
}
int val_;
};
int main(int argc, char const *argv[])
{
shared_ptr<node> pnode=make_shared<node>(10);
pnode->next=make_shared<node>(11);
return 0;
}

上述代码实现了一个单链表,并且创建了两个节点,当主函数执行结束后,两个智能指针的生命周期结束。调用了两次构造函数,内存被正确释放
现在,添加一个pre成员,指向前一个节点,实现一个双向链表,情况如下
class node
{
public:
shared_ptr<node> next;
shared_ptr<node> pre;
node(int _val):val_(_val) {
cout<<__func__<<endl;
}
~node() {
cout<<__func__<<endl;
}
int val_;
};
int main(int argc, char const *argv[])
{
shared_ptr<node> pnode=make_shared<node>(10);
pnode->next=make_shared<node>(11);
pnode->next->pre=pnode;
cout<<pnode.use_count()<<endl;
cout<<pnode->next.use_count()<<endl;
cout<<pnode->next->pre.use_count()<<endl;
return 0;
}

通过上述打印结果可知,调用了两次构造函数,但是没有调用析构函数,说明出现了内存泄漏。原因为是因为当执行pnode->next->pre=pnode;后,pnode的引用计数变为2,主函数退出后,pnode的的引用计数-1,变成1,引用计数不为0,所以,没有调用pnode的析构函数,进而没有调用pnode->next的析构函数,所以两个node对象都没有被正确释放,产生内存泄漏
解决方案:
将shared_ptr<node> pre;改为weak_ptr<node> pre;

这是因为weak_ptr本身不会增加绑定的shared_ptr的引用计数,所以当调用pnode->next->pre=pnode;后,pnode的引用计数依然是1,主函数执行结束后,调用pnode的析构函数,而pnode->next的引用计数也为1,所以也调用pnode->next的析构函数,这两个指针的引用计数均为0了,释放各自指向对象的内存,打印出node析构函数的log
所以,weak_ptr很好的解决了shared_ptr循环引用的问题,这就是weak_ptr最大的价值
九、编写自定义的删除器
除了使用default_delete,还可以通过函数指针自定义删除器
void deleter(int *p)
{
delete p;
}
void arrarydeleter(int *p)
{
delete []p;
}
void customdeleter()
{
shared_ptr<int> sp(new int[10], arrarydeleter);
shared_ptr<int> sp1(new int[10], default_delete<int[]>());
unique_ptr<int, void (*)(int *)> up(new int(10), deleter);
unique_ptr<int[], void (*)(int *)> up1(new int[10]());
}
如果用shared_ptr指向一个数组,那么需要自定义删除器,或者使用default_delete<int[]>(),shared_ptr的自定义的删除器可以不做为模板参数传入,但是unique_ptr的自定义删除器必须作为模板参数传入,必须指明删除器的类型。unique_ptr如果不指定删除器,默认删除器执行delete []T
参考
《C++ 标准库》
《C++ Primer》
欢迎大家评论交流,作者水平有限,如有错误,欢迎指出
本文详细探讨了C++中智能指针的各种应用场景,包括与容器的交互、与new操作符的使用区别、避免混用智能指针与普通指针的重要性、weak_ptr的作用以及自定义删除器的方法。
1842

被折叠的 条评论
为什么被折叠?



