【C++】在龙年拿捏智能指针

news2024/12/25 23:36:58

文章目录

  • 1 :peach:为什么需要智能指针?:peach:
  • 2 :peach:内存泄漏:peach:
    • 2.1 :apple:什么是内存泄漏:apple:
    • 2.2 :apple:内存泄漏分类:apple:
    • 2.3 :apple:如何检测内存泄漏:apple:
    • 2.4:apple:如何避免内存泄漏:apple:
  • 3 :peach:智能指针的使用及原理:peach:
    • 3.1 :apple:RAII:apple:
    • 3.2 :apple:auto_ptr 和 unique_ptr:apple:
    • 3.3 :apple:shared_ptr 和 weak_ptr:apple:
    • 3.4 :apple:定制删除器:apple:
    • 3.5 :apple:C++11和boost中智能指针的关系:apple:


1 🍑为什么需要智能指针?🍑

首先我们来看下面这样的一种场景:

int Div(int x, int y)
{
	if (y == 0)
		throw("除0错误");
	else
		return x / y;
}

void Func(int x, int y)
{
	int* p1 = new int;
	int* p2 = new int;

	Div(x, y);
	delete p1;
	delete p2;
}

int main()
{
	int x, y;
	cin >> x >> y;
	try
	{
		Func(x,y);
	}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
	}
	catch (...)
	{
		cout << "未知异常" << endl;
	}
	return 0;
}

我们思考下面的问题:
1. 如果p1这里new 抛异常会如何?
2. 如果p2这里new 抛异常会如何?
3. 如果Div调用这里又会抛异常会如何?

通过分析我们不难得知,当new p1这里抛了异常后,程序是没啥问题的,因为对象都还没有被new出来的;当new p2这里抛了异常后,是存在内存泄漏的,p1的资源没有被释放;当Div抛了异常后,同理p1和p2的资源都没有被释放。我们可以在Func函数中自行捕获一下,但是这样做是不是有点麻烦呢?如果抛出的异常有更多又应该咋办?所以引出了解决这种场景的利器:智能指针


2 🍑内存泄漏🍑

在讲解智能指针前我们先来了解下什么是内存泄漏?

2.1 🍎什么是内存泄漏🍎

内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。

2.2 🍎内存泄漏分类🍎

C/C++程序中一般我们关心两种方面的内存泄漏:

- 堆内存泄漏(Heap leak)
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free/delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。

- 系统资源泄漏
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

2.3 🍎如何检测内存泄漏🍎

  • 在linux下内存泄漏检测:👉【Linux下几种内存泄漏检测工具】👈

  • 在windows下使用第三方工具:👉【VLD工具说明】👈

  • 其他工具:👉【其他内存泄漏检测工具】👈

2.4🍎如何避免内存泄漏🍎

  1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。
  2. 采用RAII思想或者智能指针来管理资源。
  3. 有些公司内部规范使用内部实现的私有内存管理库,这套库自带内存泄漏检测的功能选项。
  4. 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。

内存泄漏非常常见,解决方案分为两种:

  • 1️⃣事前预防型,如智能指针等。
  • 2️⃣事后查错型,如内存泄漏检测工具。

3 🍑智能指针的使用及原理🍑

3.1 🍎RAII🍎

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。

使用库中智能指针要引入头文件memory
这种用法其实我们在讲解mutex时就已经浅浅的谈了一下,我们利用对象的构造和析构来帮助我们资源的获取和释放。那我们就可以自己实现一份简易版本的智能指针了:

namespace grm
{
	template <class T>
	class smart_point
	{
	public:
		smart_point(T* ptr)
			:_ptr(ptr)
		{}

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

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

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

		T* get()
		{
			return _ptr;
		}

	private:
		T* _ptr;
	};
}

我们使用时只需要这样使用即可:

	grm::smart_point<int> p1 = new int(10);
	grm::smart_point<int> p2 = new int(20);

是不是方便了很多,为了方便看出是否析构了资源我们在析构时专门打印了一句话,所以当我们运行时:
在这里插入图片描述
这样我们便实现了一份简易版本的智能指针了,但是智能指针的难点现在才开始。

3.2 🍎auto_ptr 和 unique_ptr🍎

大家思考我们上面写的代码中,有什么是还没有被考虑到的点?我们是不是还没有写拷贝构造拷贝赋值?这两个可是一个重点和难点。当我们没有实现拷贝构造和拷贝赋值的时候编译器会默认给我们生成了一份,但是生成的是一份浅拷贝,我们析构时肯定会重复析构多次而导致程序崩溃。
那有什么解决办法吗?有人或许会想到:实现一份深拷贝的拷贝构造不就好了吗?

大家想想:实现深拷贝这种方式合适吗?

答案是坚决不行的,因为我们要的就是浅拷贝呀,我们拷贝构造的目的就是让两个智能指针对象管理同一块资源,那么既然要管理同一份资源,我们的指针肯定要是同一个才行。深拷贝了那还能管理同一份资源吗?所以这种解决思路是不行的。这里最好的解决方式是利用引用计数

但是C++大佬最先在设计时并没有想到得这么全面,而是采用了另外一种方式,资源转移。这种方式被吐槽得很厉害,为什么呢?我们接下来看看:

		auto_ptr(auto_ptr<T>& ap)
			:_ptr(ap._ptr)
		{
			ap._ptr = nullptr;
		}

		auto_ptr<T>& operator=(const auto_ptr<T>& ap)
		{
			if (&ap != this)
			{
				delete _ptr;
				_ptr = ap._ptr;
				ap._ptr = nullptr;
			}

			return *this;
		}

auto_ptr的实现拷贝构造和拷贝赋值代码如上,我们可以分析分析上面的代码问题在哪里?

拷贝构造时我们将资源转移走了,那我们还拷贝个啥,不如不拷贝。最要命的是假如使用者不熟悉底层原理,用了之前的悬空指针,不就成了空指针访问了吗。

所以上面的设计是不太合理的,有很多公司都会明令禁止使用auto_ptr

设计C++的大佬们发现了这样的问题后,又设计了一个unique_ptr,直接禁止了使用拷贝构造和拷贝赋值。

关于禁止使用拷贝构造和拷贝赋值在C98的时候我们可以采取:
拷贝构造和拷贝赋值设置为私有,并且只声明,不实现
在C++11我们可以直接使用delete关键字。建议大家使用C++11的方式因为比较方便。

3.3 🍎shared_ptr 和 weak_ptr🍎

为了解决上面的问题,大佬们又想到了一种方式:使用引用计数来解决问题。我们可以使用一个计数器来帮助我们计数,当计数减到了0,我们才会去释放空间,那现在问题来了:我们应该选择怎样的数据进行计数呢?

直接使用一个整形变量肯定不行,因为指向同一份资源的计数器应该相等。那我们可以用静态成员变量吗?大家好好想想,这种方式究竟可取不可取?

答案是不可取的,为啥捏?因为静态成员变量是所有同类成员共享同一份,但是我们这里的计数是所有成员都应该共享的吗?显然不是,只有指向同一份资源的对象才会共享同一份计数器。那么我们应该使用什么类型的数据呢?

我们可以采用指针的方式,每个对象里面都存放着一个指针,让指向同一份资源的对象的指针相同不就好了吗?有了上面的思路我们就可以来手撸一个shared_ptr了:

namespace grm
{
	template <class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr)
			:_ptr(ptr)
			,_pcnt(new int(1))
		{}

		~shared_ptr()
		{
			if (--(*_pcnt) == 0)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
				delete _pcnt;
			}
		}

		shared_ptr(shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			,_pcnt(sp._pcnt)
		{
			++(*_pcnt);
		}

		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			//if (&ap != this)
			if(_pcnt!= sp._pcnt)//建议下面这种方式
			{
				if (--(*_pcnt) == 0 && _ptr)
				{
					delete _ptr;
					delete _pcnt;//别忘了计数器也要销毁
				}

				_ptr = sp._ptr;
				_pcnt = sp._pcnt;

				++(*_pcnt);
			}

			return *this;
		}

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

	private:
		T* _ptr;
		int* _pcnt;
	};
}

这样我们就完成了使用引用计数的方式维护计数器,但是大家再思考一下,上面代码中可能还会出现什么问题?

多线程并发访问的情况上面代码有问题吗?答案肯定是有的,因为我们计数器的++与- -操作有可能多个线程共同在执行,那么就一定会出现安全问题,比如下面这种场景:

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

void SharePtrFunc(grm::shared_ptr<Date>& sp, size_t n, mutex& mtx)
{
	cout << sp.get() << endl;
	for (size_t i = 0; i < n; ++i)
	{
		
		grm::shared_ptr<Date> copy(sp);
		{
		unique_lock<mutex> lk(mtx);
		copy->_year++;
		copy->_month++;
		copy->_day++;
		}
	}
}
int main()
{
	grm::shared_ptr<Date> p(new Date);
	cout << p.get() << endl;
	const size_t n = 100;
	mutex mtx;
	thread t1(SharePtrFunc, std::ref(p), n, std::ref(mtx));
	thread t2(SharePtrFunc, std::ref(p), n, std::ref(mtx));
	t1.join();
	t2.join();
	cout << p->_year << endl;
	cout << p->_month << endl;
	cout << p->_day << endl;
	cout << p.use_cnt() << endl;
	return 0;
}

当我们测试时:
在这里插入图片描述
咦,我们发现程序居然没事儿,但是这个只是一个偶然性,当我们将数据范围扩大时:
在这里插入图片描述我们发现程序直接挂掉了,这其实也符合我们的预期,因为我们在将计数器++和- -操作时并没有加锁,所以有可能崩溃。

所以我们可以通过加锁策略来处理。
为了方便加锁与解锁,我们可以将++和- -操作独立放在一个函数中,如下:

		void add_cnt()
		{
			_mtu->lock();
			++(*_pcnt);
			_mtu->unlock();
		}

		void release()
		{
			_mtu->lock();
			bool del = false;
			if (--(*_pcnt) == 0)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
				delete _pcnt;
				del = true;
			}
			_mtu->unlock();
			if (del)
				delete _mtu;
		}

整个修改的代码就如下所示:

	template <class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr)
			:_ptr(ptr)
			,_pcnt(new int(1))
			,_mtu(new mutex)
		{}

		void add_cnt()
		{
			_mtu->lock();
			++(*_pcnt);
			_mtu->unlock();
		}

		void release()
		{
			_mtu->lock();
			bool del = false;
			if (--(*_pcnt) == 0)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
				delete _pcnt;
				del = true;
			}
			_mtu->unlock();
			if (del)
				delete _mtu;
		}

		~shared_ptr()
		{
			release();
		}

		shared_ptr(shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			,_pcnt(sp._pcnt)
			,_mtu(sp._mtu)
		{
			add_cnt();
		}

		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			
			//if (&ap != this)
			if(_pcnt!= sp._pcnt)//建议下面这种方式
			{
				//if (--(*_pcnt) == 0 && _ptr)
				//{
				//	delete _ptr;
				//	delete _pcnt;//别忘了计数器也要销毁
				//}

				release();

				_ptr = sp._ptr;
				_pcnt = sp._pcnt;
				_mtu = sp._mtu;
				add_cnt();
			}
			
			return *this;
		}

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

		int use_cnt()
		{
			return (*_pcnt);
		}

	private:
		T* _ptr;
		int* _pcnt;
		mutex* _mtu;
	};

这时我们再来运行时:
在这里插入图片描述
此时便能够得到正确的结果了。

到目前为止,shared_ptr大部分场景我们都能够很好的利用了,但是假如出现下面场景时:

struct ListNode
{
	
	int _data;
	grm::shared_ptr<ListNode> _prev;
	grm::shared_ptr<ListNode> _next;
	~ListNode() { cout << "~ListNode()" << endl; }
};
int main()
{
	grm::shared_ptr<ListNode> node1(new ListNode);
	grm::shared_ptr<ListNode> node2(new ListNode);
	cout << node1.use_cnt() << endl;
	cout << node2.use_cnt() << endl;
	node1->_next = node2;
	node2->_prev = node1;
	cout << node1.use_cnt() << endl;
	cout << node2.use_cnt() << endl;
	return 0;
}

当我们编译时会爆出下面的错误信息:
在这里插入图片描述这时由于我们在实现shared_ptr的构造函数时没有给出默认构造,所以会编译错误,我们加上就好了:

		shared_ptr(T* ptr=nullptr)
			:_ptr(ptr)
			,_pcnt(new int(1))
			,_mtu(new mutex)
		{}

然后我们运行:
在这里插入图片描述
咦,为啥这里没有调用析构函数呀?(我们自己写的析构是加上了打印语句的),可是当我们屏蔽了一行代码后,就成功的打印出来了:
在这里插入图片描述这是为啥捏?
这里其实就是shared_ptr中的一个比较经典的问题:循环引用

我们先来分析分析在这种情况下究竟是怎么一回儿事:我们先分析最先那种情况,为啥使用了下面的语句后会造成node1和node2没有被释放呢?

	node1->_next = node2;
	node2->_prev = node1;

当创建了两个智能指针对象时node1和node2的引用计数都变成了1,然后执行上面两句代码,使得node1和node2的引用计数都变成了2,当出了作用域时先析构node2(此时并不会真正析构node2,只会- -引用计数),node2的引用计数都变成了1;再析构node1(此时并不会真正析构node1,只会- -引用计数),node1的引用计数都变成了1。但是现在问题来了,要先析构node2对象,就要先析构node1,因为node1对象中的_next和node2共同管理同一块资源,只有node1对象析构了,成员变量_next才析构;但是node1对象和node2的_prev共同管理同一块资源,只有node2对象析构了,成员变量_prev才有析构;但是node2的析构又依赖着node1对象的析构,而导致形成这种闭环方式,谁都不会析构,那自然就造成内存泄漏了。

这里的关系有一点绕,建议大家可以画画图多理解一下。
那为什么我们刚才屏蔽了一段代码后就不会了呢,因为此时并没有造成闭环,我们来分析分析:我们只让 node2->_prev = node1;所以此时node1的引用计数为2,node2的引用计数为1,当出了作用域后,先析构node2,由于node2的引用计数为1,所以可以直接析构,当node2析构了后,也会析构他的成员变量_prev,由于node2的_prev与node1共同管理同一块资源,所以此时node1的引用计数–变为了1;然在出了作用域在析构node1,由于引用计数为1,所以就能够直接析构了,此时node1和node2都会正常析构,自然不会造成循环引用的问题了。同理只使用 node1->_next = node2;也是同理。

那我们究竟应该如何处理这种场景呀?其实我们想想:只要让拷贝赋值时引用计数不再++就行了。而这就是weak_ptr的基本原理。

所以我们可以实现一份简易的weak_ptr出来了:

	template <class T>
	class weak_ptr
	{
	public:

		weak_ptr()
			:_ptr(nullptr)
		{}

		weak_ptr(const shared_ptr<T>& sp)
			:_ptr(sp.get())
		{}
		
		weak_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			_ptr = sp.get();
			return *this;
		}

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

	private:
		T* _ptr;
	};

这里面值得注意的细节有:

  • weak_ptr是一种特殊的智能指针,不支持RAII,可以像指针一样指向资源,但是不参与资源的管理。
  • 上述的weak_ptr是实现出的一份简易版本,实际上库中的实现比这个复杂得多(因为库中考虑得因素非常多),shared_ptr库里面得实现也比我们之前实现的复杂得多。

所以上面我们就可以使用shared_ptr+weak_ptr来解决循环引用问题:

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

大家有兴趣可以直接看看官网上的所有智能指针👉【智能指针】👈

3.4 🍎定制删除器🍎

通过上面的讲解我们还发现了一个问题:如果不是new出来的对象如何通过智能指针管理呢?比如是malloc出来的,或者是以文件方式打开的,又或者是通过new[] 创建对象。其实shared_ptr设计了一个删除器来解决这个问题,我们可以先看看官网:
在这里插入图片描述我们主要看看上面圈了的两种方式:一种是直接将删除器放在了模板参数中,另外一种是将删除器对象作为参数传给构造函数,我们下来看看这种不使用删除器的情况:

std::shared_ptr<Date> ps1(new Date[10]);

此时程序会直接挂掉:
在这里插入图片描述这是由于VS使用new[]创建对象时会在头四个字节用一个整形记录大小,所以如果我们使用delete时会直接报错,要使用delete[]才行,所以在这里我们可以给一个仿函数对象或者lambda,这里我给了一个lambda:

	std::shared_ptr<Date> ps1(new Date[10], [](Date* ptr) {
		cout << "lambda:delete" << endl;
		delete [] ptr;
		});

在这里插入图片描述
那假如我们想要将参数器对象传给构造函数,我们应该怎样修改我们自己实现的shared_ptr呢?
我们可以用包装器来接受lambda/仿函数对象:

	template <class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr=nullptr)
			:_ptr(ptr)
			,_pcnt(new int(1))
			,_mtu(new mutex)
		{}

		template<class D>
		shared_ptr(T* ptr ,D del)
			: _ptr(ptr)
			, _pcnt(new int(1))
			, _mtu(new mutex)
			,_del(del)
		{}

		void add_cnt()
		{
			_mtu->lock();
			++(*_pcnt);
			_mtu->unlock();
		}

		void release()
		{
			_mtu->lock();
			bool del = false;
			if (--(*_pcnt) == 0)
			{
				if(_ptr)
					cout << "delete:" << _ptr << endl;
				//delete _ptr;
				_del(_ptr);
				delete _pcnt;
				del = true;
			}
			_mtu->unlock();
			if (del)
				delete _mtu;
		}

		~shared_ptr()
		{
			release();
		}

		shared_ptr(shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			,_pcnt(sp._pcnt)
			,_mtu(sp._mtu)
		{
			add_cnt();
		}

		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			
			//if (&ap != this)
			if(_pcnt!= sp._pcnt)//建议下面这种方式
			{
				//if (--(*_pcnt) == 0 && _ptr)
				//{
				//	delete _ptr;
				//	delete _pcnt;//别忘了计数器也要销毁
				//}
				release();

				_ptr = sp._ptr;
				_pcnt = sp._pcnt;
				_mtu = sp._mtu;

				add_cnt();
			}
			return *this;
		}

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

		int use_cnt()const
		{
			return (*_pcnt);
		}

	private:
		T* _ptr;
		int* _pcnt;
		mutex* _mtu;
		function<void(T*)> _del = [](T* ptr) {
			cout << "lambda:delete" << endl;//这句话可加可不加,这里加上是为了看着方便
			delete ptr;
		};

	};

当我们使用自己实现的:
在这里插入图片描述
这里可以看见没啥问题。

3.5 🍎C++11和boost中智能指针的关系🍎

  • 1️⃣ C++ 98 中产生了第一个智能指针auto_ptr.
  • 2️⃣ C++ boost给出了更实用的scoped_ptr和shared_ptr和weak_ptr.
  • 3️⃣ C++ TR1,引入了shared_ptr等。不过注意的是TR1并不是标准版。
  • 4️⃣ C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。

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

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

相关文章

微服务间通信重构与服务治理笔记

父工程 依赖版本管理,但实际不引入依赖 pom.xml <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation&…

vue svelte solid 虚拟滚动性能对比

前言 由于svelte solid 两大无虚拟DOM框架&#xff0c;由于其性能好&#xff0c;在前端越来越有影响力。 因此本次想要验证&#xff0c;这三个框架关于实现表格虚拟滚动的性能。 比较版本 vue3.4.21svelte4.2.12solid-js1.8.15 比较代码 这里使用了我的 stk-table-vue(np…

GIN与Echo:选择正确Go框架的指南

您是否在Go中构建Web应用&#xff1f;选择正确的框架至关重要&#xff01;GIN和Echo是两个热门选择&#xff0c;每个都有其优势和特点。本指南将详细介绍每个框架的特性、速度、社区热度以及它们各自擅长的项目类型。最后&#xff0c;您将能够为您的下一个Web项目选择完美的框架…

SpringBoot + Disruptor 实现特快高并发处理

使用Disruptor做消息队列&#xff0c;解决内存队列的延迟问题&#xff08;在性能测试中发现竟然与I/O操作处于同样的数量级&#xff09; 【基于 Disruptor 开发的系统单线程能支撑每秒 600 万订单】 核心概念&#xff1a; Ring Buffer 环形的缓冲区&#xff0c;从3.0版本开始…

SQL 查询一张卡的最新使用记录

SQL 查询一张卡的最新使用记录 1. 问题描述 1. 问题描述 一张卡&#xff0c;有一个底表记录这个卡的基本信息&#xff0c;还有一个使用卡的记录表&#xff0c;记录着&#xff0c;这张卡的使用记录&#xff0c;但我们要获取这张卡的最新使用记录&#xff0c;该如何写SQL呢&…

【Linux命令】fuser

fuser 使用文件或文件结构识别进程。 详细 fuser命令用于报告进程使用的文件和网络套接字。fuser命令列出了本地进程的进程号&#xff0c;哪些本地进程使用file&#xff0c;参数指定的本地或远程文件。 每个进程号后面都跟随一个字母&#xff0c;该字母指示进程如何使用该文…

Python实现CCI工具判断信号:股票技术分析的工具系列(5)

Python实现CCI工具判断信号&#xff1a;股票技术分析的工具系列&#xff08;5&#xff09; 介绍算法解释 代码rolling函数介绍完整代码data代码CCI.py 介绍 在股票技术分析中&#xff0c;CCI (商品路径指标&#xff09;是一种常用的技术指标&#xff0c;用于衡量股价是否处于超…

MATLAB知识点:使用for循环时需要注意的事项

​讲解视频&#xff1a;可以在bilibili搜索《MATLAB教程新手入门篇——数学建模清风主讲》。​ MATLAB教程新手入门篇&#xff08;数学建模清风主讲&#xff0c;适合零基础同学观看&#xff09;_哔哩哔哩_bilibili 节选自​第4章&#xff1a;MATLAB程序流程控制 在使用for循环…

HarmonyOS—HAP唯一性校验逻辑

HAP是应用安装的基本单位&#xff0c;在DevEco Studio工程目录中&#xff0c;一个HAP对应一个Module。应用打包时&#xff0c;每个Module生成一个.hap文件。 应用如果包含多个Module&#xff0c;在应用市场上架时&#xff0c;会将多个.hap文件打包成一个.app文件&#xff08;称…

第 125 场 LeetCode 双周赛题解

A 超过阈值的最少操作数 I 排序然后查找第一个大于等于 k 的元素所在的位置 class Solution { public:int minOperations(vector<int> &nums, int k) {sort(nums.begin(), nums.end());return lower_bound(nums.begin(), nums.end(), k) - nums.begin();} };B 超过阈…

数据结构(一)综述

一、常见的数据结构 数据结构优点缺点数组查找快增删慢链表增删快查找慢哈希表增删、查找都快数据散列&#xff0c;对存储空间有浪费栈顶部元素插入和取出快除顶部元素外&#xff0c;存取其他元素都很慢队列顶部元素取出和尾部元素插入快存取其他元素都很慢二叉树增删、查找都快…

自学高效备考2025年AMC8数学竞赛:2000-2024年AMC8真题解析

今天继续来随机看五道AMC8的真题和解析&#xff0c;根据实践经验&#xff0c;对于想了解或者加AMC8美国数学竞赛的孩子来说&#xff0c;吃透AMC8历年真题是备考最科学、最有效的方法之一。即使不参加AMC8竞赛&#xff0c;吃透了历年真题600道和背后的知识体系&#xff0c;那么小…

深入理解Tomcat

目录&#xff1a; TomcatTomcat简介如何下载tomcatTomcat工作原理Tomcat架构图Tomcat组件Server组件Service组件Connector组件Engine组件Host组件Context组件 配置虚拟主机(Host)配置Context Tomcat Tomcat简介 Tomcat服务器是Apache的一个开源免费的Web容器。它实现了JavaEE…

计算机网络-物理层-传输媒体

传输媒体的分类 导向型-同轴电缆 导向型-双绞线 导向型-光纤 非导向型

卡密交易系统 卡密社区SUP系统源码 分销系统平台 分销商城系统开发

卡密社区SUP系统总控源码主站分销系统功能源码 跟以前的卡盟那种控制端差不多总控可以给别人开通&#xff0c;分销&#xff0c;主站&#xff0c;类似自己做系统商一样&#xff0c;自助发卡&#xff0c;卡密交易系统。 搭建环境Nginx1.22 mysql 5.7 php8.1 rids 7.2 安装方法…

避坑——Matlab c# 联合编程——Native

相同的库&#xff0c;Matlab生成供.net调用的库时会有两套&#xff0c;也就是Native&#xff08;本地&#xff09;&#xff0c;两套库各有优缺点&#xff0c;这这里就不说了&#xff0c;可以翻看网上其他博文 主要是MWStructArray&#xff0c;MWArray等数据交换对象有两套&…

C语言:qsort的使用方法

目录 1. qsort是什么&#xff1f; 2. 为什么要使用qsort 3. qsort的使用 3.1 qsort的返回值和参数 3.2 qsort的compare函数参数 3.3 int类型数组的qsort完整代码 4. qsort完整代码 1. qsort是什么&#xff1f; qsort中的q在英语中是quick&#xff0c;快速的意思了&#…

Platformview在iOS与Android上的实现方式对比

Android中早期版本Platformview的实现基于Virtual Display。VirtualDisplay方案的原理是&#xff0c;先将Native View绘制到虚显&#xff0c;然后Flutter通过从虚显输出中获取纹理并将其与自己内部的widget树进行合成&#xff0c;最后作为Flutter在 Android 上更大的纹理输出的…

【经验】f-string 的一些点

【经验】f-string 的一些点 省几个字别数错了对齐它现在几点 省几个字 让 f-string 给你写表达式&#xff0c;在 f-string 中使用 来自动打印表达式 a 10 b 25 print(f"{a b }") >>> a b 35别数错了 对于过大的数字难以一眼看出来它的数量级&#xf…

Access AR Foundation 5.1 in Unity 2022

如果已经下载安装了ARF但版本是5.0.7 可以通过下面的方式修改 修改后面的数字会自动更新 更新完成后查看版本 官方文档 Access AR Foundation 5.1 in Unity 2021 | AR Foundation | 5.1.2