前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >实用算法系列之RT-Thread链表堆管理器

实用算法系列之RT-Thread链表堆管理器

原创
作者头像
逸珺
修改于 2020-06-08 02:37:10
修改于 2020-06-08 02:37:10
80200
代码可运行
举报
运行总次数:0
代码可运行

[导读] 前文描述了栈的基本概念,本文来聊聊堆是怎么会事儿。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,函数为:

代码语言:javascript
代码运行次数:0
运行
复制
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 *。实现了静态双链表的创建
代码语言:javascript
代码运行次数:0
运行
复制
mem      = (struct heap_mem *)heap_ptr;
heap_end = (struct heap_mem *)&heap_ptr[mem->next];
  • 定义heap_mem没有定义使用多少字节为该块的用户数据字节数,节约了内存。是一个比较好的处理方式。
  • 对齐方式可配置,RT_ALIGN_SIZE默认为4字节。

向堆申请内存

用户调用rt_malloc 用于申请分配动态内存。

代码语言:javascript
代码运行次数:0
运行
复制
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实现:

代码语言:javascript
代码运行次数:0
运行
复制
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

代码语言:javascript
代码运行次数:0
运行
复制
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

代码语言:javascript
代码运行次数:0
运行
复制
/ * 配置堆区大小,可根据实际使用进行修改 */
#define HEAP_BEGIN   STM32_SRAM1_START
#define HEAP_END     STM32_SRAM1_END/* 用于板级初始化堆区 */
void rt_system_heap_init(void *begin_addr, void *end_addr)

堆的接口函数

代码语言:javascript
代码运行次数:0
运行
复制
用于动态申请内存
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”)等,通常可能会导致段错误并导致程序崩溃。这些错误可能是偶发的,而且很难调试发现。

本文分享自微信公众号 -嵌入式客栈(embInn),作者:逸珺,严禁商用,违法必究,更多更新内容请关注

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
通用权限的思路。只是一个简单的思路。
面对权限,我们要解决几个的问题。 第一个就是:我们的软件里面有哪些功能? —— 给用户自己维护角色作准备 比如添加新闻、添加产品、客户信息维护、合同管理等等,当然还可以细分一下, 客户信息维护又可以分为:客户基本信息、客户的联系人、客户报价、客户的合同等。 我的习惯是建立一个表,叫做功能结点表。 这个表可以生成左面的功能树,也可以记录项目里面一共有哪些功能。 这里的一个功能指的是两个页面,一个是列表页面,一个是表单页面。 列表页面包括查询、导出数据等功能,表单页面又可以再往下继续划分,就是可以
用户1174620
2018/02/07
5290
【自然框架】之通用权限:用PowerDesigner重新设计了一下数据库,有ER图和表关系图
      好像以前做的那个数据库设计大家都没太看懂,究其原因似乎大家都比较习惯使用PowerDesinger来设计。而我用Excel画出来的图大家看着特别别扭,而且还没有总体的图,也没有ER图,所以大家也就没有心情看了吧。呵呵。       PowerDesinger学习了一下,感谢Hayden Han 写的《PowerDesigner使用教程 —— 概念数据模型 》,通过这个文章学会了如何使用PowerDesinger来画ER图,这回画出来的应该是ER图了吧,呵呵。除了ER图,还有表关联图,而且还是由简
用户1174620
2018/02/26
2.8K0
【自然框架】之通用权限:用PowerDesigner重新设计了一下数据库,有ER图和表关系图
【自然框架】之通用权限(六):权限到节点
      “直率没有错,但是也要考虑对方的承受能力呀!对方都承受不了了,你还直率,那就是你的错了!”  ——我的名言,呵呵。     ====================我就是传说中的,可爱的、无奈的、笑笑而过的分割线====================       继续,这是第六章了。我发现,越来越难了。终于把表结构都介绍完了,来到了如何应用的阶段了。有回复说我是跳过了设计阶段,恩,设计阶段基本上是在我的脑子里。当然这是一个不好的习惯。不弄出来个UML、ER这样的东东,别人怎么理解呢?又怎么能
用户1174620
2018/02/08
8860
【自然框架】之通用权限(六):权限到节点
【自然框架】之通用权限:数据库设计的几种使用方式
      上次《【自然框架】之通用权限:用PowerDesigner重新设计了一下数据库,有ER图和表关系图 》里说了一大堆的表,好多人说太复杂了,做到权限到模块就可以了。       这个嘛,我也没有说所有的表都要一起使用呀。用哪些表那是根据情况来定的。也就是客户需求、项目需求和经验来决定了。       如果项目很简单,客户的需求也不复杂,那么做到权限到模块就可以了,大家都方便。那么这个时候“资源表组”里面就只需要用一个表就ok了,其他的表就不用了。       如果客户的需求很挑剔,客户的使用项目的
用户1174620
2018/02/26
1.1K0
【自然框架】之通用权限:数据库设计的几种使用方式
【角色】——分离开代码和权限需求,即实现代码和权限需求的解耦。
今天突然来了一个灵感,记录一下。以前总觉得说不清楚,看看这种表达方式是否可以说清。 两个原则:依赖接口编程,不要依赖实现编程;最小获知原则。 面向对象最重要的是什么?抽象。那么在权限这方面我们要如何抽
用户1174620
2018/02/26
1.1K0
【角色】——分离开代码和权限需求,即实现代码和权限需求的解耦。
【自然框架】之通用权限(四):角色表组
      继续,这是第四章了。这里涉及到了资源方面的,不过有点绕,所以这里先介绍一下表结构,在后面的章节里面,再举例子详细介绍。 通用权限想要写的文章目录:(这是第四章) 1、 简介、数据库的总体结构 2、 介绍人员表组 3、 介绍组织结构表组 4、 介绍角色表组 5、 介绍“项目自我描述表组” 6、 权限到节点 7、 权限到按钮 8、 权限到列表(表单、查询) 9、 权限的验证 10、 资源方面的权限 11、 角色管理的程序(给客户用的) 12、 权限下放 13、 个性化设置 A、、 【自然框架】之
用户1174620
2018/02/08
1.7K0
【自然框架】之通用权限(四):角色表组
【自然框架】之通用权限(一):简介、数据结构
      这次要写一整套的权限方面的文章了,无论我的想法好与不好,先写出来请大家来评判。这个系列我要详细的说明我的权限的思路、想法、实现方式、代码和Demo。可能有人会说,通用是达不到的,最多只能无限接近。恩,对于我来说,能够无限接近就可以了,当然我知道如果要达到这个目标并不是一件容易的事情,有难度才有挑战,才有意思。所以我会在权限方面不断的努力,不断的无限接近通用。也请大家多多帮忙,毕竟一个人的力量是有限的。       通用权限想要写的文章目录:(这是第一章) 1、 简介、数据库的总体结构 2、 介
用户1174620
2018/02/08
9860
【自然框架】之通用权限(一):简介、数据结构
【自然框架】之通用权限(三):组织结构表组
      继续,这是第三章了。拖得有点长,但是我也是一边写,一边在想办法,想怎么做才能让资源权限也能通用起来。看大家的回复也给了我一些提示,我也在修改我的方案。原来打算用来解决一个人虽然在业务一部,但是却可以看业务一部、业务二部的客户信息的情况,但是仔细想了一下,这么做也不行。不过还好,我又找到了另一个方法来解决,而且可以让资源权限更加通用。不过这个详细的方法要放在下一章的角色表组里面来说明了。(这是写这篇之前的想法,写完之后想法又变了。) 通用权限想要写的文章目录:(这是第三章) 1、 简介、数据库的
用户1174620
2018/02/08
2.5K0
【自然框架】之通用权限(三):组织结构表组
通用权限相关文档的下载【2009.9.7更新】
最新的下载地址:http://www.naturefw.com/nature/down.aspx 下面的地址都作废。       您可以在这里下载通用权限相关的文档、源代码、Demo等,当然现在只有一个数据的说明文档。以后会逐步增加。  不好意思,忘记说用户名、密码了。 管理员的用户名:admin,密码:123。其他用户的密码也都是123。 文档名称 上传日期 说明 详细介绍 下载 权限的演示 2009.9.9 6:28 权限到节点、按钮,权限到字段,权限到记录 数据库设计 2009.7
用户1174620
2018/02/08
8320
【自然框架】之通用权限(五):项目描述表组
      继续,这是第五章了。我发现了,写文章比写程序还要有难度。 通用权限想要写的文章目录:(这是第五章) 1、 简介、数据库的总体结构 2、 介绍人员表组 3、 介绍组织结构表组 4、 介绍角色表组 5、 介绍“项目自我描述表组” 6、 权限到节点 7、 权限到按钮 8、 权限到列表(表单、查询) 9、 权限的验证 10、 资源方面的权限 11、 角色管理的程序(给客户用的) 12、 权限下放 13、 个性化设置 A、 【自然框架】之通用权限(外传):杂谈 项
用户1174620
2018/02/08
9570
【自然框架】之通用权限(五):项目描述表组
rbac权限管理设计 7表_数据库角色权限表设计
RBAC(Role-Based Access Control,基于角色的访问控制),就是用户通过角色与权限进行关联。简单地说,一个用户拥有若干角色,每一个角色拥有若干权限。这样,就构造成“用户-角色-权限”的授权模型。在这种模型中,用户与角色之间,角色与权限之间,一般者是多对多的关系。(如下图)
全栈程序员站长
2022/11/10
5.4K0
rbac权限管理设计 7表_数据库角色权限表设计
【自然框架】 权限 的视频演示(二): 权限到字段、权限到记录
      继续。这里演示权限到字段和权限到记录。       权限到字段有两种安全级别,       1、低安全级别。有些项目不需要做到控制每一个字段是否显示,那么就可以采用这种级别。低安全级别就是:如果一个节点里面没有设置可以访问哪些字段,那么就默认为不需要做到控制字段的程度,就是说节点里的字段都是可以访问的。这么做是为了操作方便。       2、高安全级别。有些项目要求非常严格,要严格控制每一个字段是否可以访问,那么就可以采用这种安全级别。高安全级别:如果一个节点里面没有设置可以访问哪些字段,那么就
用户1174620
2018/02/26
9290
【自然框架】之 “工作日志”和“选择日期”
      上周回家办点事,更新的事情就有耽搁了。对不住大家,所以这周要努力了。       我发现要做的事情太多了,做过了哪些事情也都记不清了,所以有必要弄个“工作计划”和“工作日志”出来。为什么要弄个“工作日志”呢?有这么几个好处。 工作日志:       1、可以记录下来某个时间段做了哪些事情,以便给工作计划提供可靠的依据。       2、更新程序的时候,可以告诉大家,新的程序、Demo都增加了哪些功能。       3、这个可以算是“项目管理”的一个很基本的功能吧,以后还可以扩展。 选择日期和时间
用户1174620
2018/02/26
9130
【自然框架】之 “工作日志”和“选择日期”
【自然框架】元数据的数据库结构的详细说明和示例(一):项目描述部分
1、 Manage_Function(节点信息) 字段名 中文名 类型 大小 默认值 说明 FunctionID 节点ID int 4 1 主键 ParentID 父节点ID int 4 1 员工姓名 ParentIDPath 父节点ID的路径 nvarchar 30 _ 添加、修改时使用 NoteTitle 节点名称 nvarchar 100 _ 节点名称 PowerMark 权限标识 nvarchar 50 _ 一般情况下等于FunctionID NoteLevel 级数 int 4
用户1174620
2018/02/26
6800
【自然框架】元数据的数据库结构的详细说明和示例(一):项目描述部分
【自然框架】之通用权限的Demo(二):添加人员、添加账户、添加角色里面的账户以及列表的权限验证
      看了一下上一次发Demo的日期6月15日,已经过了半个多月,这个速度也实在是太慢了。还是心情的原因,恩,心理承受能力太弱了,哈哈。不过还是要坚持的,要继续下去。       还是先说一下这次的Demo里增加的内容吧。       1、添加人员             这个很简陋了,主要就是为了能够添加一条人员信息,然后可以给这个人员来添加账户。不过后续我会把这一块完善一下的,能够实现一些基本的人员管理的功能。       2、添加账户             先选择一个人员,然后给这个人员添加一
用户1174620
2018/02/08
1K0
【自然框架】之通用权限的Demo(二):添加人员、添加账户、添加角色里面的账户以及列表的权限验证
【开源】QuickPager ASP.NET2.0分页控件V2.0.0.3 【增加了使用说明】
最新版本:V2.0.0.7 。http://www.cnblogs.com/jyk/archive/2008/07/28/1255101.html 下载:http://www.cnblogs.com/jyk/archive/2008/07/29/1255891.html ================================     增加了几个属性、两个事件、修改了一下内部的代码。分页控件的大体结构终于确定下来了。详细说明一下功能吧。 1、基本信息     控件名称:QuickPa
用户1174620
2018/02/26
6480
【开源】QuickPager ASP.NET2.0分页控件V2.0.0.3  【增加了使用说明】
分页控件的使用能不能再简单一点呢,能不能一个页面搞定所有的列表需求?
目的: 1、一个页面(DataList.aspx)可以显示多个模块的列表功能。      一般是有一个列表需求就需要一个aspx文件,如果有100个列表,那么就会有100个aspx文件,这么多的文件(包括.aspx.cs文件)里面的内容基本是一样的,这样写起来麻烦,管理起来也不容易,命名就是一个比较头痛的问题。文件多了。打开IDE、备份程序文件、编译所需要的时间都会增长。这些都是很郁闷的事情。那么我们能不能“合并”一下呢?所有(或者大部分没有特殊情况的)列表都是用同一个aspx文件呢。      
用户1174620
2018/02/08
1.2K0
分页控件的使用能不能再简单一点呢,能不能一个页面搞定所有的列表需求?
【自然框架】之鼠标点功能现(一):单表的增删改查(即上次5月23日活动的一个主题)【Demo、源码下载】
简单的需求,点点鼠标就可以了,那么复杂的需求呢?还是要写代码,哈哈。 不要被我误导了哦,关于什么时候写代码的问题,请看这里:http://www.cnblogs.com/jyk/archive/2009/06/21/1507594.html 单表的增删改查       我有一个梦想,那就是不用敲代码,只需要点点鼠标,就可以实现客户的需求。       可能您会说这是不可能的,但是有个梦想总没有错吧。我就是想实现我的这个梦想,虽然可能一辈子都达不到,但是我还想努力一下子,不想让自己后悔。如果不给自己找一
用户1174620
2018/02/26
8510
【自然框架】之鼠标点功能现(一):单表的增删改查(即上次5月23日活动的一个主题)【Demo、源码下载】
【自然框架】通用权限的视频演示(一):添加角色,权限到功能节点和按钮
      写了几个关于权限的东东,好像大家都不大理解,也不太清楚我的权限到底能做什么,所以想来想去还是弄点视频吧,就是屏幕录像,这样大家看起来就方便了吧。       为了大家便于观看视频,我先说一下视频的步骤。       1、添加角色,选择角色可以使用的功能节点和按钮。       2、选择用户,就是给角色里面添加用户。       3、用用户的账号登录,查看效果。       4、修改角色可以使用的按钮,查看效果。       这里举了一个很简单的例子——新闻维护,有两个角色,一个是“新闻维护”一个
用户1174620
2018/02/26
9670
【自然框架】之通用权限(二):人员表组
      继续,这是第二章了。本来想在这一章里面介绍三个表组来着,但是我有点写不好的感觉,还是多分几章吧,这一章就只介绍人员表组。第二章到第五章主要是介绍表结构。我是习惯使用Excel来设计表,一开始的时候只能记录表名、字段名、字段类型、字段说明等信息,但是一直没能找到如何使用Excel来体现出来表之间的关系。前一阵子(好像是去年)突然想到了可以使用“图表”+图形(比如箭头)的方式来做表关系,第一章里的那几个图就是这么弄出来的,看着还凑合吧。       至于为什么不用PowerDesigner来做,个
用户1174620
2018/02/08
8020
【自然框架】之通用权限(二):人员表组
推荐阅读
通用权限的思路。只是一个简单的思路。
5290
【自然框架】之通用权限:用PowerDesigner重新设计了一下数据库,有ER图和表关系图
2.8K0
【自然框架】之通用权限(六):权限到节点
8860
【自然框架】之通用权限:数据库设计的几种使用方式
1.1K0
【角色】——分离开代码和权限需求,即实现代码和权限需求的解耦。
1.1K0
【自然框架】之通用权限(四):角色表组
1.7K0
【自然框架】之通用权限(一):简介、数据结构
9860
【自然框架】之通用权限(三):组织结构表组
2.5K0
通用权限相关文档的下载【2009.9.7更新】
8320
【自然框架】之通用权限(五):项目描述表组
9570
rbac权限管理设计 7表_数据库角色权限表设计
5.4K0
【自然框架】 权限 的视频演示(二): 权限到字段、权限到记录
9290
【自然框架】之 “工作日志”和“选择日期”
9130
【自然框架】元数据的数据库结构的详细说明和示例(一):项目描述部分
6800
【自然框架】之通用权限的Demo(二):添加人员、添加账户、添加角色里面的账户以及列表的权限验证
1K0
【开源】QuickPager ASP.NET2.0分页控件V2.0.0.3 【增加了使用说明】
6480
分页控件的使用能不能再简单一点呢,能不能一个页面搞定所有的列表需求?
1.2K0
【自然框架】之鼠标点功能现(一):单表的增删改查(即上次5月23日活动的一个主题)【Demo、源码下载】
8510
【自然框架】通用权限的视频演示(一):添加角色,权限到功能节点和按钮
9670
【自然框架】之通用权限(二):人员表组
8020
相关推荐
通用权限的思路。只是一个简单的思路。
更多 >
目录
  • 什么是堆?
  • 为什么要堆?
  • 谁实现堆
  • 怎么实现堆
  • 关键数据结构
  • 堆管理器初始化
  • 向堆申请内存
  • 释放内存
  • 内存泄漏
    • 怎么使用堆
  • 堆区的配置
  • 堆的接口函数
    • 总结一下
  • 堆使用常见错误
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验