近期项目里遇到瓶颈,我最后选择的方案是用链表解决又重新拾起了多年没用的链表。搞了一个星期,总算是把功能都实现了,并考虑了各种突发状况,基本上不会有程序挂掉的结果。不过过程中还是遇到了很多问题,这些问题在不断的翻书回顾中被解决。
背景是这样的,要求在液晶显示屏上创建一个窗口,里面至少有200个EDIT控件,用户可以在EDIT控件上编辑一些特定的语句,然后点击”运行“按钮时,保存现有的EDIT的内容并顺序执行各语句。当时抓耳挠腮,想卧槽这怎么搞,受做软件工程师学长的启发,我觉得用链表来操作应该没问题,编辑了几行EDIT就保存对应个数结点的链表,既节省空间而且方便顺序执行,执行到下一节点为NULL的时候结束就完了。而且链表结构的插入、删除时间复杂度都为O(1),很是方便。
首先是定义链表结构:
typedef struct _INSTRUCTOR {
uint8_t index; //该节点位于链表中的索引
uint8_t _flag; //该条程序的指令类别
char *EditContent; //该条指令的文本内容
struct _INSTRUCTOR* next; //指向下一条指令的指针
}_Instructor,*_Listptr;
_Listptr 就是_Instructor结构的指针类型了,就等于_Instructor *。注意咯,我这里的结构体没有用__attribute__定义它的属性,也就是说它是默认对齐的,一共占16bytes即0x10。为了省去考虑插入结点、删除结点时关于第一个结点的特殊情况问题,我创建了一个带有表头结点的链表:
_Listptr Ins_List_Head;//程序链表的头指针
int Create_List(void){
Ins_List_Head = (_Listptr)malloc(sizeof(_Instructor));
if( !Ins_List_Head)
return -1;
Ins_List_Head -> index = 0;
Ins_List_Head->next = (void*)0;
return 0;
}
根据调试发现,每次链表申请到的头结点位置都在0X20004320.每增加一个结点,地址增加0x10,咦?大家发现问题没有,地址连续了!当然,这是我们希望看到的现象,不连续的话会造成内存碎片,还要进行碎片回收工作。所以我觉得可能是编译器优化的结果(如果有大神知道原因的话请指正!)
下面就要每编辑完一个EDIT,就要插入一个结点了,那么是前插还是后插呢?当然是后插了,不然让我怎么顺序执行啊。
int Add_Node(int index, enum _FLAG flag, char *content)
{
int i = 0;
_Listptr q = (_Listptr)malloc(sizeof(_Instructor));
_Listptr p = Ins_List_Head;
if(index <= 0 || !q)
return -1;
q -> index = index;
q -> EditContent = content;
q -> _flag = flag;
while(p && i < index-1)
{
p = p ->next ;//此时p即为下标为index的结点的前驱节点
i ++;
}
if(!p)
return -1;
else{
q -> next = p->next ;//使前驱的后继成为新增结点的后继
p->next = q ; //使新增结点的称为前驱的后继
}
return 0;
}
这里要提一提
while循环里的循环次数为 index-1,这个-1很关键啊。道理当然很简单,插入的Node要把自己的头和尾都连接在链表上啊。我把新来的Node下标定为index,所以要在index-1处的结点就开始操作:将index-1的后继(其实就是NULL)成为新Node的后继,然后把新Node成为Index-1 的后继。一开始我粗心大意,直接 i<index ,而且没有检查每个节点的指针是否不为空(程序不健壮!),所以添加的第一个结点时程序就挂掉了,还傻了吧唧的说为什么,现在想想真实可笑!
经过修改后添加结点总算没问题了,然后就是删除结点的操作,我直接盗用书上的源码,很开心他可以直接用:
int Delete_Node(int index)
{
int i;
_Listptr p = Ins_List_Head;
_Listptr q;
if(index <= 0)
return -1;
for(i = 0; i< index - 1;i++)
p = p -> next;//此时p为index结点的前驱结点
if(!p)
{
return -1;
}
else
{
q = p->next ;//q结点即为Index结点
p -> next = q -> next;//使index结点的后继成为p的后继
free(q); //释放index结点的空间
return 0;
}
}
道理更简单,就是把一个Node从链表上拆下来的过程:只要把他的前驱Node的next指针指向它的后继Node就ok了。
别以为做完这几步就可以做关于顺序执行方面的工作了,还要考虑有的用户编到一半,突然选择放弃不编了,点击"返回"按钮回到上一窗口时,难道我还保留着这些链表结点?当然不行啦,这样的话如果用户真正再开始编辑时,然后执行程序,发现有几步操作是上次编辑的没用的命令,这不是瞎搞吗!所以还要做的一个工作就是:点击 “返回”按钮就清空所有结点,当然头结点要保留啊。
void Clear_List(void)
{
_Listptr p = Ins_List_Head ;
_Listptr q ;
int i = GetListLength() ;
while(i--)
{
q = p -> next;
p -> next = q -> next;
free(q);
}
}
你能想象这段短短的小程序我调了整整一下午
。所以人家说,一个好的程序员不在于他一天能写多少行代码,而是写多少行高效的代码,当然我不是说我是一个优秀的程序员啊。
清空链表的思想跟删除结点的思想大致一样,只不过是一直操作头指针的next。一开始我的循环体没用while,而是用的for(i =0 ;i<GetListLength();i++) ,这样执行有个问题,我每次点击"返回"按钮后,只清空了一半的Node。我静静的想了想
,这样写的话每次i<当前的结点长度,每删除一个结点长度都减一啊,瞎搞啊!!!真想抽自己大耳光。
谨以此文几年这一周蛋疼的链表生活。。。。。。