1、为何引入链表
在程序中经常面临一个问题,我们需要保存一定数量的对象,但是对象数目是不确定的,或者说是随时增加或减少的。这时候最简单的方法是创建一个足够大的数组,用来存储这些对象。我最近开发一个项目就遇到类似的问题,下面我把问题简化一下。
需求:通过PC下发一些矩形的坐标和宽高信息,每个区域有个ID编号,并在这些矩形内填充一定的数据。
通常情况下,最简单易懂的做法是,限制最多5个区域,每个区域存储1K数据。因此设置了这样的一个结构体(类似于面向对象语言里说的成员属性)。
typedef struct Area_Inf{ uint8_t ID; uint8_t X; uint8_t Y; uint8_t Width; uint8_t Height; uint8_t data_len;}Area_Inf_Typedef;
然后定义结构体的实体。
#define Area_Num 5#define Area_cache 1024
Area_Inf_Typedef Area_Info[Area_Num];uint8_t Area_Data[Area_Num*Area_cache];//存储区域的数据
/*找到ID为5的区域,并将数据拷贝出去*/void main(){ uint8_t i; uint8_t data[1024]; for(i = 0;i 《 Area_Num;i++) { if(Area_Info[i].ID == 5) { memcpy(data,&Area_Data[i*Area_cache ],Area_Info[i].data_len); } }}
上面这种做法是最简单易懂的,但不灵活,比如有客户要求10个区域,但是每个区域存储的数据很少,根本用不到1K。虽然上面的程序已经使用了宏定义,只需要修改宏定义就能实现要求。但这意味着不同的客户,需要编译不同的固件。
#define Area_Num 10#define Area_cache 512
这样的程序存在的问题:
1、在内存资源很紧缺的单片机程序中,当区域数据很少时,这种程序的处理方法浪费了大量的内存空间。
2、数值固定,需要存储更多区域,即使还有内存,还是需要修改宏定义,重新编译固件,不灵活。
这时需要引入链表来解决这个问题。
2、链表实现
链表实际上是线性表的链式存储结构,与数组不同的是,它是用一组任意的存储单元来存储线性表中的数据,存储单元不一定是连续的,且链表的长度不是固定的,链表数据的这一特点使其可以非常的方便地实现节点的插入和删除操作。链表的每个元素称为一个节点,每个节点都可以存储在内存中的不同的位置,为了表示每个元素与后继元素的逻辑关系,以便构成“一个节点链着一个节点”的链式存储结构,除了存储元素本身的信息外,还要存储其直接后继信息,因此,每个节点都包含两个部分,第一部分称为链表的数据区域,用于存储元素本身的数据信息。
对于上面的问题,我们使用链表解决,需要配合内存管理才能实现。内存管理这一块,大家可以自己编写内存管理驱动,也可以使用C库的malloc和free函数。如何字节编写内存管理驱动不是本文的重点,下文将使用C库的malloc和free函数进行内存管理。
使用链表的方式,在原有的成员属性结构体的前提上,还要再封装多一层链表管理。以单向链表为例:
typedef struct Area_Inf{ uint8_t ID; uint8_t X; uint8_t Y; uint8_t Width; uint8_t Height; uint8_t data_len; uint8_t* Area_Data;}Area_Inf_Typedef;
typedef struct Area_List_Inf{ Area_Inf_Typedef *Area_Inf; struct Area_List_Inf *next_Area_Inf; //用于指向下一个}Area_List_Inf_Typedef;
Area_List_Inf_Typedef *Head_Area_List; //链表的头指针
由于在定义的时候,只定义了一个头指针,那么它也只是个指向了Area_List_Inf_Typedef也就是链表结构体的指针,同样没有内存空间,在没有创建新增链表之前,它是一个野指针。
所以,在具体应用之前,需要先执行一个初始化操作,也就是申请空间给链表管理结构体,然后头指针指向这个空间。
/*** @brief 动态区链表初始化* @return int */int Area_List_Init(void){ //申请链表类型大小的空间,并让头指针指向它 Head_Area_List = (Area_List_Inf_Typedef*)malloc(sizeof(Area_List_Inf_Typedef)); if(Head_Area_List == NULL) return false; //同时要标记下一个信息为空 Head_Area_List-》next_Area_Inf = NULL; return true;}
通过PC下发一个新的区域信息后,增加新区域到链表末尾。
/*** @brief 在链表末尾增加一个区域参数* @param Area_Inf 增加的区域区参数指针* @return int */int Add_Area_ToList(Area_Inf_Typedef *Area_Inf){ Area_List_Inf_Typedef *p = Head_Area_List; while(p-》next_Area_Inf!=NULL) { p = p-》next_Area_Inf; } //先申请链表结构体的空间,因为后续还要继续增加 p-》next_Area_Inf = (Area_List_Inf_Typedef*)malloc(sizeof(Area_List_Inf_Typedef)); if(p-》next_Area_Inf == NULL) return false;//申请不到内存,返回失败 //指向刚刚申请的空间,并为需要存放的动态区信息申请对应的内存 p = p-》next_Area_Inf; p-》Area_Inf = (Area_Inf_Typedef*)malloc(sizeof(Area_Inf_Typedef)); if(p-》Area_Inf == NULL) { free(p);//由于申请失败,先前申请的链表空间也要释放 return false; } memcpy(p-》Area_Inf,Area_Inf,sizeof(Area_Inf_Typedef)); /*拷贝数据*/ p-》Area_Inf-》Area_Data = (uint8_t*)malloc(Area_Inf-》data_len); if(p-》Area_Inf-》Area_Data == NULL) { free(p-》Area_Inf); free(p); return false; } memcpy(p-》Area_Inf-》Area_Data,Area_Inf-》Area_Data,Area_Inf-》data_len); //标记这个链表的尾部 p-》next_Area_Inf=NULL; //添加成功 return true;}
通过PC下发一个删除指定ID的区域命令。
/*** @brief 根据区域ID删除动态区* @param num 区域ID* @return int */int Delete_Area_Accordingn_ID(int num){ int res = false; Area_List_Inf_Typedef *p = Head_Area_List; while(p-》next_Area_Inf!=NULL) { Area_List_Inf_Typedef *temp = p; p = p-》next_Area_Inf; if(p-》Area_Inf-》ID == num)//匹配到对应的值 { temp-》next_Area_Inf = p-》next_Area_Inf; //释放内存空间 free(p-》Area_Inf-》Area_Data); free(p-》Area_Inf); free(p); p=temp; res = true; } } return res;}
看了上面的驱动函数,相信大家已经明白,大家可以自行编写一些驱动,下面我实现的三个简单函数。
/*** @brief 根据区域ID找到链表* @param data_p 链表指针* @param num 区域ID编号* @return int */int Find_Area_According_ID(Area_Inf_Typedef **data_p,int num){ Area_List_Inf_Typedef *p = Head_Area_List; while(p-》next_Area_Inf!=NULL) { p = p-》next_Area_Inf; if(p-》Area_Inf-》ID == num)//匹配到对应的值 { *data_p = p-》Area_Inf; return true; } } return false;}/*** @brief 删除所有区域* */int Delete_All_Area(void){ int res = false; Area_List_Inf_Typedef *p = Head_Area_List; while(p-》next_Area_Inf!=NULL) { Area_List_Inf_Typedef *temp = p; p = p-》next_Area_Inf; temp-》next_Area_Inf = p-》next_Area_Inf; //释放内存空间 free(p-》Area_Inf-》Area_Data); free(p-》Area_Inf); free(p); p=temp; res = true; } return res;}/*** @brief 打印链表信息* */void Printf_Area_Inf(void){ int i=0; Area_List_Inf_Typedef *p = Head_Area_List; printf(“list ID X Y Width Height Area_Data
”); while(p-》next_Area_Inf!=NULL) { p = p-》next_Area_Inf; printf(“ %d %d %d %d %d %d %s
”,i,p-》Area_Inf-》ID,p-》Area_Inf-》X,p-》Area_Inf-》Y,p-》Area_Inf-》Width,p-》Area_Inf-》Height,p-》Area_Inf-》Area_Data); i++; } printf(“----------------------end-----------------------
”);}
3、测试函数
下面编写一个测试函数,可以测试,链表的初始化,增加一个区域,删除指定区域,根据ID返回区域信息,删除所有区域接口。
/*** @brief 链表测试函数* */void list_main(){ int i,j; Area_Inf_Typedef temp; Area_Inf_Typedef **data_p; data_p = NULL; printf(“------------------List test---------------------
”); if(!Area_List_Init( )) { printf(“Memory fail.。
”); } for(i=0;i《5;i++) { temp.ID = i; temp.X = 5+i; temp.Y = i; temp.Width = 10+i; temp.Height = 10+i; temp.data_len = i+1; temp.Area_Data = (uint8_t*)malloc(temp.data_len+1); for(j=0;j《temp.data_len;j++) { temp.Area_Data[j] = j+0x30; } temp.Area_Data[j] = 0; if(!Add_Area_ToList(&temp)) { printf(“Add Area %d Area_Info fail
”,i); } } Printf_Area_Inf(); printf(“
-------------Delete ID of Area is 3-------------
”); Delete_Area_Accordingn_ID(3); Printf_Area_Inf(); temp.ID = 9; temp.data_len = 10; temp.Area_Data = (uint8_t*)malloc(temp.data_len+1); for(j=0;j《temp.data_len;j++) { temp.Area_Data[j] = j+0x30; } temp.Area_Data[j] = 0; if(!Add_Area_ToList(&temp)) { printf(“Add Area %d info fail
”,temp.ID); } printf(“
--------------Add ID of Area is 9---------------
”); Printf_Area_Inf(); Find_Area_According_ID(data_p,2); temp.ID = (*data_p)-》ID; Delete_All_Area(); printf(“
--------------Delete All Area-------------------
”); Printf_Area_Inf(); while(1);}
测试结果
IAR和keil工程代码开源地址:
https://github.com/strongercjd/STM32_Linklist
(提示:公众号不支持外链接,请复制链接到浏览器下载)
如果大家手中有板子可以调试,可以看《一文了解串口打印》文章,使用串口打印。如果临时没有板子可以debug,可以模拟测试,IAR设置如下:
选择Simulator调试
打开View-》TerminalI/O,就可以看到打印信息