模塊化是最高原則之一在 《Unix 編程藝術》一書中, Unix 哲學第一條即:模塊原則),我們就當考慮如何簡潔明快的使用 C 語言實現模塊化。
除開 C/C++ ,在其它現在流行的開發語言中,缺少標准化的模塊管理機制是很難想象的。但這也是 C 語言本身的設計哲學決定的:把盡可能多的可能性留給程序員。根據實際的系統,實際的需要去定制自己需要的東西。
對於巨型的系統比如 Windows 這樣的操作系統),一般會考慮使用一種二進制級的模塊化方案。由模塊自己提供元信息,或是使用統一的管理方案比如注冊表)。稍小一點的系統我們通常開發接觸到的),則會考慮輕量一些的源碼級方案。
首先要考慮的往往是模塊的依賴關系和初始化過程。
依賴關系可以放由鏈接器或加載器來解決。尤其在使用 C 語言時,簡單的靜態庫或動態庫,都不太會引起大的麻煩。
C++ 則不然,C++ 的某些特性比如模板類靜態成員的構造)必須對早期只供 C 語言使用的鏈接器做一些增強。即使是精心編寫的 C++ 庫,也有可能出現一些意外的 bug 。這些 bug 往往需要對編譯,鏈接,加載過程很深刻的理解,才能查出來。注:我並不想以此來反對使用 C++ 做開發。
我們需要著重管理的,是模塊的初始化過程。
對於打包在一起的一個庫例如 glibc ,或是 msvcrt ),會在加載時有初始化入口,以及卸載時有結束代碼。我想說的不是這個,而是我們自己內部拆分的更小的模塊的相互依賴關系。
誰先初始化,誰後初始化,這是一個問題。
在 C++ 的語言級解決方案中,使用的是單件模塊。要麼由鏈接器決定以怎樣的次序來生成初始化代碼,這,通常會因為依賴關系和實際構造次序不同而導致 bug 注:我在某幾本 C++ 書中都見過,待核實。自己好久不寫 C++ 也沒有實際的錯誤例子);要麼使用惰性初始化方案。這個惰性初始化也不是萬能的,並且有些額外的開銷。多線程環境中尤其需要注意)
我使用 C 語言做初期設計的時候,采用的是一種足夠簡單的方法。就是,以編碼規范來規定,每個模塊必須存在一個初始化函數,有規范的名字。比如 foo 模塊的初始化入口叫
- int foo_init()
規定:凡使用特定模塊,必須調用模塊初始化函數。
為了避免模塊重復初始化,初始化函數並不直接調用,而是間接的。類似這樣:
- mod_using(foo_init);
mod_using 負責調用初始化函數,並保證不重復調用,也可以檢查循環依賴。
在這裡,我們還約定了初始化成功於否的返回值。在我們的系統中,返回 0 表示正確,1 表示失敗)然後定義了一個宏來做這個使用。
- #define USING(m) if (mod_using(m##_init,#m)) { return 1; }
注:我個人反對濫用宏。也盡可能的避免它。這裡使用宏,經過了慎重的考慮。我希望可以有一個代碼掃描器去判斷我是否漏掉了模塊初始化可能我使用了一個模塊,但忘記初始化它)。宏可以幫助代碼掃描分析器更容易實現。而且,使用宏更像是對語言做的輕微且必要的擴展。
這樣,我的系統中模塊模塊的實現代碼最後,都有一個 init 函數,裡面只是簡單的調用了 USING 來引用別的模塊。例如:
- #include "module.h"
- /*
- 我個人偏愛把 module.h 的引入放在源文件最後,初始化入口之前。
- 它裡面之定義了 USING 宏,以及相關管理函數。
- 這樣做是為了避免在代碼的其它地方去引入別的模塊。
- */
- int
- foo_init()
- {
- USING(memory); // 引用內存管理模塊
- USING(log); // 引用 log 模塊
- return 0;
- }
至於模塊的卸載,大部分需求下是不需要的。今天在這裡就不論證這一點了。
原文鏈接:http://blog.codingnow.com/2010/01/c_modularization.html