表达式元素的结构如下:
struct exp_elem{ char *body; /* 字符串体 */ int type; /* 类型 */ struct exp_elem *parent; struct exp_elem *next;};而链表,不过就是指向其第一个节点的指针,在这里,我们称它为表达式:
typedef struct exp_elem* express_t;同时我们定义一个链表对象来存放我们的词法分析的结果:
express_t the_express;为了养成良好的内存管理习惯,内存分配成功后最好立即初始化,释放后最好立即将指针置为 NULL,以防止所谓的野指针问题,所以我们先写两个经过包装过的内存分配与释放的函数:
void *malloc_space(unsigned int n){ void *dest = NULL; dest = malloc(n); if (dest != NULL) memset(dest, 0, n); return dest;} int free_space(void **p){ if (p == NULL) return -1; if (*p != NULL) { free(*p); *p = NULL; } return 0;}读者可能会疑惑这个 free_space 居然使用了二级指针,因为我们需要修改指针本身的值(置为 NULL),那么就必须传递指针本身的地址,否则修改的是形参指针,而实参并未得到修改,这个在《C语言内存管理--动态数组》一文中已经阐述过。 回归正题,词法分析每识别出一个表达式元素,比如识别出一个运算符,或者一个数,就需要在将应的链表中加入一个节点,以实现由纯文本型表达式向有结构的表达式的转换,我们实现一个初始化节点指针的函数:
int exp_elem_init(struct exp_elem **dest){ if (dest == NULL) return -1; *dest = (struct exp_elem *)malloc_space(sizeof(struct exp_elem)); if (dest == NULL) return -2; (*dest)->body = NULL; (*dest)->type = -1; (*dest)->parent = NULL; (*dest)->next = NULL; return 0;}这样,我们就可以使用它来创建节点:
struct exp_elem *pnode = NULL;i = exp_elem_init(&pnode);if (i != 0) return -1;/* TODO .... */创建并初始化后,需要给各个成员赋值,于是我们写一个函数来完成:
int exp_elem_fill(struct exp_elem *dest, const char *v_body, int v_type) { if (dest == NULL) return -2; dest->body = strdup(v_body); dest->type = v_type; return 0;}利用此函数便可以完成填充节点各成员的需求,但有一个地方需要注意,即 strdup 函数是有内存分配的,我们在释放一个节点的时候一定要记得释放 body 成员所占用的空间。 现在我们需要实现把一个节点加到链表中去,函数的原型应当如下:
int express_add_elem(express_t **exp, struct exp_elem *v_elem);这里为什么使用二级指针呢,因为向链表中插入元素的过程是有可能修改头指针的,如果是向链表尾部插入元素,则只有插入第一个元素的时候需要修改头指针,而如果是向链表头部插入元素,则每插入一个元素都需要修改头指针,出于性能的考虑,我们选择向链表头部插入元素,虽然真正的计算器是应该向尾部插入的。接口定义好了,有一个问题值得讨论一下,就是在这个函数内部是直接把指针 v_elem 插入链表中呢,还是把它所指向的节点复制一个新的并插入链表,如果采用前者,那么在主函数中是绝对不能释放指针的,而采用后者则是必须释放的。这个看你自己喜好了,但我倾向于复制一个新的,因为如果有多个指针指向同一块内存空间,在释放的时候会产生野指针问题,我向来喜欢在程序中尽力使每一块申请的内存都只有一个指针在引用。具体实现如下:
int express_add_elem(express_t **exp, struct exp_elem *v_elem){ struct exp_elem *p_elem = NULL; int i = 0; if (exp == NULL || v_elem == NULL) return -1; if (v_elem->parent != NULL || v_elem->next != NULL) return -2; /* 在插入链表之前,它应是一个孤立的节点 */ i = exp_elem_init(&p_elem); if (i != 0) return -3; i = exp_elem_fill(p_elem, v_elem->body, v_elem->type); if (i != 0) return -4; p_elem->parent = NULL; p_elem->next = *exp; if (*exp != NULL) (*exp)->parent = p_elem; *exp = p_elem; return 0;}这样,当新的节点创建好之后,就可以这样插入链表中去:
i = express_add_elem(&the_express, p_elem);其中 p_elem 是新创建的节点指针。 有创建就应有释放,下面这个函数可以完成节点的释放工作:
int exp_elem_free(struct exp_elem **v_elem){ if (v_elem == NULL) return -1; if (*v_elem == NULL) return 0; free_space(&((*v_elem)->body)); if ((*v_elem)->parent != NULL) { exp_elem_free(&((*v_elem)->parent)); } if ((*v_elem)->next != NULL) { exp_elem_free(&((*v_elem)->next)); } *v_elem = NULL; return 0;}需要注意的是函数内部,用了递归的方法把一个节点的前一个节点和后一个节点都释放掉了,这里有一个重要的原则问题:如果结构本身包含有指针,而且分配的是堆上的内存,那么在释放结构之前一定要先释放这些指针所指向的内存,否则就是一块再也找不到地址的内存,从而造成内存泄漏。有了这个释放节点的函数,销毁链表的工作就变得非常轻松了,只要释放链表的第一个节点就可以了,因为它会递归的把其他后继节点都给释放掉:
int express_destroy(express_t *exp){ int i = 0; if (exp == NULL) return -1; i = exp_elem_free((struct exp_elem **)exp); if (i != 0) return -2; return 0;}但是需要注意的是如果需要删除链表中的某个元素,千万不能直接把节点地址传给 exp_elem_free 函数,因为这会导致整个链表都被删除掉,必须先把这个节点从链表中断开来,再传给这个函数。 --<全文完>--