[微知識]模塊的封裝(一):C語言類的封裝
是的,你沒有看錯,我們要討論的是C語言而不是C++語言中類的封裝。在展開知識點之前,我首先要
重申兩點:
1、面向對象是一種思想,基本與所用的語言是無關的。當你心懷面向對象時,即使使用QBasic也能寫
出符合面向對象思想的代碼,更不要說C語言了。舉一個反例,很多人初學C++的時候,並沒有掌
握面向對象的思想,活生生的把類當結構體來使用的也不在少數吧。
2、面向對象的最基本的出發點是“將數據以及處理數據的方法封裝在一起”,至於繼承、派生、多態之類
的則是后面擴展的東西。在C語言中,如果用結構體來保存數據,並將處理這些數據的函數與結構體
的定義封裝在同一個.c文件中,則該.c文件就可以視作一個類。如果將指向具體函數的函數指針與結
構體的其他成員封裝在同一個結構體中,則該“對象”的使用甚至與C++相差無幾了。
以上的內容是面向對象的C語言(Object-Oriented C Programming with ANSI-C)技術的基本出發
點。作為引子,在使用OOC技術的時候,我們會遇到這么一個問題:是的,我們可以用結構體模擬類,將所
有的成員變量都放在結構體中,並將這一結構體放在類模塊的接口頭文件中,但是問題是結構體里的成員變量
都是public的,如何保護他們使其擁有private的屬性呢?解決的方法就是掩碼結構體(Masked Structure)
那么什么是掩碼結構體呢?在回答這個問題前,我們先看下面的例子。已知我們定義了一下用於在C語言
里面進行類封裝的宏,如下所示:
1 #define EXTERN_CLASS(__NAME,...) \ 2 typedef union __NAME __NAME;\ 3 __VA_ARGS__\ 4 union __NAME {\ 5 uint_fast8_t chMask[(sizeof(struct { 6 7 #define END_EXTERN_CLASS(__NAME) \ 8 }) + sizeof(uint_fast8_t) - 1) / sizeof(uint_fast8_t)];\ 9 }; 10 11 #define DEF_CLASS(__NAME,...)\ 12 typedef union __NAME __NAME;\ 13 __VA_ARGS__\ 14 typedef struct __##__NAME __##__NAME;\ 15 struct __##__NAME{ 16 17 #define END_DEF_CLASS(__NAME) \ 18 };\ 19 union __NAME {\ 20 uint_fast8_t chMask[(sizeof(__##__NAME) + sizeof(uint_fast8_t) - 1) / sizeof(uint_fast8_t)];\ 21 }; 22 23 #define CLASS(__NAME) __##__NAME
假設我要封裝一個基於字節的隊列類,不妨叫做Queue,因此我們建立了一個類文件queue.c和對應的接口頭文件
queue.h。假設我們約定queue.c不包含queue.h(這么做的好處很多,在以后的內容里在講解當然對掩碼結構體
的技術來說,模塊的實現是否包含模塊的接口頭文件並不是關鍵)。
我們首先想到是定義一個類來表示隊列,他的一個可能的形式如下:
1 //! \name byte queue 2 //! @{ 3 typedef struct { 4 uint8_t *pchBuffer; //!< queue buffer 5 uint16_t hwBufferSize; //!< buffer size 6 uint16_t hwHead; //!< head pointer 7 uint16_t hwTail; //!< tail pointer 8 uint16_t hwCounter; //!< byte counter 9 }queue_t; 10 //! @}
目前為止一起都還OK,由於quue.c文件不包含queue.h,因此我們決定在兩個文件中各放一個定義。由於.h文件包含了
數據隊列的完整信息,使用該模塊的人可能會因為種種原因直接訪問甚至修改隊列結構體中 的數據------也行在這個例子
中不是那么明顯,但是在你某個其他應用模塊的例子中,你放在結構體里面的某個信息可能對模塊的使用者來說,直接操作
更為便利,因此悲劇發生了----原本你假設“所有操作都應該由queue.c來完成”的格局打破了,使用者可以輕而易舉的修改
和訪問結構體的內容-------而這些內容在面向對象的思想中原本應該是私有的,無法訪問的(private)。原本測試完好的
系統,因為這種出乎意料的外界干涉而導致不穩定,甚至crash了。當你氣沖沖的找到這么“非法”訪問你結構體的人時,對方
居然推了推眼鏡,一臉無辜的看着你說“根據接口的最小信息公開原則,難道你放在頭文件里面的信息不是大家可以放心使用
的么?”
OTZ。。。。埡口無言,然后你會隱約覺得太陽穴微微的在跳動。。。
且慢,如果我們通過一開始提供的宏分別對queue.h和queue.c中的定義改寫一番,也許就是另外一個局面了:
queue.h
1 ... 2 //! \name byte queue 3 //! @{ 4 EXTERN_CLASS(queue_t) 5 uint8_t *pchBuffer; //!< queue buffer 6 uint16_t hwBufferSize; //!< buffer size 7 uint16_t hwHead; //!< head pointer 8 uint16_t hwTail; //!< tail pointer 9 uint16_t hwCounter; //!< byte counter 10 END_EXTERN_CLASS(queue_t) 11 //! @} 12 ... 13 extern bool queue_init(queue_t *ptQueue, uint8_t *pchBuffer, uint16_t hwSize); 14 extern bool enqueue(queue_t *ptQueue, uint8_t chByte); 15 extern bool dequeue(queue_t *ptQueue, uint8_t *pchByte); 16 extern bool is_queue_empty(queue_t *ptQueue); 17 ...
queue.c
1 ... 2 //! \name byte queue 3 //! @{ 4 EXTERN_CLASS(queue_t) 5 uint8_t *pchBuffer; //!< queue buffer 6 uint16_t hwBufferSize; //!< buffer size 7 uint16_t hwHead; //!< head pointer 8 uint16_t hwTail; //!< tail pointer 9 uint16_t hwCounter; //!< byte counter 10 END_EXTERN_CLASS(queue_t) 11 //! @} 12 ... 13 extern bool queue_init(queue_t *ptQueue, uint8_t *pchBuffer, uint16_t hwSize); 14 extern bool enqueue(queue_t *ptQueue, uint8_t chByte); 15 extern bool dequeue(queue_t *ptQueue, uint8_t *pchByte); 16 extern bool is_queue_empty(queue_t *ptQueue); 17 ...
對照前面的宏,我們實際上可以手工將上面的內容展開,可以看到實際上類型queue_t是一個掩碼結構體,
里面只有一個起到掩碼作業的數組chMask,其大小和真正后台的類型_queue_t相同-----這就是掩碼結
構體結構體實現私有成員保護的秘密。解決了私有成員的保護問題,剩下還有一個問題,對於queue.c的
函數來說queue_t只是一個數組,那么正常的功能如何實現呢?下面的代碼片段為你解釋一切:
1 ... 2 bool is_queue_empty(queue_t *ptQueue) 3 { 4 CLASS(queue_t) *ptQ = (CLASS(queue_t) *)ptQueue; 5 if (NULL == ptQueue) { 6 return true; 7 } 8 return ((ptQ->hwHead == ptQ->hwTail) && (0 == ptQ->Counter)); 9 } 10 ...
從編譯器的角度來講,這種從queue_t到_queue_t類型的轉換是邏輯上的,並不會因此產生額外的代碼,
簡而言之,使用掩碼結構體幾乎是沒有代價的----如果你找出了所謂的代價,一方面不妨告訴我,另一方
面不妨考慮這個代價和模塊的封裝相比是否是可以接受的。