Linux中國

如何組織構建多文件 C 語言程序(一)

大家常說計算機編程的藝術部分是處理複雜性,部分是命名某些事物。此外,我認為「有時需要添加繪圖」是在很大程度上是正確的。

在這篇文章里,我會編寫一個小型 C 程序,命名一些東西,同時處理一些複雜性。該程序的結構大致基於我在 《如何寫一個好的 C 語言 main 函數》 文中討論的。但是,這次做一些不同的事。準備好你喜歡的飲料、編輯器和編譯器,放一些音樂,讓我們一起編寫一個有趣的 C 語言程序。

優秀 Unix 程序哲學

首先,你要知道這個 C 程序是一個 Unix 命令行工具。這意味著它運行在(或者可被移植到)那些提供 Unix C 運行環境的操作系統中。當貝爾實驗室發明 Unix 後,它從一開始便充滿了設計哲學。用我自己的話來說就是:程序只做一件事,並做好它,並且對文件進行一些操作。雖然「只做一件事,並做好它」是有意義的,但是「對文件進行一些操作」的部分似乎有點兒不合適。

事實證明,Unix 中抽象的 「文件」 非常強大。一個 Unix 文件是以文件結束符(EOF)標誌為結尾的位元組流。僅此而已。文件中任何其它結構均由應用程序所施加而非操作系統。操作系統提供了系統調用,使得程序能夠對文件執行一套標準的操作:打開、讀取、寫入、定址和關閉(還有其他,但說起來那就複雜了)。對於文件的標準化訪問使得不同的程序共用相同的抽象,而且可以一同工作,即使它們是不同的人用不同語言編寫的程序。

具有共享的文件介面使得構建可組合的的程序成為可能。一個程序的輸出可以作為另一個程序的輸入。Unix 家族的操作系統默認在執行程序時提供了三個文件:標準輸入(stdin)、標準輸出(stdout)和標準錯誤(stderr)。其中兩個文件是只寫的:stdoutstderr。而 stdin 是只讀的。當我們在常見的 Shell 比如 Bash 中使用文件重定向時,可以看到其效果。

$ ls | grep foo | sed -e 's/bar/baz/g' > ack

這條指令可以被簡要地描述為:ls 的結果被寫入標準輸出,它重定向到 grep 的標準輸入,grep 的標準輸出重定向到 sed 的標準輸入,sed 的標準輸出重定向到當前目錄下文件名為 ack 的文件中。

我們希望我們的程序在這個靈活又出色的生態系統中運作良好,因此讓我們編寫一個可以讀寫文件的程序。

喵嗚喵嗚:流編碼器/解碼器概念

當我還是一個露著豁牙的孩子懵懵懂懂地學習計算機科學時,學過很多編碼方案。它們中的有些用於壓縮文件,有些用於打包文件,另一些毫無用處因此顯得十分愚蠢。列舉最後這種情況的一個例子:哞哞編碼方案

為了讓我們的程序有個用途,我為它更新了一個 21 世紀 的概念,並且實現了一個名為「喵嗚喵嗚」 的編碼方案的概念(畢竟網上大家都喜歡貓)。這裡的基本的思路是獲取文件並且使用文本 「meow」 對每個半位元組(半個位元組)進行編碼。小寫字母代表 0,大寫字母代表 1。因為它會將 4 個比特替換為 32 個比特,因此會擴大文件的大小。沒錯,這毫無意義。但是想像一下人們看到經過這樣編碼後的驚訝表情。

$ cat /home/your_sibling/.super_secret_journal_of_my_innermost_thoughts
MeOWmeOWmeowMEoW...

這非常棒。

最終的實現

完整的源代碼可以在 GitHub 上面找到,但是我會寫下我在編寫程序時的思考。目的是說明如何組織構建多文件 C 語言程序。

既然已經確定了要編寫一個編碼和解碼「喵嗚喵嗚」格式的文件的程序時,我在 Shell 中執行了以下的命令 :

$ mkdir meowmeow
$ cd meowmeow
$ git init
$ touch Makefile     # 編譯程序的方法
$ touch main.c       # 處理命令行選項
$ touch main.h       # 「全局」常量和定義
$ touch mmencode.c   # 實現對喵嗚喵嗚文件的編碼
$ touch mmencode.h   # 描述編碼 API
$ touch mmdecode.c   # 實現對喵嗚喵嗚文件的解碼
$ touch mmdecode.h   # 描述解碼 API
$ touch table.h      # 定義編碼查找表
$ touch .gitignore   # 這個文件中的文件名會被 git 忽略
$ git add .
$ git commit -m "initial commit of empty files"

簡單的說,我創建了一個目錄,裡面全是空文件,並且提交到 git。

即使這些文件中沒有內容,你依舊可以從它的文件名推斷每個文件的用途。為了避免萬一你無法理解,我在每條 touch 命令後面進行了簡單描述。

通常,程序從一個簡單 main.c 文件開始,只有兩三個解決問題的函數。然後程序員輕率地向自己的朋友或者老闆展示了該程序,然後為了支持所有新的「功能」和「需求」,文件中的函數數量就迅速爆開了。「程序俱樂部」的第一條規則便是不要談論「程序俱樂部」,第二條規則是盡量減少單個文件中的函數。

老實說,C 編譯器並不關心程序中的所有函數是否都在一個文件中。但是我們並不是為計算機或編譯器寫程序,我們是為其他人(有時也包括我們)去寫程序的。我知道這可能有些奇怪,但這就是事實。程序體現了計算機解決問題所採用的一組演算法,當問題的參數發生了意料之外的變化時,保證人們可以理解它們是非常重要的。當在人們修改程序時,發現一個文件中有 2049 函數時他們會詛咒你的。

因此,優秀的程序員會將函數分隔開,將相似的函數分組到不同的文件中。這裡我用了三個文件 main.cmmencode.cmmdecode.c。對於這樣小的程序,也許看起來有些過頭了。但是小的程序很難保證一直小下去,因此哥忒拓展做好計劃是一個「好主意」。

但是那些 .h 文件呢?我會在後面解釋一般的術語,簡單地說,它們被稱為頭文件,同時它們可以包含 C 語言類型定義和 C 預處理指令。頭文件中不應該包含任何函數。你可以認為頭文件是提供了應用程序介面(API)的定義的一種 .c 文件,可以供其它 .c 文件使用。

但是 Makefile 是什麼呢?

我知道下一個轟動一時的應用都是你們這些好孩子們用 「終極代碼粉碎者 3000」 集成開發環境來編寫的,而構建項目是用 Ctrl-Meta-Shift-Alt-Super-B 等一系列複雜的按鍵混搭出來的。但是如今(也就是今天),使用 Makefile 文件可以在構建 C 程序時幫助做很多有用的工作。Makefile 是一個包含如何處理文件的方式的文本文件,程序員可以使用其自動地從源代碼構建二進位程序(以及其它東西!)

以下面這個小東西為例:

00 # Makefile
01 TARGET= my_sweet_program
02 $(TARGET): main.c
03    cc -o my_sweet_program main.c

# 符號後面的文本是注釋,例如 00 行。

01 行是一個變數賦值,將 TARGET 變數賦值為字元串 my_sweet_program。按照慣例,也是我的習慣,所有 Makefile 變數均使用大寫字母並用下劃線分隔單詞。

02 行包含該 步驟 recipe 要創建的文件名和其依賴的文件。在本例中,構建 目標 target my_sweet_program,其依賴是 main.c

最後的 03 行使用了一個製表符號(tab)而不是四個空格。這是將要執行創建目標的命令。在本例中,我們使用 C 編譯器 C compiler 前端 cc 以編譯鏈接為 my_sweet_program

使用 Makefile 是非常簡單的。

$ make
cc -o my_sweet_program main.c
$ ls
Makefile  main.c  my_sweet_program

構建我們喵嗚喵嗚編碼器/解碼器的 Makefile 比上面的例子要複雜,但其基本結構是相同的。我將在另一篇文章中將其分解為 Barney 風格。

形式伴隨著功能

我的想法是程序從一個文件中讀取、轉換它,並將轉換後的結果存儲到另一個文件中。以下是我想像使用程序命令行交互時的情況:

$ meow < clear.txt > clear.meow
$ unmeow < clear.meow > meow.tx
$ diff clear.txt meow.tx
$

我們需要編寫代碼以進行命令行解析和處理輸入/輸出流。我們需要一個函數對流進行編碼並將結果寫到另一個流中。最後,我們需要一個函數對流進行解碼並將結果寫到另一個流中。等一下,我們在討論如何寫一個程序,但是在上面的例子中,我調用了兩個指令:meowunmeow?我知道你可能會認為這會導致越變越複雜。

次要內容:argv[0] 和 ln 指令

回想一下,C 語言 main 函數的結構如下:

int main(int argc, char *argv[])

其中 argc 是命令行參數的數量,argv 是字元指針(字元串)的列表。argv[0] 是包含正在執行的程序的文件路徑。在 Unix 系統中許多互補功能的程序(比如:壓縮和解壓縮)看起來像兩個命令,但事實上,它們是在文件系統中擁有兩個名稱的一個程序。這個技巧是通過使用 ln 命令創建文件系統鏈接來實現兩個名稱的。

在我筆記本電腦中 /usr/bin 的一個例子如下:

$ ls -li /usr/bin/git*
3376 -rwxr-xr-x. 113 root root     1.5M Aug 30  2018 /usr/bin/git
3376 -rwxr-xr-x. 113 root root     1.5M Aug 30  2018 /usr/bin/git-receive-pack
...

這裡 gitgit-receive-pack 是同一個文件但是擁有不同的名字。我們說它們是相同的文件因為它們具有相同的 inode 值(第一列)。inode 是 Unix 文件系統的一個特點,對它的介紹超越了本文的內容範疇。

優秀或懶惰的程序可以通過 Unix 文件系統的這個特點達到寫更少的代碼但是交付雙倍的程序。首先,我們編寫一個基於其 argv[0] 的值而作出相應改變的程序,然後我們確保為導致該行為的名稱創建鏈接。

在我們的 Makefile 中,unmeow 鏈接通過以下的方式來創建:

# Makefile
...
$(DECODER): $(ENCODER)
        $(LN) -f $< $@
       ...

我傾向於在 Makefile 中將所有內容參數化,很少使用 「裸」 字元串。我將所有的定義都放置在 Makefile 文件頂部,以便可以簡單地找到並改變它們。當你嘗試將程序移植到新的平台上時,需要將 cc 改變為某個 cc 時,這會很方便。

除了兩個內置變數 $@$< 之外,該 步驟 recipe 看起來相對簡單。第一個便是該步驟的目標的快捷方式,在本例中是 $(DECODER)(我能記得這個是因為 @ 符號看起來像是一個目標)。第二個,$< 是規則依賴項,在本例中,它解析為 $(ENCODER)

事情肯定會變得複雜,但它還在管理之中。

via: https://opensource.com/article/19/7/structure-multi-file-c-part-1

作者:Erik O'Shaughnessy 選題:lujun9972 譯者:萌新阿岩 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的電子郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國