C基礎 之 list 庫奧義

来源:https://www.cnblogs.com/life2refuel/archive/2018/06/05/9131511.html
-Advertisement-
Play Games

list 是電腦軟體體系基石. 是數據結構開始和結束. 同樣也是 C 解決複雜問題的跳板. ...


前言 - 關於 list 思考

  list 是最基礎的數據結構也是數據結構的基礎. 高級 C 代碼紐帶也是 list.

扯一點, 當你走進了 C 的殿堂, 那麼你和 list 增刪改查那就是一輩子丫 ~ 這裡不妨分享一下作者對於 list 認知經歷的幾個階段 (比心)
i) 原生鏈表
struct list {
    struct list * next;
    ...
}

鏈表結構和業務數據綁定在一起. 朴實無華麗, 重劍可破軍

ii) 萬能鏈表
struct list {
    struct list * next;
    void * node;
}

所有業務結點抽象為 void * 萬能指針. 瑕疵是存在 sizeof (void *) 記憶體浪費.

像一杯甜酒喝起來還挺爽, 只是熱量有點高.
iii) 內核鏈表
struct $list {
    struct $list * next;
};

#define $LIST_HEAD struct $list $node

$LIST_HEAD 巨集放在需要實現的鏈式結構的頭位置. 有點繼承味道, 例如下麵這樣

struct list {
    $LIST_HEAD;
    ...
}

住用利用隱含條件 &list = &(list.$node) => list->next = &(list.$node)->next.

鏈表結點首地址和業務結點首地址值想等. 在內核看挺常用的. 四兩撥千斤 ~   iv) 註冊鏈表
struct $list {
    struct $list * next;
};

#define $LIST struct $list $node;

typedef struct {
    struct $list * root; // 存儲鏈表的頭節點

    icmp_f fadd; // 鏈表中插入數據執行的方法
    icmp_f fget; // 鏈表中查找數據執行的方法
    node_f fdie; // 鏈表中刪除數據執行的方法
} * list_t;

//
// list_next - 獲取結點n的下一個結點.
// n        : 當前結點
//
#define list_next(n) ((void *)((struct $list *)(n))->next)

//
// list_create - 構建 list 對象
// fadd : 插入數據方法
// fget : 獲取數據方法
// return : 創建好的鏈表對象
//
#define list_create(fadd, fget) \
list_create_((icmp_f)fadd, (icmp_f)fget)

inline list_t list_create_(icmp_f fadd, icmp_f fget) {
    list_t list = malloc(sizeof *list);
    list->root = NULL;
    list->fadd = fadd;
    list->fget = fget;
    list->fdie = NULL;
    return list;
}

註冊行為定義如下

//
// icmp_f - 比較行為的類型
// : int add_cmp(const void * now, const void * node)
//
typedef int (* icmp_f)();

//
// node_f - 銷毀當前對象節點
// : void list_die(void * node);
//
typedef void (* node_f)(void * node);

當時產生這個想法是太迷戀基於函數註冊的方式. 希望一次註冊終身受用.

但在實戰中發現, C 很多時候只用到局部部分. 功能越強大, 考慮的越全面, 代碼寫起來就越難受. 等同於衣服太多, 搬家就會很麻煩. 領證還要房子車子票子, 這麼麻煩, 那結個 pi 呀. 必須要整改 :)
v) 取捨鏈表
struct list {
    struct list * next;
    ...
}

or

//
// list.h 通用的單鏈表庫
// void * list = NULL;
//
struct $list {
    struct $list * next;
};

#define $LIST struct $list $node;

簡單業務上使用第一個原生鏈表, 在特定場合(順序有要求)使用內核鏈表.

成熟在於取捨, 渣往往是抉擇的時候不定, 遇到的時候不剋制. 有感情那 OK, 有票子 那 OK, else 自己玩毛 ~
  說了這麼多沒用的, 希望讀者能夠理解作者關於鏈表結構的思考心路. 本文後續重點就是講解 $LIST ~  

正文 - 介面設計

   list 首先從總體介面設計感受此中氣息

//
// list.h 通用的單鏈表庫
// void * list = NULL;
// 
struct $list {
    struct $list * next;
};

#define $LIST struct $list $node;

//
// list_next - 獲取結點n的下一個結點.
// n        : 當前結點
//
#define list_next(n) ((void *)((struct $list *)(n))->next)

//
// list_delete - 鏈表數據銷毀操作
// list     : 基礎的鏈表結構
// pist     : 指向基礎的鏈表結構
// fdie     : 鏈表中刪除數據執行的方法
// return   : void
//
#define list_delete(list, fdie)                                         \
list_delete_(&(list), (node_f)(fdie))
extern void list_delete_(void ** pist, node_f fdie);

//
// list_get - 匹配得到鏈表中指定值
// list     : 基礎的鏈表結構
// fget     : 鏈表中查找數據執行的方法
// left     : 待查找的結點內容 
// return   : 查找到的節點, NULL 表示沒有查到
//
#define list_get(list, fget, left)                                      \
list_get_((list), (icmp_f)(fget), (const void *)(intptr_t)(left))
extern void * list_get_(void * list, icmp_f fget, const void * left);

//
// list_pop - 匹配彈出鏈表中指定值
// list     : 基礎的鏈表結構
// pist     : 指向基礎的鏈表結構
// fget     : 鏈表中查找數據執行的方法
// left     : 待查找的結點內容 
// return   : 查找到的節點, NULL 表示沒有查到 
//
#define list_pop(list, fget, left)                                      \
list_pop_(&(list), (icmp_f)(fget), (const void *)(intptr_t)(left))
extern void * list_pop_(void ** pist, icmp_f fget, const void * left);

//
// list_add - 鏈表中添加數據, 從小到大 fadd(left, ) <= 0
// list     : 基礎的鏈表結構
// pist     : 指向基礎的鏈表結構
// fadd     : 插入數據方法
// left     : 待插入的鏈表結點
// return   : void
//
#define list_add(list, fadd, left)                                      \
list_add_(&(list), (icmp_f)(fadd), (void *)(intptr_t)(left))
extern void list_add_(void ** pist, icmp_f fadd, void * left);

大量用到一個巨集技巧

// list     : 基礎的鏈表結構
// pist     : 指向基礎的鏈表結構
#define list_delete(list, fdie)                                         \
list_delete_(&(list), (node_f)(fdie))

通過巨集將一維指針轉成二維指針來使用. 缺點是指針不可複製. 或者複製後不能再使用上一個指針.

(等同於破壞型智能指針)優勢在於瀟灑, 寧可 BUG 不斷, 也要帥氣到底 ~

 

介面實現部分

開頭從 delete 講起. C 可以沒有 create(alloc) , 但一定要有 delete(free). 來不及銷毀證據那就不用出去嗨了.

//
// list_delete - 鏈表數據銷毀操作
// pist     : 指向基礎的鏈表結構
// fdie     : 鏈表中刪除數據執行的方法
// return   : void
//
void 
list_delete_(void ** pist, node_f fdie) {
    if (pist && fdie) {
        // 詳細處理鏈表數據變化
        struct $list * head = *pist;
        while (head) {
            struct $list * next = head->next;
            fdie(head);
            head = next;
        }
        *pist = NULL;
    }
}

核心招式在於 *pist = NULL; 希望置空. (雖然沒有卵用, 因為指針可複製, 存在多個引用)

如果場景不允許複製的話, 可以一用. 

 

對於後面幾個函數核心設計圍繞頭結點處理上, 如果處理的對象是頭結點, 需要重新設置.

//
// list_get - 匹配得到鏈表中指定值
// list     : 基礎的鏈表結構
// fget     : 鏈表中查找數據執行的方法
// left     : 待查找的結點內容 
// return   : 查找到的節點, NULL 表示沒有查到
//
void * 
list_get_(void * list, icmp_f fget, const void * left) {
    if (fget) {
        struct $list * head = list;
        while (head) {
            if (fget(left, head) == 0)
                return head;
            head = head->next;
        }
    }
    return NULL;
}

//
// list_pop - 匹配彈出鏈表中指定值
// pist     : 指向基礎的鏈表結構
// fget     : 鏈表中查找數據執行的方法
// left     : 待查找的結點內容 
// return   : 查找到的節點, NULL 表示沒有查到 
//
void * 
list_pop_(void ** pist, icmp_f fget, const void * left) {
    struct $list * head, * next;
    if (!pist || fget)
        return NULL;

    // 看是否是頭節點
    head = *pist;
    if (fget(left, head) == 0) {
        *pist = head->next;
        return head;
    }

    // 不是頭節點挨個處理
    while (!!(next = head->next)) {
        if (fget(left, next) == 0) {
            head->next = next->next;
            return next;
        }
        head = next;
    }

    return NULL;
}

//
// list_next - 獲取結點n的下一個結點.
// n        : 當前結點
//
#undef  list_next
#define list_next(n) ((struct $list *)(n))->next

//
// list_add - 鏈表中添加數據, 從小到大 fadd(left, ) <= 0
// pist     : 指向基礎的鏈表結構
// fadd     : 插入數據方法
// left     : 待插入的鏈表結點
// return   : void
//
void 
list_add_(void ** pist, icmp_f fadd, void * left) {
    struct $list * head;
    if (!pist || !fadd || !left)
        return;
    
    // 看是否是頭結點
    head = *pist;
    if (!head || fadd(left, head) <= 0) {
        list_next(left) = head;
        *pist = left;
        return;
    }

    // 不是頭節點, 挨個比對
    while (head->next) {
        if (fadd(left, head->next) <= 0)
            break;
        head = head->next;
    }

    // 添加最終的連接關係
    list_next(left) = head->next;
    head->next = left;
}

很多代碼強烈推薦自己多打幾遍. 這是實踐派絕招, 可以啥都不懂, 但會寫(有思考更好)應該也是及格吧. 

其中 list_next 巨集設計思路也很灑脫. 對外暴露是讀操作, 對內是寫操作.

 

這裡不妨贈送個測試介面

//
// node_f - 銷毀當前對象節點
//  : void list_die(void * node); 
//
typedef void (* node_f)(void * node);

//
// list_each - 鏈表迴圈處理函數, 僅僅測試而已
// list     : 基礎的鏈表結構
// feach    : 處理每個結點行為函數
// return   : void
//
#define list_each(list, feach)                                          \
list_each_((list), (node_f)(feach))
extern void list_each_(void * list, node_f feach);

void 
list_each_(void * list, node_f feach) {    
    if (list && feach) {
        struct $list * head = list;
        while (head) {
            struct $list * next = head->next;
            feach(head);
            head = next;
        }
    }
}

 

list 使用 demo 可以參照這下麵的寫法

#define INT_NAME (64)

struct peoples {
    $LIST

    double age;
    char name[INT_NAME + 1];
};

// peoples_add : 預設年齡從小到大排序, 並且獲取
inline static int peoples_add(struct peoples * left, struct peoples * node) {
    return (int)(left->age - node->age);
}

// peoples_each : 單純的列印介面信息
inline static void peoples_each(struct peoples * node) {
    printf("age = %9.6lf, name = %s\n", node->age, node->name);
}

//
// list test demo
//
void list_test(void) {
    void * peops = NULL;

    // 這裡添加數據
    struct peoples peop[5];
    for (int i = 0; i < LEN(peop); ++i) {
        peop[i].age = rand() % 100 + rand() * 1.0 / rand();
        snprintf(peop[i].name, LEN(peop[i].name), "peop_%d", i);
        list_add(peops, peoples_add, peop + i);
    }

    // 這裡列印數據
    list_each(peops, peoples_each);
}

到這關於 list 瞭解的一切都傳入糖果中 : ) 更好例子, 基於 list 設計了重覆定時器例子

  timer - https://github.com/wangzhione/structc/blob/master/structc/base/timer.c

(扯一點, 定時器有很多實現思路. 採用 list, heap, double list, array + list 都有, 看應用領域.) 能夠寫好 list,

算數據結構結業了吧. 想起朴實的大學數學老師說, 走出學校的時候還記得數學分析, 那數學系就算學合格了.

現在想起來有些心痛, 真實在.  對於大家都懂的需要多練習,  對於都不明白的需要多調研.

順勢而為, 伴傑遮天.

 

後記 - 有序展望

  錯誤和成長是難免的歡迎指正 ~ :-

小雨中的回憶 - https://music.163.com/#/song?id=119664

 

:- >

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 說到提高檢索效率,就必然提到索引。今天就來為大家講述搜索引擎中最常見的索引方式——倒排索引。 ...
  • 恢復內容開始 從上個隨筆停止的地方開始,我們將設置資料庫,創建第一個模型。並快速介紹django自動生成的管理站點。 資料庫設置: 打開文件mysite/settings.py 預設情況下,配置使用SQLite。這是新手最簡單的選擇。 雖然你不暫時不用其他資料庫,但是還是要說明一下的: ENGINE ...
  • 1. 什麼是偽共用 CPU 緩存系統中是以緩存行(cache line)為單位存儲的。目前主流的 CPU Cache 的 Cache Line 大小都是 64 Bytes。在多線程情況下,如果需要修改“共用同一個緩存行的變數”,就會無意中影響彼此的性能,這就是偽共用(False Sharing)。 ...
  • 今天是 Github 嫁入豪門的第 2 天,炒得沸沸揚揚的微軟 Github 收購事件於昨天(06月04日)塵埃落定,微軟最終以 75 億美元正式收購 Github。 隨後,Gitlab 趁勢帶了一波節奏,在其官網上祝賀 Github 被微軟收購,並表示此次收購代表著軟體開發者的影響力的日漸增長,將 ...
  • ​ Web程式開發中最重要的莫過於關係型資料庫,即SQL 資料庫,另外文檔資料庫(如 mongodb)、鍵值對資料庫(如 redis)慢慢變得流行. 原因 : 我們不直接使用這些資料庫引擎提供的 Python 包,而是使用對象關係映射(Object Relational Mapper, ORM)框架 ...
  • LAMP環境搭建的博客,在提交內容的時候TP5框架報了一個錯誤,Call to undefined function imagecreatefrompng(); 出現這個問題一般都是GD庫未正確安裝或配置,在伺服器上查詢是否安裝輸入命令: 原來是沒有安裝GD庫,在centOS系統上安裝GD庫可以直接 ...
  • Java源文件編碼格式不是ANSI時引起的錯誤 & 使用Java包不當引起的錯誤:找不到或無法載入主類,及其解決方法。 ...
  • Java開源生鮮電商平臺-電商促銷業務分析設計與系統架構(源碼可下載) 說明:Java開源生鮮電商平臺-電商促銷業務分析設計與系統架構,列舉的是常見的促銷場景與源代碼下載 左側為享受促銷的資格,常見為這三種: 首單 大於或等於某個會員級別 特定會員組:比如女性,月消費滿1000等等,都是通過查詢條件 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...