内存学习——堆(heap)

news2024/11/18 21:48:17

目录

    • 一、概念
    • 二、自定义malloc函数
    • 三、Debug运行
    • 四、heap_4简单分析
      • 4.1 heap管理链表结构体
      • 4.2 堆初始化
      • 4.3 malloc使用
      • 4.4 free使用

一、概念

内存分为堆和栈两部分:

栈(Stack)是一种后进先出(LIFO)的数据结构,用于存储函数的调用栈、内存的分配操作、表达式求值的临时变量以及与程序中的控制流相关的数据。每当程序执行函数调用、变量声明或其他类型的操作时,都会在栈中添加一个栈帧(Stack Frame),用于存储函数的执行环境。

栈内存:主要存放函数地址、函数参数、局部变量等,空间较小,远小于堆内存,所以常有栈溢出错误。它由系统自动申请和回收,只由单线程使用,访问速度快,是连续内存,集中在内存块附近。

堆(Heap)则是用于分配程序中动态数据结构的内存空间,它的生命周期不由程序的函数调用栈管理,因此堆空间通常会被程序员直接管理。堆内存通常是一个可以被看做是一棵树的数组对象,它满足堆的性质:堆中某个节点的值总是不大于或不小于其父节点的值。堆空间为程序提供了极为灵活的空间分配和管理手段,既可以手动管理,也可以交由垃圾回收机制自动管理。

堆内存:主要存放new出来的对象和malloc申请的空间,空间大。它由程序分配,使用new或malloc申请,使用free或delete释放。堆内存中的实体数据地址都存储在栈变量中(即引用),以便能够高速访问。

总的来说,堆和栈在程序中都扮演着重要的角色。栈是一种高效的内存结构,用于存放基础数据类型和引用类型的变量,大大简化内存的管理,提高了程序的执行效率。堆空间则为程序提供了极为灵活的空间分配和管理手段。

被管理的内存称为堆,未被管理的内存称为栈:

  • 堆, heap,就是一块空闲的内存,需要提供管理函数
    • malloc:从堆里划出一块空间给程序使用
    • free:用完后,再把它标记为"空闲"的,可以再次使用
  • 栈, stack,函数调用时局部变量保存在栈中,当前程序的环境也是保存在栈中
    • 可以从堆中分配一块空间用作栈

在这里插入图片描述

二、自定义malloc函数

代码:

char heap_buf[1024]; //自定义1024字节内存的数组,模拟堆
int pos = 0; //指向堆数组可用空间的首地址

void *my_malloc(int size) //自定义malloc函数
{
	int old_pos = pos; //记录开辟空间的首地址
	pos += size; //malloc的空间大小
	return &heap_buf[old_pos]; //返回开辟空间的首地址
}

void my_free(void *buf) //可用自定义malloc函数,但是无法自定义free函数,后面分析原因
{
	/* err */
}


int main(void)
{
	char ch = 65; // char ch = 'A';
	int i;
	char *buf = my_malloc(100); //使用自定义的malloc函数在自定义堆数组中开辟100字节空间
		
	for (i = 0; i < 26; i++)
		buf[i] = 'A' + i; //在新开辟的空间中依次填入ABC……XYZ
	
	return 0;
}

三、Debug运行

1、查看heap_buf的首地址

在这里插入图片描述

2、查看malloc的buf首地址与heap_buf的首地址相同

在这里插入图片描述

3、多运行几次,ABCDE字母填入buf空间里,在heap_buf中也可以看到ABCDE

在这里插入图片描述

四、heap_4简单分析

4.1 heap管理链表结构体

对堆的管理意味着需要有一个链表结构体对空闲的堆空间和已使用的堆空间进行管理。

参考Heap_4的堆管理链表结构体:

typedef unsigned long size_t;

/* Define the linked list structure.  This is used to link free blocks in order
 * of their memory address. */
typedef struct A_BLOCK_LINK
{
    struct A_BLOCK_LINK * pxNextFreeBlock; /*<< The next free block in the list. */
    size_t xBlockSize;                     /*<< The size of the free block. */
} BlockLink_t;

链表结构体包含2部分:

  • 下一个空闲块
  • 当前块的size

4.2 堆初始化

当第一次使用malloc时,会对Heap进行初始化:

static void prvHeapInit( void ) /* PRIVILEGED_FUNCTION */
{
    BlockLink_t * pxFirstFreeBlock;
    uint8_t * pucAlignedHeap;
    portPOINTER_SIZE_TYPE uxAddress;
    size_t xTotalHeapSize = configTOTAL_HEAP_SIZE;

    /* 确保堆从正确对齐的边界开始。 */
    uxAddress = ( portPOINTER_SIZE_TYPE ) ucHeap;

    if( ( uxAddress & portBYTE_ALIGNMENT_MASK ) != 0 )
    {
        uxAddress += ( portBYTE_ALIGNMENT - 1 );
        uxAddress &= ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK );
        xTotalHeapSize -= uxAddress - ( portPOINTER_SIZE_TYPE ) ucHeap;
    }

    pucAlignedHeap = ( uint8_t * ) uxAddress;

    /* xStart用于保存指向空闲块列表中第一项的指针。void强制转换用于防止编译器警告。 */
    xStart.pxNextFreeBlock = ( void * ) pucAlignedHeap;
    xStart.xBlockSize = ( size_t ) 0;

    /* pxEnd用于标记空闲块列表的末尾,并插入到堆空间的末尾。 */
    uxAddress = ( ( portPOINTER_SIZE_TYPE ) pucAlignedHeap ) + xTotalHeapSize;
    uxAddress -= xHeapStructSize;
    uxAddress &= ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK );
    pxEnd = ( BlockLink_t * ) uxAddress;
    pxEnd->xBlockSize = 0;
    pxEnd->pxNextFreeBlock = NULL;

    /* 首先,有一个空闲块,它的大小是占用整个堆空间,减去pxEnd占用的空间。 */
    pxFirstFreeBlock = ( BlockLink_t * ) pucAlignedHeap;
    pxFirstFreeBlock->xBlockSize = ( size_t ) ( uxAddress - ( portPOINTER_SIZE_TYPE ) pxFirstFreeBlock );
    pxFirstFreeBlock->pxNextFreeBlock = pxEnd;

    /* 只有一个块存在——它覆盖了整个可用的堆空间。 */
    xMinimumEverFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;
    xFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;
}

通过代码可以看到,对Heap初始化时会创建一个xStart和*pxEnd :

//heap_4中定义:
/* 创建两个列表链接来标记列表的开始和结束。 */
PRIVILEGED_DATA static BlockLink_t xStart;
PRIVILEGED_DATA static BlockLink_t * pxEnd = NULL;
//heap_2中定义:
/* 创建两个列表链接来标记列表的开始和结束。 */
PRIVILEGED_DATA static BlockLink_t xStart, xEnd;

heap_2中的xStart, xEnd,用2个结构体代表头尾,不占用堆空间;

heap_4中是xStart, *pxEnd = NULL,pxEnd与heap_2中xEnd不一样,pxEnd占用了堆空间。这样做能够适配heap_5,heap_5支持多块不连续的内存合并,使用pxEnd链表,可以直接将pxEnd指向下一个非连续堆。

4.3 malloc使用

假设malloc开辟了3块空间,第一块空间从heap头开始100字节,第二块空间接着第一块空间100字节,第三块空间接着第二块50字节,最后释放了第二块,则示意图如下:

在这里插入图片描述

对照代码分析:

void * pvPortMalloc( size_t xWantedSize )
{
    BlockLink_t * pxBlock;
    BlockLink_t * pxPreviousBlock;
    BlockLink_t * pxNewBlockLink;
    void * pvReturn = NULL;
    size_t xAdditionalRequiredSize;

    vTaskSuspendAll();
    {
        /* 如果这是第一次调用malloc,那么堆将需要初始化来设置空闲块列表。 */
        if( pxEnd == NULL )
        {
            prvHeapInit();
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }

        if( xWantedSize > 0 )
        {
            /* 所需的大小必须增加,这样除了请求的字节数之外,它还可以包含BlockLink_t结构。为了对齐,可能还需要一些额外的增量。 */
            xAdditionalRequiredSize = xHeapStructSize + portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK );

            if( heapADD_WILL_OVERFLOW( xWantedSize, xAdditionalRequiredSize ) == 0 )
            {
                xWantedSize += xAdditionalRequiredSize;
            }
            else
            {
                xWantedSize = 0;
            }
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }

        /* 检查我们尝试分配的块大小是否太大,以至于设置了顶部位。BlockLink_t结构的块大小成员的顶部位用于确定谁拥有该块-应用程序或内核,因此它必须是空闲的。 */
        if( heapBLOCK_SIZE_IS_VALID( xWantedSize ) != 0 )
        {
            if( ( xWantedSize > 0 ) && ( xWantedSize <= xFreeBytesRemaining ) )
            {
                /* 从起始(最低地址)块开始遍历列表,直到找到一个足够大的块。 */
                pxPreviousBlock = &xStart;
                pxBlock = xStart.pxNextFreeBlock;

                while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) )
                {
                    pxPreviousBlock = pxBlock;
                    pxBlock = pxBlock->pxNextFreeBlock;
                }

                /* 如果到达终点标记,则没有找到足够大小的块。 */
                if( pxBlock != pxEnd )
                {
                    /* 返回指向的内存空间-在开始时跳过BlockLink_t结构。 */
                    pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) + xHeapStructSize );

                    /* 该块正在返回使用,因此必须从空闲块列表中删除。 */
                    pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;

                    /* 如果块大于要求,则可以将其分成两个。 */
                    if( ( pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE )
                    {
                        /* 这个块将被分成两部分。根据请求的字节数创建一个新的块。void强制转换用于防止编译器发出字节对齐警告。 */
                        pxNewBlockLink = ( void * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize );
                        configASSERT( ( ( ( size_t ) pxNewBlockLink ) & portBYTE_ALIGNMENT_MASK ) == 0 );

                        /* 计算从单个块分割出的两个块的大小。 */
                        pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;
                        pxBlock->xBlockSize = xWantedSize;

                        /* 将新块插入空闲块列表中。 */
                        prvInsertBlockIntoFreeList( pxNewBlockLink );
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }

                    xFreeBytesRemaining -= pxBlock->xBlockSize;

                    if( xFreeBytesRemaining < xMinimumEverFreeBytesRemaining )
                    {
                        xMinimumEverFreeBytesRemaining = xFreeBytesRemaining;
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }

                    /* 块正在被返回——它被应用程序分配和拥有,没有“下一个”块。 */
                    heapALLOCATE_BLOCK( pxBlock );
                    pxBlock->pxNextFreeBlock = NULL;
                    xNumberOfSuccessfulAllocations++;
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }

        traceMALLOC( pvReturn, xWantedSize );
    }
    ( void ) xTaskResumeAll();

    #if ( configUSE_MALLOC_FAILED_HOOK == 1 )
    {
        if( pvReturn == NULL )
        {
            vApplicationMallocFailedHook();
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }
    }
    #endif /* if ( configUSE_MALLOC_FAILED_HOOK == 1 ) */

    configASSERT( ( ( ( size_t ) pvReturn ) & ( size_t ) portBYTE_ALIGNMENT_MASK ) == 0 );
    return pvReturn;
}

4.4 free使用

假设使用free释放内存时,输入的参数pv是Heap中实际使用地址的首地址,但是malloc的空间除了实际使用的部分外还有一个头部的链表结构体,因此需要将pv地址向前移动xHeapStructSize,把malloc开辟空间的头部链表结构体与实际使用部分一同释放并插入到空闲Heap中:

void vPortFree( void * pv )
{
    uint8_t * puc = ( uint8_t * ) pv;
    BlockLink_t * pxLink;

    if( pv != NULL )
    {
        /* 被释放的内存在其前面有一个BlockLink_t结构。 */
        puc -= xHeapStructSize;

        /* 这种类型转换是为了防止编译器发出警告。 */
        pxLink = ( void * ) puc;

        configASSERT( heapBLOCK_IS_ALLOCATED( pxLink ) != 0 );
        configASSERT( pxLink->pxNextFreeBlock == NULL );

        if( heapBLOCK_IS_ALLOCATED( pxLink ) != 0 )
        {
            if( pxLink->pxNextFreeBlock == NULL )
            {
                /* 该块被返回到堆中——它不再被分配。 */
                heapFREE_BLOCK( pxLink );
                #if ( configHEAP_CLEAR_MEMORY_ON_FREE == 1 )
                {
                    ( void ) memset( puc + xHeapStructSize, 0, pxLink->xBlockSize - xHeapStructSize );
                }
                #endif

                vTaskSuspendAll();
                {
                    /* 将此块添加到空闲块列表中。 */
                    xFreeBytesRemaining += pxLink->xBlockSize;
                    traceFREE( pv, pxLink->xBlockSize );
                    prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) );
                    xNumberOfSuccessfulFrees++;
                }
                ( void ) xTaskResumeAll();
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }
    }
}

其中heap4中把相邻的空闲内存合并为一个更大的空闲内存是在prvInsertBlockIntoFreeList函数中实现的:

static void prvInsertBlockIntoFreeList( BlockLink_t * pxBlockToInsert ) /* PRIVILEGED_FUNCTION */
{
    BlockLink_t * pxIterator;
    uint8_t * puc;

    /* 遍历列表,直到发现一个空闲块的下一个空闲块指针地址比插入的块的地址高,即得到比插入块地址小的相邻空闲块。
    问题:假设释放heap中地址最高的一段时,所有的空闲块地址都比要插入的块的地址低,此时该如何执行?*/
    for( pxIterator = &xStart; pxIterator->pxNextFreeBlock < pxBlockToInsert; pxIterator = pxIterator->pxNextFreeBlock )
    {
        /* 这里什么都不用做,只需要迭代到正确的位置。 */
    }

    /* 插入的块和比其地址低的相邻块是否构成连续的内存块? 体现相邻空闲空间插入思想*/
    puc = ( uint8_t * ) pxIterator;

    if( ( puc + pxIterator->xBlockSize ) == ( uint8_t * ) pxBlockToInsert )
    {
        pxIterator->xBlockSize += pxBlockToInsert->xBlockSize;
        pxBlockToInsert = pxIterator;
    }
    else
    {
        mtCOVERAGE_TEST_MARKER();
    }

    /* 插入的块和比其地址高的相邻块是否构成一个连续的内存块? 体现相邻空闲空间插入思想*/
    puc = ( uint8_t * ) pxBlockToInsert;

    if( ( puc + pxBlockToInsert->xBlockSize ) == ( uint8_t * ) pxIterator->pxNextFreeBlock )
    {
        if( pxIterator->pxNextFreeBlock != pxEnd )
        {
            /* 把两个块组成一个大的块。 */
            pxBlockToInsert->xBlockSize += pxIterator->pxNextFreeBlock->xBlockSize;
            pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock->pxNextFreeBlock;
        }
        else
        {
            pxBlockToInsert->pxNextFreeBlock = pxEnd;
        }
    }
    else
    {
        pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock;
    }

    /* 如果被插入的块和比其地址低的相邻块不连续,而和比其地址高的相邻块连续,因此和比其地址低的相邻块的pxNextFreeBlock指针应该由原本指向和比其地址高的相邻块变为指向被插入的块 */
    if( pxIterator != pxBlockToInsert )
    {
        pxIterator->pxNextFreeBlock = pxBlockToInsert;
    }
    else
    {
        mtCOVERAGE_TEST_MARKER();
    }
}

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

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

相关文章

STM32-GPIO编程

一、GPIO 1.1 基本概念 GPIO&#xff08;General-purpose input/output&#xff09;通用输入输出接口 --GP 通用 --I input输入 --o output输出 通用输入输出接口GPIO是嵌入式系统、单片机开发过程中最常用的接口&#xff0c;用户可以通过编程灵活的对接口进行控制&#xff0c;…

MATLAB离线附加功能包下载与安装教程

MATLAB离线附加功能包下载与安装教程 本文介绍如何下载与安装MATLAB离线附加功能包,便于大家更加高效的使用MATLAB。 目录 MATLAB离线附加功能包下载与安装教程一、下载1. 获取MATLAB试用版账号2. 使用MATLAB Online搜索所需要的资源包3. 下载所需要的资源包二、安装由于不是…

【QED】井字棋

目录 题目背景题目描述输入格式输出格式测试样例 思路核心代码 题目背景 井字棋&#xff0c;英文名叫Tic-Tac-Toe&#xff0c;是一种在 3 3 3 \times 3 33格子上进行的连珠游戏&#xff0c;和五子棋类似。游戏时&#xff0c;由分别代表O和X的两名玩家轮流在棋盘格子里留下棋子…

uni-app 设置当前page界面进入直接变为横屏模式

首先 我们打开项目的 manifest.json 在左侧导航栏中找到 源码视图 然后找到 app-plus 配置 在下面加上 "orientation": [//竖屏正方向"portrait-primary",//竖屏反方向"portrait-secondary",//横屏正方向"landscape-primary",//横屏…

LIMoE:使用MoE学习多个模态

文章链接&#xff1a;Multimodal Contrastive Learning with LIMoE: the Language-Image Mixture of Experts 发表期刊&#xff08;会议&#xff09;: NeurIPS 2022 目录 1.背景介绍稀疏模型 2.内容摘要Sparse Mixture-of-Experts ModelsContrastive LearningExperiment Analy…

JVM类加载器ClassLoader的源码分析

1、ClassLoader与现有类加载器的关系 ClassLoader与现有类加载器的关系&#xff1a; ClassLoader是一个抽象类。如果我们给定了一个类的二进制名称&#xff0c;类加载器应尝试去定位或生成构成定义类的数据。一种典型的策略是将给定的二进制名称转换为文件名&#xff0c;然后去…

VUEX使用总结

1、Store 使用 文件内容大概就是这三个。通俗来讲actions负责向后端获取数据的&#xff0c;内部执行异步操作分发 Action&#xff0c;调用commit提交一个 mutation。 mutations通过Action提交commit的数据进行提交荷载&#xff0c;使state有数据。 vuex的数据是共享的&#xf…

(三)五种最新算法(SWO、COA、LSO、GRO、LO)求解无人机路径规划MATLAB

一、五种算法&#xff08;SWO、COA、LSO、GRO、LO&#xff09;简介 1、蜘蛛蜂优化算法SWO 蜘蛛蜂优化算法&#xff08;Spider wasp optimizer&#xff0c;SWO&#xff09;由Mohamed Abdel-Basset等人于2023年提出&#xff0c;该算法模型雌性蜘蛛蜂的狩猎、筑巢和交配行为&…

Docker 安装Apache Superset 并实现汉化和快速入门

什么是Apache Superset Apache Superset是一个现代化的企业级商业智能Web应用程序。Apache Superset 支持用户的各种数据类型可视化和数据分析&#xff0c;支持简单图饼图到复杂的地理空间图表。Apache Superset 是一个轻量级、简单化、直观化、可配置的BI 框架。 Docker 安…

Mongodb 添加索引 优化记录

因 每晚12点20分定时任务做数据统计&#xff0c;mongodb 50万条数据开始&#xff0c;每天晚上CPU报警&#xff1a;CPU>95&#xff0c;并耗时3分钟以上. 2023-12-08 00:20:00.023 [Thread-95] INFO c.q.i.q.jobhandler.dataMongoDBXxlJob - 定时生成记录开始 ………… …………

每日一道c语言

任务描述 题目描述:输入10个互不相同的整数并保存在数组中&#xff0c;找到该最大元素并删除它&#xff0c;输出删除后的数组 相关知识&#xff08;略&#xff09; 编程要求 请仔细阅读右侧代码&#xff0c;结合相关知识&#xff0c;在Begin-End区域内进行代码补充&#xf…

二叉树查找值为x的结点(C语言)

目录 前言 查找值为x的结点 返回值为指针 返回值为布尔类型 整体代码 前言 在二叉树结点个数、叶子结点个数、树的高度、第k层结点个数的计算&#xff08;C语言&#xff09;中&#xff0c;我们解决了关于二叉树的部分问题&#xff0c;但是还有一个问题我们放在本篇解决。 …

04_W5500_TCP_Server

上一节我们完成了TCP_Client实验&#xff0c;这节使用W5500作为服务端与TCP客户端进行通信。 目录 1.W5500服务端要做的&#xff1a; 2.代码分析&#xff1a; 3.测试&#xff1a; 1.W5500服务端要做的&#xff1a; 服务端只需要打开socket&#xff0c;然后监听端口即可。 2…

二叉树结点个数、叶子结点个数、树的高度、第k层结点个数的计算(C语言)

目录 前言 分治算法 模拟二叉树代码 结点个数计算 错误方法 不便利方法 基于分治思想的方法 叶子结点个数 树的高度 第k层结点的个数 前言 在链式二叉树的前序、中序、后续遍历中我们模拟了一棵二叉树&#xff0c;并实现了它的前、中、后序遍历&#xff0c;现在我们来…

Leetcode—231.2的幂【简单】

2023每日刷题&#xff08;五十四&#xff09; Leetcode—231.2的幂 实现代码 class Solution { public:bool isPowerOfTwo(int n) {if(n < 0) {return false;}long long ans 1;while(ans < n) {ans * 2;}if(ans n) {return true;}return false;} };运行结果 之后我会…

Echarts饼图中显示百分比

开发中遇到一个需求&#xff0c;要在饼图上显示数据百分比&#xff0c;下图&#xff1a; 查了echarts 文档&#xff0c;并不能通过简单的配置来实现&#xff0c;原因如下&#xff1a;在单个serie的label中&#xff0c;只能设置一个label&#xff0c;位置可以选择在饼图内部inne…

坚鹏:兴业银行EAST5.0政策解读及数据质量提升方案培训

兴业银行股份有限公司&#xff08;简称“兴业银行”&#xff09;成立于1988年8月&#xff0c;2022年总资产9.27万亿元&#xff0c;是经国务院、中国人民银行批准成立的首批股份制商业银行之一&#xff0c;总行设在福州市。现已发展成为横跨境内外&#xff0c;线上线下结合&…

【论文精读】REACT: SYNERGIZING REASONING AND ACTING IN LANGUAGE MODELS

REACT: SYNERGIZING REASONING AND ACTING IN LANGUAGE MODELS 前言ABSTRACT1 INTRODUCTION2 REACT: SYNERGIZING REASONING ACTING3 KNOWLEDGE-INTENSIVE REASONING TASKS3.1 SETUP3.2 METHODS3.3 RESULTS AND OBSERVATIONS 4 DECISION MAKING TASKS5 RELATED WORK6 CONCLUSI…

m_map导入本地地形数据

m_map绘制地形图时&#xff0c;虽然自带有1的地形图以及从NOAA下载的1分的地形图&#xff08;详见&#xff1a;Matlab下地形图绘图包m_map安装与使用&#xff09;&#xff0c;但有时需要对地形图分辨率的要求更高&#xff0c;便无法满足。 此时&#xff0c;需要导入本地地形数…

代理IP和网络加速器的区别有哪些

随着互联网的普及&#xff0c;越来越多的人开始使用网络加速器来提高网络速度。然而&#xff0c;很多人并不清楚代理IP和网络加速器之间的区别。本文将详细介绍两者的概念及区别。 一、代理IP 代理IP是一种通过代理服务器进行网络连接的方式。在使用流冠代理IP时&#xff0c;用…