C++智能指针

指针指向某块内存区域,使用普通指针会导致一些程序错误

内存泄漏:如果指针指向的内存是动态分配的,那么即使指针离开了作用域,动态分配的内存也不会自动释放,如果动态分配的内存不手动释放,则会导致内存泄漏

悬空指针:如果指针指向一个已经被释放的内存区域,那么这个指针就是悬空指针,使用悬空指针会导致不可预料的结果

野指针:定义了一个指针,却未初始化指向一个有效的内存区域,这个指针就成了野指针,使用野指针访问内存,一般会导致segmentation fault错误

使用智能指针可以避免上述错误的发生

智能指针是一个对象,它封装了一个指向另一个对象的指针,当智能指针对象离开作用域后,会被自动销毁,销毁过程中会调用析构函数来删除封装的对象

在c++中使用智能指针,你需要包含头文件 #include<memory>

演示类

之后的演示代码都用这个矩形类进行演示

复制代码
class Rect
{
public:
	Rect(int InWidth,int InHeight):Width(InWidth),Height(InHeight){ cout<<"构造函数调用"<<endl;};
	~Rect(){ cout <<"当前面积为:"<< Width*Height <<"析构函数调用"<<endl; };
	int area() const { return Width*Height; }
private:
	int Width;
	int Height;	
};

unique_ptr

unique_ptr与它管理的指针是一对一的关系,也就是不能有两个unique_ptr对象指向同一个地址

unique_ptr在离开作用域时,会自动销毁,调用所包含对象的析构函数

创建方法

常用下面两种方法创建

复制代码
	unique_ptr<T> ptr = make_unique<T>(参数);  //传入构造函数的参数
	unique_ptr<T> ptr(new T(参数));

示例:

复制代码
	unique_ptr<int> p1 = make_unique<int>(2);
	cout<< *p1 <<endl;

	unique_ptr<Rect> p2 = make_unique<Rect>(2,4);
	cout<<"p2的面积是:"<<p2->area()<< endl;
	
	unique_ptr<Rect> p3(new Rect(5,9));
	cout<<"p3的面积是:"<<p3->area()<< endl;
常用方法

unique_ptr重载了 *->运算符,让它可以像普通指针一样使用

复制代码
//返回unique_ptr管理的原始指针
T* get(); 

//重载->运算符,内部使用了get()将操作转发给原始指针
T* operator->();                   

//重载*运算符,内部使用了get()将操作转发给原始指针
T& operator*();            

//用于解除unique_ptr封装,返回原始指针,返回的指针不再受智能指针管理,需要注意手动释放
T* release();           

//删除原有对象,接管新的对象
void reset(T* newObject);

//与其他unique_ptr交换管理的对象
void swap(unique_ptr<T>& other);
删除对象

当unique_ptr 离开其包含对象的作用域时,会删除包含的对象,并调用析构函数

复制代码
	{
		unique_ptr<Rect> p1 = make_unique<Rect>(1,1);
	}

将unique_ptr 设为nullptr时 ,会删除包含的对象,并调用析构函数

复制代码
	unique_ptr<Rect> p2 = make_unique<Rect>(2,2);
    p2 = nullptr;

当使用move转移所有权时

下面示例中, move() 将p4管理的指针转移给p3,释放p3原来管理的对象,p4包含的对象变为空指针

复制代码
   	unique_ptr<Rect> p3 = make_unique<Rect>(3,3);
   	unique_ptr<Rect> p4 = make_unique<Rect>(4,4);
	p3 = move(p4);  

使用reset接管新的指针时,会删除原有对象

复制代码
	unique_ptr<Rect> p5 = make_unique<Rect>(5,5);
	p5.reset(new Rect(6,6));            //可传入nullptr
unique_ptr 不能进行拷贝和复制

因为unique_ptr对管理的对象具有独占性,因此不能调用拷贝构造函数,也不能进行赋值

复制代码
   	unique_ptr<Rect> p1 = make_unique<Rect>(1,1);
   	unique_ptr<Rect> p2 = p1;   // error

   	unique_ptr<Rect> p1 = make_unique<Rect>(1,1);
   	unique_ptr<Rect> p2;   
   	p2 = p1;                   // error

只能使用move 转移所有权

复制代码
 	unique_ptr<Rect> p3 = make_unique<Rect>(3,3);
   	unique_ptr<Rect> p4 = move(p3);
unique_ptr与函数

当函数参数为unique_ptr时,由于不能调用拷贝构造函数,所以不能直接传入

复制代码
void Draw(unique_ptr<Rect> p)
{
	cout<<"画了一个矩形,面积为:"<< p->area() <<endl;
}

int main()
{
	unique_ptr<Rect> p1 = make_unique<Rect>(2,4);
	//Draw(p1)                 //error 由于不能调用拷贝构造函数,所以不能直接传入
	Draw(move(p1));            //转移所有权,Draw1函数结束时,对象随之释放
	//cout<< p1->area()<<endl;  //error 所有权被转移,不能使用

	return 0;
}

为了解决上面出现的问题,可以使用引用,不转移所有权

复制代码
void Draw(unique_ptr<Rect>& p)
{
	cout<<"画了一个矩形,面积为:"<< p->area() <<endl;
}

int main()
{
	unique_ptr<Rect> p1 = make_unique<Rect>(2,4);
	Draw(p1);               
	cout<< p1->area()<<endl;  

	return 0;
}

或者直接使用原始类型

复制代码
void Draw1(Rect* p)
{
	cout<<"画了一个矩形1,面积为:"<< p->area() <<endl;
}
void Draw2(const Rect& p)
{
	cout<<"画了一个矩形2,面积为:"<< p.area() <<endl;
}

int main()
{
	unique_ptr<Rect> p1 = make_unique<Rect>(2,4);
	Draw1(p1.get());  
	Draw2(*p1);             
	cout<< p1->area()<<endl;  

	return 0;
}
使用建议

unique_ptr仅仅是对指针进行一层包装,所以产生的性能影响很小,但是它可以实现内存的自动管理,unique_ptr很适合在指针数组、容器中使用

shared_ptr

与unique_ptr不同,多个shared_ptr对象可以管理同一个指针,

但是其内部维护了一个引用计数器,引用计数器的值表示管理这个指针的shared_ptr的数量,

当引用计数器归零时,会将被管理的对象销毁,释放内存.

创建

创建方法与unique_ptr相似

复制代码
	shared_ptr<Rect> p1 = make_shared<Rect>(2,4);
	shared_ptr<Rect> p2(new Rect(5,6));
常用方法

shared_ptr重载了 *->运算符,让它可以像普通指针一样使用

复制代码
//返回unique_ptr管理的原始指针
T* get(); 

//重载->运算符,内部使用了get()将操作转发给原始指针
T* operator->();                   

//重载*运算符,内部使用了get()将操作转发给原始指针
T& operator*();            

//shared_ptr没有release方法
//T* release();           

//删除原有对象,接管新的对象
void reset(T* newObject);

//与其他shared_ptr交换管理的对象
void swap(shared_ptr<T>& other);

//获取引用计数器的值
long use_count();

//返回引用计数是否为1
bool unique();
示例

获取引用计数

复制代码
	shared_ptr<Rect> p1 = make_shared<Rect>(4,6);
	shared_ptr<Rect> p2 =p1;
	shared_ptr<Rect> p3(p1);
	cout<< p1.use_count()<<endl;
类型转换

shared_ptr提供了几个辅助函数,用于对封装指针类型的动态转换、静态转换、常量转换

dynamic_pointer_cast \ static_pointer_cast \ const_pointer_cast

转换失败则返回 nullptr

复制代码
class Parent
{
public:
	virtual ~Parent(){};
	void print(){ cout<<"Parent"<<endl;}
};

class Child :public Parent
{
public:
	void print(){ cout<<"Child"<<endl;}
};

int main()
{	
	shared_ptr<Child> p1 = make_shared<Child>();
	shared_ptr<Parent> p2 = dynamic_pointer_cast<Parent>(p1);
	shared_ptr<Parent> p3 = static_pointer_cast<Parent>(p1);
	
	//由于p2 p3与p1管理的是同一个指针,所以会增加引用计数 输出3
	cout<< p1.use_count()<<endl;  
	
	return 0;
}

int main()
{	
	shared_ptr<Parent> p1 = make_shared<Parent>();
	shared_ptr<Child> p2 = dynamic_pointer_cast<Child>(p1); //转换失败
	shared_ptr<Child> p3 = static_pointer_cast<Child>(p1);

	//由于p1本身为Parent类型 不能转成Child类型 转换失败 引用计数为2
	cout<< p1.use_count()<<endl;
	
	return 0;
}

int main()
{	
	shared_ptr<Parent> p1 = make_shared<Child>();
	shared_ptr<Child> p2 = dynamic_pointer_cast<Child>(p1);
	shared_ptr<Child> p3 = static_pointer_cast<Child>(p1);

	//由于p1本身为Child类型 转换成功 引用计数为3
	cout<< p1.use_count()<<endl;
	
	return 0;
}
循环引用

当出现循环引用时,引用计数器无法归零,无法释放所指向的资源,造成内存泄漏

复制代码
class Student
{
public:
	Student(const string& name):Name(name){ cout<<"Student构造函数调用"<<endl;}
	~Student(){ cout<<"Student析构函数调用"<<endl;}

	string Name;
	shared_ptr<Student> Deskmate;
};

int main()
{	
	shared_ptr<Student> s1(new Student("张三"));
	shared_ptr<Student> s2(new Student("李四"));
	s1->Deskmate = s2;
	s2->Deskmate = s1;

	//s1和s2产生循环引用 无法释放
	return 0;
}

weak_ptr

weak_ptr 需要与shared_ptr结合使用,用来避免循环引用,

weak_ptr 只对shared_ptr管理的对象进行观测,不改变管理对象的引用计数

创建

使用weak+ptr,需要首先创建shared_ptr

复制代码
	shared_ptr<Student> s1(new Student("张三"));
	weak_ptr<Student> ws1 = s1;
	weak_ptr<Student> ws2(s1);
示例

weak_ptr 可以通过 use_count() 获取引用计数,也可以通过expired() 获取指针是否可用

复制代码
	weak_ptr<Rect> wr;
	{
		shared_ptr<Rect> r1 = make_shared<Rect>(2,4);
		shared_ptr<Rect> r2 = r1;
		wr = r1;
		cout<<"引用计数:"<< wr.use_count()<<endl;    //输出: 2
		cout<<"指针是否销毁:"<< wr.expired()<<endl;  //输出: 0
	}
	
	cout<<"引用计数:"<< wr.use_count()<<endl;    //输出: 0
	cout<<"指针是否销毁:"<< wr.expired()<<endl;   //输出: 1

使用weak_ptr 对前面的示例进行小小的改动 ,就可以解除循环引用

复制代码
class Student
{
public:
	Student(const string& name):Name(name){ cout<<"Student构造函数调用"<<endl;}
	~Student(){ cout<<"Student析构函数调用"<<endl;}

	string Name;
	weak_ptr<Student> Deskmate;
};

int main()
{	
	shared_ptr<Student> s1(new Student("张三"));
	shared_ptr<Student> s2(new Student("李四"));
	s1->Deskmate = s2;
	s2->Deskmate = s1;

	//循环引用解除,正常释放
	return 0;
}
通过weak_ptr 获取 shared_ptr

使用 lock() 函数,直接获取对应的shared_ptr,如果获取的结果非空 那么就是可用的

不需要使用 expired() 函数判断

lock() 函数是一个原子操作,其他线程不能读取、修改、中断该操作,因此保证了多线程是获取的shared_ptr是安全的

在多线程环境下使用expired()函数,只有结果为1时才有效,也就是expired()线程不安全

复制代码
	weak_ptr<Rect> wr;
	{
		shared_ptr<Rect> r1 = make_shared<Rect>(2,4);
		shared_ptr<Rect> r2 = r1;
		wr = r1;
		
		shared_ptr<Rect> r3 = wr.lock(); //成功 引用计数+1
		cout<<"r3 = " <<r3 <<endl;      //输出: r3 = 0x1f23f0 
	}
	shared_ptr<Rect> r4 = wr.lock();    //不成功 引用计数不变
	cout<<"r4 = " <<r4 <<endl;          //输出: r4 = 0

另外可以将weak_ptr作为shared_ptr构造函数的参数,但是如果所观察的对象已经失效

会产生 bad_weak_ptr 异常

复制代码
	weak_ptr<Rect> wr;
	{
		shared_ptr<Rect> r1 = make_shared<Rect>(2,4);
		shared_ptr<Rect> r2 = r1;
		wr = r1;
		
		shared_ptr<Rect> r3(wr);
		cout<<"r3 = " <<r3 <<endl;      //输出: r3 = 0xd623f0
	}
	
	try
	{
		shared_ptr<Rect> r4(wr);            //产生异常
		cout<<"r4 = " <<r4 <<endl;         
	}
	catch(bad_weak_ptr)
	{
		cout<<"对象失效"<<endl;
	}
实现机制

shared_ptr 和 weak_ptr 内部中既包含控制的对象的指针也包含一个指向控制块的指针

控制块对象 是在shared_ptr第一次接管对象的时候动态创建的

控制块中有两个参数, use_count 和 weak_count

use_count 用于记录当前有多少个shared_ptr在管理整个对象

weak_count 用于记录当前有多少个weak_ptr在观察整个对象

新建 shared_ptr 只会增加 use_count 而不会增加 weak_count

新建 weak_ptr 只会增加 weak_count 而不会增加 use_count

销毁 shared_ptr 会将use_count 减 1

销毁 weak_ptr 会将 weak_count 减 1

当 shared_ptr 离开作用域被销毁 , use_count 引用计数归零时, 会将被管理的对象释放,

此时 use_count 为0,weak_ptr无法继续使用, 当weak_ptr离开作用域后被被销毁,

最后当 use_count与 weak_count都归0时,控制块被销毁

相关推荐
小冻梨6662 小时前
ABC445 C - Sugoroku Destination题解
c++·算法·深度优先·图论·
白太岁2 小时前
C++:(4) 内存布局、编译流程、关键字及其链接性
c语言·汇编·jvm·c++
白太岁2 小时前
Muduo:(4) 主从 Reactor、事件循环、跨线程无锁唤醒及其线程池
c++·网络协议·tcp/ip
喜欢吃燃面2 小时前
基础算法:枚举(上)
c++·学习·算法
郝学胜-神的一滴2 小时前
计算思维:数字时代的超级能力
开发语言·数据结构·c++·人工智能·python·算法
兵哥工控2 小时前
mfc 线程启动、挂起、恢复、停止实例
c++·mfc·线程
xiaoye-duck2 小时前
《算法题讲解指南:优选算法-滑动窗口》--09长度最小的子数串,10无重复字符的最长字串
c++·算法
白太岁2 小时前
Muduo:(5) 主 Reactor 之 Acceptor 与 SubReactor 的分发
服务器·网络·c++·网络协议·tcp/ip
Non importa3 小时前
二分法:算法新手第三道坎
c语言·c++·笔记·qt·学习·算法·leetcode