C/C++语言基础--C++智能指针(unique_ptr、shared_ptr、week_ptr)

发布于:2024-12-07 ⋅ 阅读:(156) ⋅ 点赞:(0)

本专栏目的

  • 更新C/C++的基础语法,包括C++的一些新特性

前言

  • 在C、C++语言中,最经典的特性就是指针,他和内存相关,但是我们常常申请内存后忘记释放而导致内存泄漏,C++提供了智能指针去解决这个内存泄漏问题
  • C语言后面也会继续更新知识点,如内联汇编;
  • 欢迎收藏 + 关注,本人将会持续更新。

智能指针

简介

智能指针是一个模板类,封装了裸指针,可以对指针进行安全的操作。

  • 使用RAII特点,将对象生命周期使用栈来管理
  • 智能指针区分了所有权,因此使用责任更为清晰
  • 智能指针大量使用操作符重载和函数内联特点,调用成本和裸指针无差别

为什么要使用智能指针

可以方便我们使用指针的时候,不用担心内存释放的问题

unique_ptr(类模板)

👁 翻译:独有指针

☑️ 头文件:

🎪 特点:

  • 两个unique_ptr 不能指向一个对象,不能进行复制操作只能进行移动操作,📧 也就是说一个对象申请的内存如果用这个unique_ptr智能指针去管理,只能用一个unique_ptr去管理,不能用同时多个unique_ptr管理。
  • 不支持: 算术运算,不能通过unique_ptr 操作指针的++、--;
  • 复制:只允许移动,不允许赋值;
  • 作用域: 离开作用域自动释放内存。

构造对象

1、可以使用unique_ptr提供的构造函数构造对象,可以构造对象,也可以构造数组!

  • 自己先申请一个内存,然后交接unique_ptr管理
std::unique_ptr<Test> p1(new Test);
std::unique_ptr<Test[]> p2(new Test[5]);   //注意一下数组的申请

还可以这样:

auto p3 = std::make_unique<Test>();
auto p4 = std::make_unique<Test[]>(5);     //注意Text[]   一定得声明数组

2、使用make_unique构造,然后使用auto自动类型推导,就很方便了。

  • make_unique 是将类型告诉他,它会自动申请一个内存。
unique_ptr<Test> up = p1;				//尝试引用已删除的函数
unique_ptr<Test> up1 = std::move(p1);	//可以移动

值得注意的是,unique_ptr禁用了拷贝和赋值操作,只能进行移动。

删除器

在某些时候,默认的释放操作不能满足咱们的需要,这个时候就需要自定义删除器。(在构造智能指针的第二个参数指定他的释放类型)

// 可以用:全局函数、lambda、函数包装器、仿函数作为删除器,如下:
void del_global(Test* ptr){delete ptr;}					//全局函数
auto del_lambda = [](Test* ptr) {delete ptr; };			//lambda
std::function<void(Test*)> del_function = del_lambda;	//函数包装器
struct Del_Object										//仿函数
{
	void operator()(Test* ptr)
	{
		delete ptr;
	}
};

int main()
{
   	std::unique_ptr<Test, decltype(del_global)*>	p2(new Test, del_global);
    // void(*)(Text*)    typeid(Del_Object).name()
	std::unique_ptr<Test, decltype(del_lambda)>		p3(new Test, del_lambda);
	std::unique_ptr<Test, decltype(del_function)>	p4(new Test, del_function);
	std::unique_ptr<Test, decltype(Del_Object())>	p5(new Test, Del_Object());
    
    //获取删除器  get_deleter方法
    auto delfun = p1.get_deleter();
    
    return 0;
}

指针使用

🏗 重载: 类中只用了大量的重载运算符,故和平常指针使用起来基本没差别。

判断

因为重载了operator bool()函数,所以可以直接判断,指针是否为nullptr。

if(p1)
{
    
}
if(!p1)
{
    
}

解引用

因为重载了operator*()函数,所以可以直接对指针解引用。

*p1;

访问成员

因为重载了operator->()函数,所以可以直接获取对象的成员。

p1->~Test();

下标访问

因为重载了operator[]()函数,如果管理的是数组,则可以通过下标访问元素。

p1[n];

获取/释放

get

使用get()获取管理的对象原生指针,如果unique_ptr为空则返回nullptr。

void test()
{
	int* pp = nullptr;
	{
		auto p = std::make_unique<int>(2);
		std::cout << *p << std::endl;
		pp = p.get();
	}
	std::cout << *pp << std::endl;	//输出垃圾值
}
release

使用release()可以返回原生指针并释放所有权,智能指针管理的指针会变为nullptr。

🎫 注意: 这个时候就需要自己手动释放内存了。

void test()
{
	auto p = std::make_unique<int>(23);
	auto pp = p.release();			//智能指针p不在管理pp对象了 , 释放所有权,并且返回原生指针
	delete pp;						//需要自己释放内存
}
reset

使用reset()释放unique_ptr管理的对象,有一个可选参数,如果为nullptr,则只会释放对象,并指向nullptr

注意:这个函数是释放unique_ptr对象管理的指针,使其不在管理这个指针,这个时候如果传递了对象指针,则释放之后unique_ptr会接管传递的对象。

void test()
{
	unique_ptr<int> up;
				
	up.reset(new int(-22));		//获取指针的所有权
	cout << *up << endl;

	up.reset(new int(666));		//释放管理的内存,获取新的指针
	cout << *up << endl;

	up.reset();						//释放管理的内存
}

shared_ptr

unique_ptr是一个独享指针,与同一时刻只能有一个unique_ptr指向一个对象,而shared_ptr是一个共享指针,同一时刻可以有多个shared_ptr指向同一对象;

unique_ptr会记录有多少个shared_ptr共同指向一个对象,这便是所谓的引用计数

🔽 **注意 注意 注意 **:use_count() 记录有多少个shared_ptr共同指向一个对象,但是shared_ptr 不是通过原生指针赋值才使use_count + 1 的,是通过赋值shared_ptr

🎲 一旦某个对象的引用计数变为0,这个对象会被自动删除.

常见错误

  • 用原生指针多次初始化
  • 不明白 shared_ptr 中构造函数中 用了 explicit
  • 用栈指针赋值
  • 使用shared_ptr的get()初始化另一个shared_ptr
  • 作为函数参数传递时,不要乱用引用,因为他本身有引用计数,乱用可能导致指针提前释放

shared_ptr 内部指向两个位置

  • 指向对象的指针;
  • 用于控制引用计数数据的指针。

构造对象

可以使用shared_ptr提供的构造函数构造对象,可以构造对象,也可以构造数组!

std::shared_ptr<Test> sp(new Test);
std::shared_ptr<Test[]> sp1(new Test[5]);

还可以这样:

auto sp2 = std::make_shared<Test>();
/值得会注意的是,使用       make_shared 不支持创建数组
//auto sp3 = std::make_shared<Test>(5);

使用make_shared构造,然后使用auto自动类型推导,就很方便了,这也是本人常用的。

删除器

在某些时候,默认的释放操作不能满足咱们的需要,这个时候就需要自定义删除器。

std::shared_ptr<Test> sp(new Test, [](auto* ptr) {delete ptr; });

指针使用

和unique_ptr一样

获取/释放

use_count(是一个函数)

使用use_count获取所指对象的引用计数。如果这是一个空的shared_ptr,函数返回零。

unique

检查所管理对象是否仅由当前shared_ptr管理。(判断shared_ptr是不是独占的(只有本对象拥有资源的所有权))

get

unique_ptr;

reset

这个和unique_ptr一样

void reset();					//仅释放,(如果有多个shared_ptr共享一个对象,那么就在引用-1,等到为0时,会自动释放资源
void reset(_Ux* _Px);			//先释放,并接管_Px
void reset(_Ux* _Px, _Dx _Dt)	//先释放,并接管_Px,并给_Px指定_Dt删除器

weak_ptr

简介

shared_ptr可以用来避免内存泄漏,可以自动释放内存,但是在使用中可能存在循环引用,使引用计数失效,从而导致内存泄漏的情况。如下代码所示:

class Widget
{
public:
	Widget() { std::cout << __FUNCTION__ << std::endl; }
	~Widget() { std::cout << __FUNCTION__ << std::endl; }
	void setParent(std::shared_ptr<Widget>& parent) { _ptr = parent; }
private:
	std::shared_ptr<Widget> _ptr;
};

int main()
{
	{
		std::shared_ptr<Widget> w1 = std::make_shared<Widget>();
		std::shared_ptr<Widget> w2 = std::make_shared<Widget>();
		std::cout << w1.use_count() << " " << w2.use_count() << std::endl;
		w1->setParent(w2);
		w2->setParent(w1);
		std::cout << w1.use_count() << " " << w2.use_count() << std::endl;
	}

	return 0;
}

输出结果:

Widget::Widget
Widget::Widget
1 1
2 2

我们发现,并没有调用析构函数,也就是对象没有释放,为什么会发生这样的事情呢?

  • w1->_ptr 引用了w2,w2->_ptr 引用了w1,这样就形成了循环引用。
  • 当w1超出作用域,释放的时候,发现管理的对象引用计数不为1,则不释放对象
  • 当w2超出作用域,释放的时候,发现管理的对象引用计数也不为1,也不是放对象
  • 这样就发生了内存泄漏

weak_ptr

  • weak_ptr: 的出现是为了解决,shared_ptr的循环引用问题的,weak_ptr是对shared_ptr 对象的一种弱引用,它不会增加对象的引用计数

  • 循环引用:两个对象,每个对象都有一个shared_ptr成员,他们这个成员相互引用;

  • weak_ptr不会增加shared_ptr的引用次数,weak_ptr只能shared_ptr构造;

  • shared_ptr可以直接赋值给week_ptrweek_ptr可通过调用lock函数来获得shared_ptr

解决循环引用

shared_ptr智能指针的循环引用导致的内存泄漏问题,可以通过weak_ptr解决。只需要将std::shared_ptr<Widget> _ptr改为weak_ptr:

std::weak_ptr<Widget> _ptr;

输出结果为:

Widget::Widget
Widget::Widget
1 1
1 1
Widget::~Widget
Widget::~Widget

weak_ptr函数

  • **use_count():**获取当前观察的资源的引用计数
  • **expired():**判断所观察资源是否已经释放
  • **lock():**返回一个指向共享对象的shared_ptr,如果对象被释放,则返回一个空shared_ptr:

使用场景:weak_ptr不改变其所共享的shared_ptr实例的引用计数,那就可能存在weak_ptr指向的对象被释放掉这种情况。
这时就不能使用weak_ptr直接访问对象。必须用expired()判断weak_ptr指向对象是否存在!

我们需要使用enable_shared_from_this?

  • 需要在对象内部有一个指向当前对象的shared_ptr指针
  • 当一个程序中,类的成员函数需要传递this指针给其他函数时候,如果构造另外一个shared_ptr传进去的化,回出错,因为这个相当于用另外一个智能指针去管理了,不是传递本身的智能指针

什么时候用enable_shared_ptr?

  • 当我们需要在外部多次使用一个相同的shared_ptr时候。
  • 当类的成员函数需要传递this指针给其他函数的时候(这个时候资源共享)
  • 如下:

int main()
{
	//或直接构造智能指针↓
	auto obj = std::make_shared<SObject>();

	//获取指向obj的智能指针
	auto sp = obj->shared_from_this();  
	auto sp1 = obj->shared_from_this();

	std::cout << sp.use_count() << " " << sp1.use_count() << std::endl;
	if (!sp)
	{
		std::cout << "is nullptr" << std::endl;
	}
	return 0;
}

输出:

1 1   // 共同使用一个shared_ptr

这个需要结合一些项目场景去理解,后面我们会更新aiso网络去构建Tcp服务器的文章,那里会有涉及到这个场景。