# 2025q1 Homework1 (lab0)
contributed by < `JUSTUSVMOS` >
{%hackmd NrmQUGbRQWemgwPfhzXj6g %}
### 開發環境
```shell
$ gcc --version
gcc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0
$ lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Address sizes: 39 bits physical, 48 bits virtual
Byte Order: Little Endian
CPU(s): 8
On-line CPU(s) list: 0-7
Vendor ID: GenuineIntel
Model name: Intel(R) Core(TM) i7-9700K CPU @ 3.60GHz
CPU family: 6
Model: 158
Thread(s) per core: 1
Core(s) per socket: 8
Socket(s): 1
Stepping: 13
BogoMIPS: 7200.01
```
## Linux Kernal 風格鏈結串列
在 Linux kernel 中,鏈結串列的基本元件是 `struct list_head`。其定義通常如下:
```c
struct list_head {
struct list_head *prev;
struct list_head *next;
};
```
```graphviz
digraph list_head_only {
rankdir=TB;
graph [ranksep=0.2];
node [shape=record, style=bold, margin=0.05];
subgraph cluster_00 {
label="list_head";
style=bold;
head [label="{ {<next>next} | {<prev>prev} }", width=1.35, fixedsize=true];
}
}
```
- 雙向鏈結串列:list_head 包含兩個指標,分別指向前一個及後一個節點,形成一個雙向鏈結串列。
- 循環鏈結串列:設計上,鏈結串列通常是循環的,這表示鏈結串列頭的 prev 指向最後一個節點,而最後一個節點的 next 指向鏈結串列頭。這種設計可以簡化插入與刪除操作時對邊界情形的處理。
為了靈活運用鏈結串列,Linux kernel 採用「嵌入式」設計,只需在自訂型別中加入一個 `struct list_head` 成員,即可將該型別放入鏈結串列。:
`element_t`:
通過嵌入 struct list_head 成員,使得每個 element_t 可以成為一個節點,串聯成一個循環雙向鏈結串列,便於管理隊列中的各個元素。
```c
typedef struct {
int data;
struct list_head list;
} element_t;
```
```graphviz
digraph list {
rankdir=TB;
graph [ranksep=0.1];
node [shape=record, style=bold, margin=0.05];
subgraph cluster_0 {
label="element_t";
style=bold;
// list head 區塊
subgraph cluster_00 {
label="list_head";
style=bold;
head [label="{ {<next>next} | {<prev>prev} }", width=1.5, fixedsize=true];
}
value [label="value", width=1.65, fixedsize=true];
head -> value [style=invis, weight=10];
}
}
```
`queue_contex_t`:
包含一個指向實際鏈結串列(由 element_t 節點組成)的 q 指標,以及一個 chain 成員用於將多個隊列上下文連成一個雙向鏈結串列,另外還有記錄隊列大小及唯一標識的成員。
```c
typedef struct {
struct list_head *q;
struct list_head chain;
int size;
int id;
} queue_contex_t;
```
- q 是一個指向 struct list_head 的指標,指向該隊列的頭節點。這個鏈結串列頭管理著由 element_t 組成的節點列表。
- chain 是一個 struct list_head 變數,用於將多個 queue_contex_t 結構串聯在一起。
這樣可以形成一個雙向鏈結串列,使得各個隊列上下文可以被集中管理。
- size 用於記錄該隊列中的元素數量。
- id 用於唯一標識該隊列。
```graphviz
digraph list {
rankdir=TB;
graph [ranksep=0.1]; // 調整上下節點間的間距
node [shape=record, style=bold, margin=0.05];
subgraph cluster_0 {
label="queue_contex_t";
style=bold;
// list head 區塊
subgraph cluster_00 {
label="list_head";
style=bold;
head [label="{ {<next>next} | {<prev>prev} }", width=1.5, fixedsize=true];
}
q [label="q", width=1.65, fixedsize=true];
size [label="size", width=1.65, fixedsize=true];
id [label="id", width=1.65, fixedsize=true];
head -> q -> size ->id [style=invis, weight=10];
}
}
```
## 指定的佇列操作
### q_delete_dup
此函式刪除排已序鍊錶中所有重複的節點,只保留不重複的數字
我利用 `prev` 和 `next ` 指標來遍歷排序鏈表,檢測重複的節點。當發現重複值時,你用指標跳過所有重複節點,並重新連接和,移除重複部分,確保鏈表只保留不重複的節點
```c
if (curr->value == nxt->value) {
while (nxt->value == curr->value) {
nxt = nxt->next;
}
prev->next = nxt;
nxt->prev = prev;
curr = nxt;
} else {
prev = curr;
curr = curr->next;
}
```
---
#### 使用 Valgind檢查後報錯
```
ERROR: There is no queue, but 14 blocks are still allocated
ERROR: There is no queue, but 14 blocks are still allocated
ERROR: Freed queue, but 14 blocks are still allocated
==309464== 323 bytes in 7 blocks are still reachable in loss record 1 of 2
==309464== at 0x4846828: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
...
==309464== 448 bytes in 7 blocks are still reachable in loss record 2 of 2
==309464== at 0x4846828: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
```
經檢查後發現,我只是繞過重複節點,更新指標接法,但原本的 到 節點依然存在,記憶體沒釋放,這就造成了 memory leak(你在 valgrind 裡看到的 14 blocks)。
```diff
if (curr->value == nxt->value) {
while (nxt->value == curr->value) {
nxt = nxt->next;
}
+ while (nxt->value == curr->value) {
+ to_delete = nxt;
+ nxt = nxt->next;
+ unlink(to_delete); // list_del()
+ free(to_delete); // q_release_element()
+ }
+
+ unlink(curr); // list_del()
+ free(curr); // q_release_element()
prev->next = nxt;
nxt->prev = prev;
curr = nxt;
} else {
prev = curr;
curr = curr->next;
}
```
在刪除每個重複節點(包含 curr 本身)時:
- 先從鍊表中移除
- 呼叫 q_release_element() 釋放 value 和 element_t
這就避免了記憶體洩漏
太好了!你已經成功把 `q_delete_dup` 的邏輯、Valgrind 報錯原因、修正 diff 都用條理清楚的筆記方式整理出來了👏
接下來我就**依照你這種風格**,幫你寫一份 **`q_merge()` 的問題分析 + 修正說明(含 `diff`)**,語氣、格式、風格都跟你提供的 `q_delete_dup` 範本一致。
---
### q_merge
此函式將多個 queue 合併成一個單一的排序 queue,並依據參數 `descend` 進行升/降冪排序。
在 merge 的過程中,我使用 `list_splice_init()` 將所有 queue 的元素節點轉移到第一個 queue 中(`first->q`),然後針對它執行排序。
```c
if (qc->q && !list_empty(qc->q)) {
list_splice_init(qc->q, first->q); // 把元素合併進第一個 queue
first->size += qc->size;
qc->q = NULL; // ❌ 嘗試清除原 queue 指標
}
qc->size = 0;
```
#### 使用 Valgrind 檢查後報錯:
```
ERROR: Freed queue, but 2 blocks are still allocated
==367659== 112 bytes in 2 blocks are still reachable in loss record 1 of 1
==367659== by 0x10FDAC: q_new (queue.c:16)
```
---
這個錯誤是因為 `qc->q = NULL;` 這行在邏輯上**斷開了 queue 的記憶體指標**,導致 framework 在最後呼叫 `q_free()` 時,無法釋放 `qc->q` 中原本 malloc 的記憶體,進而產生 **memory leak**。
其實 `list_splice_init()` 已經會清空 `qc->q` 的 list 結構,因此不需要額外設 `qc->q = NULL`。
只要保留指標,**讓 framework 的 `q_free()` 能正常處理這個空 queue**,就能避免記憶體洩漏。
```diff
if (qc->q && !list_empty(qc->q)) {
list_splice_init(qc->q, first->q);
first->size += qc->size;
- qc->q = NULL;
+ INIT_LIST_HEAD(qc->q); // 清空 list,但保留指標供 framework 使用
}
qc->size = 0;
```
- 不要抹除 `qc->q` 指標,否則 `q_free()` 找不到它
- 用 `INIT_LIST_HEAD(qc->q)` 清空內容,保證不會重複釋放
- `q_free()` 呼叫時,仍能進入處理釋放流程(即使 list 為空)
---