【C++】智能指针总结:auto_ptr、unique_ptr、share_ptr、weak_ptr(技术介绍 + 代码实现)

news2024/12/24 2:56:33

文章目录

  • 0. 概述
    • 智能指针,智能在哪儿?
    • RAII 的介绍
    • 四个智能指针的特点:
  • 1. auto_ptr(C++98)
      • 🐎核心功能的简单实现
  • 2. unique_ptr(C++11)
      • 🐎核心功能的简单实现
  • 3. shared_ptr(C++11)
      • 🐎核心功能的简单实现(手撕 shared_ptr 版本)🔺
      • 🐎核心功能的扩展实现
    • 3.1 shared_ptr 的 多线程问题
    • 3.2 share_ptr 循环引用的问题
      • 循环引用的产生
      • 循环引用的原因分析
  • 4. weak_ptr(C++11)
      • 🐎核心功能的简单实现
  • 了解:定制删除器


0. 概述

智能指针,智能在哪儿?

  • 使用了模板类,建立的是 智能指针对象,自动调用智能指针类型的构造和析构函数。也就是说,对于动态开辟的空间如果用智能指针保存,就不需要手动释放啦,极大程度降低了内存泄漏的风险。
    • 这样利用对象生命周期进行程序资源控制的技术就是 RAII。
  • *-> 的重载,使 智能指针对象 具有指针的行为能力,能让用户像使用指针一样的使用。

RAII 的介绍

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。

在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:

  • 不需要显式地释放资源;
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效。

四个智能指针的特点:

我把四个智能指针的特点介绍在前面,你若还有什么细节问题再去具体的栏目下翻找吧~

  • auto_ptr:管理权转移,通过拷贝构造函数和赋值重载函数来实现。
    • 原对象拷贝给新对象的时候,原对象就会被设置为nullptr,此时就只有新对象指向一块资源空间。
    • 会出现指针悬空问题。
  • unique_ptr:禁用拷贝构造和赋值构造
    • unique_ptr(unique_ptr&) = delete;
    • operator=(unique_ptr&) = delete;
  • share_ptr:引用计数
    • 计数的对象在堆上,所有线程都能访问,因此需要锁保证其安全性
    • 会出现循环引用的问题
  • weak_ptr:弱关联性
    • weak_ptr 类的对象它可以指向 shared_ptr,并且不会改变 shared_ptr 的引用计数

1. auto_ptr(C++98)

核心功能:管理权转移

管理权转移的同时也会导致 原指针悬空,容易造成野指针问题,不推荐使用。

🐎核心功能的简单实现

namespace ttang
{
	template<class T>
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}

		~auto_ptr()
		{
			if (_ptr)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
			}
		}
		// 无力吐槽的神拷贝...
		// 管理权转移:导致的是原来的指针悬空,很多公司明令禁止使用 auto_ptr
		auto_ptr(auto_ptr<T>& ap)
			:_ptr(ap._ptr)
		{
			ap._ptr = nullptr;
		}
		
		T* operator=(auto_ptr<T>& ap)
		{
			T* tmp = ap._ptr;
			ap._ptr = nullptr;
			return tmp;
		}

		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};
--------------------------------------------------
	void test_auto()
	{
		auto_ptr<int> ap1(new int(1));
		auto_ptr<int> ap2(ap1);

		*ap1 = 1; // err...管理权转移以后导致ap1悬空,不能访问
		*ap2 = 1;
	}
}

2. unique_ptr(C++11)

核心功能:防拷贝(= delete 声明拷贝构造和复制重载)

unique_ptr 的指针,简单粗暴,是防了拷贝,不过也只解决了不需要拷贝的场景。
(ps:从 boost 里面吸收来的)
(pps:需要拷贝的场景就需要使用到接下来会介绍的 shared_ptr 和 weak_ptr 了)

🐎核心功能的简单实现

namespace ttang
{
	template<class T>
	class unique_ptr
	{
	private:
		T* _ptr;
	public:
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}

		~unique_ptr()
		{
			if (_ptr)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
			}
		}

		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}

		// C++11思路:设置不许再实现了,语法直接支持的(不需要私有了)
		unique_ptr(const unique_ptr<T>& up) = delete;
		unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;	// 严格来说赋值也封了更好一点

		// C++98思路:只声明不实现,但是用的人可能会在外面强行定义,所以再加一条,声明为私有
	//private:
		// unique_ptr(const unique_ptr<T>& up);
		// unique_ptr<T>& operator=(const unique_ptr<T>& up);
	};
--------------------------------------------------
	void test_unique()
	{
		unique_ptr<int> up1(new int(1));
		unique_ptr<int> up2(up1);	// err...
	}
}

3. shared_ptr(C++11)

核心功能:引用计数

之前在一开始的概述部分介绍了两个智能指针为什么智能的原因,走到了 shared_ptr,我们的智能指针就真的更神了,他甚至还引申出 智能指针三大件 的说法:

  • RAII
  • 想指针一样使用
  • 可以拷贝(浅拷贝!!)

shared_ptr 允许拷贝,意在允许多个智能指针可以指向同一块资源,并且能够保证共享的资源只会被释放一次,程序不会多次析构而崩溃。为了保证在最后一个智能指针使用完毕才释放,C++11 使用了引用计数的技术。

在具体对 shared_ptr 实现之前,对于这里的 引用计数,可以稍微探讨一下:

1)如果要使用引用计数,设置一个静态变量 count 行不行呢?不行,因为静态变量属于所有对象。而 每实例化一个对象都可能多有个资源,每个资源应该配对一个引用计数

2)如果是多个线程去调用引用计数,还需要保证其线程安全,那就加个锁吧。

3)计数加锁后,shared_ptr 本身就会是线程安全的,但是他生成的对象不是线程安全的。

🐎核心功能的简单实现(手撕 shared_ptr 版本)🔺

namespace ttang
{
	template<class T>
	class shared_ptr{
	private:
		T* _ptr;	// 用指针,一个资源的多个指针要看见并修改这一个count
		int* _pcount;
	public:
		// 【构造】
		shared_ptr(T* ptr = NULL)
			: _ptr(ptr)
			, _pcount(new int(1)) 
		{}
		// 【拷贝构造】
		shared_ptr(const shared_ptr& s)
			: _ptr(s.ptr)
			, _pcount(s._pcount)
		{
			*(_pcount)++;
		}
		// 【赋值重载】
		/*
		* 【1】正常赋值:
		* sp1 = sp4;					被赋值sp1原来有资源,的肯定要把原来的资源--,sp4要++
		* 【2】自己给自己赋值:
		* sp1 = sp1;				自己给自己	
		* sp1 = sp2;(管理一个资源)	也是自己给自己,这时候 if(&sp != this) 就不得行了哦,防不了这一种
		*/
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			// 检查不是自己赋给自己
			if(this != &sp)
			{ 
				// 赋值之前查一下自己的_pcount, 
				if(--(*_pcount) == 0)
				{
					delete _ptr;
					delete _pcount;
				}
				_ptr = s._ptr;
				_pcount = s._pcount;
				*(_pcount)++;
			}
			return *this;
		}
		
		// 模拟指针行为,解引用 return 数据内容
		T& operator*()
		{
			return *_ptr;
		}
		// 模拟指针行为,箭头指向 return 指针自己
		T* operator->()
		{
			return _ptr;
		}
		//【析构】
		~shared_ptr()
		{ 
			--(*_pcount);
			if(*_pcount == 0)
			{
				delete _ptr;
				delete _pcount;
				_ptr = nullptr;
				_pcount = nullptr;
			}
		}
		// 还可以扩展,看下面的代码示例
	};
}

🐎核心功能的扩展实现

在上面的基础上:

  • 加锁,保证计数的线程安全
  • 定制删除器,供用户可以自定义传入删除器
namespace ttang
{
	template<class T>
	class shared_ptr
	{
	private:
		T* _ptr;
		int* _pcount;								
		mutex* _pmtx;								// 锁也得是指针,因为是多个指针指向同一把锁
		function<void(T*)> _del = [](T* ptr) {		// 解决对 deletor 的保存问题,需要一个缺省的!!
			cout << "lambda delete:" << ptr << endl;
			delete ptr;
		};
	public:
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			, _pcount(new int(1))	// 每个资源都分配一个引用计数count
			, _pmtx(new mutex)		// 每个资源都有一把锁,保证自己资源计数安全
		{}

		// 定制删除器(通过仿函数实现的!--是可调用对象,所以我们拿的一个function定义_del)
		template<class D>
		shared_ptr(T* ptr, D del)
			: _ptr(ptr)
			, _pcount(new int(1))
			, _pmtx(new mutex)
			, _del(del)
		{}
		
		~shared_ptr()
		{
			Release();
		}

		void Release()
		{
			_pmtx->lock();

			bool deleteFlag = false;

			if (--(*_pcount) == 0)
			{
				if (_ptr)
				{
					//cout << "delete:" << _ptr << endl;
					//delete _ptr;
					_del(_ptr);			// 如果_del 不给缺省的话,这里默认的构造可能会出问题
				}
				delete _pcount;

				deleteFlag = true;
				// delete _pmtx;  锁也要释放的呀,可以下面又要解锁。如何解决?
			}

			_pmtx->unlock();

			if (deleteFlag)
			{
				delete _pmtx;
			}
		}

		void AddCount()
		{
			_pmtx->lock();

			++(*_pcount);

			_pmtx->unlock();
		}

		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pcount(sp._pcount)
			, _pmtx(sp._pmtx)
		{
			AddCount();
		}

		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr)
			{
				Release();

				_ptr = sp._ptr;
				_pcount = sp._pcount;
				_pmtx = sp._pmtx;

				AddCount();
			}

			return *this;
		}

		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}

		T* get()
		{
			return _ptr;
		}

		int use_count()
		{
			return *_pcount;
		}
	};
	
	void test_shared()
	{
		shared_ptr<int> sp1(new int(1));
		shared_ptr<int> sp2(sp1);
		shared_ptr<int> sp3(sp2);

		shared_ptr<int> sp4(new int(10));

		//sp1 = sp4;
		sp4 = sp1;

		sp1 = sp1;
		sp1 = sp2;
	}
}

3.1 shared_ptr 的 多线程问题

我们说 shared_ptr本身是线程安全的,因为计数是加锁保护的;
那 shared_ptr 管理的对象是否是线程安全呢?不安全。

如果需要多线程访问资源,需要程序员手动加锁。

🌰举例:

namespace ttang
{
	struct Date
	{
		int _year = 0;
		int _month = 0;
		int _day = 0;
	};

	void SharePtrFunc(ttang::shared_ptr<Date>& sp, size_t n, mutex& mtx)
	{
		//cout << sp.get() << endl;
		//cout << &sp << endl;

		for (size_t i = 0; i < n; ++i)
		{
			// 智能指针拷贝会++计数,析构会--计数,这里是线程安全的。
			ttang::shared_ptr<Date> copy(sp);

			mtx.lock();
			sp->_year++;
			sp->_day++;
			sp->_month++;
			mtx.unlock();
		}
	}

	void test_shared_safe()
	{
		ttang::shared_ptr<Date> p(new Date);
		cout << p.get() << endl;

		const size_t n = 10000;
		mutex mtx;
		thread t1(SharePtrFunc, ref(p), n, ref(mtx));	// 线程中以引用传递对象参数,必须加一个ref(),是一个库函数,否则会认为是传值传参会报错。
		thread t2(SharePtrFunc, ref(p), n, ref(mtx));	// 13 底下可以检测,19 是直接报错
		//cout << &p << endl;	

		t1.join();
		t2.join();

		cout << p.use_count() << endl;

		cout << p->_year << endl;
		cout << p->_month << endl;
		cout << p->_day << endl;
	}
}

3.2 share_ptr 循环引用的问题

这里举例一个 List 数据结构

我们按照传统通常这样去定义一个节点:

struct ListNode{
	ListNode* _next;
	ListNode* _prev;
	// ...
};

然后这样去使用他:

ListNode* n1 = new ListNode;
ListNode* n2 = new ListNode;

n1->_next = n2;
n2->_prev = n1;	

//...	
如果在这里有异常抛出,后面的代码就不会执行了~
//...

delete n1;
delete n2;

但事实上,这样使用会出现一个问题,如果在 delete 节点前,就抛异常,代码运行逻辑就出去了,会导致没有释放的情况。好说,我们学过了 RAII,可以选择利用对象的生命周期来实现对资源的控制,于是乎可以在使用时将节点定义成 shared_ptr。

循环引用的产生

Node 节点定义成 shared_ptr,要完成 Node1->_next = Node2,就同样需要在结构体里把 _next 和 _prev 定义成 shared_ptr。

那么代码就应该写成这样:

struct ListNode{
	ttang::shared_ptr<ListNode> _next;
	ttang::shared_ptr<ListNode> _prev;
	// ...
};

然后这样去使用他

std::shared_ptr<ListNode> n1(new ListNode);	// std 里面只能这样显示的调构造
std::shared_ptr<ListNode> n2(new ListNode);

cout << n1.use_count() << endl;	//1
cout << n2.use_count() << endl;	//1

n1->_next = n2;	
n2->_prev = n1;	

cout << n1.use_count() << endl;	//2
cout << n2.use_count() << endl;	//2

向上面这样,shared_ptr 管理的两个节点相互指向,奇怪的事情就出现了:

在这里插入图片描述

可以看到,随着程序的结束,两个原本应该随进程周期结束而析构的指针,并没有析构,也就是说出现了未释放、内存泄露的情况。怎么回事呢?

循环引用的原因分析

在这里插入图片描述

  • 如图所示:当 Node1_ptr 被建立的时候,其引用就为 1 了,在被 Node2_ptr 指向的时候,引用就变成了 2。同理 Node2_ptr 也一样。
  • 当程序结束的时候两个 ptr 对象都理应调用自己的析构函数,但是其内部的指针互相指向引用计数始终不为 0 无法析构,这导致了两个对象没有真正的被回收。
  • 书面原理:成员的生命周期,取决于对象的生命周期,对象的生命周期结束则成员调用析构函数,成员源于被另一个智能指针管理,无法释放,形成闭环。
  • 这就是循环引用产生的原因。

针对上述问题的出现,C++11 提供了一个解决方案:


4. weak_ptr(C++11)

核心功能:弱连接 ,专门用来解决 shared_ptr 的循环引用问题。

注意有三:

  • 他不是常规的智能指针,不支持 RAII
  • 支持像指针一样
  • 专门设计出来,辅助解决 shared_ptr 的循环引用问题

weak_ptr 的智能指针可以指向 shared_pre 的指针指向的资源,而不增加 share_ptr 的引用计数。

于是定义结点的代码应该修改成这样:

struct ListNode
{	
	std::weak_ptr<ListNode> _next;		
	std::weak_ptr<ListNode> _prev;
	int _val;
	
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};

🐎核心功能的简单实现

namespace ttang
{
	template<class T>
	class weak_ptr	// 超简单实现,库里肯定不是这样滴
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{}

		weak_ptr(const shared_ptr<T>& sp)
			:_ptr(sp.get())
		{}

		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}

		T* get()
		{
			return _ptr;
		}

	private:
		T* _ptr;
	};
}

了解:定制删除器

特殊定制析构方式,不难,看代码吧。
结合 ttang::shared 里的,构造的时候第二个参数传入可调用对象就行。

//  定制删除器 -- 可调用对象
template<class T>
struct DeleteArray
{
	void operator()(T* ptr)
	{
		cout << "void operator()(T* ptr)" << endl;
		delete[] ptr;
	}
};
struct Date
{
	int _year = 0;
	int _month = 0;
	int _day = 0;
};

void test_shared_deletor()
{
	ttang::shared_ptr<Date> sp0(new Date);

	ttang::shared_ptr<Date> spa1(new Date[10], DeleteArray<Date>());
	ttang::shared_ptr<Date> spa2(new Date[10], [](Date* ptr) {
		cout << "lambda delete[]:" << ptr << endl;
		delete[] ptr;
		});

	ttang::shared_ptr<FILE> spF3(fopen("Test.cpp", "r"), [](FILE* ptr) {
		cout << "lambda fclose:" << ptr << endl;
		fclose(ptr);
		});
}

🥰如果本文对你有些帮助,欢迎👉 点赞 收藏 关注,你的支持是对作者大大莫大的鼓励!!(✿◡‿◡) 若有差错恳请留言指正~~


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1148923.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

LSMW应用

案 执行第二行 第六步直接跳过.

【2023MathorCup大数据竞赛】B题完整解答过程(思路+模型文档+代码+结果)

B题完整解答过程 写在最后技术文档&#xff08;部分&#xff09;问题分析假设符号说明1 基于自适应ARIMA-LR模型的需求量预测&#xff08;问题一第一小问&#xff09;1.1 ARIMA模型的建立1.2 LR模型的建立1.3 自适应混合ARIMA-LR模型的建立1.4 ARIMA模型的求解1.5 LR模型的求解…

图像分类任务ViT与CNN谁更胜一筹?DeepMind用实验证明

精华置顶 墙裂推荐&#xff01;小白如何1个月系统学习CV核心知识&#xff1a;链接 点击CV计算机视觉&#xff0c;关注更多CV干货 今天跟大家分享DeepMind发表的一篇技术报告&#xff0c;通过实验得出&#xff0c;CNN与ViT的架构之间虽然存在差异&#xff0c;但同等计算资源的预…

力扣刷题 day59:10-29

1.子集 给你一个整数数组 nums &#xff0c;数组中的元素 互不相同 。返回该数组所有可能的子集&#xff08;幂集&#xff09;。 解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。 方法一&#xff1a;二进制枚举 #方法一&#xff1a;二进制枚举 def subsets(nums)…

C# “依赖注入” 中的 “三种生命周期”

&#x1f680;简介 依赖注入&#xff08;Dependency Injection&#xff0c;简称DI&#xff09;是一种实现控制反转&#xff08;IoC&#xff09;的技术&#xff0c;用于减少代码之间的耦合度。通过依赖注入&#xff0c;一个类可以从外部获取其依赖的对象&#xff0c;而不是自己…

MyBatis-Plus 与 Druid 结合 Dynamic-datasource 实现多数据源操作数据库

MyBatis-Plus 官网&#xff1a;https://baomidou.com/ MyBatis-Plus 官方文档&#xff1a;https://baomidou.com/pages/24112f/ dynamic-datasource 文档&#xff08;付费&#xff09;&#xff1a;https://www.kancloud.cn/tracy5546/dynamic-datasource/2264611 创建数据库…

EM算法解析+代码

大纲 数学基础&#xff1a;凸凹函数&#xff0c;Jensen不等式&#xff0c;MLEEM算法公式&#xff0c;收敛性HMM高斯混合模型 一、数学基础 1. 凸函数 通常在实际中&#xff0c;最小化的函数有几个极值&#xff0c;所以最优化算法得出的极值不确实是否为全局的极值&#xff…

腾讯云2023年双11服务器优惠活动及价格表

腾讯云2023年双11大促活动正在火热进行中&#xff0c;腾讯云推出了一系列服务器优惠活动&#xff0c;云服务器首年1.8折起&#xff0c;买1年送3个月&#xff01;境外云服务器15元/月起&#xff0c;买更多省更多&#xff01;下面给大家分享腾讯云双11服务器优惠活动及价格表&…

Ps:平滑选区边缘

Ps 中某些传统的选区工具&#xff0c;如快速选择工具等&#xff0c;在进行选区后容易产生锯齿状等不规则的边界。此时&#xff0c;可利用“选择并遮住”来平滑选区边缘。 ◆ ◆ ◆ 操作方法与技巧 1、&#xff08;可选&#xff09;新建纯色填充图层&#xff0c;并置于原图图层…

【论文精读1】MVSNet架构各组织详解

一、训练流程 1. 特征提取 提取N个输入图像的深层特征用作深度匹配 与传统三维重建方法类似&#xff0c;第一步是提取图像特征&#xff08;SIFT等特征子&#xff09;&#xff0c;不同点在于本文使用8层的卷积网络从图像当中提取更深层的图像特征表示&#xff0c;网络结构如下…

ProEssentials pro v9 历史更新列表--注册版

ProEssentials标准版和专业版之间的唯一区别是可以渲染的数据点和注释的数量。标准版与专业版一样拥有所有的功能和接口。所有版本包括WPF、WinForm、WebForm、ActiveX、VCL和DLL接口。标准版仅限于8000个数据点和800个图表注释。此限制适用于每个控件实例。你可以运行多个控件…

《数字图像处理-OpenCV/Python》连载(33)使用掩模图像控制处理区域

**本书京东优惠购书链接&#xff1a;https://item.jd.com/14098452.html** **本书CSDN独家连载专栏&#xff1a;https://blog.csdn.net/youcans/category_12418787.html** 第 5 章 图像的算术运算 在OpenCV中&#xff0c;图像是以Numpy数组格式存储的&#xff0c;图像的算术运…

抓包分析DSCP字段在FTP/RSTP协议中的应用

抓包分析DSCP字段在FTP协议中的应用 简介 本文介绍DSCP字段的作用&#xff0c;以及抓包分析DSCP字段在FTP协议中的应用。最后通过实验证明有可能DSCP字段实际上对普通用户没啥用&#xff0c;原因是运营商可能会将用户设置的DSCP字段重置。 DSCP IP报文中有个TOS字段 &#…

C语言选择排序

1.选择排序(Selection sort) 选择排序是简单直观的排序算法。 基本思想&#xff1a;从首元素开始&#xff0c;首元素与它后面的所有元素进行比较&#xff0c;找到数列中最小的元素&#xff0c;与首元素值交换。然后下一个元素与它后面的元素比较&#xff0c;得到第二小的元素…

Zynq UltraScale+ XCZU5EV 纯VHDL解码 IMX214 MIPI 视频,2路视频拼接输出,提供vivado工程源码和技术支持

目录 1、前言免责声明 2、我这里已有的 MIPI 编解码方案3、本 MIPI CSI2 模块性能及其优越性4、详细设计方案设计原理框图IMX214 摄像头及其配置D-PHY 模块CSI-2-RX 模块Bayer转RGB模块伽马矫正模块VDMA图像缓存Video Scaler 图像缓存DP 输出 5、vivado工程详解PL端FPGA硬件设计…

C语言实现输入 n 个字符串,把其中以字母 A 打头的字符串输出

完整代码&#xff1a; // 输入 n 个字符串&#xff0c;把其中以字母 A 打头的字符串输出。 #include<stdio.h> #include<stdlib.h> //字符串的最大长度 #define N 20int main(){int n;printf("请输入字符串个数n:");scanf("%d",&n);//读取…

使用OBS Browser+访问华为云OBS存储【Windows】

背景 项目中使用华为云 S3 存储,java 代码中通过华为云 OBS 提供的esdk-obs-java 来访问文件。 但是,通过 JAVA SDK 方式不太方便运维,所以我们需要一款可视化的客户端软件。 华为云 OBS 自身也提供了一款客户端软件,名为 OBS Browser+。 OBS Browser+简介 OBS Browse…

【洛谷算法题】P5709-Apples Prologue / 苹果和虫子【入门2分支结构】

&#x1f468;‍&#x1f4bb;博客主页&#xff1a;花无缺 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! 本文由 花无缺 原创 收录于专栏 【洛谷算法题】 文章目录 【洛谷算法题】P5709-Apples Prologue / 苹果和虫子【入门2分支结构】&#x1f30f;题目描述&am…

深入理解指针3

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 前言 1. 字符指针变量 2. 数组指针变量 2.1 数组指针变量是什么&#xff1f; 2.2 数组指针变量怎么初始化 3. 二维数组传参的本质 4. 函数指针变量 4.1 函数指针变量的创…

比亚迪应届生爆料,真实的工作状态!

一位比亚迪的应届生发帖子称&#xff0c;校招入职比亚迪一个月&#xff0c;爱上了这里的工作文化&#xff0c;不怎么加班&#xff0c;工作日常就是开会对问题&#xff0c;顺便吵架扯皮&#xff0c;不用写令人头疼的代码&#xff0c;但是却拿着高工资&#xff0c;感觉现在的生活…