C语言内存管理(初级)----链表

    上一篇文章里实现了二维动态数组的创建和销毁,现在来看一个稍加复杂一点的实例:链表,读者需具有链表的基本知识,本文的链表实现与读者所熟知的实现有一些差异。
    假定我们要写一个计算器程序,它接受一个字符串形式的表达式,然后计算并输出其结果,我们先要解决的是它的词法分析部分,这是一个把输入的字符串分割成若干基本的表达式元素的过程,这些表达式元素包含运算符、运算数、括号,各自具有不同的属性,比如运算符具有优先级属性。我们需要将这些表达式元素存放在一个链表中,在计算过程中只需要递归计算就可以了。我们就来实现这个链表。

    表达式元素的结构如下:

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 函数,因为这会导致整个链表都被删除掉,必须先把这个节点从链表中断开来,再传给这个函数。
--<全文完>--

转载于:https://my.oschina.net/zhcosin/blog/96107

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值