C++11智能指针

在看陈硕的《Linux多线程服务器编程》时谈及到了智能指针,这里利用对比分析法将智能指针相关知识进行整理,知识一定要有输出才能算是自己的,不是吗?
文章参考:
http://blog.guoyb.com/2016/08/02/cpp11-5/
https://www.geeksforgeeks.org/auto_ptr-unique_ptr-shared_ptr-weak_ptr-2/
https://www.fluentcpp.com/2017/08/25/knowing-your-smart-pointers

一、原始指针及内存问题

指向对象的原始指针(raw pointer)是坏的。在C++中一般在构造函数中进行资源申请,在析构函数中进行资源释放,在C语言中有可能在某函数内部进行资源申请,在函数调用处进行资源释放,资源的申请和释放不在同一处非常容易产生内存泄漏问题。

C++中的动态内存管理是通过new和delete两个操作符来完成的。new操作符,为对象分配内存并调用对象所属类的构造函数,返回一个指向该对象的指针。delete调用时,销毁对象,并释放对象所在的内存。
C++中常见的内存问题大致有:

  1. 缓冲区溢出(buffer overrun)
  2. 空悬指针/野指针(dangling pointer/wild pointer)
  3. 重复释放(double delete)
  4. 内存泄漏(memory leak)
  5. 不配对的new[]/delete
  6. 内存碎片(memory fragmentation)

有经验的C++程序员大概会想到使用智能指针,但是也需要注意一些技巧(比如考虑使用shared_ptr会出现循环引用问题)。这里我们首先探究智能指针是如何实现的。
在这里插入图片描述
如上图,两个指针P1和P2指向堆上同一个对象Object,P1和P2假定在不同的线程中,假定线程A中通过P1指针将对象销毁且置为NULL,那么P2就成了悬垂指针,如上面右图所示。
那么我们该怎么做呢?正确的方法应该是所有不再被使用的对象都应该将其视为垃圾偷偷地销毁(垃圾回收原理:所有人都用不到的东西一定是垃圾!)。
通过加入中间层间接地完成对资源的销毁,即引入proxy代理。
在这里插入图片描述
proxy对象结构上是一个类,行为上是指针,通过引用计数完成对当前指向对象的指针计数。此时有两个指针sp1和sp2指向对象Object。当sp1和sp2依次析构后,引用计数降为0,此时就可以安全地销毁proxy和Object了。
这也正是引用计数型智能指针的本质!

二、智能指针

智能指针采用引用计数是完成资源管理的常用手法,当引用计数为0时,对象即被销毁。
在STL库中对应的实现主要有shared_ptr、weak_ptr两种,二者都是类模板(class template),定义在<memory>头文件里。
二者的“计数”在主流平台上是原子操作,无锁,性能不俗;线程安全性和STL中的string一样。
不同点:

  • shared_ptr控制对象的生命周期。是强引用(想象成铁丝绑住堆上的对象),只要有一个执行x对象的shared_ptr存在,x对象就不会析构。当指向对象x的最后一个shared_ptr析构或reset()的时候,x保证会被销毁。
  • weak_ptr不控制对象的生命周期,是弱引用(想象棉线轻轻拴住堆上的对象),但可以知道对象是否还活着。如果对象还活着,那么它可以提升为有效的shared_ptr,如果对象已经死了,返回一个空的shared_ptr。
shared_ptr基本用法

shared_ptr采用引用计数的方式管理所指向的对象。当有一个新的shared_ptr指向同一个对象时(复制shared_ptr等),引用计数加1。当shared_ptr离开作用域时,引用计数减1。当引用计数为0时,释放所管理的内存。

这样做的好处在于解放了程序员手动释放内存的压力。
一般我们使用make_shared来获得shared_ptr。

#include <memory>
#include <iostream>
using namespace std;

int main(){
    shared_ptr<string> p1 = make_shared<string>("");
	if(p1 && p1->empty())
		*p1 = "hello";
	 cout << *p1 << " "<<endl;

    shared_ptr<int> p2 = make_shared<int>(42);
    cout << *p2 << " "<<endl;

    shared_ptr<string> p3 = make_shared<string>(10,'9');
    cout << *p3 << " "<<endl;

	shared_ptr<int> p4 = make_shared<int>();
    cout << *p4 << " "<<endl;

	cout<<"test shared_ptr use_count:"<<endl;
	cout<<"p1 cnt:"<<p1.use_count()<<"\tp2 cnt:"<<p2.use_count()<<endl;

	auto p5 = p2;
	cout<<"p1 cnt:"<<p1.use_count()<<"\tp2 cnt:"<<p2.use_count()<<"\tp5 cnt:"<<p5.use_count()<<endl;

    return 0;
}

在这里插入图片描述
make_shared标准库函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。
这里我们测试了use_count的用法,可以看到由于p2和p5都指向了值为42的对象,因此二者的引用计数都为2。
shared_ptr和new
shared_ptr可以使用一个new表达式返回的指针进行初始化。

	cout<<"test shared_ptr and new:"<<endl;
	//shared_ptr<int> p4(new int(1024));
	shared_ptr<int> p5 = new int(1024); // wrong, no implicit constructor
	cout<<*p4<<endl;

但是,不能将一个new表达式返回的指针赋值给shared_ptr。
在这里插入图片描述
另外,特别需要注意的是,不要混用new和shared_ptr!

void process(shared_ptr<int> ptr)
{
	cout<<"in process use_count:"<<ptr.use_count()<<endl;
}

cout<<"don't mix shared_ptr and normal pointer:"<<endl;
shared_ptr<int> p5(new int(1024));
process(p5);
int v5 = *p5;
cout<<"v5: "<<v5<<endl;

int *p6 = new int(1024);
process(shared_ptr<int>(p6));
int v6 = *p6;
cout<<"v6: "<<v6<<endl;

在这里插入图片描述
可以看到,第二次process p6时,shared_ptr的引用计数为1,当离开process的作用域时,会释放对应的内存,此时p6成为了悬挂指针。

所以,一旦将一个new表达式返回的指针交由shared_ptr管理之后,就不要再通过普通指针访问这块内存!
shared_ptr.reset
shared_ptr可以通过reset方法重置指向另一个对象,此时原对象的引用计数减一。

	shared_ptr<int> p1 = make_shared<int>(42);
    cout << *p1 << " "<<endl;
	
	auto p2 = p1;
	cout<<"p1 cnt:"<<p1.use_count()<<"\tp2 cnt:"<<p2.use_count()<<endl;

	p1.reset(new int(404));
	cout<<"p1 cnt:"<<p1.use_count()<<"\tp2 cnt:"<<p2.use_count()<<endl;

在这里插入图片描述
shared_ptr deleter
可以定制一个deleter函数,用于在shared_ptr释放对象时调用。

	cout<<"test shared_ptr deleter:"<<endl;
	int *p7 = new int(1024);
	shared_ptr<int> p8(p7, print_at_delete);
	p8 = make_shared<int>(1025);
	cout << *p8 <<endl;

在这里插入图片描述

unique_ptr基本用法

这块知识可以参考《C++Primer》中文第五版P418

unique_ptr对于所指向的对象,正如其名字所示,是独占的。所以,不可以对unique_ptr进行拷贝、赋值等操作,但是可以通过release函数和reset函数在unique_ptr之间转移控制权。

	cout<<"test unique_ptr base usage:"<<endl;
	unique_ptr<int> up1(new int(1024));
	cout<<"up1: "<<*up1<<endl;
	unique_ptr<int> up2(up1.release());
	cout<<"up2: "<<*up2<<endl;
	//unique_ptr<int> up3(up1); // wrong, unique_ptr 不支持拷贝
	//up2 = up1; // wrong, unique_ptr 不支持赋值
	unique_ptr<int> up4(new int(1025));
	up4.reset(up2.release());
	cout<<"up4: "<<*up4<<endl;

在这里插入图片描述
release()返回的指针通常用来初始化另一个智能指针或给另外一个智能指针赋值。
unique_ptr作为参数和返回值
上述对于拷贝的限制,有两个特殊情况,即unique_ptr可以作为函数的返回值和参数使用,这时虽然也有隐含的拷贝存在,但是并非不可行的。

unique_ptr<int> clone(int p)
{
	//正确:从int*创建一个unique_ptr<int>
	return unique_ptr<int>(new int(p));
}

void process_unique_ptr(unique_ptr<int> up)
{
	cout<<"process unique ptr: "<<*up<<endl;
}

cout<<"test unique_ptr parameter and return value:"<<endl;
auto up5 = clone(1024);
cout<<"up5: "<<*up5<<endl;
process_unique_ptr(move(up5));//
//cout<<"up5 after process: "<<*up5<<endl; // would cause segmentfault

这里的clone(),编译器知道要返回的对象将要被销毁,因此执行一种特殊的“拷贝”。
unique智能指针的所有权问题需要使用std::move进行转移。

早期标准库使用的是auto_ptr,它具有unique_ptr的部分特性,但不是全部。特别是我们不能再容器内保存auto_ptr,也不能从函数返回auto_ptr。一般推荐使用unique_ptr。

unique_ptr deleter
unique_ptr同样可以设置deleter,和shared_ptr不同的是,它需要在模板参数中指定deleter的类型。好在我们有decltype这个利器,不然写起来好麻烦。

cout<<"test unique_ptr deleter:"<<endl;
int *p9 = new int(1024);
unique_ptr<int, decltype(print_at_delete) *> up6(p9, print_at_delete);
unique_ptr<int> up7(new int(1025));
up6.reset(up7.release());

在这里插入图片描述
weak_ptr
weak_ptr一般和shared_ptr配合使用。它可以指向shared_ptr所指向的对象,但是却不增加对象的引用计数。这样就有可能出现weak_ptr所指向的对象实际上已经被释放了的情况。因此,weak_ptr有一个lock函数,尝试取回一个指向对象的shared_ptr。

cout<<"test weak_ptr basic usage:"<<endl;
auto p10 = make_shared<int>(1024);
weak_ptr<int> wp1(p10);
cout<<"p10 use_count: "<<p10.use_count()<<endl;

	shared_ptr<int> p11 = wp1.lock();
	if(p11) 
	{
		cout<<"wp1: "<<*p11<<" use count: "<<p11.use_count()<<endl;
	}
	else
	{
		cout << "p11 is empty"<<endl;
	}
	p10.reset(new int(1025)); // this will cause wp1.lock() return a false obj
	p11.reset(new int(1026));
	shared_ptr<int> p12 = wp1.lock();
	if(p12) 
	{
		cout<<"wp1: "<<*p12<<" use count: "<<p12.use_count()<<endl;
	}
	else
	{
		cout << "p12 is empty"<<endl;
	}

	if (!wp1.expired()){//等价于p12 != nullptr
			cout<<"shared_ptr ok\n"<<endl;
			cout<<"wp1: "<<*p12<<" use count: "<<p12.use_count()<<endl;
	}
	else
	{
		cout << "p12 is empty"<<endl;
	}

在这里插入图片描述
就像陈硕说的,weak_ptr类型的指针当不空时,可以promoting为shared_ptr类型指针p11,此时存储结果为1024的引用计数为2.
之后将这两个指针全部指向其他存储对象,那么此时存储结果为1024的引用计数为0,p12为空。


对于weak_ptr指针来说,却可以通过一些方法来探测被赋值过来的shared_ptr指针的有效性,同时weak_ptr指针也可以间接操纵shared_ptr指针。以下主要介绍两个方法:

  • lock() ,weak_ptr指针调用lock()方法会获得一个返回值:shared_ptr。而这个返回值就是被赋值过来的shared_ptr指针,那么指针都获得了,当然可以操纵它。
  • expired() ,该方法主要用来探测shared_ptr指针的有效性。shared_ptr一旦被释放,指针就会被置为nullptr。

当然weak_ptr主要解决互引用问题,这里就不展开了。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值