如何組織構建多檔案 C 語言程式(一)
準備好你喜歡的飲料、編輯器和編譯器,放一些音樂,然後開始構建一個由多個檔案組成的 C 語言程式。
大家常說計算機程式設計的藝術部分是處理複雜性,部分是命名某些事物。此外,我認為“有時需要新增繪圖”是在很大程度上是正確的。
在這篇文章裡,我會編寫一個小型 C 程式,命名一些東西,同時處理一些複雜性。該程式的結構大致基於我在 《如何寫一個好的 C 語言 main 函式》 文中討論的。但是,這次做一些不同的事。準備好你喜歡的飲料、編輯器和編譯器,放一些音樂,讓我們一起編寫一個有趣的 C 語言程式。
優秀 Unix 程式哲學
首先,你要知道這個 C 程式是一個 Unix 命令列工具。這意味著它執行在(或者可被移植到)那些提供 Unix C 執行環境的作業系統中。當貝爾實驗室發明 Unix 後,它從一開始便充滿了設計哲學。用我自己的話來說就是:程式只做一件事,並做好它,並且對檔案進行一些操作。雖然“只做一件事,並做好它”是有意義的,但是“對檔案進行一些操作”的部分似乎有點兒不合適。
事實證明,Unix 中抽象的 “檔案” 非常強大。一個 Unix 檔案是以檔案結束符(EOF)標誌為結尾的位元組流。僅此而已。檔案中任何其它結構均由應用程式所施加而非作業系統。作業系統提供了系統呼叫,使得程式能夠對檔案執行一套標準的操作:開啟、讀取、寫入、定址和關閉(還有其他,但說起來那就複雜了)。對於檔案的標準化訪問使得不同的程式共用相同的抽象,而且可以一同工作,即使它們是不同的人用不同語言編寫的程式。
具有共享的檔案介面使得構建可組合的的程式成為可能。一個程式的輸出可以作為另一個程式的輸入。Unix 家族的作業系統預設在執行程式時提供了三個檔案:標準輸入(stdin
)、標準輸出(stdout
)和標準錯誤(stderr
)。其中兩個檔案是隻寫的:stdout
和 stderr
。而 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.c
、mmencode.c
和 mmdecode.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 行包含該步驟要建立的檔名和其依賴的檔案。在本例中,構建目標是 my_sweet_program
,其依賴是 main.c
。
最後的 03 行使用了一個製表符號(tab
)而不是四個空格。這是將要執行建立目標的命令。在本例中,我們使用 C 編譯器前端 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
$
我們需要編寫程式碼以進行命令列解析和處理輸入/輸出流。我們需要一個函式對流進行編碼並將結果寫到另一個流中。最後,我們需要一個函式對流進行解碼並將結果寫到另一個流中。等一下,我們在討論如何寫一個程式,但是在上面的例子中,我呼叫了兩個指令:meow
和 unmeow
?我知道你可能會認為這會導致越變越複雜。
次要內容: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
...
這裡 git
和 git-receive-pack
是同一個檔案但是擁有不同的名字。我們說它們是相同的檔案因為它們具有相同的 inode 值(第一列)。inode 是 Unix 檔案系統的一個特點,對它的介紹超越了本文的內容範疇。
優秀或懶惰的程式可以通過 Unix 檔案系統的這個特點達到寫更少的程式碼但是交付雙倍的程式。首先,我們編寫一個基於其 argv[0]
的值而作出相應改變的程式,然後我們確保為導致該行為的名稱建立連結。
在我們的 Makefile
中,unmeow
連結通過以下的方式來建立:
# Makefile
...
$(DECODER): $(ENCODER)
$(LN) -f $< $@
...
我傾向於在 Makefile
中將所有內容引數化,很少使用 “裸” 字串。我將所有的定義都放置在 Makefile
檔案頂部,以便可以簡單地找到並改變它們。當你嘗試將程式移植到新的平臺上時,需要將 cc
改變為某個 cc
時,這會很方便。
除了兩個內建變數 $@
和 $<
之外,該步驟看起來相對簡單。第一個便是該步驟的目標的快捷方式,在本例中是 $(DECODER)
(我能記得這個是因為 @
符號看起來像是一個目標)。第二個,$<
是規則依賴項,在本例中,它解析為 $(ENCODER)
。
事情肯定會變得複雜,但它還在管理之中。
via: https://opensource.com/article/19/7/structure-multi-file-c-part-1
作者:Erik O'Shaughnessy 選題:lujun9972 譯者:萌新阿巖 校對:wxy
相關文章
- 如何組織構建多檔案 C 語言程式(二)
- 程式語言基本組成:組織結構
- C語言標頭檔案組織與包含原則C語言
- c語言多檔案編譯C語言編譯
- objective-C 的程式碼檔案組織Object
- c語言拷貝檔案程式C語言
- Objective-C檔案組織介紹(附程式碼)Object
- 【C語言】linux下多檔案編譯C語言Linux編譯
- C語言檔案與目錄(一)C語言
- C語言檔案操作C語言
- C語言(檔案操作)C語言
- 第3周專案4-考了語文數學的學生(多檔案形式組織程式)
- C語言(檔案加解密)C語言解密
- C語言-檔案讀寫C語言
- 談談現代組織如何構建資料治理
- 程式語言基本組成:控制結構
- 用c語言處理檔案C語言
- C語言 檔案IO的使用C語言
- c語言檔案操作相關C語言
- C語言實現檔案加密C語言加密
- C語言 - 標頭檔案包含C語言
- C語言/C++讀取檔案資訊C語言C++
- 如何構建設計語言系統
- 如何組織軟體模組的程式碼結構?
- 使用Go語言構建一個解釋型語言Go
- C語言檔案與目錄(五)檔案鎖C語言
- C語言判斷檔案存在和建立檔案C語言
- C語言關於多原始檔的呼叫C語言
- 如何系統學習C 語言(下)之 檔案篇
- C語言檢視變數位元組程式C語言變數
- C語言程式設計獲取PE檔案DOS頭C語言程式設計
- 眾多程式語言如何抉擇
- 程式語言基本組成:資料結構資料結構
- 獲得檔案的大小(c語言)C語言
- C語言檔案與目錄(二)C語言
- C語言檔案與目錄(三)C語言
- C語言第一個程式C語言
- 聊聊C語言/C++—程式和程式語言C語言C++