contributed by <miaoshahaha
>
$ gcc --version
gcc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0
$lscpu
rchitecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Address sizes: 46 bits physical, 48 bits virtual
Byte Order: Little Endian
CPU(s): 20
On-line CPU(s) list: 0-19
Vendor ID: GenuineIntel
Model name: Intel(R) Core(TM) i5-14500
CPU family: 6
Model: 191
Thread(s) per core: 2
Core(s) per socket: 14
Socket(s): 1
Stepping: 2
CPU(s) scaling MHz: 19%
CPU max MHz: 5000.0000
CPU min MHz: 800.0000
BogoMIPS: 5222.40
在實作 lab0-c 之前,要先詳閱 queue.h
以及 list.h
,其中雙向鏈結串列的設計如下:
struct list_head {
struct list_head *prev;
struct list_head *next;
};
並且將 list_head 嵌入至結構體 element_t,可藉由 container_of
巨集推算出 list 節點的記憶體位址。
typedef struct {
char *value;
struct list_head list;
} element_t;
container_of
巨集可以在給定成員的型態及名稱,傳回成員的位址減去結構體的起始位址。
#define container_of(ptr, type, member) \
((type *) ((char *) (ptr) - offsetof(type, member)))
q_new
建立新的空佇列
struct list_head *q_new()
{
struct list_head *head = malloc(sizeof(struct list_head));
if (!head) {
return NULL;
}
INIT_LIST_HEAD(head);
return head;
}
先宣告一個 list_head
結構體 head
,並且分配記憶體空間,如果分配空間失敗,則回傳 NULL
,這邊用到了 INIT_LIST_HEAD
,可以將 head
的 next
跟 prev
指標指向自己。
這邊最一開始就要注意,當我們呼叫 malloc
的時候,並不保證每一次都會成功的分配記憶體空間,所以在呼叫 malloc
之後,必須確認是否成功分配記憶體空間,以防止 Segmentation Fault:
C99 (7.20.3)
If the space cannot be allocated, a null pointer is returned.
q_free
釋放佇列所佔用的記憶體空間
void q_free(struct list_head *head)
{
if (!head)
return;
element_t *entry, *safe = NULL;
list_for_each_entry_safe (entry, safe, head, list) {
q_release_element(entry);
}
free(head);
}
當我們要釋放記憶體空間的時候,除了要釋放雙向環狀鏈結佇列之外,還必須
連帶將 element_t
結構體中的元素釋放,釋放的方法是遍歷佇列中每個節點,透過 list_for_each_entry
可以拜訪每個節點,而其程式碼為:
#define list_for_each_entry(entry, head, member) \
for (entry = list_entry((head)->next, typeof(*entry), member); \
&entry->member != (head); \
entry = list_entry(entry->member.next, typeof(*entry), member))
裡面的 list_entry
就是藉由 container_of
來得知其中元素所在的記憶體位址為何。
#define list_entry(node, type, member) container_of(node, type, member)
q_insert_head
在佇列開頭插入給定的新節點
bool q_insert_head(struct list_head *head, char *s)
{
if (!head)
return false;
element_t *new_ele = malloc(sizeof(element_t));
if (!new_ele)
return false;
new_ele->value = strdup(s);
if (!new_ele->value) {
free(new_ele);
return false;
}
list_add(&new_ele->list, head);
return true;
}
做插入的時候先分配記憶體空間給結構體 element_t
,並且用 strdup
函式複製字串,它可以將副本的起始位址的指標回傳,最後我們透過 list_add
來新增節點至佇列開頭的下一個位置。
q_insert_tail
在佇列尾端插入給定的新節點
bool q_insert_tail(struct list_head *head, char *s)
{
if (!head)
return false;
element_t *new_ele = malloc(sizeof(element_t));
if (!new_ele)
return false;
new_ele->value = strdup(s);
if (!new_ele->value) {
free(new_ele);
return false;
}
list_add_tail(&new_ele->list, head);
return true;
}
這裡的做法跟插入節點到開頭唯一的差別就是用到了 list_add_tail
,
我們可以透過 list_add
以及 list_add_tail
來決定要將節點新增到頭部或尾端。
q_remove_head
在佇列開頭移去給定的節點
element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize)
{
if (!head || list_empty(head))
return NULL;
element_t *ele = list_first_entry(head, element_t, list);
if (sp) {
memcpy(sp, ele->value, bufsize - 1);
sp[bufsize - 1] = '\0';
}
list_del(&ele->list);
return ele;
}
這裡的做法是要移去給定的節點,這裡有個要留意的地方是,在 queue.h
的標頭檔有說明 'remove' 和 'delete' 是不同的,我們只需要移除節點即可,而不需要釋放記憶體空間。
移去給定節點方法是,如果 *sp
不是 NULL
則將元素移除,並且將移除元素字串複製 bufsize - 1
的大小到 *sp
,然後在最後加上 NULL terminator。這裡找出字串位址的方法是透過 list_first_entry
這個巨集先找出元素的位址,再間接的找出元素中 *value
的位址。
再來有個困難的地方是如何將字串複製到 *sp
,直覺的想法是直接用 strcpy
或者是 strncpy
,但我們需要的是 bufsize - 1
的大小,所以必須要給定一個複製的範圍,因此不能用 strcpy
,所以想法是用 strncpy
來複製字串到 *sp
當中,不過這邊有個考量是,在 C 語言規格書中所規範的 strncpy
是:
C99 (7.21.2.4)
If the array pointed to by s2 is a string that is shorter than n characters, null characters are appended to the copy in the array pointed to by s1, until n characters in all have been written.
如果複製字串小於我們所給定的大小,strncpy
會將超過原本字串的部分全部補 '0' ,因為是不必要的操作所以會導致效率不佳。其他的複製字串的方式就是使用 memcpy
,它的差別是給定多少的大小,它就複製多少的空間,這個方法符合 q_release_head
的需求,所以我們只需要按照它的需求,複製 bufsize - 1
大小,並且在最後自行補上 NULL terminator 就可以。
q_remove_tail
在佇列尾端移去給定的節點
element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize)
{
if (!head || list_empty(head))
return NULL;
element_t *ele = list_last_entry(head, element_t, list);
if (sp) {
memcpy(sp, ele->value, bufsize - 1);
sp[bufsize - 1] = '\0';
}
list_del(&ele->list);
return ele;
}
這裡的方法大致上跟 q_remove_head
相同,不一樣的地方在我們要移除的是尾端的節點,因此就用 list_last_entry
來得到尾端元素的記憶體位址,就可以做移除了。
q_size
計算目前佇列的總節點量
int q_size(struct list_head *head)
{
if (!head || list_empty(head))
return 0;
struct list_head *next;
int cnt = 0;
list_for_each (next, head) {
cnt++;
}
return cnt;
}
這裡只要遍歷每個節點即可,用一個計數器來計算節點數量。
q_delete_mid
移走佇列中間的節點
bool q_delete_mid(struct list_head *head)
{
if (!head || list_empty(head))
return false;
struct list_head *slow = head->next, *fast = head->next->next;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
element_t *del_ele = list_entry(slow, element_t, list);
list_del(&del_ele->list);
q_release_element(del_ele);
return true;
}
要找出中間節點的方法只要透過快慢指標就可以快速找到,當找到中間節點之後,再透過 list_entry
得到元素的記憶體位址,接著移除節點,並釋放元素佔據的記憶體空間即可,這裡透過 queue.h
中的靜態變數 q_release_element
來釋放元素記憶體空間。
q_swap
void q_swap(struct list_head *head)
{
if (!head)
return;
struct list_head *node, *safe;
for (node = (head)->next, safe = node->next;
node != (head) && safe != (head);
node = node->next, safe = node->next) {
list_move(node, safe);
}
}
這邊要針對佇列中兩兩節點做交換,接著用 list_move
來交換節點。
q_reverse
void q_reverse(struct list_head *head)
{
if (!head)
return;
struct list_head *node;
struct list_head *safe;
list_for_each_safe (node, safe, head) {
list_move(node, head);
}
}
以反向順序重新排列鏈結串列,並且不配置額外的記憶體空間來操作,這個做法可以透過遍歷每個節點,並且將正在拜訪的節點移到頭部,如此即可將鏈結串列反向排列。
q_reverseK
void q_reverseK(struct list_head *head, int k)
{
if (!head)
return;
struct list_head *tmp = head;
LIST_HEAD(tmp_head);
struct list_head *node, *safe;
int cnt = 0;
list_for_each_safe (node, safe, head) {
cnt++;
if (cnt == k) {
list_cut_position(&tmp_head, tmp, node);
q_reverse(&tmp_head);
list_splice_init(&tmp_head, tmp);
}
cnt = 0;
tmp = node->next;
}
}
透過 q_release
來進行反向順序的操作,並且使用計數器計算遍歷過的節點,當遍歷的節點等於 k
,便將該段串列進行反向排列。
在 qtest.c
中會呼叫 console_init()
命令,我們可以在 console_init()
中新增 shuffle
命令,觀察新增命令使用了 ADD_COMMAND
巨集如下:
#define ADD_COMMAND(cmd, msg, param) add_cmd(#cmd, do_##cmd, msg, param)
所以要建立新的命令,只要在 qtest.c
檔案中先提供 do_*
函式,並且在 console_init()
中新增即可,假設要新增 shuffle
命令:
bool do_shuffle(int argc, char *argv[])
{
/* my_shuffle_function*/
return (bool) printf("Hello, World\n");
}