上一篇文章裡實現了二維動態數組的建立和銷毀,現在來看一個稍加複雜一點的執行個體:鏈表,讀者需具有鏈表的基本知識,本文的鏈表實現與讀者所熟知的實現有一些差異。
假定我們要寫一個計算機程式,它接受一個字串形式的運算式,然後計算並輸出其結果,我們先要解決的是它的詞法分析部分,這是一個把輸入的字串分割成若干基本的運算式元素的過程,這些運算式元素包含運算子、運算數、括弧,各自具有不同的屬性,比如運算子具有優先順序屬性。我們需要將這些運算式元素存放在一個鏈表中,在計算過程中只需要遞迴計算就可以了。我們就來實現這個鏈表。
運算式元素的結構如下:
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 函數,因為這會導致整個鏈表都被刪除掉,必須先把這個節點從鏈表中斷開來,再傳給這個函數。
--<全文完>--