C 語言程式設計教學:函式庫 (Library)

PUBLISHED ON SEP 16, 2018 — PROGRAMMING

C 語言沒有模組 (module) 的概念,但 C 編譯器的確支援多檔案編譯,讓我們可以將 C 程式碼以有意義的方式組織起來,在精神上也是用模組的概念整理 C 程式碼。雖然 C 語言也沒有套件 (package) 的概念,若某個 C 專案是函式庫 (library),我們可以把該專案視為一個 C 語言的套件。

一般 C 入門教材並不注重 C 函式庫的使用,但實際上我們不會每個函式庫都自己刻,而會藉由使用預先寫好的函式庫,減少重造輪子的時間,專注在我們想要實作的核心功能上。

註:使用外部函式庫要注恴授權範圍,初學者往往直接忽視這一塊就任意地使用外部函式庫。

在類 Unix 系統中,的確可以透過系統套件管理程式 (如 RPM 或 APT 等) 安裝 C 函式庫,這並不是原本 C 語言內建的功能,而是交由各個系統自行去實作。早期的 Windows 並不注重 C (或 C++) 套件的議題,在分享 C (或 C++) 函式庫時就沒有那麼方便,有些第三方方案,像是 Conan,企圖解決套件相關的議題;近年來 C++ 重新抬頭,微軟推出 vcpkg,也是另一個 C (或 C++) 套件的方案。

註:在不同程式語言中,對模組 (module) 和套件 (package) 賦予的實質意義各有不同,閱讀程式設計相關的文獻時,需注意其語境。

在本文中,我們使用一個小範例來說明如何撰寫 C 函式庫。由於 C 沒有規範專案如何安排,我們按照常見的 C 函式庫的專案架構來安排我們的程式碼。在此專案中,我們撰寫以 GNU Make 為基礎的 Makefile 來管理編譯流程,使用 Make 的好處是不用綁定特定的 IDE,只要編輯器支援 C 就可以使用此專案。這個範例專案位於這裡,這是一個二元搜尋樹的練習,但我們重點會放在如何以 C 撰寫套件,不會深入探討二元樹的實作,也不會額外講解 Makefile 的語法。

註:讀者可到這裡觀看 GNU Make 的教學,或是自行找尋其他的線上教材。

C 函式庫包括標頭檔 (header) 和二進位檔兩個部分,標頭檔存有該套件的公開界面,包括型別、函式、巨集等;二進位檔則是編譯後的套件實作內容。二進位檔又依其發布方式分為靜態函式庫和動態函式庫兩種;這兩種函式庫格式會影響應用程式發布的方式,靜態函式庫會直接將程式碼包進主程式中而動態函式庫會在執行時才去呼叫。

分享 C 函式庫時可以不公開 C 程式碼,只要有標頭檔和二進位檔即可使用該套件。近年來流行的開放原始碼運動算是一種軟體授權的策略或模式,對執行函式庫本身不是必備的。至於類 Unix 系統上常見的 /usr/include/usr/lib 等存放標頭檔或二進位檔的位置是由系統另外定義的,而非 C (或 C++) 內建的特性。

註:Windows 中,有一部分函式庫會額外使用 .def 檔案,基本上這也可視為函式庫的公開界面。

以本例來說,其中一個標頭檔如下:

#ifndef BSTREE_H
#define BSTREE_H

#ifndef __cplusplus
    #include <stdbool.h>
#endif

#ifdef __cplusplus
extern "C" {
#endif

typedef struct bstree_int BSTreeInt;

BSTreeInt * algo_bstree_int_new(void);
bool algo_bstree_int_is_empty(BSTreeInt *self);
bool algo_bstree_int_find(BSTreeInt *self, int value);
int algo_bstree_int_min(BSTreeInt *self);
int algo_bstree_int_max(BSTreeInt *self);
bool algo_bstree_int_insert(BSTreeInt *self, int value);
bool algo_bstree_int_delete(BSTreeInt *self, int value);
void algo_bstree_int_free(void *self);

#ifdef __cplusplus
}
#endif

#endif // BSTREE_H

標頭檔中放的是函式庫的宣告部分,實作則會另外放在 C 程式碼中。依照 C 語言的慣例,一般會用 .h 做為標頭檔的副檔名。

一開始時會用 include guard 的手法,避免重覆 include 時引發錯誤;另外,如果這個函式庫要從 C++ 呼叫,也要加入一些樣板程式碼。接著,就會開始寫宣告,包括引用的外部函式庫、型別、函式定義、巨集等。在這個例子中,我們不想暴露結構的內部實作,使用了 forward declaration 的小技巧。

接著,來看 bstree.c 的程式碼 (節錄):

#include <assert.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include "algo/bstree.h"
#include "bstree_internal.h"
#include "bstnode.h"

// More code ...

在我們這個例子中,重要的並不是二元樹如何實作,而是觀察檔案間的連動關係。細心的讀可應該可以發現,我們將 BSTreeIntNodeInt 兩個型別部分的程式碼拉出來,這是因為我們將此套件的實作拆開在 bstree.cbstiter.c 兩個檔案中,為了避免重覆的程式碼,我們將共同需要的部分拉出來,放在 *bstree_internal.h*、bstnode.hbstnode.c 中。

再看 bstiter.c 的程式碼 (節錄):

#include <assert.h>
#include <stdlib.h>
#include <stdio.h>
#include "algo/bstree.h"
#include "bstree_internal.h"
#include "bstnode.h"
#include "algo/bstiter.h"

// More code ...

從這裡可看出,這兩個模組都有用到共同的程式碼。這些程式碼會編譯成二進位檔案,而不會隨標頭檔發布出去,所以我們沒有放在 include 資料夾而放在 src 資料夾中。

如果讀者有看 bstiter.c 的程式碼,可發現我們在程式碼中額外塞入一個佇列,這是為了實作迭代器所需的資料結構,由於我們沒有規畫公開這個資料結構,從我們的程式碼可看出在標頭檔中完全都沒有這個佇列相關的資訊。

基本上,只要知道標頭標和實作程式碼間的關係,用模組化的概念寫 C 程式碼並不會太困難。至於多個檔案在編譯時要如何串連,這就牽涉到編譯器指令的操作,這也就是為什麼我們要在先前的文章中介紹 C 編譯器的指令。一開始覺得編譯器指令太難的話,不妨先用 IDE 現有的功能來完成這一部分,待熟悉這個流程後再轉用 CMake 或 GNU Make 等跨 IDE 的專案管理程式。

TAGS: C 語言, LIBRARY
comments powered by Disqus