實用演算法系列之RT-Thread鏈表堆管理器

来源:https://www.cnblogs.com/embInn/archive/2020/05/25/12953799.html
-Advertisement-
Play Games

[導讀] 前文描述了棧的基本概念,本文來聊聊堆是怎麼會事兒。RT Thread 在社區廣受歡迎,閱讀了其內核代碼,實現了堆的管理,代碼設計很清晰,可讀性很好。故一方面瞭解RT Thread內核實現,一方面可以弄清楚其堆的內部實現。將學習體會記錄分享,希望對於堆的理解及實現有一個更深入的認知。 註,文 ...


[導讀] 前文描述了棧的基本概念,本文來聊聊堆是怎麼會事兒。RT-Thread 在社區廣受歡迎,閱讀了其內核代碼,實現了堆的管理,代碼設計很清晰,可讀性很好。故一方面瞭解RT-Thread內核實現,一方面可以弄清楚其堆的內部實現。將學習體會記錄分享,希望對於堆的理解及實現有一個更深入的認知。

註,文中代碼分析基於rt-thread-v4.0.2 版本。

什麼是堆?

C語言堆是由malloc(),calloc(),realloc()等函數動態獲取記憶體的一種機制。使用完成後,由程式員調用free()等函數進行釋放。使用時,需要包含stdlib.h頭文件。

C++預言的堆管理則是使用new操作符向堆管理器申請動態記憶體分配,使用delete操作符將使用完畢記憶體的釋放給堆管理器。

註:本文只描述C的堆管理器實現相關內容。

以C語言為例,將上面的描述,翻譯成一個圖:

要動態管理一片記憶體,且需要動態分配釋放,這樣一個需求。很顯然C語言需要將動態記憶體區抽象描述起來並實現動態管理。事實上,C語言中堆管理器其本質是利用數據結構將堆區抽象描述,所需要描述的方面:

  • 可用於分配的記憶體
  • 正在使用的記憶體塊
  • 釋放掉的記憶體塊

再利用相應演算法對於這類數據結構對象進行動態管理而實現的堆管理器。

經常看到各種演算法書很多只講演算法原理,而不講應用實例,往往體會不深。私以為可以做些改善。學而不能致用,何必費力去學。所以不是晦澀難懂的演算法無用,而是沒有去真正結合應用。可以再進一步想,如果演算法沒有應用場景,也一定會在技術發展的歷程中逐漸被世人遺忘。所以建議學習閱讀演算法書籍時,找些實例來看看,一定會加深對演算法的理解領悟。這是比較重要的題外話,送給大家以共勉。

所以從本質上講,堆管理器就是數據結構+演算法實現的動態記憶體管理器,管理記憶體的動態分配以及釋放。

為什麼要堆?

C編程語言對記憶體管理方式有靜態,自動或動態三種方式。 靜態記憶體分配的變數通常與程式的可執行代碼一起分配在主存儲器中,併在程式的整個生命周期內有效。 自動分配記憶體的變數在棧上分配,並隨著函數的調用和返回而申請或釋放。 對於靜態分配記憶體和自動分配記憶體的生命周期,分配的大小必須是編譯時常量(可變長度自動數組[5]除外)。 如果所需的記憶體大小直到運行時才知道(例如,如果要從用戶或磁碟文件中讀取任意大小的數據),則使用固定大小的數據對象則滿足不了要求了。試想,即便假定都知道要多大記憶體,如在windows/Linux下有那麼多應用程式,每個應用程式載入時都將運行中所需的記憶體採樣靜態分配策略,則如多個程式運行記憶體將很快耗盡。

分配的記憶體的生命周期也可能引起關註。 靜態或自動分配都不能滿足所有情況。 自動分配記憶體不能在多個函數調用之間保留,而靜態數據在程式的整個生命周期中必然保留,無論是否真正需要(所以都採用這樣的策略必然造成浪費)。 在許多情況下,程式員在管理分配的記憶體的生命周期具有更多的靈活性。

通過使用動態記憶體分配則避免了這些限制/缺點,在動態記憶體分配中,更明確(但更靈活)地管理記憶體,通常是通過從免費存儲區(非正式地稱為“堆”)中分配記憶體(為此目的而構造的記憶體區域)進行分配的。 在C語言中,庫函數malloc用於在堆上分配一個記憶體塊。 程式通過malloc返回的指針訪問該記憶體塊。 當不再需要記憶體時,會將指針傳遞給free,從而釋放記憶體,以便可以將其用於其他目的。

誰實現堆

如果一問道這個問題,馬上會說C編譯器。不錯C編譯器實現了堆管理器,而事實上並非編譯器在編譯的過程中實現動態記憶體管理器,而是C編譯器所實現的C庫實現了堆管理器,比如ANSI C,VC, IAR C編譯器,GNU C等其實都需要一些C庫的支持,那麼這些庫的內部就隱藏了這麼一個堆管理器。眼見為實吧,還是以IAR ARM 8.40.1 為例,其堆管理器就實現在:

.\IAR Systems\Embedded Workbench 8.3\arm\src\lib\dlib\heap

一看有這麼多的源碼,那麼對於應用開發而言,有哪些選項需要進行配置呢?

支持四個選項:

  • Automatic:
    • 如果您的應用程式中有對堆記憶體分配常式的調用,但沒有對堆釋放常式的調用,則鏈接程式將自動選擇無空閑堆。
    • 如果您的應用程式中有對堆記憶體分配常式的調用,則鏈接程式會自動選擇高級堆。
    • 例如,如果在庫中調用了堆記憶體分配常式,則鏈接程式會自動選擇基本堆。
  • Advanced heap:高級堆(--advanced_heap)為廣泛使用該堆的應用程式提供有效的記憶體管理。 特別是,重覆分配和釋放記憶體的應用程式可能會在空間和時間上獲得較少的開銷。 高級堆的代碼明顯大於基本堆的代碼。
  • Basic heap: 基本堆(--basic_heap)是一個簡單的堆分配器,適用於不經常使用堆的應用程式。 特別是,它可以用於僅分配堆記憶體而從不釋放堆記憶體的應用程式中。 基本堆並不是特別快,並且在反覆釋放記憶體的應用程式中使用它很可能導致不必要的堆碎片化。 基本堆的代碼遠小於高級堆的大小。
  • No-free heap:無可用堆(--no_free_heap)使用此選項可以使用最小的堆實現。 因為此堆不支持釋放或重新分配,所以它僅適用於在啟動階段為各種緩衝區分配堆記憶體的應用程式,以及永不釋放記憶體的應用程式。

但是如果認為僅僅標準C庫負責實現堆管理器,則這種理解並不全面。回到事物的本質,堆管理器是利用數據結構及演算法動態管理一片記憶體的分配與釋放。那麼有這樣需求的地方,都可能需要實現一個堆管理器。

堆管理器的實現很大程度取決於操作系統以及硬體體系架構。大體上需要實現堆記憶體管理器的有兩大類:

  • 應用程式,應用程式需要堆記憶體管理器,是顯而易見的。比如常見的windows/Linux下的應用程式,都需要堆記憶體管理器。而上述的cortex M或者其他單片機程式使用C/C++編程時都需要堆記憶體管理器。
  • 操作系統內核,操作系統內核需要像應用程式一樣分配記憶體。 但是,內核中malloc的實現通常與C庫使用的實現有很大不同。 例如,記憶體緩衝區可能需要符合DMA施加的特殊限制,或者可能從中斷上下文中調用記憶體分配功能。這需要與操作系統內核的虛擬記憶體子系統緊密集成的malloc實現。比如Linux內核就需要實現內核版本的堆管理器,對外提供kmalloc/vmalloc申請記憶體,kfree/vfree用於釋放記憶體。

怎麼實現堆

對於RT-Thread的內核而言,也實現了一個內核堆管理器,這裡就來梳理一下RT-Thread內核版本的小堆管理器的實現,同時來瞭解一下鏈表數據結構及演算法操作的實例應用。

其堆管理器實現位於.\rt-thread-v4.0.2\rt-thread\src下mem.c,memheap.c以及mempool.c。

關鍵數據結構

其堆管理器主要的數據結構為heap_mem。

  • heap_mem

堆管理器初始化

堆管理器的初始化入口在mem.c,函數為:

void rt_system_heap_init(void *begin_addr, void *end_addr)
{
    struct heap_mem *mem;
    /*按4位元組對齊轉換地址*/
    /*如0x2000 0001~0x2000 0003,轉後為0x2000 0004*/
    rt_ubase_t begin_align = RT_ALIGN((rt_ubase_t)begin_addr, RT_ALIGN_SIZE);
    /*如0x3000 0001~0x3000 0003,轉後為0x3000 0000*/
    rt_ubase_t end_align   = RT_ALIGN_DOWN((rt_ubase_t)end_addr, RT_ALIGN_SIZE);
    
    /*調試信息,函數不可用於中斷內部*/
    RT_DEBUG_NOT_IN_INTERRUPT;

    /* 分配地址範圍至少能存儲兩個heap_mem */
    if ((end_align > (2 * SIZEOF_STRUCT_MEM)) &&
        ((end_align - 2 * SIZEOF_STRUCT_MEM) >= begin_align))
    {
        /* 計算可用堆區,4位元組對齊 */
        mem_size_aligned = end_align - begin_align - 2 * SIZEOF_STRUCT_MEM;
    }
    else
    {
        rt_kprintf("mem init, error begin address 0x%x, and end address 0x%x\n",
                   (rt_ubase_t)begin_addr, (rt_ubase_t)end_addr);

        return;
    }

    /* heap_ptr指向堆區起始地址 */
    heap_ptr = (rt_uint8_t *)begin_align;

    RT_DEBUG_LOG(RT_DEBUG_MEM, ("mem init, heap begin address 0x%x, size %d\n",
                                (rt_ubase_t)heap_ptr, mem_size_aligned));

    /* 初始化堆起始描述符 */
    mem        = (struct heap_mem *)heap_ptr;
    mem->magic = HEAP_MAGIC;
    mem->next  = mem_size_aligned + SIZEOF_STRUCT_MEM;
    mem->prev  = 0;
    mem->used  = 0;
#ifdef RT_USING_MEMTRACE
    rt_mem_setname(mem, "INIT");
#endif

    /* 初始化堆結束描述符 */
    heap_end        = (struct heap_mem *)&heap_ptr[mem->next];
    heap_end->magic = HEAP_MAGIC;
    heap_end->used  = 1;
    heap_end->next  = mem_size_aligned + SIZEOF_STRUCT_MEM;
    heap_end->prev  = mem_size_aligned + SIZEOF_STRUCT_MEM;
#ifdef RT_USING_MEMTRACE
    rt_mem_setname(heap_end, "INIT");
#endif

    rt_sem_init(&heap_sem, "heap", 1, RT_IPC_FLAG_FIFO);

    /* 初始化釋放指針指向堆的開始 */
    lfree = (struct heap_mem *)heap_ptr;
}

傳入鏈接堆區的記憶體起始地址,以及結束地址。以STM32為例,傳入0x20000000--0x20018000,96k位元組

上述rt_system_heap_init( 0x20000000,0x20018000),主要做了下圖這麼一件事情。

將堆管理頭尾描述符進行了初始化,並指向對應的記憶體地址。用圖翻譯一下:

技巧點:

  • 利用類型強制轉換將記憶體數據轉換為struct heap_mem *。實現了靜態雙鏈表的創建
mem      = (struct heap_mem *)heap_ptr;
heap_end = (struct heap_mem *)&heap_ptr[mem->next];
  • 定義heap_mem沒有定義使用多少位元組為該塊的用戶數據位元組數,節約了記憶體。是一個比較好的處理方式。
  • 對齊方式可配置,RT_ALIGN_SIZE預設為4位元組。

向堆申請記憶體

用戶調用rt_malloc 用於申請分配動態記憶體。

void *rt_malloc(rt_size_t size)
{
    rt_size_t ptr, ptr2;
    struct heap_mem *mem, *mem2;

    if (size == 0)
        return RT_NULL;

    RT_DEBUG_NOT_IN_INTERRUPT;
    /*按四位元組對齊申請,如申請5位元組,則實際按8位元組申請*/
    if (size != RT_ALIGN(size, RT_ALIGN_SIZE))
        RT_DEBUG_LOG(RT_DEBUG_MEM, ("malloc size %d, but align to %d\n",
                                    size, RT_ALIGN(size, RT_ALIGN_SIZE)));
    else
        RT_DEBUG_LOG(RT_DEBUG_MEM, ("malloc size %d\n", size));

    /* 按四位元組對齊申請,如申請5位元組,則實際按8位元組申請 */
    size = RT_ALIGN(size, RT_ALIGN_SIZE);

    if (size > mem_size_aligned)
    {
        RT_DEBUG_LOG(RT_DEBUG_MEM, ("no memory\n"));
        return RT_NULL;
    }

    /* 每塊的長度必須至少為MIN_SIZE_ALIGNED=12 STM32*/
    if (size < MIN_SIZE_ALIGNED)
        size = MIN_SIZE_ALIGNED;

    /* 獲取堆保護信號量 */
    rt_sem_take(&heap_sem, RT_WAITING_FOREVER);

    for (ptr = (rt_uint8_t *)lfree - heap_ptr;
         ptr < mem_size_aligned - size;
         ptr = ((struct heap_mem *)&heap_ptr[ptr])->next)
    {
        mem = (struct heap_mem *)&heap_ptr[ptr];

        /*如果該塊未使用,且滿足大小要求*/
        if ((!mem->used) && (mem->next - (ptr + SIZEOF_STRUCT_MEM)) >= size)
        {
            /* mem沒有被使用,至少完美的配合是可能的:
             * mem->next - (ptr + SIZEOF_STRUCT_MEM) 計算出mem的“用戶數據大小” */
            if (mem->next - (ptr + SIZEOF_STRUCT_MEM) >=
                (size + SIZEOF_STRUCT_MEM + MIN_SIZE_ALIGNED))
            {
                /* (除了上面的,我們測試另一個結構heap_mem (SIZEOF_STRUCT_MEM)
                 * 是否包含至少MIN_SIZE_ALIGNED的數據也適合'mem'的'用戶數據空間')
                 * -> 分割大的塊,創建空的餘數,
                 * 餘數必須足夠大,以包含MIN_SIZE_ALIGNED大小數據:
                 * 如果mem->next - (ptr + (2*SIZEOF_STRUCT_MEM)) == size,
                 * struct heap_mem 會適合,在mem2及mem2->next沒有使用
                 */
                ptr2 = ptr + SIZEOF_STRUCT_MEM + size;

                /* create mem2 struct */
                mem2       = (struct heap_mem *)&heap_ptr[ptr2];
                mem2->magic = HEAP_MAGIC;
                mem2->used = 0;
                mem2->next = mem->next;
                mem2->prev = ptr;
#ifdef RT_USING_MEMTRACE
                rt_mem_setname(mem2, "    ");
#endif
                /*將ptr2插入mem及mem->next之間 */
                mem->next = ptr2;
                mem->used = 1;

                if (mem2->next != mem_size_aligned + SIZEOF_STRUCT_MEM)
                {
                    ((struct heap_mem *)&heap_ptr[mem2->next])->prev = ptr2;
                }
#ifdef RT_MEM_STATS
                used_mem += (size + SIZEOF_STRUCT_MEM);
                if (max_mem < used_mem)
                    max_mem = used_mem;
#endif
            }
            else
            {
                mem->used = 1;
#ifdef RT_MEM_STATS
                used_mem += mem->next - ((rt_uint8_t *)mem - heap_ptr);
                if (max_mem < used_mem)
                    max_mem = used_mem;
#endif
            }
            /* 設置塊幻數 */
            mem->magic = HEAP_MAGIC;
#ifdef RT_USING_MEMTRACE
            if (rt_thread_self())
                rt_mem_setname(mem, rt_thread_self()->name);
            else
                rt_mem_setname(mem, "NONE");
#endif

            if (mem == lfree)
            {
                /* 尋找下一個空閑塊並更新lfree指針*/
                while (lfree->used && lfree != heap_end)
                    lfree = (struct heap_mem *)&heap_ptr[lfree->next];

                RT_ASSERT(((lfree == heap_end) || (!lfree->used)));
            }

            rt_sem_release(&heap_sem);
            RT_ASSERT((rt_ubase_t)mem + SIZEOF_STRUCT_MEM + size <= (rt_ubase_t)heap_end);
            RT_ASSERT((rt_ubase_t)((rt_uint8_t *)mem + SIZEOF_STRUCT_MEM) % RT_ALIGN_SIZE == 0);
            RT_ASSERT((((rt_ubase_t)mem) & (RT_ALIGN_SIZE - 1)) == 0);

            RT_DEBUG_LOG(RT_DEBUG_MEM,
                         ("allocate memory at 0x%x, size: %d\n",
                          (rt_ubase_t)((rt_uint8_t *)mem + SIZEOF_STRUCT_MEM),
                          (rt_ubase_t)(mem->next - ((rt_uint8_t *)mem - heap_ptr))));

            RT_OBJECT_HOOK_CALL(rt_malloc_hook,
                                (((void *)((rt_uint8_t *)mem + SIZEOF_STRUCT_MEM)), size));

            /* 返回除mem結構之外的記憶體地址 */
            return (rt_uint8_t *)mem + SIZEOF_STRUCT_MEM;
        }
    }
    /* 釋放堆保護信號量 */
    rt_sem_release(&heap_sem);

    return RT_NULL;
}

其基本思路,從空閑塊鏈表開始檢索記憶體塊,如檢索到某塊空閑且滿足申請大小且其剩餘空間至少能存儲描述符,則滿足了申請要求,則將後續記憶體頭部生成描述,更新前後指針,標記幻數以及塊已被使用標記,將該塊插入鏈表。返回申請成功的記憶體地址。如果檢索不到,則返回空指針,表示申請失敗,堆目前沒有滿足要求的記憶體可供使用。實際上,上述代碼在運行時將堆記憶體區按照下述示意圖進行動態維護。

概括一下:

  • heap_ptr總是指向堆起始地址,heap_end總是指向最後一個塊,兩者配合可以實現邊界保護,在釋放記憶體時使用。
  • lfree 總是指向最地址最小的空閑塊,因此在動態申請記憶體時,總是從該塊進行檢索是否有滿足申請要求的記憶體塊可供使用。
  • used=1表示該塊被占用,非空閑。used=0表示該塊空閑。
  • magic 欄位幻數,起始就是一個特殊標記字,與used=0配合,用於檢測異常,試想一下如果僅僅用used=0判斷塊是空閑,則易出錯,或者需要加其他的輔助代碼,才能保證代碼的健壯性。
  • 動態記憶體管理申請比較慢,需要檢索鏈表,以及額外的記憶體開銷。
  • rt_realloc 及rt_calloc 不做分析了

釋放記憶體

釋放記憶體由rt_free實現:

void rt_free(void *rmem)
{
    struct heap_mem *mem;

    if (rmem == RT_NULL)
        return;

    RT_DEBUG_NOT_IN_INTERRUPT;

    RT_ASSERT((((rt_ubase_t)rmem) & (RT_ALIGN_SIZE - 1)) == 0);
    RT_ASSERT((rt_uint8_t *)rmem >= (rt_uint8_t *)heap_ptr &&
              (rt_uint8_t *)rmem < (rt_uint8_t *)heap_end);

    RT_OBJECT_HOOK_CALL(rt_free_hook, (rmem));
    /* 申請釋放地址不在堆區 */
    if ((rt_uint8_t *)rmem < (rt_uint8_t *)heap_ptr ||
        (rt_uint8_t *)rmem >= (rt_uint8_t *)heap_end)
    {
        RT_DEBUG_LOG(RT_DEBUG_MEM, ("illegal memory\n"));

        return;
    }

    /* 獲取塊描述符 */
    mem = (struct heap_mem *)((rt_uint8_t *)rmem - SIZEOF_STRUCT_MEM);

    RT_DEBUG_LOG(RT_DEBUG_MEM,
                 ("release memory 0x%x, size: %d\n",
                  (rt_ubase_t)rmem,
                  (rt_ubase_t)(mem->next - ((rt_uint8_t *)mem - heap_ptr))));


    /* 獲取堆保護信號量 */
    rt_sem_take(&heap_sem, RT_WAITING_FOREVER);

    /* 待釋放的記憶體,其塊描述符需是使用狀態 */
    if (!mem->used || mem->magic != HEAP_MAGIC)
    {
        rt_kprintf("to free a bad data block:\n");
        rt_kprintf("mem: 0x%08x, used flag: %d, magic code: 0x%04x\n", mem, mem->used, mem->magic);
    }
    RT_ASSERT(mem->used);
    RT_ASSERT(mem->magic == HEAP_MAGIC);
    /* 清除使用標誌 */
    mem->used  = 0;
    mem->magic = HEAP_MAGIC;
#ifdef RT_USING_MEMTRACE
    rt_mem_setname(mem, "    ");
#endif

    if (mem < lfree)
    {
        /* 更新空閑塊lfree指針 */
        lfree = mem;
    }

#ifdef RT_MEM_STATS
    used_mem -= (mem->next - ((rt_uint8_t *)mem - heap_ptr));
#endif

    /* 如臨近塊也處於空閑態,則合併整理成一個更大的塊 */
    plug_holes(mem);
    rt_sem_release(&heap_sem);
}
RTM_EXPORT(rt_free);

合併空閑塊plug_holes

static void plug_holes(struct heap_mem *mem)
{
    struct heap_mem *nmem;
    struct heap_mem *pmem;

    RT_ASSERT((rt_uint8_t *)mem >= heap_ptr);
    RT_ASSERT((rt_uint8_t *)mem < (rt_uint8_t *)heap_end);
    RT_ASSERT(mem->used == 0);

    /* 前向整理 */
    nmem = (struct heap_mem *)&heap_ptr[mem->next];
    if (mem != nmem &&
        nmem->used == 0 &&
        (rt_uint8_t *)nmem != (rt_uint8_t *)heap_end)
    {
        /*如果mem->next是空閑,且非尾節點,則合併*/
        if (lfree == nmem)
        {
            lfree = mem;
        }
        mem->next = nmem->next;
        ((struct heap_mem *)&heap_ptr[nmem->next])->prev = (rt_uint8_t *)mem - heap_ptr;
    }

    /* 後向整理 */
    pmem = (struct heap_mem *)&heap_ptr[mem->prev];
    if (pmem != mem && pmem->used == 0)
    {
        /* 如mem->prev空閑,將mem與mem->prev合併 */
        if (lfree == mem)
        {
            lfree = pmem;
        }
        pmem->next = mem->next;
        ((struct heap_mem *)&heap_ptr[mem->next])->prev = (rt_uint8_t *)pmem - heap_ptr;
    }
}

動態記憶體的釋放相對比較簡單,其思路主要是判斷傳入地址是否在堆區,如是堆記憶體,則判斷其塊信息是否合法。如果合法,則將使用標誌清除。同時如果臨近塊如果是空閑態,則利用plug_holes將空閑塊進行合併,合併成一個大的空閑塊。

記憶體泄漏

使用free釋放記憶體失敗會導致不可重用記憶體的累積,程式不再使用這些記憶體。這將浪費記憶體資源,並可能在耗盡這些資源時導致分配失敗。

怎麼使用堆

堆區的配置

對於STM32而言,位於board.h

/ * 配置堆區大小,可根據實際使用進行修改 */
#define HEAP_BEGIN   STM32_SRAM1_START
#define HEAP_END     STM32_SRAM1_END

/* 用於板級初始化堆區 */
void rt_system_heap_init(void *begin_addr, void *end_addr)

堆的介面函數

用於動態申請記憶體
void *rt_malloc(rt_size_t size)
/*追加申請記憶體,此函數將更改先前分配的記憶體塊。*/
void *rt_realloc(void *rmem, rt_size_t newsize)
/* 申請的記憶體被初始化為0 */
void *rt_calloc(rt_size_t count, rt_size_t size)

記憶體分配不能保證成功,而是可能返回一個空指針。使用返回的值,而不檢查分配是否成功,將調用未定義的行為。這通常會導致崩潰,但不能保證會發生崩潰,因此依賴於它也會導致問題。

對於申請的記憶體,使用前必須進行返回值判斷,否則申請失敗,且任繼續使用。將會出現意想不到的錯誤!!

總結一下

通過對RT-Thread的小堆管理器實現的梳理,層層遞進更深入理解以下一些要點:

  • 為什麼需要堆,為什麼堆是C/C++運行時的基礎之一。堆可實現動態記憶體管理的多樣性,在犧牲一定開銷情況下(申請/釋放開銷,以及記憶體開銷),可以提供記憶體的利用率,在一定程度上解決記憶體不足的需求。
  • 可以更深入的理解鏈表實用價值,理解靜態實現方法的一些技巧。
  • 通過更深入的理解堆的實現,可以更好的使用堆。
  • 理解堆管理器究竟在哪裡實現的,C/C++標準庫,以及操作系統內核都可能實現堆管理器。
  • RT-Thread的小堆實現是一個比較簡單和比較好的學習堆管理的例子,事實上堆的實現還有更複雜的場景,比如基於SLAB堆管理器實現,以及IAR中庫的堆實現還需要使用樹這個數據結構。

堆使用常見錯誤

  • 使用前沒有檢查分配失敗:記憶體分配不能保證成功,不成功時返回一個空指針。使用返回的空指針,而直接操作這個空指針。可能會導致程式崩潰。
  • 記憶體泄露:使用free釋放記憶體也可能會失敗,失敗會導致不可重用記憶體的累積,這些記憶體將在堆區不再能被使用。這將浪費記憶體資源,並可能會隨著程式的運行耗盡所有堆記憶體。
  • 邏輯錯誤:所有的分配須使用相同的模式:使用malloc申請分配記憶體,使用free釋放記憶體。如果使用後而不釋放。例如在調用free釋放之後或在調用malloc之前使用記憶體、也或者兩次調用free釋放記憶體(“double free”)等,通常可能會導致段錯誤並導致程式崩潰。這些錯誤可能是偶發的,而且很難調試發現。

文章出自微信公眾號:嵌入式客棧,更多內容,請關註本人公眾號,嚴禁商業使用,違法必究


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 從 Gradle 角度看,Android 插件是由 Google 的 Android 團隊開發的一個第三方插件。 從 Android 的角度看,Android 插件是基於 Gradle 構建的,是和 Android studio 完美搭配的新一代構建系統。 ...
  • 目錄:andorid jar/庫源碼解析 Apktool.jar: 作用: 1、用於對APK文件進行解包,成可以讀的smali和xml,png等資源文件。 2、同時,把解碼之後的數據,重新打包成APK文件。 慄子: 使用命令的方式使用 1、apktool d xxx.apk // 解碼 apk文件 ...
  • 一,JavaScript是什麼? 1,JavaScript簡稱:js,是一種瀏覽器解釋型語言,嵌套在HTML文件中交給瀏覽器解釋執行。主要用來實現網頁的動態效果,用戶交互及前後端的數據傳輸等。 2,JavaScript 組成 1,核心語法 - ECMAScript (ES5-ES6) 規範了Java ...
  • 在開發大型Web應用或複雜交互的網站,不免會遇到一些頁面性能瓶頸的問題。本篇介紹一下如何利用Chrome的性能面板分析網站的性能瓶頸,應該對你有所幫助。 註意,為了減少一些Chrome插件對性能評估產生噪音,最好打開隱身模式訪問頁面進行測試。 將Chrome切換到隱身模式,然後打開該頁面進行測試: ...
  • 在組件特定時期,觸發的條件,統稱為生命周期; + 組件生命周期分為三部分: 組件創建階段 :組件創建階段的生命周期函數,有一個顯著的特點:創建階段的生命周期函數,組件一生只執行一次; componentWillMount:組件將要被掛載,此時還沒有開始渲染虛擬dom render:第一次開始渲染真正 ...
  • # 6.動畫 - 1. transition 過渡 transition-property:all;//監聽屬性 transition-duration:1s;//過渡時間 transition-timing-function:linear;//運動速率 transition-delay:1s;// ...
  • 老實說,在實際編程中,訪問者設計模式應用的並不多,至少我是這樣認為的,因為它的主要使用場景並不多。那麼肯定會有人問,訪問者模式的主要使用場景是什麼呢?繼續往下看便知。新聞聯播看多了之後首先要說的是,設計模式中的“訪問者”和現實生活中的“訪問者”其本質是一回事。雖然設計模式中的不太熟悉,但現實生活中的 ...
  • MMU存在的意義 [導讀] 本文從記憶體管理的發展歷程角度層層遞進,介紹MMU的誕生背景,工作機制。而忽略了具體處理器的具體實現細節,將MMU的工作原理從概念上比較清晰的梳理了一遍。 MMU誕生之前: 在傳統的批處理系統如DOS系統,應用程式與操作系統在記憶體中的佈局大致如下圖: 應用程式直接訪問物理內 ...
一周排行
    -Advertisement-
    Play Games
  • C#TMS系統代碼-基礎頁面BaseCity學習 本人純新手,剛進公司跟領導報道,我說我是java全棧,他問我會不會C#,我說大學學過,他說這個TMS系統就給你來管了。外包已經把代碼給我了,這幾天先把增刪改查的代碼背一下,說不定後面就要趕鴨子上架了 Service頁面 //using => impo ...
  • 委托與事件 委托 委托的定義 委托是C#中的一種類型,用於存儲對方法的引用。它允許將方法作為參數傳遞給其他方法,實現回調、事件處理和動態調用等功能。通俗來講,就是委托包含方法的記憶體地址,方法匹配與委托相同的簽名,因此通過使用正確的參數類型來調用方法。 委托的特性 引用方法:委托允許存儲對方法的引用, ...
  • 前言 這幾天閑來沒事看看ABP vNext的文檔和源碼,關於關於依賴註入(屬性註入)這塊兒產生了興趣。 我們都知道。Volo.ABP 依賴註入容器使用了第三方組件Autofac實現的。有三種註入方式,構造函數註入和方法註入和屬性註入。 ABP的屬性註入原則參考如下: 這時候我就開始疑惑了,因為我知道 ...
  • C#TMS系統代碼-業務頁面ShippingNotice學習 學一個業務頁面,ok,領導開完會就被裁掉了,很突然啊,他收拾東西的時候我還以為他要旅游提前請假了,還在尋思為什麼回家連自己買的幾箱飲料都要叫跑腿帶走,怕被偷嗎?還好我在他開會之前拿了兩瓶芬達 感覺感覺前面的BaseCity差不太多,這邊的 ...
  • 概述:在C#中,通過`Expression`類、`AndAlso`和`OrElse`方法可組合兩個`Expression<Func<T, bool>>`,實現多條件動態查詢。通過創建表達式樹,可輕鬆構建複雜的查詢條件。 在C#中,可以使用AndAlso和OrElse方法組合兩個Expression< ...
  • 閑來無聊在我的Biwen.QuickApi中實現一下極簡的事件匯流排,其實代碼還是蠻簡單的,對於初學者可能有些幫助 就貼出來,有什麼不足的地方也歡迎板磚交流~ 首先定義一個事件約定的空介面 public interface IEvent{} 然後定義事件訂閱者介面 public interface I ...
  • 1. 案例 成某三甲醫預約系統, 該項目在2024年初進行上線測試,在正常運行了兩天後,業務系統報錯:The connection pool has been exhausted, either raise MaxPoolSize (currently 800) or Timeout (curren ...
  • 背景 我們有些工具在 Web 版中已經有了很好的實踐,而在 WPF 中重新開發也是一種費時費力的操作,那麼直接集成則是最省事省力的方法了。 思路解釋 為什麼要使用 WPF?莫問為什麼,老 C# 開發的堅持,另外因為 Windows 上已經裝了 Webview2/edge 整體打包比 electron ...
  • EDP是一套集組織架構,許可權框架【功能許可權,操作許可權,數據訪問許可權,WebApi許可權】,自動化日誌,動態Interface,WebApi管理等基礎功能於一體的,基於.net的企業應用開發框架。通過友好的編碼方式實現數據行、列許可權的管控。 ...
  • .Net8.0 Blazor Hybird 桌面端 (WPF/Winform) 實測可以完整運行在 win7sp1/win10/win11. 如果用其他工具打包,還可以運行在mac/linux下, 傳送門BlazorHybrid 發佈為無依賴包方式 安裝 WebView2Runtime 1.57 M ...