1. 程式人生 > >C語言記憶體管理(初級)----連結串列

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 函式,因為這會導致整個連結串列都被刪除掉,必須先把這個節點從連結串列中斷開來,再傳給這個函式。
--<全文完>--