C++ | 定长内存池 | 对象池

news2024/9/28 17:56:57

文章目录

  • C++ | 定长内存池 | 对象池
    • 一、内存池的引入
    • 二、代码中的内存池实现 - `ObjectPool`类
      • (一)整体结构
      • (二)内存分配 - `New`函数
      • (三)内存回收 - `Delete`函数
    • 三、内存池在`TreeNode`示例中的性能测试演示
    • 四、脱离malloc,直接在堆上按页申请空间
    • 五、总结
      • (一)内存池的优势
      • (二)代码的思考

C++ | 定长内存池 | 对象池

在C++编程的世界里,内存管理是一个至关重要的话题。今天,我们就来深入研究一下基于下面这段代码的内存池概念。

先上代码:

#include<iostream>
#include<vector>
#include <time.h>
using std::cout;
using std::endl;

//定长内存池,一次申请N大小的空间
//template<size_t N>
//class ObjectPool {
//};

template<class T>
class ObjectPool {
public:
	T* New() {
		T* obj = nullptr;
		if (_freelist) 
		//_freelist非空,说明有还回来的内存,优先使用还回来的内存
		//可以使用类似无头结点链表的头删
		{
			void* next = *(void**)_freelist;
			obj = (T*)_freelist;
			_freelist = next;
		}
		else
		{
			if (_remainbytes < sizeof(T)) {
				_remainbytes = 128 * 1024;
				_memory = (char*)malloc(128 * 1024);
				if (_memory == nullptr) {
					throw std::bad_alloc();
				}
			}
			obj = (T*)_memory;		//申请出去的新的空间的起始位置
			//_memory += sizeof(T);		//新的空间被申请后,剩余空间的起始位置
			size_t objsize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
			_memory += objsize;
			_remainbytes -= sizeof(T);	//剩余的空间
		}
		//定位new,显式调用T的构造函数初始化
		new(obj)T;

		return obj;
	}
	void Delete(T* obj) {
		//显式调用析构函数清理对象
		obj->~T();

		//就相当于无头结点链表的头插
			*(void**)obj = _freelist;
			_freelist = *(void**)obj;
	}
private:
	//void* _memory;
	//为了方便地址上的加减运算,使用char类型,char类型占一个字节
	char* _memory = nullptr;	//指向大块内存的指针
	void* _freelist = nullptr;	//指向待释放的空间
	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 = 5;

	// 每轮申请释放多少次
	const size_t N = 100000;

	std::vector<TreeNode*> v1;
	v1.reserve(N);

	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();

	std::vector<TreeNode*> v2;
	v2.reserve(N);

	ObjectPool<TreeNode> TNPool;
	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;
}

int main()
{
	TestObjectPool();

	return 0;
}

一、内存池的引入

在很多C++程序中,频繁地进行内存的分配和释放(例如使用newdelete操作符)可能会带来性能上的开销,尤其是在处理大量小对象时。内存池(Memory Pool)技术应运而生,它就像是一个预先准备好的内存资源仓库,能够更高效地管理内存的分配和回收。

二、代码中的内存池实现 - ObjectPool

(一)整体结构

我们来看代码中的ObjectPool类,这是一个模板类(template<class T>),这意味着它可以为不同类型的对象提供内存池服务。

template<class T>
class ObjectPool {
public:
	T* New();
	void Delete(T* obj);
private:
	//void* _memory;
	//为了方便地址上的加减运算,使用char类型,char类型占一个字节
	char* _memory = nullptr;	//指向大块内存的指针
	void* _freelist = nullptr;	//指向待释放的空间
	size_t _remainbytes = 0;    // 大块内存在切分过程中剩余字节数
};
  1. 成员变量
    • _memory:这是一个char*类型的指针,初始化为nullptr。它的作用是指向大块内存。选择char*类型是为了方便进行地址的加减运算,因为char类型在内存中占用一个字节。这个大块内存就是我们内存池的核心资源,用来存储对象。
    • _freelist:类型为void*,初始值是nullptr。它就像是一个管理空闲内存块的列表,当对象被释放后,相关的内存块会被添加到这个列表中,以便后续的复用。
    • _remainbytes:这是一个size_t类型的变量,初始化为0。它记录了大块内存在切分过程中剩余的字节数。这个变量对于判断何时需要重新申请大块内存非常关键。

(二)内存分配 - New函数

T* ObjectPool::New() {
		T* obj = nullptr;
		if (_freelist) 
		//_freelist非空,说明有还回来的内存,优先使用还回来的内存
		//可以使用类似无头结点链表的头删
		{
			void* next = *(void**)_freelist;
			obj = (T*)_freelist;
			_freelist = next;
		}
		else
		{
			if (_remainbytes < sizeof(T)) {
				_remainbytes = 128 * 1024;
				/*如果空间无法被T整除,可能会出现会有一块内存不够一个T对象的空间,
				所以会导致_remainBytes的结果非零,而空间又不够用了,
				因此if()中的判定应该是_remainBytes < sizeof(T),
				即剩余内存不够一个对象大小时,则重开大块空间,以避免越界访问*/
				_memory = (char*)malloc(128 * 1024);
				if (_memory == nullptr) {
					throw std::bad_alloc();
				}
			}
			obj = (T*)_memory;		//申请出去的新的空间的起始位置
			//_memory += sizeof(T);		//新的空间被申请后,剩余空间的起始位置
			size_t objsize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
			_memory += objsize;
			/*由于指针变量的大小是4个字节(32位)或者8个字节(64位),而回收内存的时候是通过
			链表回收的,需要存储加一个节点的地址,因此如果当T类型的类型大小小于指针大小的时候
			无法存储一个地址,例如32位下指针大小4个字节,而char类型1个字节,这时候回收链表中
			只有一个字节的大小的话显然无法存放一个地址,因此需要作比较,如果类型大小小于指针变量
			类型大小,则返回一个指针变量的大小。*/
			_remainbytes -= sizeof(T);	//剩余的空间
		}
		//定位new,显式调用T的构造函数初始化
		new(obj)T;

		return obj;
	}
  1. 优先使用空闲内存
    • 当调用New函数来获取一个对象时,首先会检查_freelist是否为空(if (_freelist))。如果_freelist不为空,说明有已经回收但还未重新分配的内存。这时候就会采用类似无头结点链表的头删操作来获取内存块。具体来说,先获取_freelist指向的内存块(通过void* next = *(void**)_freelist;),然后将这个内存块作为新对象的地址(obj = (T*)_freelist;),最后更新_freelist指向该内存块中的下一个空闲内存的指针(_freelist = next;)。
  2. 从大块内存分配
    • 如果_freelist为空,就需要从大块内存_memory中分配空间。在分配之前,会检查_remainbytes是否小于sizeof(T)。如果是,就意味着当前大块内存剩余空间不足以分配一个T类型的对象了。这时候,会重新申请一块128 * 1024字节的内存(_remainbytes = 128 * 1024; _memory = (char*)malloc(128 * 1024);),并且在申请失败时抛出std::bad_alloc异常。
    • 在分配内存时,还有一个很巧妙的地方。由于在回收内存时是通过链表来管理的,需要存储下一个节点的地址,而不同机器上指针类型的大小可能不同(32位机器上为4字节,64位机器上为8字节)。所以在计算分配的字节数时,会比较sizeof(T)sizeof(void*)的大小(size_t objsize = sizeof(T) < sizeof(void*)? sizeof(void*) : sizeof(T);),取较大值来确保能够正确存储下一个空闲对象的指针。然后将新对象的地址赋给obj,并更新_memory_remainbytesobj = (T*)_memory; _memory += objsize; _remainbytes -= sizeof(T);)。
    • 最后,通过定位newnew(obj)T;)显式调用T的构造函数来初始化这个新对象。

(三)内存回收 - Delete函数

void ObjectPool::Delete(T* obj) {
		//显式调用析构函数清理对象
		obj->~T();
		//就相当于无头结点链表的头插
			*(void**)obj = _freelist;
			_freelist = *(void**)obj;
		/*之所以使用void类型来强制转换,只因为在不同位机器下,地址的字节位数不同
		比如在32位下,指针类型的大小是四个字节,而在64位下,指针类型的大小是八个字节
		因此如果想要通过二级指针来修改obj的指向,同时兼顾不同机器,使用void类型
		来进行强制类型转换。*/
	}
  1. 清理对象资源
    • 当调用Delete函数来释放对象时,首先会显式调用对象的析构函数(obj->~T();),这一步确保对象内部的资源被正确清理。
  2. 归还内存到空闲列表
    • 然后,将释放的内存块添加到_freelist的头部,这一操作类似于无头结点链表的头插操作。通过*(void**)obj = _freelist; _freelist = *(void**)obj;来实现。这里使用void*类型进行强制转换是为了在不同位机器下(地址字节位数不同)能够正确地通过二级指针修改obj的指向,从而将对象的内存块添加到空闲内存列表中。

三、内存池在TreeNode示例中的性能测试演示

struct TreeNode
	//二叉树节点
{
	int _val;
	TreeNode* _left;
	TreeNode* _right;

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

void TestObjectPool()
{
	// 申请释放的轮次
	const size_t Rounds = 5;

	// 每轮申请释放多少次
	const size_t N = 100000;

	std::vector<TreeNode*> v1;
	v1.reserve(N);

	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();

	std::vector<TreeNode*> v2;
	v2.reserve(N);

	ObjectPool<TreeNode> TNPool;
	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;
}
int main()
{
	TestObjectPool();
	return 0;
}
  • debug下运行效果:

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • release下运行效果:

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    可以看到使用对象池的程序运行效率会高不少

  1. 普通的new/delete操作
    • TestObjectPool函数中,首先对普通的newdelete操作进行了测试。创建了一个std::vector<TreeNode*>类型的v1,并预留了N个元素的空间(v1.reserve(N);)。
    • 然后通过两层循环,外层循环Rounds次,内层循环N次。在每次内层循环中,使用new创建一个TreeNode对象并添加到v1中(v1.push_back(new TreeNode);),然后再使用delete释放这些对象(delete v1[i];),最后清空v1向量(v1.clear();)。通过记录这个过程开始的时间(size_t begin1 = clock();)和结束的时间(size_t end1 = clock();),就可以计算出使用普通new/delete操作的耗时。
  2. 使用内存池的操作
    • 接着,创建了一个ObjectPool<TreeNode>类型的对象池TNPool,同样创建了一个std::vector<TreeNode*>类型的v2并预留N个元素的空间。
    • 也是通过类似的两层循环,在每次内层循环中,使用对象池的New函数获取TreeNode对象并添加到v2中(v2.push_back(TNPool.New());),然后使用对象池的Delete函数释放对象(TNPool.Delete(v2[i]);),最后清空v2向量。同样记录这个过程开始的时间(size_t begin2 = clock();)和结束的时间(size_t end2 = clock();),从而得到使用内存池操作的耗时。
    • 最后,通过cout输出两种方式的耗时(cout << "new cost time:" << end1 - begin1 << endl; cout << "object pool cost time:" << end2 - begin2 << endl;)。从这个结果可以直观地看到在频繁创建和销毁TreeNode对象的场景下,使用内存池能够带来明显的性能提升。

四、脱离malloc,直接在堆上按页申请空间

#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;
}

预处理:

  1. #ifdef指令
    • #ifdef是C和C++中的预处理指令。它用于条件编译,在这里的含义是“如果定义了(ifdefif defined的缩写)符号_WIN32”。
    • 在不同的编译环境下,可能会预定义一些特定的符号。在Windows环境下编译时,通常会预定义_WIN32这个符号(在64位Windows下可能还会预定义_WIN64,但这里只关注_WIN32相关的逻辑)。
  2. 包含头文件<windows.h>
    • _WIN32被定义时(即代码在Windows环境下编译),#include <windows.h>会被执行。<windows.h>是一个非常重要的Windows平台的头文件,它包含了大量Windows系统相关的函数声明、数据类型定义、宏定义等内容。例如,Windows下的图形界面编程(使用Windows API)、系统调用、进程和线程相关的操作等很多功能都需要这个头文件中的定义来支持。
  3. #else#endif
    • #else是与#ifdef配合使用的预处理指令,表示“否则”的情况。在这里,如果_WIN32没有被定义(即代码不是在Windows环境下编译),那么会执行#else后面的内容。不过在这段代码中,#else后面只是一个注释(//),没有实际的代码,可能是预留的用于在非Windows环境下(如Linux、macOS等)添加相关代码的地方。
    • #endif#ifdef - #else结构的结束标志,表示条件编译块的结束。如果没有#endif,编译器会报错,因为它不知道条件编译块在哪里结束。

SystemAlloc函数:

  1. 函数声明部分
    • inline static void* SystemAlloc(size_t kpage)
      • inline关键字:这是一个C++ 中的关键字,用于建议编译器将函数内联化。内联函数的主要目的是减少函数调用的开销。当函数被标记为内联时,编译器在编译时会尝试将函数体直接嵌入到调用该函数的地方,而不是像普通函数调用那样进行压栈、跳转等操作。
      • static关键字:在这里表示函数具有内部链接性,即这个函数只能在当前的源文件中被访问,不能被其他源文件访问。
      • void*:表示函数的返回类型是一个通用指针(指向未知类型数据的指针)。
      • SystemAlloc:函数名。
      • size_t kpage:函数接受一个size_t类型(无符号整数类型,通常用于表示对象的大小或数组的长度等)的参数kpage
  2. #ifdef _WIN32条件编译部分(Windows平台下的实现)
    • void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
      • VirtualAlloc是Windows系统中的一个函数,用于在进程的虚拟地址空间中分配内存。
        • 第一个参数0:表示让系统自动选择分配内存的起始地址。
        • 第二个参数kpage << 13:这里使用了左移运算符<<kpage是传入的参数,表示某种数量(可能是页数之类的概念),左移13位相当于将kpage乘以2^13(即8192)。这意味着以页为单位计算要分配的内存大小(在Windows中,内存页大小通常是4KB或8KB,这里假设是8KB,那么kpage就表示要分配的页数)。
        • 第三个参数MEM_COMMIT | MEM_RESERVE:这是内存分配类型的标志组合。MEM_COMMIT表示提交内存,即将物理存储映射到进程的虚拟地址空间;MEM_RESERVE表示保留一块地址空间,这块空间可以被后续的操作(如提交内存)使用。
        • 第四个参数PAGE_READWRITE:表示分配的内存具有可读可写的保护属性。
  3. #else部分(非Windows平台下的占位代码)
    • 在非Windows平台下(即_WIN32未被定义时),这里只是一个注释// linux下brk mmap等,说明在Linux平台下可能会使用brkmmap等系统调用或函数来实现类似的内存分配功能,但目前没有实际的代码实现。
  4. 内存分配失败处理部分
    • if (ptr == nullptr)
      • 这个if语句用于检查内存分配是否成功。如果ptrnullptr(即VirtualAlloc函数在Windows平台下分配内存失败,或者在非Windows平台下如果有实现且分配失败时也应该遵循类似的逻辑),则表示内存分配失败。
    • throw std::bad_alloc();
      • 当内存分配失败时,函数会抛出std::bad_alloc异常。std::bad_alloc是C++ 标准库中定义的用于表示内存分配失败的异常类型。当使用new操作符分配内存失败时,也会抛出这个异常。
  5. 函数返回部分
    • return ptr;
      • 如果内存分配成功(在Windows平台下VirtualAlloc成功或者在非Windows平台下假设的brkmmap等操作成功),函数将返回分配得到的内存地址指针。

使用这种方式修改后的代码如下:

#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;
}

template<class T>
class ObjectPool {
public:
	T* New() {
		T* obj = nullptr;
		if (_freelist) 
		{
			void* next = *(void**)_freelist;
			obj = (T*)_freelist;
			_freelist = next;
		}
		else
		{
			if (_remainbytes < sizeof(T)) {
				_remainbytes = 128 * 1024;
				//_memory = (char*)malloc(128 * 1024);
				_memory = (char*)SystemAlloc(_remainbytes >> 13);
				if (_memory == nullptr) {
					throw std::bad_alloc();
				}
			}
			obj = (T*)_memory;		//申请出去的新的空间的起始位置
			//_memory += sizeof(T);		//新的空间被申请后,剩余空间的起始位置
			size_t objsize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
			_memory += objsize;
			_remainbytes -= sizeof(T);	//剩余的空间
		}
		//定位new,显式调用T的构造函数初始化
		new(obj)T;

		return obj;
	}
	void Delete(T* obj) {
		obj->~T();
			*(void**)obj = _freelist;
			_freelist = *(void**)obj;
	}
private:
	char* _memory = nullptr;	//指向大块内存的指针
	void* _freelist = nullptr;	//指向待释放的空间
	size_t _remainbytes = 0;    // 大块内存在切分过程中剩余字节数
};

五、总结

(一)内存池的优势

  1. 性能提升
    • 通过避免频繁的系统级内存分配(malloc)和释放(free)操作,内存池减少了内存管理的开销。在处理大量对象的创建和销毁时,这种优势更加明显。就像我们在TestObjectPool函数中的测试结果一样,使用内存池操作TreeNode对象比普通的new/delete操作要快很多。
  2. 内存碎片减少
    • 内存池通过预先分配大块内存,并对其进行有效的管理和复用,减少了内存碎片的产生。这有助于提高内存的利用率,使程序能够更稳定地运行,尤其是在长时间运行的程序或者内存资源紧张的环境中。

(二)代码的思考

  1. 可扩展性
    • 由于ObjectPool是一个模板类,它可以方便地应用于各种类型的对象,只要这些对象的构造函数和析构函数定义正确。这为代码的复用和扩展提供了很大的便利,使我们可以在不同的项目中轻松地使用这个内存池来管理不同类型的对象。
  2. 局限性
    • 内存池的实现也存在一些局限性。例如,它是基于定长内存池的思想,每次重新申请内存时固定为128 * 1024字节。在处理大小差异很大的对象时,可能会导致内存浪费或者频繁重新申请内存的情况。另外,这个代码没有考虑多线程环境下的并发安全问题,如果在多线程中同时使用这个内存池,可能会导致数据竞争和错误的内存管理操作。

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

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

相关文章

文件和目录

文件和目录 获取文件属性 通过 ls 命令可以查看到文件的很多属性内容&#xff0c;这些文件属性的内容可以通过以下几个函数获取: #include <sys/types.h> #include <sys/stat.h> #include <unistd.h>int stat(const char *pathname, struct stat *statbuf…

第一节- C++入门

1. &#x1f680;&#x1f680;C关键字(C98) &#x1f33c;&#x1f33c;C总计63个关键字&#xff0c;C语言32个关键字 ps&#xff1a;下面我们只是看一下C有多少关键字&#xff0c;不对关键字进行具体的讲解。后面我们学到以后再细讲。 2. 命名空间 在C/C中&#xff0c;变量…

基于深度学习的学情智能监测系统设计与实现(PYQT+YOLOv8+训练数据集+论文+部署文档)

摘要 本文设计并实现了一个基于深度学习的学情智能监测系统&#xff0c;该系统通过对学生学习状态的自动监测与分析&#xff0c;旨在辅助教师更精准地把握学生学习情况&#xff0c;进而优化教学策略和提升教学质量。在论文中&#xff0c;详细描述了系统的设计与实现过程&#…

【web开发】Spring Boot 快速搭建Web项目(三)

Date: 2024.08.31 18:01:20 author: lijianzhan 简述&#xff1a;根据上篇原文Spring Boot 快速搭建Web项目&#xff08;二&#xff09;&#xff0c;由于已经搭建好项目初始的框架&#xff0c;以及自动创建了一个启动类文件&#xff08;TestWebApplication.java&#xff09; …

一种路径敏感的数据依赖分析算法

Falcon 1.方法1.1.Basic Rule1.2.改进算法1.3.跨函数分析 2.Evaluation2.1.设置2.2.value-flow分析2.3.Thin Slicing2.4.Bug Detection 参考文献 这篇工作发表于PLDI 24&#xff0c;提出了一种context- 以semi-path-sensitive的数据依赖分析算法&#xff0c;解决path-sensitive…

如何使用ssm实现基于web的山东红色旅游信息管理系统的设计与实现

TOC ssm716基于web的山东红色旅游信息管理系统的设计与实现jsp 绪论 1.1研究背景 从古到今&#xff0c;信息的录入&#xff0c;存储&#xff0c;检索都受制于社会生产力的发展&#xff0c;不仅仅浪费大量的人力资源还需要浪费大量的社会物资&#xff0c;并且不能长时间的保…

信息安全工程师(24)网络安全体系建设原则与安全策略

一、网络安全体系建设原则 网络空间主权原则&#xff1a;维护网络空间主权是网络安全的首要原则。这要求国家在网络空间的管理、运营、建设和使用等方面具有完全自主的权利和地位&#xff0c;不受任何外部势力的干涉和侵犯。网络安全与信息化发展并重原则&#xff1a;网络安全与…

Midjourney 使用教程——入门篇

目录标题 一、前提二、Midjourney 使用文档三、如何注册使用Midjourney四、结合GPT快速生成Midjourney 构图指令五、其他 一、前提 先连接国外代理服务器。没有的可以退下了。 二、Midjourney 使用文档 Discord 快速入门 注意&#xff1a;如图所示&#xff0c;需要10美刀一…

【HTML5】html5开篇基础(4)

1.❤️❤️前言~&#x1f973;&#x1f389;&#x1f389;&#x1f389; Hello, Hello~ 亲爱的朋友们&#x1f44b;&#x1f44b;&#xff0c;这里是E绵绵呀✍️✍️。 如果你喜欢这篇文章&#xff0c;请别吝啬你的点赞❤️❤️和收藏&#x1f4d6;&#x1f4d6;。如果你对我的…

已解决:“ModuleNotFoundError:No module named apex”

首先遇到这个问题不可以直接简单粗暴的使用&#xff1a;“pip install apex”直接安装模块来解决&#xff0c;这样的话程序还是会继续报错“ModuleNotFoundError&#xff1a;No module named apex”&#xff0c;别问我怎么知道&#xff0c;问就是深受其害&#xff01; 去网上查…

Android实现图片滚动和页签控件功能的实现代码

首先题外话&#xff0c;今天早上起床的时候&#xff0c;手滑一下把我的手机甩了出去&#xff0c;结果陪伴我两年半的摩托罗拉里程碑一代就这么安息了&#xff0c;于是我今天决定怒更一记&#xff0c;纪念我死去的爱机。 如果你是网购达人&#xff0c;你的手机上一定少不了淘宝…

热题系列章节21

补充题14. 阿拉伯数字转中文数字 补充题9. 36进制加法 85. 最大矩形 class Solution:def maximalRectangle(self, matrix: List[List[str]]) -> int:if not matrix:return 0m,nlen(matrix),len(matrix[0])# 记录当前位置上方连续“1”的个数pre[0]*(n1)res0for i in range…

带你0到1之QT编程:二十、QT与MySQL喜结连理,构建数据库应用开发

此为QT编程的第二十谈&#xff01;关注我&#xff0c;带你快速学习QT编程的学习路线&#xff01; 每一篇的技术点都是很很重要&#xff01;很重要&#xff01;很重要&#xff01;但不冗余&#xff01; 我们通常采取总-分-总和生活化的讲解方式来阐述一个知识点&#xff01; …

基于elasticsearch存储船舶历史轨迹

文章目录 引言轨迹文档定时创建索引手动添加索引并为索引添加别名POST请求批量插入文档数据查询文档数据引言 需求: 存储轨迹,提供站点查询显示 实现:每天创建索引,使用POST请求往Elasticsearch批量插入文档数据 依赖 <dependency><groupId>org.springframe…

JAVA红娘婚恋相亲交友系统源码全面解析

在数字化时代&#xff0c;红娘婚恋相亲交友系统成为了连接单身男女的重要桥梁。JAVA作为一种流行的编程语言&#xff0c;为开发这样的系统提供了强大的支持。编辑h17711347205以下是对JAVA红娘婚恋相亲交友系统源码的全面解析&#xff0c;以及三段示例代码的展示。 系统概述 …

Stable Diffusion绘画 | 插件-Deforum:动态视频生成

Deforum 与 AnimateDiff 不太一样&#xff0c; AnimateDiff 是生成丝滑变化视频的&#xff0c;而 Deforum 的丝滑程度远远没有 AnimateDiff 好。 它是根据对比前面一帧的画面&#xff0c;然后不断生成新的相似图片&#xff0c;来组合成一个完整的视频。 Deforum 的优点在于可…

DevExpress WPF中文教程:如何解决编辑单元格值的常见问题?

DevExpress WPF拥有120个控件和库&#xff0c;将帮助您交付满足甚至超出企业需求的高性能业务应用程序。通过DevExpress WPF能创建有着强大互动功能的XAML基础应用程序&#xff0c;这些应用程序专注于当代客户的需求和构建未来新一代支持触摸的解决方案。 无论是Office办公软件…

明达技术工业级边缘计算网关:智能制造的智慧纽带

在工业互联网的浪潮中&#xff0c;智能制造正引领着传统制造业的深刻变革&#xff0c;显著提升着生产效能与运营效率。在这场转型中&#xff0c;数据处理与分析能力成为了企业核心竞争力的重要一环。工业级边缘计算网关&#xff0c;作为连接工业设备与云端智能的桥梁&#xff0…

华为-单臂路由

1、什么是单臂路由 单臂路由&#xff08;Single-Arm Routing&#xff09;是一种网络架构和配置技术&#xff0c;它允许路由器通过一个物理接口来管理多个虚拟局域网&#xff08;VLAN&#xff09;之间的通信。 这个物理接口被配置为Trunk模式&#xff0c;以便能够传输来自不同VL…

完数因子输出-C语言

1.问题&#xff1a; 找出1000之内的所有完数&#xff0c;并输出其因子。 2.解答&#xff1a; 一个数如果恰好等于它的因子之和&#xff0c;这个数就称为完数。1不用判断&#xff0c;直接从2开始&#xff0c;因为1的因子只有1。 3.代码&#xff1a; #include<stdio.h>…