主頁 > 後端開發 > 實用演算法系列之RT-Thread鏈表堆管理器

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

2020-09-13 12:23:24 後端開發

[導讀] 前文描述了堆疊的基本概念,本文來聊聊堆是怎么會事兒,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”)等,通常可能會導致段錯誤并導致程式崩潰,這些錯誤可能是偶發的,而且很難除錯發現,

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

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/24348.html

標籤:C

上一篇:資料結構上機實驗(2)

下一篇:MMU那些事兒

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more