【C++破局】C++内存管理之new与deleted剖析

news2025/3/17 1:53:01

作者主页

📚lovewold少个r博客主页

   ⚠️本文重点c++内存管理部分知识点梳理

👉【C-C++入门系列专栏】博客文章专栏传送门

😄每日一言:花有重开日,人无再少年!

目录

C/C++的内存分配机制

内存分区

1. 内核空间(Kernel Space):

2. 栈空间(Stack):

3. 内存映射段(Memory Mapping Segment):

4. 堆(Heap):

5. 数据段(Data Segment):

6. 代码段(Code Segment):

C与C++的动态内存管理方法

malloc,calloc,realloc的内存开辟函数

内存泄露  

C++的内存管理方式

new/delete操作内置类型

 new的基本用法

delete的基本用法

malloc/free和new/delete的区别

operator new与operator delete函数

重载operator new和operator delete(了解)

new和delete的底层原理

new的底层原理:

delete的底层原理:

定位new表达式

总结


前言

        在c语言的学习过程中,我们学习了c语言的动态管理内容。本章的内容主要是从C/C++的内存分配区域入手,深入浅出的讲解C++的内存分配机制和new与delete。

C/C++的内存分配机制

内存分区

1. 内核空间(Kernel Space)

   位置: 在内存的顶部,通常是最高的地址空间。
   用途:内核空间用于存放操作系统的内核代码和数据结构。这部分内存通常是受保护的,只有内核态的代码可以访问。

2. 栈空间(Stack)

   栈是向下增长的:对于栈来说,栈内存的地址是从高地址向低地址增长的。新的栈帧会被放置在当前栈顶的上方。栈帧的释放会导致栈顶向低地址移动

    位置:通常位于用户空间和内核空间之间。
    用途:栈用于存储函数的局部变量、函数参数、返回地址和函数调用的上下文信息。栈是一个后进先出(LIFO)的数据结构。
    自动分配和释放:栈内存是由系统自动分配和释放的,函数调用时分配,函数返回时释放。

3. 内存映射段(Memory Mapping Segment)

    位置:通常位于用户空间和内核空间之间。
    用途:内存映射段包括可执行文件的映射、共享库的映射以及动态链接器的映射。这些映射将磁盘上的二进制文件映射到内存中,以便执行。

4. 堆(Heap)

   堆是向上增长:堆的内存地址是由低地址向高地址增长的,新分配的内存位于已经分配的内存的上方。

   位置:通常位于内核空间和栈空间之间。
   用途:堆用于动态分配内存,程序员可以通过函数如 malloc、calloc、realloc 来在堆上分配内存。堆上的内存需要手动释放,否则可能导致内存泄漏。
   动态大小:堆的大小不是固定的,可以根据需要动态分配和释放。

5. 数据段(Data Segment)

   位置:通常位于内核空间和栈空间之间。
   用途:数据段包括已初始化的全局变量和静态变量。这些变量在程序启动时就被初始化,并在整个程序的执行周期内保持不变。

6. 代码段(Code Segment)

   位置:通常位于内核空间和栈空间之间。
   用途:代码段包含程序的机器代码,即可执行指令。这部分内存是只读的,用于存储程序的执行代码。

只读数据段中,包含了两种常量:

1. 字符串常量:
   例如,C/C++中的字符串字面值(如"Hello, World!")。
   这些字符串常量存储在只读数据段中,因为它们在程序执行期间是不可修改的。
2. 其他常量:
   例如,C/C++中的全局常量(使用 const 关键字声明的常量)。
   这些常量也通常存储在只读数据段中。


C与C++的动态内存管理方法

malloc,calloc,realloc的内存开辟函数

malloc, calloc, 和 realloc 是在C语言中用于分配内存的三个常见函数。

malloc(Memory Allocation):

示例:

void Test()
{
	int* p1 = (int*)malloc(sizeof(int));
	if (p1 == NULL)
	{
		perror("malloc");
		exit(EXIT_FAILURE);
	}
	free(p1);
}

  • 原型:void* malloc(size_t size);

  • 功能:用于动态分配指定大小的内存块。

  • 参数:size 表示要分配的字节数。

  • 返回值:如果分配成功,返回指向分配内存的指针;如果失败,返回 NULL

calloc(Contiguous Allocation):

示例:

#include<stdlib.h>
void Test()
{
	int* p1 = (int*)calloc(4,sizeof(int));
	if (p1 == NULL)
	{
		perror("calloc");
		exit(EXIT_FAILURE);
	}
	free(p1);
}
  • 原型:void *calloc(size_t num_elements, size_t element_size);

  • 功能:用于动态分配指定数量和大小的内存块,并将每个字节初始化为零。

  • 参数:num_elements表示要分配的元素数量,element_size表示每个元素的大小(字节数)。

  • 返回值:如果分配成功,返回指向分配内存的指针;如果失败,返回 NULL

realloc(Re-allocation):

示例:

#include<stdlib.h>
void Test()
{
	int* p1 = (int*)calloc(4,sizeof(int));
	if (p1 == NULL)
	{
		perror("calloc");
		exit(EXIT_FAILURE);
	}
	int* p2 = (int*)calloc(p1, sizeof(int)*10);
	if (p1 == NULL)
	{
		perror("malloc");
		exit(EXIT_FAILURE);
	}
	free(p2);
}
  • 原型:void *realloc(void *ptr, size_t new_size);

  • 功能:用于更改先前分配的内存块的大小。

  • 参数:ptr是之前由 malloc, calloc  realloc 返回的指针;new_size 是新的内存块大小。

  • 返回值:如果分配成功,返回指向重新分配内存的指针;如果失败,返回 NULL。如果 ptr  NULL,则其行为等同于 malloc(new_size)

        以上函数主要要从函数功能进行区分,要注意的是无论是哪一种函数开辟或者扩容的空间,后面都需要手动free进行释放。同时对于realloc我们扩容的空间释放只需要释放后者,他的作用是根据需要重新分配内存,可能会将现有的内存块的内容移动到新的位置,然后释放原来的内存块。如果realloc内部决定在现有块的末尾或相邻位置分配新的内存,它将返回原始块的指针,而不是分配新的块。因此,如果新的内存块被分配在了原来的内存块上,释放原来的内存块也就释放了新的内存块,因为它们实际上是同一块内存。对于用户来说,只需保留realloc返回的指针即可,而不需要显式释放原来的内存块,需要避免多次释放。

内存泄露  

        内存泄漏是指在程序运行时,动态分配的内存没有被释放,导致系统无法再次使用这些内存块。当一个程序在运行过程中分配了内存,但在不再需要这些内存时没有释放,就会发生内存泄漏。这对一个操作系统或者服务器的话影响是非常巨大的。

        内存泄漏可能导致程序运行时占用的内存不断增加,最终耗尽系统的可用内存,从而导致程序性能下降或崩溃。内存泄漏是一种常见的编程错误,因此在开发过程中应该特别注意及时释放不再需要的内存。就比如我们经常使用一个软件的时候,长时间运转我们会发现程序消耗的内存越来越大,手机或者电脑越来越卡顿,这可能就存在内存泄露的问题,内存不能及时释放,只能新开辟空间用于后续进程,反复叠加下对性能的消耗会越来越大。合理的掌握内存分配和管理,能极大的提高程序运行的效率进而提高用户使用的体验。

        因此无论我们是使用方式的什么开辟的空间,动态内存开辟的空间需要程序员自己去掌控,要及时的free开辟的空间。

C++的内存管理方式

        在C++中,由于C++向下兼容,虽然你仍然可以使用malloc和free,但是推荐使用newdelete。首先我们知道,C++是一门面向对象的语言,和C面向过程不同,我们要体现C++的优越性就一定要对底层问题的处理上进行封装调用。

        malloc很显然能完成对于内存开辟问题,但是在C++中,我们通常是构建一个自定义的类,而构建一个类后我们需要完成对类的初始化。在使用malloc的时候我们能清晰的感觉到,首先对void*的返回值我们要进行强制类型转换,并且还需要传递空间内存大小完成开辟的工作。然后我们还需要手动调用构造函数进行构造么,这也太麻烦了。

        因此对于这些地方使用极为不便和无能无力我们就需要使用新的操作方式。

new/delete

        在C++中,new和delete是用于动态内存管理操作符,它们分别用于在堆上分配和释放内存。这两个操作符提供了对动态内存的灵活控制,特别适用于需要在运行时确定内存大小的情况。

 new的基本用法

        new用于在堆上动态分配内存,并返回指向新分配内存的指针。它同时会调用对象的构造函数(如果有的话),对新分配的内存进行初始化。(一个对象就轻松被new出来咯!!!)

void Test()
{
    //动态申请一个int类型的空间
	int* ptr1 = new int;
    //动态申请一个int类型的空间并初始化为10
    int* ptr2 = new int(10);
    //动态申请10个int类型的空间
	int* ptr3 = new int[10];
}

delete的基本用法

delete用于释放通过new分配的内存。在释放内存之前,delete会调用对象的析构函数(如果有的话),对内存中的对象进行清理工作。 (一个对象的终点)

void Test()
{
    //动态申请一个int类型的空间
    int* ptr1 = new int;
    //动态申请一个int类型的空间并初始化为10
    int* ptr2 = new int(10);
    //动态申请10个int类型的空间
    int* ptr3 = new int[10];

    delete ptr1;//释放单个对象的空间
    delete ptr2;
    delete[] ptr3;//释放数组空间
}

注意:要时刻注意一一对应

  • 对于每个new,应该有一个相应的delete,对于每个new[],应该有一个相应的delete[]
  • 不要混合使用newdeletemallocfree,也不要混合使用new[]和delete[]mallocfree
  • 在使用newnew[]时,应该使用相应的deletedelete[]来释放内存,以确保正确调用析构函数。

malloc/free和new/delete的区别

        我们知道,malloc并不会去调用构造函数,free也不会去调用析构函数。对于自定义类型的空间处理上new和delete不仅能完成空间的开辟还能完成调用构造函数和析构函数。对于自定义类来讲显然new和delete更甚一筹,而对于非自定义类型的时候,两者并无太大区别,不过new和delete在使用方式上也相对更加简略了。

class A
{
public:
	A(int date = 0)
	{
		_date = date;
		printf("a()  ");
		printf("_date=%d\n", _date);
	}
	~A()
	{
		printf("~a()\n");
	}
private:
	int _date;
};
int main()
{
	printf("____________a__________________\n");
	A* a = new A;
	delete a;

	printf("____________b__________________\n");
	A* b = new A(10);
	delete b;

	printf("____________c__________________\n");
	A* c = new A[10];
	delete[] c;
	printf("____________d__________________\n");
	A* d = new A[5]{ 1,2,3,4 };
	delete[] d;
	return 0;
}

在内存管理上来讲当分配内存失败的时候两者也有区别

  • 在分配内存失败时,malloc返回 NULL,需要手动检查。
  • new运算符在分配内存失败时抛出异常,可以通过异常处理机制来处理。

operator new与operator delete函数

     在 C++ 中,new是一个关键字,它用于动态分配内存并构造对象,而operator new是与new相关的一个全局函数,用于执行实际的内存分配。delete同理。我们这里进一步剖析new和delete的底层原理。

/*
operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间失败,
尝试执行空 间不足应对措施,如果改应对措施用户设置了,则继续申请,否则抛异常。
*/
 
void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
	// try to allocate size bytes
	void* p;
	while ((p = malloc(size)) == 0)
		if (_callnewh(size) == 0)
		{
			// report no memory
			// 如果申请内存失败了,这里会抛出bad_alloc 类型异常
			static const std::bad_alloc nomem;
			_RAISE(nomem);
		}
 
	return (p);
}
 
 
/*
operator delete: 该函数最终是通过free来释放空间的
*/
 
void operator delete(void* pUserData)
{
	_CrtMemBlockHeader* pHead;
 
	RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
 
 
	if (pUserData == NULL)
		return;
 
	_mlock(_HEAP_LOCK); /* block other threads */
	__TRY
		/* get a pointer to memory block header */
		pHead = pHdr(pUserData);
 
		/* verify block type */
		_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
 
		_free_dbg(pUserData, pHead->nBlockUse);
 
	__FINALLY
		_munlock(_HEAP_LOCK); /* release other threads */
	__END_TRY_FINALLY
	
	return;
}
 
/*
free的实现
*/
#define free(p) _free_dbg(p, _NORMAL_BLOCK)

        operator new与operator delete是两个全局函数,在上述代码中是其函数的实现原理,我们不难发现,其函数实际上也是通过malloc来申请空间的,成功申请后就直接返回,否则执行用户提供的空间应对不足的措施,如果用户继续申请就会抛异常。operator最终也是通过调用free来释放空间的。

        抛异常是c++中一种在程序执行过程中遇到错误或异常情况时,通过特殊的语句将程序的控制权传递给异常处理机制。这个过程通常称为"抛出异常"。异常是一种表示程序错误或不正常情况的机制。当某个异常条件发生时,程序会停止当前执行路径,然后查找能够处理这个异常的代码块。如果找到了匹配的异常处理代码块,程序会跳转到该处进行处理。如果找不到对应的处理代码,程序可能会终止运行。(后面会详细讲解)

        在main函数中,我们使用try块来包裹可能抛出异常的代码,然后使用catch块来捕获并处理异常。如果异常被抛出,控制流将跳转到匹配的catch块。

        这里我们演示一下抛异常和捕获异常:

#include <iostream>
using namespace std;

int main(void)
{
    char* p2 = nullptr;
    try 
    {
        char* p2 = new char[1024u * 1024u * 1024u * 2u - 1];
    }
    catch (const exception& e) 
    {
        cout << e.what() << endl;
    }
    printf("%p\n", p2);

    return 0;
}

        在上述演示中,我们申请了大面积的内存空间,程序并没有出现C语言的崩溃现象,而是以抛异常的方式优雅的退出并展示了错误原因。

重载operator new和operator delete(了解)

        一般情况下,不对这两个函数进行重载,但是在一些特定的情况下,我们可以完成一些特殊的请求。比如在申请和释放空间的时候有一些特殊的需求,打印一些日志信息,帮助用户检测是否存在内存泄露的情况之类的场景需求。

void* operator new(std::size_t size) 
{
    void* ptr = std::malloc(size);
    std::cout << "自定义new分配了 " << size << " 字节的内存,其地址空间为 " << ptr << std::endl;
    return ptr;
}

void operator delete(void* ptr) noexcept 
{
    std::cout << "自定义 delete:释放了地址为 " << ptr << " 的内存" << std::endl;
    std::free(ptr);
}

int main() 
{
    int* p = new int;
    delete p;

    int* arr = new int[5];
    delete[] arr;

    return 0;
}

new和delete的底层原理

new的底层原理:

  1. 计算分配的总内存大小:new首先计算要分配的总内存大小,包括对象的大小以及额外的管理信息。

  2. 调用operator new分配内存:new调用operator new函数进行实际的内存分配。可以重载它以提供自定义的分配逻辑。

  3. 调用对象的构造函数: 如果new用于分配单个对象,它会调用对象的构造函数,完成对象的初始化。

  4. 返回分配的内存地址: 最后,new返回指向新分配对象。

delete的底层原理:

  1. 调用对象的析构函数: 在使用delete释放对象之前,会调用对象的析构函数,进行对象资源的清理。

  2. 调用operator delete释放内存:delete调用operator delete函数释放之前分配的内存。可以重载它以提供自定义的释放逻辑。

  3. 返回释放的内存: 最终,操作系统可以重新利用被释放的内存。

数组的原理即是调用相应的operator new[]对多个对象的空间申请和构造,销毁同理。

定位new表达式

定位new表达式是在一个已经分配了原始的内存空间中显示的调用构造函数完成对对象的初始化操作。

new (ptr) Type(init);

其中,prt是指向已分配内存的指针,Type是对象的类型,init是可选的初始化参数(可以带参也可以不带参)。

这种形式的new主要用于在预分配的内存区域中创建对象,配合内存池使用,因为内存池分配的内存没有初始化。而不是在堆上分配新的内存。这对于一些特殊的需求,如实现自定义内存池或在固定地址处构造对象等情况非常有用。

手动调用析构函数: 当你不再需要这个对象时,需要手动调用析构函数来释放资源

int main()
{
	// p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行
	A* p1 = (A*)malloc(sizeof(A));
	new(p1)A;  // 注意:如果A类的构造函数有参数时,此处需要传参
	
	p1->~A();
	free(p1);

	A* p2 = (A*)operator new(sizeof(A));
	new(p2)A(10);
	p2->~A();//可以显示的调用析构函数,构造函数不可以。
	operator delete(p2);
	return 0;
}

总结

        本章主要从C/C++的不同内存管理机制入手,深入浅出的讲解new和delete的底层原理和一些扩展知识。对于文章中涉及到内存池和抛异常等机制我们会在后续单独讲解,本章点到为止。希望能对大家深入理解new和delete有所帮助!


    作者水平有限,如有错误欢迎指正!


    

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

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

相关文章

Vue中的常用指令v-html / v-show / v-if / v-else / v-on / v-bind / v-for / v-model

前言 持续学习总结输出中&#xff0c;Vue中的常用指令v-html / v-show / v-if / v-else / v-on / v-bind / v-for / v-model 概念&#xff1a;指令&#xff08;Directives&#xff09;是Vue提供的带有 v- 前缀 的特殊标签属性。可以提高操作 DOM 的效率。 vue 中的指令按照不…

Java Web——HTTP协议

目录 1. HTTP协议概述 1.1. HTTP数据传输格式 1.2. HTTP协议特点 2. HTTP 1.0和HTTP 1.1 3. HTTP请求协议 3.1. GET方式请求协议 3.2. POST方式请求协议 3.3. GET请求和POST请求的区别 4. HTTP相应协议 4.1. 响应状态码 如果两个国家进行会晤需要遵守一定的礼节。所以…

WMS配送中心主要业务流程

业务流程图 入库 波次出库 按门店和门店所属送货路线确定出库波次 入库 出库 移库、封仓 门店欠货能要点 1. 日常补货&#xff1a;分拣仓位商品小于当前商品在该位置的补货下限的时候&#xff1b;生成对此进行补货任务&#xff1b;补货完成后确认任务&#xff0c;系统变更库存…

win10使用mingw安装OpenCV4.8

1. cmake安装 下载链接如下https://github.com/Kitware/CMake/releases/download/v3.27.7/cmake-3.27.7-windows-x86_64.zip 解压后放到指定目录后&#xff0c;添加bin目录到环境变量即可。 2. mingw安装 下载链接如下(下图的x86_64-posix-sjlj)&#xff1a; Download x86_…

DevChat:提升编程效率的AI编程助手

一、前言 1、当前开发的痛点&#x1f616; 在软件开发过程中&#xff0c;开发者经常需要编写复杂的代码&#xff0c;如数据结构、算法、网络通信等&#xff0c;这些都需要耗费大量的时间和精力。同时&#xff0c;不同的编程语言和框架也会给开发者带来许多不便&#xff0c;例如…

Hadoop入门——数据分析基本步骤

文章目录 1.概述2.分析步骤2.1第一步 明确分析目的和思路2.2第二步 数据收集2.3第三步 数据处理2.4第四步 数据分析2.5第五步 数据展现2.6第六步 报告撰写 3.总结 1.概述 2.分析步骤 2.1第一步 明确分析目的和思路 2.2第二步 数据收集 2.3第三步 数据处理 2.4第四步 数据分析 …

C语言每日一题(28) 反转链表

牛客网 BM1 反转链表 题目描述 描述 给定一个单链表的头结点pHead(该头节点是有值的&#xff0c;比如在下图&#xff0c;它的val是1)&#xff0c;长度为n&#xff0c;反转该链表后&#xff0c;返回新链表的表头。 数据范围&#xff1a; 0≤n≤1000 要求&#xff1a;空间复…

数据分析实战 | SVM算法——病例自动诊断分析

目录 一、数据分析及对象 二、目的及分析任务 三、方法及工具 四、数据读入 五、数据理解 六、数据准备 七、模型训练 八、模型应用及评价 一、数据分析及对象 CSV文件——“bc_data.csv” 数据集链接&#xff1a;https://download.csdn.net/download/m0_70452407/88…

【C++】智能指针(一)

这篇文章介绍下C的智能指针&#xff0c;当然&#xff0c;可能没有你想的那么智能。 为什么需要智能指针1 void remodel(string& str) {string* ps new string(str);str *ps;return; }这里不讨论这个函数有没有意义&#xff0c;在这段代码中&#xff0c;很明显&#xff…

arduino 简易智能花盆

编辑器&#xff1a;arduino IDE 主板&#xff1a;arduino uno 传感器&#xff1a; 0.96寸的OLED屏&#xff08;四脚&#xff09; 声音模块 土壤温湿度模块 DS18B20温度模块&#xff08;这里用到防水的&#xff09; 光敏电阻模块&#xff08;买成三脚的了只能显示高低&#x…

state 和 props 有什么区别?

一、state 一个组件的显示形态可以由数据状态和外部参数所决定&#xff0c;而数据状态就是 state&#xff0c;一般在 constructor 中初始化 当需要修改里面的值的状态需要通过调用 setState 来改变&#xff0c;从而达到更新组件内部数据的作用&#xff0c;并且重新调用组件 r…

Linux操作系统下对c语言程序的编译和执行过程gcc,编译链接过程

目录 1.gcc,g,gdb安装 2.gcc分布编译链接 3.编译链接过程 4.执行 4.1 如何执行 4.2 两步执行与一步执行 4.3 多文件的编译执行 1.gcc,g,gdb安装 命令行写gcc,g,gdb根据提示安装:sudo apt install gcc/g/gdb 2.gcc分布编译链接 (1)预编译: gcc -E main.c -o main.i (2)…

视频剪辑技巧:探索画中画视频剪辑,如何制作引人入胜的视觉效果

在视频制作领域&#xff0c;画中画视频剪辑是一种备受瞩目的技术&#xff0c;它可以将多个视频画面叠加在一起&#xff0c;形成一种独特的视觉效果。这种剪辑技巧可以让观众同时看到两个或多个视频片段&#xff0c;创造出一种引人入胜的视觉体验。在开始画中画视频剪辑之前&…

C++算法:矩阵中的最长递增路径

涉及知识点 拓扑排序 题目 给定一个 m x n 整数矩阵 matrix &#xff0c;找出其中 最长递增路径 的长度。 对于每个单元格&#xff0c;你可以往上&#xff0c;下&#xff0c;左&#xff0c;右四个方向移动。 你 不能 在 对角线 方向上移动或移动到 边界外&#xff08;即不允…

工商业微电网储能盈利方式研究笔记

1. 光储微电网 1.1. 关于光储微电网 光储微电网可以看成是一组由分布式光伏、储能装置、本地负荷组成的包括发、输、配、用管理系统在内的小型局域电网&#xff0c;并通过唯一的公共连接点接入大电网&#xff0c;既可以并网运行也可以独立运行。 发展分布式光储微电网的意义…

STM32 X-CUBE-AI:Pytorch模型部署全流程

文章目录 概要版本&#xff1a;参考资料STM32CUBEAI安装CUBEAI模型支持LSTM模型转换注意事项模型转换模型应用1 错误类型及代码2 模型创建和初始化3 获取输入输出数据变量4 获取模型前馈输出模型应用小结 小结 概要 STM32 CUBE MX扩展包&#xff1a;X-CUBE-AI部署流程&#xf…

ROS 多级tf坐标转换

题目 现有一移动机器人&#xff0c;该机器人的基坐标系为“base_link”&#xff0c;机器人包含3个子坐标系分别为“joint1”&#xff0c;“joint2”&#xff0c;“joint3”。 要求&#xff1a;利用多坐标转换&#xff0c;实现joint1下的坐标向joint2下的坐标转换&#xff0c;…

AMD64内存属性详解

本文参考文档为AMD64 Architecture Programmer’s Manual Volume 2: System Programming&#xff0c;版本号3.41&#xff0c;这不是对原英文文档的翻译&#xff0c;但是所有内容的排版都是根据原手册的排版来的&#xff0c;如有与官方文档冲突的内容&#xff0c;以官方文档为准…

[LeetCode]-622. 设计循环队列

目录 662. 设计循环队列 题目 思路 代码 662. 设计循环队列 622. 设计循环队列 - 力扣&#xff08;LeetCode&#xff09;https://leetcode.cn/problems/design-circular-queue/ 题目 设计你的循环队列实现。 循环队列是一种线性数据结构&#xff0c;其操作表现基于 FIFO&…

推荐几个宝藏app

立冬后&#xff0c;真尼玛冷&#xff0c;哎&#xff01;记得多穿点衣服呀&#xff0c;老铁们&#xff01;&#xff01; GKD 去广告神器 下载网址&#xff1a;https://github.com/gkd-kit/gkd 特性&#xff1a; 它不仅支持跳过开屏广告&#xff0c;还支持跳过弹窗广告等&#xf…