如何組織構建多文件 C 語言程序(二)
在第一篇中,我設計了一個名為喵嗚喵嗚的多文件 C 程序,該程序實現了一個玩具編解碼器。我也提到了程序設計中的 Unix 哲學,即在一開始創建多個空文件,並建立一個好的結構。最後,我創建了一個 Makefile
文件夾並闡述了它的作用。在本文中將另一個方向展開:現在我將介紹簡單但具有指導性的喵嗚喵嗚編解碼器的實現。
當讀過我的《如何寫一個好的 C 語言 main 函數》後,你會覺得喵嗚喵嗚編解碼器的 main.c
文件的結構很熟悉,其主體結構如下:
/* main.c - 喵嗚喵嗚流式編解碼器 */
/* 00 系統包含文件 */
/* 01 項目包含文件 */
/* 02 外部聲明 */
/* 03 定義 */
/* 04 類型定義 */
/* 05 全局變數聲明(不要用)*/
/* 06 附加的函數原型 */
int main(int argc, char *argv[])
{
/* 07 變數聲明 */
/* 08 檢查 argv[0] 以查看該程序是被如何調用的 */
/* 09 處理來自用戶的命令行選項 */
/* 10 做點有用的事情 */
}
/* 11 其它輔助函數 */
包含項目頭文件
位於第二部分中的 /* 01 項目包含文件 */
的源代碼如下:
/* main.c - 喵嗚喵嗚流式編解碼器 */
...
/* 01 項目包含文件 */
#include "main.h"
#include "mmecode.h"
#include "mmdecode.h"
#include
是 C 語言的預處理命令,它會將該文件名的文件內容拷貝到當前文件中。如果程序員在頭文件名稱周圍使用雙引號(""
),編譯器將會在當前目錄尋找該文件。如果文件被尖括弧包圍(<>
),編譯器將在一組預定義的目錄中查找該文件。
main.h 文件中包含了 main.c 文件中用到的定義和類型定義。我喜歡儘可能多將聲明放在頭文件里,以便我在我的程序的其他位置使用這些定義。
頭文件 mmencode.h 和 mmdecode.h 幾乎相同,因此我以 mmencode.h
為例來分析。
/* mmencode.h - 喵嗚喵嗚流編解碼器 */
#ifndef _MMENCODE_H
#define _MMENCODE_H
#include <stdio.h>
int mm_encode(FILE *src, FILE *dst);
#endif /* _MMENCODE_H */
#ifdef
、#define
、#endif
指令統稱為 「防護」 指令。其可以防止 C 編譯器在一個文件中多次包含同一文件。如果編譯器在一個文件中發現多個定義/原型/聲明,它將會產生警告。因此這些防護措施是必要的。
在這些防護內部,只有兩個東西:#include
指令和函數原型聲明。我在這裡包含了 stdio.h
頭文件,以便於能在函數原型中使用 FILE
定義。函數原型也可以被包含在其他 C 文件中,以便於在文件的命名空間中創建它。你可以將每個文件視為一個獨立的命名空間,其中的變數和函數不能被另一個文件中的函數或者變數使用。
編寫頭文件很複雜,並且在大型項目中很難管理它。不要忘記使用防護。
喵嗚喵嗚編碼的最終實現
該程序的功能是按照位元組進行 MeowMeow
字元串的編解碼,事實上這是該項目中最簡單的部分。截止目前我所做的工作便是支持允許在適當的位置調用此函數:解析命令行,確定要使用的操作,並打開將要操作的文件。下面的循環是編碼的過程:
/* mmencode.c - 喵嗚喵嗚流式編解碼器 */
...
while (!feof(src)) {
if (!fgets(buf, sizeof(buf), src))
break;
for(i=0; i<strlen(buf); i++) {
lo = (buf[i] & 0x000f);
hi = (buf[i] & 0x00f0) >> 4;
fputs(tbl[hi], dst);
fputs(tbl[lo], dst);
}
}
簡單的說,當文件中還有數據塊時( feof(3)
),該循環讀取(feof(3)
)文件中的一個數據塊。然後將讀入的內容的每個位元組分成兩個 hi
和 lo
的 半位元組 。半位元組是半個位元組,即 4 個位。這裡的奧妙之處在於可以用 4 個位來編碼 16 個值。我將 hi
和 lo
用作 16 個字元串查找表 tbl
的索引,表中包含了用半位元組編碼的 MeowMeow
字元串。這些字元串使用 fputs(3)
函數寫入目標 FILE
流,然後我們繼續處理緩存區的下一個位元組。
該表使用 table.h 中的宏定義進行初始化,在沒有特殊原因(比如:要展示包含了另一個項目的本地頭文件)時,我喜歡使用宏來進行初始化。我將在未來的文章中進一步探討原因。
喵嗚喵嗚解碼的實現
我承認在開始工作前花了一些時間。解碼的循環與編碼類似:讀取 MeowMeow
字元串到緩衝區,將編碼從字元串轉換為位元組
/* mmdecode.c - 喵嗚喵嗚流式編解碼器 */
...
int mm_decode(FILE *src, FILE *dst)
{
if (!src || !dst) {
errno = EINVAL;
return -1;
}
return stupid_decode(src, dst);
}
這不符合你的期望嗎?
在這裡,我通過外部公開的 mm_decode()
函數公開了 stupid_decode()
函數細節。我上面所說的「外部」是指在這個文件之外。因為 stupid_decode()
函數不在該頭文件中,因此無法在其他文件中調用它。
當我們想發布一個可靠的公共介面時,有時候會這樣做,但是我們還沒有完全使用函數解決問題。在本例中,我編寫了一個 I/O 密集型函數,該函數每次從源中讀取 8 個位元組,然後解碼獲得 1 個位元組寫入目標流中。較好的實現是一次處理多於 8 個位元組的緩衝區。更好的實現還可以通過緩衝區輸出位元組,進而減少目標流中單位元組的寫入次數。
/* mmdecode.c - 喵嗚喵嗚流式編解碼器 */
...
int stupid_decode(FILE *src, FILE *dst)
{
char buf[9];
decoded_byte_t byte;
int i;
while (!feof(src)) {
if (!fgets(buf, sizeof(buf), src))
break;
byte.field.f0 = isupper(buf[0]);
byte.field.f1 = isupper(buf[1]);
byte.field.f2 = isupper(buf[2]);
byte.field.f3 = isupper(buf[3]);
byte.field.f4 = isupper(buf[4]);
byte.field.f5 = isupper(buf[5]);
byte.field.f6 = isupper(buf[6]);
byte.field.f7 = isupper(buf[7]);
fputc(byte.value, dst);
}
return 0;
}
我並沒有使用編碼器中使用的位移方法,而是創建了一個名為 decoded_byte_t
的自定義數據結構。
/* mmdecode.c - 喵嗚喵嗚流式編解碼器 */
...
typedef struct {
unsigned char f7:1;
unsigned char f6:1;
unsigned char f5:1;
unsigned char f4:1;
unsigned char f3:1;
unsigned char f2:1;
unsigned char f1:1;
unsigned char f0:1;
} fields_t;
typedef union {
fields_t field;
unsigned char value;
} decoded_byte_t;
初次看到代碼時可能會感到有點兒複雜,但不要放棄。decoded_byte_t
被定義為 fields_t
和 unsigned char
的 聯合。可以將聯合中的命名成員看作同一內存區域的別名。在這種情況下,value
和 field
指向相同的 8 位內存區域。將 field.f0
設置為 1
也將會設置 value
中的最低有效位。
雖然 unsigned char
並不神秘,但是對 fields_t
的類型定義(typedef
)也許看起來有些陌生。現代 C 編譯器允許程序員在結構體中指定單個位欄位的值。欄位所在的類型是一個無符號整數類型,並在成員標識符後緊跟一個冒號和一個整數,該整數指定了位欄位的長度。
這種數據結構使得按欄位名稱訪問位元組中的每個位變得簡單,並可以通過聯合中的 value
欄位訪問組合後的值。我們依賴編譯器生成正確的移位指令來訪問欄位,這可以在調試時為你節省不少時間。
最後,因為 stupid_decode()
函數一次僅從源 FILE
流中讀取 8 個位元組,所以它效率並不高。通常我們嘗試最小化讀寫次數,以提高性能和降低調用系統調用的開銷。請記住:少量的讀取/寫入大的塊比大量的讀取/寫入小的塊好得多。
總結
用 C 語言編寫一個多文件程序需要程序員要比只是是一個 main.c
做更多的規劃。但是當你添加功能或者重構時,只需要多花費一點兒努力便可以節省大量時間以及避免讓你頭痛的問題。
回顧一下,我更喜歡這樣做:多個文件,每個文件僅有簡單功能;通過頭文件公開那些文件中的小部分功能;把數字常量和字元串常量保存在頭文件中;使用 Makefile
而不是 Bash 腳本來自動化處理事務;使用 main()
函數來處理命令行參數解析並作為程序主要功能的框架。
我知道我只是蜻蜓點水般介紹了這個簡單的程序,並且我很高興知道哪些事情對你有所幫助,以及哪些主題需要詳細的解釋。請在評論中分享你的想法,讓我知道。
via: https://opensource.com/article/19/7/structure-multi-file-c-part-2
作者:Erik O'Shaughnessy 選題:lujun9972 譯者:萌新阿岩 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive