C++知识点36——使用智能指针的注意事项(下)

本文详细探讨了C++中智能指针的各种应用场景,包括与容器的交互、与new操作符的使用区别、避免混用智能指针与普通指针的重要性、weak_ptr的作用以及自定义删除器的方法。

四、智能指针与容器

当把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函数给别的智能指针初始化或者赋值

get函数从shared_ptr对象中返回指向对象的普通指针,用于向不能使用智能指针的程序传递一个普通指针,但是get虽然返回一个普通指针,但是不要delete该指针,因为指向的内容的生命周期由智能指针来决定。如果delete该指针,那么智能指针的引用计数为0时,会再次delete,造成二次delete错误。

此外,也不要使用get函数给别的智能指针初始化或者赋值

示例

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》

 

欢迎大家评论交流,作者水平有限,如有错误,欢迎指出

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值