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); // 128KBif (_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); // 128KBif (_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); // 128KBif (_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); // 128KBif (_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); // 128KBif (_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); // 128KBif (_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); // 128KBif (_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和freesize_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 _WIN32void* ptr = VirtualAlloc(0, kpage<<13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else// linux下brk mmap等
#endifif (ptr == nullptr)throw std::bad_alloc();return ptr;
}

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

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

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

相关文章

Java面向对象知识总结+思维导图

&#x1f516;面向对象 &#x1f4d6; Java作为面向对象的编程语言&#xff0c;我们首先必须要了解类和对象的概念&#xff0c;本章的所有内容和知识都是围绕类和对象展开的&#xff01; ▐ 思维导图1 ▐ 类和对象的概念 • 简单来说&#xff0c;类就是对具有相同特征的一类事…

【Spring】认识 IoC 容器和 Servlet 容器

认识 IoC 容器和 Servlet 容器 1.认识容器1.1 IoC 容器1.2 loC 的实现方法1.2.1 依赖注入1.2.2 依赖查找 1.3 认识 Servlet 容器 2.用 IoC 管理 Bean2.1 创建一个 Bean2.2 编写 User 的配置类2.3 编写测试类 3.用 Servlet 处理请求3.1 注册 Servlet 类3.2 开启 Servlet 支持 1.…

力扣:1738. 找出第 K 大的异或坐标值

1738. 找出第 K 大的异或坐标值 给你一个二维矩阵 matrix 和一个整数 k &#xff0c;矩阵大小为 m x n 由非负整数组成。 矩阵中坐标 (a, b) 的 值 可由对所有满足 0 < i < a < m 且 0 < j < b < n 的元素 matrix[i][j]&#xff08;下标从 0 开始计数&…

晶圆厂的PE转客户工程师前景怎么样?

知识星球&#xff08;星球名&#xff1a; 芯片制造与封测技术社区&#xff0c;星球号&#xff1a; 63559049&#xff09;里的学员问&#xff1a; 目前在晶圆厂做PE&#xff0c;倒班oncall压力太大把身体搞坏了&#xff0c;现在有一个design house的CE客户工程师的offer&…

跨境选品师不是神话:普通人也能轻松掌握,开启全球贸易新篇章!

随着互联网技术的飞速发展&#xff0c;跨境电商行业已成为全球经济的新增长点。在这个背景下&#xff0c;一个新兴的职业——跨境选品师&#xff0c;逐渐走进了人们的视野。那么&#xff0c;跨境选品师究竟是做什么的?普通人又该如何成为优秀的跨境选品师呢? 一、跨境选品师的…

ABAQUS应用07-实现拉伸和压缩刚度不同的弹簧建模

文章目录 0、背景描述1、步骤 0、背景描述 到目前为止&#xff0c;本文的内容我还没有具体实践过&#xff0c;但是个人认为后期是会用到的。比如说&#xff0c;对于风电机组地基转动刚度的设置&#xff0c;土体就是一种拉压刚度并不相同的材料。所以现在先记录下来&#xff0c…

如何用Java实现SpringCloud Alibaba Sentinel的熔断功能?

在Java中使用Spring Cloud Alibaba Sentinel实现熔断功能的步骤如下&#xff1a; 添加依赖 在项目的pom.xml文件中添加Spring Cloud Alibaba Sentinel的依赖&#xff1a; <dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud…

Java进阶学习笔记24——Object类

Object类: Object类是Java中所有类的祖宗类&#xff0c;因此&#xff0c;Java中所有类的对象都可以直接使用Object类中提供的一些方法。 所有类都是Object类的子孙类。 API文档&#xff1a; Object类的成员方法&#xff1a; Object类的常见方法&#xff1a; Student类&…

Arthas反编译与重新加载class

一、背景 因为其他研发部门同事给的产品jar包存在一个问题&#xff0c;就是http底层的超时时间默认为60s&#xff0c;但是最近调用外部接口同步数据&#xff0c;这个数据量太大导致超时超过60s&#xff0c;每次同步都不成功。但是客户目前对此情况特别不满意&#xff0c;需要紧…

勒索病毒的策略与建议

随着网络技术的快速发展&#xff0c;勒索病毒攻击成为全球范围内日益严重的网络安全威胁。勒索病毒通过加密用户文件或锁定系统来勒索赎金&#xff0c;给个人和企业带来了巨大的损失。因此&#xff0c;了解如何应对勒索病毒攻击至关重要。本文将概述一些有效的防范措施和应对策…

2024电工杯数学建模B题完整论文讲解(含每一问python代码+数据)

大家好呀&#xff0c;从发布赛题一直到现在&#xff0c;总算完成了2024电工杯数学建模B题大学生平衡膳食食谱的优化设计及评价完整的成品论文。 本论文可以保证原创&#xff0c;保证高质量。绝不是随便引用一大堆模型和代码复制粘贴进来完全没有应用糊弄人的垃圾半成品论文。 …

Linux网络编程:HTTPS协议

目录 1.预备知识 1.1.加密和解密 1.2.常见加密方式 1.2.1.对称加密 1.2.2.非对称加密 ​编辑 1.3.数据摘要&#xff08;数据指纹&#xff09;和数据签名 1.4.证书 1.4.1.CA认证 1.4.2.证书和数字签名 2.HTTPS协议 2.1.自行设计HTTPS加密方案 2.1.1.只使用对称加密 …

uniapp使用uni.chooseImage选择图片后对其是否符合所需的图片大小和类型进行校验

uni.chooseImage的返回值在H5平台和其他平台的返回值有所差异&#xff0c;具体差异看下图 根据图片可以看出要想判断上传的文件类型是不能直接使用type进行判断的&#xff0c;所以我使用截取字符串的形式来判断&#xff0c;当前上传图片的后缀名是否符合所需要求。 要求&#…

驱动与系统学习网址

DRM&#xff08;Direct Rendering Manager&#xff09;学习简介-CSDN博客 Android Qcom Display学习(零)-CSDN博客 https://blog.csdn.net/hexiaolong2009/category_9705063.htmlhttps://blog.csdn.net/hexiaolong2009/category_9705063.htmlRender Hell —— 史上最通俗易懂…

区间合并-leetcode合并石头的最低成本-XMUOJ元素共鸣:深层次的唤醒

题目 思路 话不多说&#xff0c;直接上代码 附上INT_MAX和INT_MIN 【C】详解 INT_MAX 和 INT_MIN&#xff08;INT_MAX 和 INT_MIN是什么&#xff1f;它们的用途是什么&#xff1f;如何防止溢出&#xff1f;&#xff09;_c int max-CSDN博客 代码 /* leetcode合并石头的最低…

clone方法总结Java

Java中Object类当中有许多方法&#xff0c;如图所示&#xff1a; clone方法就是其中一种&#xff0c;分为浅拷贝&#xff0c;深拷贝举一个例子&#xff1a; 浅拷贝&#xff1a; 在Person类当中右键鼠标然后&#xff0c;选中Generate&#xff1a; 然后重写clone方法 protecte…

【微服务】springboot 构建镜像多种模式使用详解

目录 一、前言 二、微服务常用的镜像构建方案 3.1 使用Dockerfile 3.2 使用docker plugin插件 3.3 使用docker compose 编排文件 三、环境准备 3.1 服务器 3.2 安装JDK环境 3.2.1 创建目录 3.2.2 下载安装包 3.2.3 配置环境变量 2.2.4 查看java版本 3.3 安装maven …

pyqt 浮动窗口QDockwidget

pyqt 浮动窗口QDockwidget QDockwidget效果代码 QDockwidget QDockWidget 是 PyQt中的一个控件&#xff0c;它提供了一个可以停靠在主窗口边缘或者浮动在屏幕上的窗口小部件&#xff08;widget&#xff09;。QDockWidget 允许用户自定义其界面&#xff0c;并提供了灵活的停靠和…

Elasticsearch集群许可证过期问题解决方法汇总

最近在使用elasticsearch的过程中,使用elastic-head进行可视化展示集群的状态和信息,从2024年5月18日突然elastic-head无法现在集群的状态界面啦,elasticsearch集群状态是正常,命令如下: curl -X GET "localhost:9200/_cluster/health?pretty" 在google页面上通过…

引流500+创业粉,抖音口播工具

在抖音平台运营一个专注于口播的工具号&#xff0c;旨在集结超过500位热衷于创业的粉丝&#xff0c;这需要精心筹划的内容策略和周到的运营计划。首先&#xff0c;明确你的口播工具号所专注的领域&#xff0c;无论是分享创业经验、财务管理技巧还是案例分析&#xff0c;确保你所…