C++实现定长内存池

项目介绍

        本项目实现的是一个高并发的内存池,它的原型是Google的一个开源项目tcmalloc,tcmalloc全称Thread-Caching Malloc,即线程缓存的malloc,实现了高效的多线程内存管理,用于替换系统的内存分配相关函数malloc和free。tcmalloc的知名度也是非常高的,不少公司都在用它,比如Go语言就直接用它做了自己的内存分配器。该项目就是把tcmalloc中最核心的框架简化后拿出来,模拟实现出一个mini版的高并发内存池,目的就是学习tcmalloc的精华。该项目主要涉及C/C++、数据结构(链表、哈希桶)、操作系统内存管理、单例模式、多线程、互斥锁等方面的技术。

内存池介绍

池化技术

  在说内存池之前,我们得先了解一下“池化技术”。所谓“池化技术”,就是程序先向系统申请过量的资源,然后自己进行管理,以备不时之需。

  之所以要申请过量的资源,是因为申请和释放资源都有较大的开销,不如提前申请一些资源放入“池”中,当需要资源时直接从“池”中获取,不需要时就将该资源重新放回“池”中即可。这样使用时就会变得非常快捷,可以大大提高程序的运行效率。

  在计算机中,有很多使用“池”这种技术的地方,除了内存池之外,还有连接池、线程池、对象池等。以服务器上的线程池为例,它的主要思想就是:先启动若干数量的线程,让它们处于睡眠状态,当接收到客户端的请求时,唤醒池中某个睡眠的线程,让它来处理客户端的请求,当处理完这个请求后,线程又进入睡眠状态。

内存池

  内存池是指程序预先向操作系统申请一块足够大的内存,此后,当程序中需要申请内存的时候,不是直接向操作系统申请,而是直接从内存池中获取;同理,当释放内存的时候,并不是真正将内存返回给操作系统,而是将内存返回给内存池。当程序退出时(或某个特定时间),内存池才将之前申请的内存真正释放。

内存池主要解决的问题

  内存池主要解决的就是效率的问题,它能够避免让程序频繁的向系统申请和释放内存。这里我们可以举一个例子,就好比我们要生活费,今天早上你吃了一碗面花了五元,然后你里面打电话告诉你的爸爸妈妈说今天早上花了五元吃面,你给我转5元,中午吃了一个黄焖鸡,花了12元,告诉爸爸妈妈说今天中午花了12元,那么给我转12元,我们会发现这里太低效了,每次都需要向爸爸妈妈要,不如这样,告诉爸爸妈妈你这个月大概需要800元生活费,让爸爸妈妈一次性给你,这样你就不需要频繁的打电话告诉爸爸妈妈要生活费,非常高效,其次,内存池作为系统的内存分配器,还需要尝试解决内存碎片的问题。

内存碎片分为内部碎片和外部碎片:

外部碎片是一些空闲的小块内存区域,由于这些内存空间不连续,以至于合计的内存足够,但是不能满足一些内存分配申请需求。
内部碎片是由于一些对齐的需求,导致分配出去的空间中一些内存无法被利用。
注意: 内存池尝试解决的是外部碎片的问题,同时也尽可能的减少内部碎片的产生。

malloc

  我们之前一直说C/C++我们要申请内存,我们就需要在堆空间中申请,但是实际上C/C++中我们要动态申请内存一般情况下并不是直接去堆申请的,而是通过malloc函数去申请的,包括C++中的new实际上也是调用了operator new,它底层也是封装了malloc函数的,因为它要符合C++抛异常的机制。

我们申请内存块时是先调用malloc,malloc再去向操作系统申请内存。malloc实际就是一个内存池,malloc相当于向操作系统“批发”了一块较大的内存空间,然后“零售”给程序用,当全部“售完”或程序有大量的内存需求时,再根据实际需求向操作系统“进货”。

malloc的实现方式有很多种,一般不同编译器平台用的都是不同的。比如Windows的VS系列中的malloc就是微软自行实现的,而Linux下的gcc用的是glibc中的ptmalloc。

定长内存池的实现

        malloc其实就是一个通用的内存池,在什么场景下都可以使用,但这也意味着malloc在什么场景下都不会有很高的性能,因为malloc并不是针对某种场景专门设计的。

  定长内存池就是针对固定大小内存块的申请和释放的内存池,由于定长内存池只需要支持固定大小内存块的申请和释放,因此我们可以将其性能做到极致,并且在实现定长内存池时不需要考虑内存碎片等问题,因为我们申请/释放的都是固定大小的内存块。

  我们可以通过实现定长内存池来熟悉一下对简单内存池的控制,其次,这个定长内存池后面会作为高并发内存池的一个基础组件。既然我们这里是实现定长的内存池,所以我们第一步就需要实现定长的功能,如何实现呢?

使用我们的非类型模板参数,表示此时申请的对象的空间大小都是N。

// 非类型的模板参数
template <size_t N>
class ObjectPool
{

};

还有另外一种方式,根据传入的模板参数的大小来获得申请的空间,比如传入的是int,那就是4个字节,传入的是double,那就是8个字节。

// 模板参数
// 定长内存池
template <class T>
class ObjectPool
{

};

我们现在实现的是第二种方式,为了更贴切第二中方式的含义,这也就是为什么我们给这个类起名为ObjectPool,因为它是根据对象的大小来申请空间的,我们再来看看定长内存池的需要一些什么样的成员呢?首先我们肯定需要一大块的堆空间内存

// 模板参数
// 定长内存池
template <class T>
class ObjectPool
{
private:
	void* _memory; // 申请一大块堆上的内存空间的指针
};

此时我们看看我们设计的成员变量有没有什么问题,我们申请的这一大块的堆空间内存,首先我们肯定是要划分出一段空间来供调用者使用,比如调用者说他需要10个字节的空间,难道此时我们直接将memory的起始地址并且加上10,将这个段空间给调用者嘛?此时我们要记住,void*它是不支持解引用和加加减减的操作的,所以此时要想切割出10个字节的空间,我们就需要将void*强制类型转换为char*才可以,那为什么我们不直接将memory的类型设置为char*呢?这样不就更方便一点嘛!

// 模板参数
// 定长内存池
template <class T>
class ObjectPool
{
private:
	char* _memory; // 申请一大块堆上的内存空间的指针
};

同时我们应该还要想到,未来有很多个人需要不同大小的内存空间,于是就想我们的内存池中拿,但是未来当他们部分人用完了,要归还的时候,我们不能直接把它释放归还给操作系统,因为我们当时申请了多少就应该释放多少,同时我们也不能归还到我们的内存池,这样会出现碎片化的问题,所以我们是不是还要对这些释用完的内存进行管理起来,我们可以将这些释放回来的定长内存块链接成一个链表,这里我们将管理释放回来的内存块的链表叫做自由链表,为了能找到这个自由链表,我们还需要一个指向自由链表的指针,此时在这个链表中,我们不进行解引用和加加减减的操作,并且我们也不知道归还的内存的指针的类型,我们此时指针的类型可以定义成void*。此时有一个小细节,我们这个内存块的空间在32位平台下内存块空间至少大于4字节,因为我们还要存储下一个内存块的指针,这样我们才能管理起来,我们规定内存块的前4个字节存储下一个内存块的地址。

// 模板参数
// 定长内存池
template <class T>
class ObjectPool
{
private:
	char* _memory; // 申请一大块堆上的内存空间的指针
	void* _freeList; // 自由链表
};

随后我们利用构造函数的初始化列表将成员进行初始化,这里我们都设置位空指针即可。

// 模板参数
// 定长内存池
template <class T>
class ObjectPool
{
public:
	ObjectPool()
		: _memory(nullptr)
		, _freeList(nullptr)
	{}
private:
	char* _memory; // 申请一大块堆上的内存空间的指针
	void* _freeList; // 自由链表
};

随后我就要申请一大块内存空间,如果此时申请空间失败,我们抛出一个异常,并让程序直接退出。

# include <iostream>

using  std::cin; 
using  std::cout;
using std::bad_alloc;

// 模板参数
// 定长内存池
template <class T>
class ObjectPool
{
public:
	ObjectPool()
		: _memory(nullptr)
		, _freeList(nullptr)
	{}
	T* New()
	{
		if (_memory == nullptr)
		{
			// malloc的返回值是void*,这里需要强制类型转换
			_memory = (char*)malloc(128 * 1024); // 128KB
			if (_memory == nullptr)
			{
				throw bad_alloc();
			}
		}
	}
private:
	char* _memory; // 申请一大块堆上的内存空间的指针
	void* _freeList; // 自由链表
};

现在我们就可以根据传入的模板参数T的类型,给它分配一个T*的空间。

// 模板参数
// 定长内存池
template <class T>
class ObjectPool
{
public:
	ObjectPool()
		: _memory(nullptr)
		, _freeList(nullptr)
	{}
	T* New()
	{
		if (_memory == nullptr)
		{
			// malloc的返回值是void*,这里需要强制类型转换
			_memory = (char*)malloc(128 * 1024); // 128KB
			if (_memory == nullptr)
			{
				throw bad_alloc();
			}
		}

		T* obj = (T*)_memory;
		_memory += sizeof(T);

		return obj;
	}
private:
	char* _memory; // 申请一大块堆上的内存空间的指针
	void* _freeList; // 自由链表
};

我们看看此时我们的代码有没有问题,当我们申请的空间都被用完的时候,此时我们应该再去申请空间,但是此时的_memory的指针不为空指针,此时还是有地址的,不过此时不属于你,你是不可以使用的,所以我们的代码还是有问题的,我们不应该使用_memory为空来判断申请空间,它只适用于第一次申请空间的情况,此时为了解决这个问题,我们还可以定义一个成员变量,表示剩余空间的大小,然后根据这个来判断是否需要申请空间。

template <class T>
class ObjectPool
{
public:
	ObjectPool()
		: _memory(nullptr)
		, _freeList(nullptr)
	{}
	T* New()
	{
		if (_remainBytes == 0)
		{
			// malloc的返回值是void*,这里需要强制类型转换
			_remainBytes = 128 * 1024;
			_memory = (char*)malloc(_remainBytes); // 128KB
			if (_memory == nullptr)
			{
				throw bad_alloc();
			}
		}

		T* obj = (T*)_memory;
		_memory += sizeof(T);
		_remainBytes -= sizeof(T);
		return obj;
	}
private:
	char* _memory; // 申请一大块堆上的内存空间的指针
	void* _freeList; // 自由链表
	size_t _remainBytes = 0;  //指向大块内存在切分过程中剩余字节数
};

同时我们这里如果最后剩余的空间不够一个T类型的大小,那么我们上面的判断还是有问题的,此时就可能出现越界的问题,所以我们还要改一下判断。

// 模板参数
// 定长内存池
template <class T>
class ObjectPool
{
public:
	ObjectPool()
		: _memory(nullptr)
		, _freeList(nullptr)
	{}
	T* New()
	{
		// 当剩余的内存不够一个对象大小时,需要重现开一个大块空间
		if (_remainBytes < sizeof(T))
		{
			// malloc的返回值是void*,这里需要强制类型转换
			_remainBytes = 128 * 1024;
			_memory = (char*)malloc(_remainBytes); // 128KB
			if (_memory == nullptr)
			{
				throw bad_alloc();
			}
		}

		T* obj = (T*)_memory;
		_memory += sizeof(T);
		_remainBytes -= sizeof(T);
		return obj;
	}
private:
	char* _memory; // 申请一大块堆上的内存空间的指针
	void* _freeList; // 自由链表
	size_t _remainBytes = 0;  //指向大块内存在切分过程中剩余字节数
};

现在我们再来处理一下对象归还回来的空间,改怎么处理呢?当现在有一个内存块归还了,我们要让freeList指向这个内存块,这个内存的前4个字节指向NULL,怎么做呢?我们肯定需要找到这个4字节,可以直接对象归还回来的空间强制类型转换为int*,此时解引用就可以访问到这个这个空间,然后将其设置为空。

void Delete(T* obj)
{
	if (_freeList == nullptr)
	{
		_freeList = obj;
		*((int*)obj) = nullptr;
	}
}

但是还有点问题,指针在不同的平台下指针的大小是不同的,32位平台下指针的大小是4个字节,64位平台下的指针的大小是8个字节,所以我们的代码不具有移植性,但是我们怎么知道我们的平台是32位的还是64位的呢?当然我们可以通过sizeof来判断,但是不够优雅,我们写一个优雅的方法。

void Delete(T* obj)
{
	if (_freeList == nullptr)
	{
		_freeList = obj;
		// *((int*)obj) = nullptr;
		*((void**)obj) = nullptr;
	}
}

此时我们是将obj强制类型转化位void**,void**解引用看的就是一个void*的大小,此时void*是一个指针,32位平台下指针的大小是4个字节,64位平台下的指针的大小是8个字节,很好的就做到了平台的区分,随后在来归还一个内存块,此时肯定要进自由链表来管理的,此时是头插还是尾插呢?我们知道尾插需要找尾,时间复杂度尾O(N),而头插的效率为O(1),所以我们选择头插法。

void Delete(T* obj)
{
	if (_freeList == nullptr)
	{
		_freeList = obj;
		// *((int*)obj) = nullptr;
		*((void**)obj) = _freeList;
	}
	else
	{
		// 头插法
		*((void**)obj) = _freeList;
		_freeList = obj;
	}
}

此时我们会发现我们的头插方法也适用于freeList为空的情况,所以我们的代码就可以简省一点。

void Delete(T* obj)
{
	// 头插法
	*((void**)obj) = _freeList;
	_freeList = obj;
}

同时我们这里需要注意一个点,我们并不是每次都会向我们的大块内存拿空间,如果我们的freeList里面有了很多归还的空间,我们可以来使用一下他们,此时就需要拿ferrList的第一个空间块,需要进行头删。

// 模板参数
// 定长内存池
template <class T>
class ObjectPool
{
public:
	ObjectPool()
		: _memory(nullptr)
		, _freeList(nullptr)
	{}
	T* New()
	{
		T* obj = nullptr;
		// 优先把还回来的内存块对象,再次重复利用
		if (_freeList != nullptr)
		{
			void* next = *((void**)_freeList);
			obj = (T*)_freeList;
			_freeList = next;
			return obj;
		}
		else
		{
			// 当剩余的内存不够一个对象大小时,需要重现开一个大块空间
			if (_remainBytes < sizeof(T))
			{
				// malloc的返回值是void*,这里需要强制类型转换
				_remainBytes = 128 * 1024;
				_memory = (char*)malloc(_remainBytes); // 128KB
				if (_memory == nullptr)
				{
					throw bad_alloc();
				}
			}

			obj = (T*)_memory;
			_memory += sizeof(T);
			_remainBytes -= sizeof(T);
			return obj;
		}
	}
	void Delete(T* obj)
	{
		// 头插法
		*((void**)obj) = _freeList;
		_freeList = obj;
	}
private:
	char* _memory; // 申请一大块堆上的内存空间的指针
	void* _freeList; // 自由链表
	size_t _remainBytes = 0;  //指向大块内存在切分过程中剩余字节数
};

同时我们上面的代码还存在一个问题,如果此时调用者需要的空间只需要一个字节,那么此时它要归还的时候,存储自由链表中,但是节点中至少要大于4个字节,不然存储不了下一个内存块的指针啊,所以我们这里还需要处理一下,对于不满足存储一个指针的空间,我们将空间的大小设置为指针的大小。

// 模板参数
// 定长内存池
template <class T>
class ObjectPool
{
public:
	ObjectPool()
		: _memory(nullptr)
		, _freeList(nullptr)
	{}
	T* New()
	{
		T* obj = nullptr;
		// 优先把还回来的内存块对象,再次重复利用
		if (_freeList != nullptr)
		{
			void* next = *((void**)_freeList);
			obj = (T*)_freeList;
			_freeList = next;
		}
		else
		{
			// 当剩余的内存不够一个对象大小时,需要重现开一个大块空间
			if (_remainBytes < sizeof(T))
			{
				// malloc的返回值是void*,这里需要强制类型转换
				_remainBytes = 128 * 1024;
				_memory = (char*)malloc(_remainBytes); // 128KB
				if (_memory == nullptr)
				{
					throw bad_alloc();
				}
			}

			obj = (T*)_memory;
			size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
			_memory += objSize;
			_remainBytes -= objSize;
		}
		return obj;
	}
	void Delete(T* obj)
	{
		// 头插法
		*((void**)obj) = _freeList;
		_freeList = obj;
	}
private:
	char* _memory; // 申请一大块堆上的内存空间的指针
	void* _freeList; // 自由链表
	size_t _remainBytes = 0;  //指向大块内存在切分过程中剩余字节数
};

同时我们这里申请了空间,但是我们没有初始化,我们可以将传入的对象通过构造函数进行初始化,并且再归还的时候调用析构函数释放对象。

// 模板参数
// 定长内存池
template <class T>
class ObjectPool
{
public:
	ObjectPool()
		: _memory(nullptr)
		, _freeList(nullptr)
	{}
	T* New()
	{
		T* obj = nullptr;
		// 优先把还回来的内存块对象,再次重复利用
		if (_freeList != nullptr)
		{
			void* next = *((void**)_freeList);
			obj = (T*)_freeList;
			_freeList = next;
		}
		else
		{
			// 当剩余的内存不够一个对象大小时,需要重现开一个大块空间
			if (_remainBytes < sizeof(T))
			{
				// malloc的返回值是void*,这里需要强制类型转换
				_remainBytes = 128 * 1024;
				_memory = (char*)malloc(_remainBytes); // 128KB
				if (_memory == nullptr)
				{
					throw bad_alloc();
				}
			}

			obj = (T*)_memory;
			size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
			_memory += objSize;
			_remainBytes -= objSize;
		}

		// 定位new,显示调用构造函数进行初始化
		new(obj)T;
		return obj;
	}
	void Delete(T* obj)
	{
		// 显示调用析构函数清理对象
		obj->~T();
		// 头插法
		*((void**)obj) = _freeList;
		_freeList = obj;
	}
private:
	char* _memory; // 申请一大块堆上的内存空间的指针
	void* _freeList; // 自由链表
	size_t _remainBytes = 0;  //指向大块内存在切分过程中剩余字节数
};

此时我们的定长内存池就已经实现完成啦!我们来测试一下。

struct TreeNode
{
	int _val;
	TreeNode* _left;
	TreeNode* _right;
	TreeNode()
		:_val(0)
		, _left(nullptr)
		, _right(nullptr)
	{}
};

void TestObjectPool()
{
	// 申请释放的轮次
	const size_t Rounds = 3;
	// 每轮申请释放多少次
	const size_t N = 1000000;
	std::vector<TreeNode*> v1;
	v1.reserve(N);

	//malloc和free
	size_t begin1 = clock();
	for (size_t j = 0; j < Rounds; ++j)
	{
		for (int i = 0; i < N; ++i)
		{
			v1.push_back(new TreeNode);
		}
		for (int i = 0; i < N; ++i)
		{
			delete v1[i];
		}
		v1.clear();
	}
	size_t end1 = clock();

	//定长内存池
	ObjectPool<TreeNode> TNPool;
	std::vector<TreeNode*> v2;
	v2.reserve(N);
	size_t begin2 = clock();
	for (size_t j = 0; j < Rounds; ++j)
	{
		for (int i = 0; i < N; ++i)
		{
			v2.push_back(TNPool.New());
		}
		for (int i = 0; i < N; ++i)
		{
			TNPool.Delete(v2[i]);
		}
		v2.clear();
	}
	size_t end2 = clock();

	cout << "new cost time:" << end1 - begin1 << endl;
	cout << "object pool cost time:" << end2 - begin2 << endl;
}

我们上面也提到,我们的malloc本章也是一个内存池,如果我们不想要这个内存池,我们也可以自己去堆上申请空间,此时在Windows下,可以调用VirtualAlloc函数;在Linux下,可以调用brk或mmap函数。

#ifdef _WIN32
	#include <Windows.h>
#else
	//...
#endif

//直接去堆上申请按页申请空间
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32
	void* ptr = VirtualAlloc(0, kpage<<13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else
	// linux下brk mmap等
#endif
	if (ptr == nullptr)
		throw std::bad_alloc();
	return ptr;
}

可以看到在这个过程中,定长内存池消耗的时间比malloc/free消耗的时间要短。这就是因为malloc是一个通用的内存池,而定长内存池是专门针对申请定长对象而设计的,因此在这种特殊场景下定长内存池的效率更高,正所谓“尺有所短,寸有所长”,最后我们这里的定长内存也不需要手动释放,因为我们也无法释放,因为归还的内存已经乱了,那此时不就会出现内存泄漏的问题嘛?不会,只要我们的进程是正常退出的,最后会自动帮我们释放内存的,所以不用担心。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于UDP的TFTP文件传输-实现网盘上传下载功能

数据传输模式&#xff1a;octet(二进制模式) #include<head.h> char* down_up_request(char* buf,char* filename,int rw,int sockfd,struct sockaddr_in in); int download(struct sockaddr_in in,char* filename,char* buf,int sockfd); int upload(struct sockaddr_in…

数据结构(七)查找

2024年5月26日一稿(王道P291) 7.1 查找的基本概念 7.2 顺序查找和折半查找 7.2.1 顺序查找 7.2.2 折半查找 7.2.3 分块查找 7.3 树形查找 7.3.1 二叉排序树(BST)

FFmpeg+QT播放器实战1---UI页面的设计

1、播放器整体布局的设计 该部分使用QT的UI工具&#xff0c;进行整体页面设置&#xff0c;如下图1所示&#xff1a; 2、控制布局的设计 创建ctrBar的UI页面并进行页面布局设置&#xff0c;如下图2所示&#xff1a; 将图1中ctrBarWind对象提升为ctrBar类(该界面替代原先的控…

Modbus TCP转Profinet网关测试配置案例

本案例采用XD-ETHPN20网关做为Modbus TCP通信协议设备与Profinet通信协议设备连接的桥梁。Modbus TCP是一种基于TCP/IP协议的工业通信协议&#xff0c;而Profinet则是用于太网通信的协议。Modbus TCP转Profinet网关可实现这两种不同协议之间的数据交换和传输&#xff0c;极大地…

实在智能TARS:面向垂直领域自主训练的类GPT大模型

一、写在前面 在数字化浪潮的推动下&#xff0c;企业正寻求突破传统生产力的局限&#xff0c;以实现更高效、更智能的运营模式。实在智能科技有限公司的TARS产品&#xff0c;以其前沿的人工智能技术&#xff0c;为企业注入了新质生产力&#xff0c;引领着智能化转型的新潮流。…

数据结构(三)循环链表

文章目录 一、循环链表&#xff08;一&#xff09;概念&#xff08;二&#xff09;示意图&#xff08;三&#xff09;操作1. 创建循环链表&#xff08;1&#xff09;函数声明&#xff08;2&#xff09;注意点&#xff08;3&#xff09;代码实现 2. 插入&#xff08;头插&#x…

专业渗透测试 Phpsploit-Framework(PSF)框架软件小白入门教程(十二)

本系列课程&#xff0c;将重点讲解Phpsploit-Framework框架软件的基础使用&#xff01; 本文章仅提供学习&#xff0c;切勿将其用于不法手段&#xff01; 接上一篇文章内容&#xff0c;讲述如何进行Phpsploit-Framework软件的基础使用和二次开发。 我们&#xff0c;继续讲一…

【Django项目】 音乐网站spotify复刻

代码&#xff1a;https://github.com/tomitokko/spotify-clone 注&#xff1a;该项目不是自己提供mp3文件&#xff0c;而是使用spotify 的api接口获取。

用实践结果告诉你为啥说 CloudFlare 是赛博菩萨?

最近几天明月都没有更新博客了,主要是接了几个 CloudFlare 代维配置的活儿,有需要加速优化的,有需要排除疑难故障的,有需要提高防御攻击能力的甚至还有纯粹为了体验“打不死”装逼需要的。总之,各种各样的需求,五花八门的,好在 CloudFlare 都能一一满足,最主要的是这些…

C++入门:从C语言到C++的过渡(3)

目录 1.内联函数 1.1内联函数的定义 1.2特性 2.auto关键字 2.1auto的简介 2.2注意事项 3.范围for 4.nullptr空指针 1.内联函数 在C语言中&#xff0c;无论使用宏常量还是宏函数都容易出错&#xff0c;而且无法调试。而C为了弥补这一缺陷&#xff0c;引入了内联函数的概…

【NumPy】关于numpy.clip()函数,看这一篇文章就够了

&#x1f9d1; 博主简介&#xff1a;阿里巴巴嵌入式技术专家&#xff0c;深耕嵌入式人工智能领域&#xff0c;具备多年的嵌入式硬件产品研发管理经验。 &#x1f4d2; 博客介绍&#xff1a;分享嵌入式开发领域的相关知识、经验、思考和感悟&#xff0c;欢迎关注。提供嵌入式方向…

前端手写文件上传;使用input实现文件拖动上传

使用input实现文件拖动上传 vue2代码&#xff1a; <template><div><div class"drop-area" dragenter"highlight" dragover"highlight" dragleave"unhighlight" drop"handleDrop"click"handleClick&quo…

嵌入式C语言中结构体使用详解

各位开发者大家好,今天给大家分享一下,嵌入式C语言中结构体的使用方法。 第一个:内存对齐 内存对齐是指一个数据类型在内存中存放时,对其地址的要求。简单来说内存对齐就是使得其内存地址是该类型大小的整数倍,例如 double 类型的变量,其内存地址需要是8的倍数(double大…

数据结构(四)

数据结构&#xff08;四&#xff09; 算法算法的特征算法和程序的区别怎么样评判一个算法的好坏 常见的查找算法线性树状哈希查找构建哈希函数的方法质数求余法解决冲突 算法 一堆指令的有序集合 算法的特征 唯一性&#xff1a;每一句话只有一种解释 有穷性&#xff1a;算法能…

澳大利亚.德国-门户媒体投放通稿:需要注意什么地方

概述 在现代社会&#xff0c;新闻媒体的投放成为企业和组织宣传推广的重要手段之一。澳大利亚和德国作为全球重要的经济和科技中心&#xff0c;其新闻媒体也备受关注。本文将介绍澳大利亚和德国的一些主要新闻媒体&#xff0c;并讨论发表新闻稿时需要注意的地方。 澳大利亚媒…

Python小游戏——俄罗斯方块

文章目录 项目介绍环境配置代码设计思路1.初始化和导入库&#xff1a;2.定义颜色和屏幕尺寸&#xff1a;3.定义游戏逻辑&#xff1a;4.游戏循环&#xff1a; 源代码效果图 项目介绍 俄罗斯方块游戏是一款经典的益智游戏&#xff0c;玩家通过旋转和移动各种形状的方块&#xff…

VolWeb:集中式增强型数字取证内存分析平台

关于VolWeb VolWeb是一款最新开发的集中式增强型数字取证内存分析平台&#xff0c;该平台基于Volatility 3框架实现其功能&#xff0c;该工具旨在辅助广大研究人员执行安全分析和事件应急响应等任务。 VolWeb可以提供集中式、可视化的增强型网络应用程序&#xff0c;并提高安全…

如何在 Elasticsearch 中选择精确 kNN 搜索和近似 kNN 搜索

作者&#xff1a;来自 Elastic Carlos Delgado kNN 是什么&#xff1f; 语义搜索&#xff08;semantic search&#xff09;是相关性排名的强大工具。 它使你不仅可以使用关键字&#xff0c;还可以考虑文档和查询的实际含义。 语义搜索基于向量搜索&#xff08;vector search&…

flink cdc mysql整理与总结

文章目录 一、业务中常见的需要数据同步的场景CDC是什么FlinkCDC是什么CDC原理为什么是FlinkCDC业务场景flink cdc对应flink的版本 二、模拟案例1.阿里云flink sql2.开源flink sql(单机模式)flink 安装安装mysql3.flink datastream 三、总结 提示&#xff1a;以下是本篇文章正文…

行业分析---造车新势力之蔚来汽车

1 前言 在之前的博客中&#xff0c;笔者分析了苹果《行业分析---我眼中的Apple Inc.》&#xff0c;苹果已经成为世界级的公司。随后也分析了电动汽车公司特斯拉《行业分析---马斯克的Tesla》&#xff0c;特斯拉也在不断成长。目前能分析的新能源汽车公司不多&#xff0c;小米汽…