|
【說在前面的話】
“為什么要使用C語言來實(shí)現(xiàn)面向?qū)ο箝_發(fā)?”
“直接用C++不就好了么?”
想必很多人在第一次面對(duì) OOPC(Object-Oriented-Programming-with-ANSI-C)的時(shí)候,都會(huì)情不自禁的發(fā)出類似的疑問。其實(shí),任何針對(duì)上述問題的討論,其本身都是充滿爭(zhēng)議的——換句話說,無論我給出怎樣的答案,都無法令所有人滿意——正因如此,本文也無意去趟這攤渾水。
我寫這篇文章的目的是為那些長(zhǎng)期在MDK環(huán)境下從事C語言開發(fā)的朋友介紹一種方法:幫助大家在偶爾需要用到“面向?qū)ο蟆备拍畹臅r(shí)候,能簡(jiǎn)便快捷的使用C語言“搞定”面向?qū)ο箝_發(fā)。
在開始后續(xù)內(nèi)容之前,我們需要約定和強(qiáng)調(diào)一些基本原則:
“零消耗”原則:即,我們所要實(shí)現(xiàn)的所有面向?qū)ο蟮奶匦远紤?yīng)該是“零資源消耗”或至少是“極小資源消耗”。這里的原理是:能在編譯時(shí)刻(Compiletime)搞定的事情,絕不拖到運(yùn)行時(shí)刻(Runtime)。務(wù)實(shí)原則:即,我們不在形式上追求與C++類似,除非它的代價(jià)是零或者非常小。“按需實(shí)現(xiàn)”原則:即,對(duì)任何類的實(shí)現(xiàn)來說,我們并不追求把所有的OO特性都實(shí)現(xiàn)出來——這完全沒有必要——我們僅根據(jù)實(shí)際應(yīng)用的需求來實(shí)現(xiàn)最小的、必要的面向?qū)ο蠹夹g(shù)。“傻瓜化”原則:即,類的建立和使用都必須足夠傻瓜化。最好所見即所得。
在上述前提下,我們就快速進(jìn)入到今天的內(nèi)容吧。
【僅需一次的準(zhǔn)備階段】首先,我們要下載 PLOOC的 CMSIS-Pack,具體鏈接如下:
https://github.com/GorgonMeducer/PLOOC/releases
當(dāng)然,如果你因?yàn)槟承┰驘o法訪問Github,也可以在關(guān)注【裸機(jī)思維】公眾號(hào)后發(fā)送關(guān)鍵字 “PLOOC” 來獲取網(wǎng)盤鏈接。
下載成功后,直接雙擊安裝包即可。
5n4iynzqp4b6408861220.png (15.49 KB, 下載次數(shù): 0)
下載附件
保存到相冊(cè)
5n4iynzqp4b6408861220.png
2024-8-29 12:16 上傳
一般來說,部署會(huì)非常順利,但如果出現(xiàn)了安裝錯(cuò)誤,比如下面這種:
icvz1k5dqho6408861320.png (46.39 KB, 下載次數(shù): 0)
下載附件
保存到相冊(cè)
icvz1k5dqho6408861320.png
2024-8-29 12:16 上傳
則很可能是您所使用的MDK版本太低導(dǎo)致的——是時(shí)候更新下MDK啦。關(guān)注【裸機(jī)思維】公眾號(hào)后發(fā)送關(guān)鍵字"MDK",即可獲得最新的MDK網(wǎng)盤鏈接。
PLOOC 是 Protected-Low-overhead-Object-Oriented-programming-with-ansi-C 的縮寫,顧名思義,是一個(gè)強(qiáng)調(diào)地資源消耗且為私有類成員提供保護(hù)的一個(gè)面向?qū)ο竽0濉?br />
它是一個(gè)開源項(xiàng)目,如果你喜歡,還請(qǐng)多多Star哦!
https://github.com/GorgonMeducer/PLOOC
【如何快速嘗鮮】
為了簡(jiǎn)化用戶對(duì) OOC 的學(xué)習(xí)成本,PLOOC提供了一個(gè)無需任何硬件就可以直接仿真執(zhí)行的例子工程。該例子工程以隊(duì)列類為例子,展示了:類的定義方式
如何實(shí)現(xiàn)類的方法(Method)
如何為類定義接口(Interface)
如何定義派生類
如何重載接口
如何在派生類中訪問基類受保護(hù)的成員(Protected Member)……
很多時(shí)候千言萬語敵不過代碼幾行——學(xué)習(xí)OOC確是如此。
例子工程的獲取非常簡(jiǎn)單。首先打開 Pack-Installer,在Device列表中找到Arm,選擇任意一款Cortex-M內(nèi)核(比如 Arm Cortex-M3)。在列表中選擇ARMCMx(比如下圖中的ARMCM3)。
yt01snvbhuj6408861420.png (69.27 KB, 下載次數(shù): 0)
下載附件
保存到相冊(cè)
yt01snvbhuj6408861420.png
2024-8-29 12:16 上傳
此時(shí),在右邊的Example選項(xiàng)卡中,就可以看到最底部出現(xiàn)了一個(gè)名為 plooc_example (uVision Simulator)的例子工程。單擊Copy,在彈出窗口中選擇一個(gè)目錄位置來保存工程:
rfzask3vhl36408861520.png (73.57 KB, 下載次數(shù): 2)
下載附件
保存到相冊(cè)
rfzask3vhl36408861520.png
2024-8-29 12:16 上傳
單擊OK后將打開自動(dòng)打開如下所示的 MDK 界面:
u0vmzfwdrvf6408861620.png (68.58 KB, 下載次數(shù): 3)
下載附件
保存到相冊(cè)
u0vmzfwdrvf6408861620.png
2024-8-29 12:16 上傳
直接單擊編譯,如果一切順利,應(yīng)該沒有任何編譯錯(cuò)誤:
ara4rxk0o4x6408861720.png (16.18 KB, 下載次數(shù): 0)
下載附件
保存到相冊(cè)
ara4rxk0o4x6408861720.png
2024-8-29 12:16 上傳
此時(shí),我們可以直接進(jìn)入調(diào)試模式:
wmqjy2axlsv6408861820.png (114.38 KB, 下載次數(shù): 2)
下載附件
保存到相冊(cè)
wmqjy2axlsv6408861820.png
2024-8-29 12:16 上傳
可以看到,調(diào)試指針停在了 main() 函數(shù)的起始位置。我們先不著急開始全速運(yùn)行。通過菜單打開 "Debug (printf) Viewer" 窗口:
wsreorhsvxf6408861920.png (88 KB, 下載次數(shù): 2)
下載附件
保存到相冊(cè)
wsreorhsvxf6408861920.png
2024-8-29 12:16 上傳
一開始該窗口會(huì)出現(xiàn)在屏幕下方的窗體中,通過拖動(dòng)的方式,我們可以將其挪到醒目的位置。此時(shí),全速運(yùn)行就可以看到例子工程所要展示的效果了:
zxrbua3tzws6408862020.png (148.69 KB, 下載次數(shù): 1)
下載附件
保存到相冊(cè)
zxrbua3tzws6408862020.png
2024-8-29 12:16 上傳
如果你無法在Debug (printf) Viewer 中看到輸出,則很可能是沒有正確配置調(diào)試的方式:
ojy2e3afjrr6408862120.png (48.82 KB, 下載次數(shù): 0)
下載附件
保存到相冊(cè)
ojy2e3afjrr6408862120.png
2024-8-29 12:16 上傳
選擇 Models Cortex-M Debugger 以后,單擊 Settings 按鈕。單擊Command 文本框邊上的"..."按鈕,找到 Keil_v5 安裝目錄下的VHT(或者FVP)所在的路徑。注意要選名稱中 Cortex-M3 為后綴的可執(zhí)行文件。需要注意的是,不同版本的MDK VHT/FVP 的路徑可能存在差異,但一定是保存在Keil_v5/Arm目錄下的一個(gè)子目錄里。設(shè)置好路徑后,我們單擊 Target 邊上的 "..." 按鈕來測(cè)試 FVP 是否工作正常:
qy1j3y0rejz6408862220.png (49.12 KB, 下載次數(shù): 1)
下載附件
保存到相冊(cè)
qy1j3y0rejz6408862220.png
2024-8-29 12:16 上傳
如果一切順利,我們會(huì)看到如下的窗口,注意勾選 armcortexm3ct 選項(xiàng)(默認(rèn)就是勾選的)。單擊OK即可。
okabsqwroth6408862320.png (53.83 KB, 下載次數(shù): 0)
下載附件
保存到相冊(cè)
okabsqwroth6408862320.png
2024-8-29 12:16 上傳
如果你不幸看到的是這樣的錯(cuò)誤窗口:
nrundmq442i6408862420.png (99.11 KB, 下載次數(shù): 0)
下載附件
保存到相冊(cè)
nrundmq442i6408862420.png
2024-8-29 12:16 上傳
則說明你的MDK不是Professional License。安裝Professional License一般都可以解決。
一切設(shè)置妥當(dāng)后,單擊OK關(guān)閉配置頁面即可。
該例子只展示了C99模式下使用PLOOC所構(gòu)建的隊(duì)列類(enhanced_byte_queue_t)的效果:
static enhanced_byte_queue_t s_tQueue;
printf("Hello PLOOC!\r
\r
"); static uint8_t s_chQueueBuffer[QUEUE_BUFFER_SIZE]; enhanced_byte_queue_t *ptQueue = __new_class(enhanced_byte_queue, s_chQueueBuffer, sizeof(s_chQueueBuffer));
//! you can enqueue ENHANCED_BYTE_QUEUE.Enqueue(&s_tQueue, 'p'); ENHANCED_BYTE_QUEUE.Enqueue(&s_tQueue, 'L'); ENHANCED_BYTE_QUEUE.Enqueue(&s_tQueue, 'O'); ENHANCED_BYTE_QUEUE.Enqueue(&s_tQueue, 'O'); ENHANCED_BYTE_QUEUE.Enqueue(&s_tQueue, 'C'); ENHANCED_BYTE_QUEUE.use_as__i_byte_queue_t.Enqueue(&s_tQueue.use_as__byte_queue_t, '.'); ENHANCED_BYTE_QUEUE.use_as__i_byte_queue_t.Enqueue(&s_tQueue.use_as__byte_queue_t, '.'); ENHANCED_BYTE_QUEUE.use_as__i_byte_queue_t.Enqueue(&s_tQueue.use_as__byte_queue_t, '.');
//! you can dequeue do { uint_fast16_t n = ENHANCED_BYTE_QUEUE.Count(&s_tQueue); uint8_t chByte; printf("There are %d byte in the queue!\r
", n); printf("let's peek!\r
"); while(ENHANCED_BYTE_QUEUE.Peek.PeekByte(&s_tQueue, &chByte)) { printf("%c\r
", chByte); } printf("There are %d byte(s) in the queue!\r
", ENHANCED_BYTE_QUEUE.Count(&s_tQueue)); printf("Let's remove all peeked byte(s) from queue... \r
"); ENHANCED_BYTE_QUEUE.Peek.GetAllPeeked(&s_tQueue); printf("Now there are %d byte(s) in the queue!\r
", ENHANCED_BYTE_QUEUE.Count(&s_tQueue)); } while(0); __free_class(enhanced_byte_queue, ptQueue);
其輸出為:
oyiz23d5mvm6408862520.png (12.62 KB, 下載次數(shù): 1)
下載附件
保存到相冊(cè)
oyiz23d5mvm6408862520.png
2024-8-29 12:16 上傳
類 enhanced_byte_queue_t 實(shí)際上是從基類 byte_queue_t 基礎(chǔ)上派生出來的,并添加了一個(gè)非常有用的功能:可以連續(xù)的偷看(Peek)隊(duì)列里的內(nèi)容,并可以在需要的時(shí)候,要么1)將已經(jīng)偷看的內(nèi)容實(shí)際都取出來;要么2)從頭開始偷看——上述代碼就展示了這一功能。
PLOOC 相較普通的OOC模板來說,除了可以隱藏類的私有成員(private member)以外,還能夠以零運(yùn)行時(shí)成本實(shí)現(xiàn)重載(Overload)——用通俗的話說就是:PLOOC允許擁有不同參數(shù)數(shù)量、不同參數(shù)類型的多個(gè)函數(shù)擁有相同的名字。
要獲得這樣的功能,就要打開 C11(最好是GNU11)的支持。當(dāng)我們打開工程配置,在“C/C++”選項(xiàng)卡中將 Language C 設(shè)置為 c11(最好是gnu11):
53dpjc2tiq06408862620.png (52.16 KB, 下載次數(shù): 1)
下載附件
保存到相冊(cè)
53dpjc2tiq06408862620.png
2024-8-29 12:16 上傳
重新編譯后,進(jìn)入調(diào)試模式,將在輸出窗口中看到額外的信息:
4jyf5k1xkot6408862720.png (54.67 KB, 下載次數(shù): 0)
下載附件
保存到相冊(cè)
4jyf5k1xkot6408862720.png
2024-8-29 12:16 上傳
這些信息實(shí)際上對(duì)應(yīng)如下的代碼:
#if defined(__STDC_VERSION__) && __STDC_VERSION__ > 199901L LOG_OUT("\r
-[Demo of overload]------------------------------\r
"); LOG_OUT((uint32_t) 0x12345678); LOG_OUT("\r
"); LOG_OUT(0x12345678); LOG_OUT("\r
"); LOG_OUT("PI is "); LOG_OUT(3.1415926f); LOG_OUT("\r
"); LOG_OUT("\r
Show BYTE Array:\r
"); LOG_OUT((uint8_t *)main, 100);
LOG_OUT("\r
Show Half-WORD Array:\r
"); LOG_OUT((uint16_t *)(((intptr_t)&main) & ~0x1), 100/sizeof(uint16_t));
LOG_OUT("\r
Show WORD Array:\r
"); LOG_OUT((uint32_t *)(((intptr_t)&main) & ~0x3), 100/sizeof(uint32_t));#endif
你看,同一個(gè)函數(shù) LOG_OUT() 當(dāng)我們給它不同數(shù)量和類型的參數(shù)時(shí),居然可以實(shí)現(xiàn)不同的輸出效果,是不是特別神奇——這就是面向?qū)ο箝_發(fā)中重載的魅力所在。請(qǐng)記。此時(shí)我們?nèi)匀皇褂玫氖荂語言,而不是C++;在C99下,我們可以實(shí)現(xiàn)擁有不同參數(shù)個(gè)數(shù)的函數(shù)共享同一個(gè)名字;在C11下,我們可以實(shí)現(xiàn)擁有相同參數(shù)個(gè)數(shù)但類型不同的函數(shù)共享同一個(gè)名字;我們?cè)谶\(yùn)行時(shí)刻的開銷是0,一切在編譯時(shí)刻就已經(jīng)塵埃落定了。我們并沒有為這項(xiàng)特性犧牲任何代碼空間。
例子工程可以幫助我們快速的熟悉 OOC 的開發(fā)模式,那么在任意的普通工程中,我們要如何使用 PLOOC模板呢?
【PLOOC在任意普通工程中的部署】PLOOC 模板其實(shí)是一套頭文件,既沒有庫(lib)也沒有C語言源代碼,更別提匯編了。
在任意的MDK工程中,只要你已經(jīng)安裝了此前我們提到過的CMSIS-Pack,就可以通過下述工具欄中標(biāo)注的按鈕,打開RTE配置界面:
vw4gox0lkt16408862820.png (2.23 KB, 下載次數(shù): 1)
下載附件
保存到相冊(cè)
vw4gox0lkt16408862820.png
2024-8-29 12:16 上傳
找到 Language Extension選項(xiàng),將其展開后勾選PLOOC,單擊OK關(guān)閉窗口。
elvf2azkajb6408862920.png (47.32 KB, 下載次數(shù): 1)
下載附件
保存到相冊(cè)
elvf2azkajb6408862920.png
2024-8-29 12:16 上傳
此時(shí),我們就可以在工程管理器中看到一個(gè)新的列表項(xiàng)“Language Extension”:
sswbiyuiqo46408863020.png (13.55 KB, 下載次數(shù): 1)
下載附件
保存到相冊(cè)
sswbiyuiqo46408863020.png
2024-8-29 12:16 上傳
它是不可展開的,別擔(dān)心,這就足夠了。打開工程配置,如果你使用的是 Arm Compiler 6(armclang):
ateesdgvzc46408863120.png (51.67 KB, 下載次數(shù): 0)
下載附件
保存到相冊(cè)
ateesdgvzc46408863120.png
2024-8-29 12:16 上傳
則我們需要在 C/C++選項(xiàng)中:
將Language C設(shè)置為 gnu11(或者最低c99):(推薦,而不是必須)在Misc Controls中添加對(duì)微軟擴(kuò)展的支持
-fms-extensions
如果你使用的是 Arm Compiler 5(armcc):
則需要在 C/C++ 選項(xiàng)卡中開啟對(duì) GNU Extension 和 C99的支持:
遺憾的是作為一款已經(jīng)停止更新的編譯器,Arm Compiler 5 既不支持C11,也不支持微軟擴(kuò)展(-fms-extensions),這意味著PLOOC中的重載特性無法發(fā)揮最大潛能,著實(shí)有點(diǎn)遺憾(但擁有不同參數(shù)數(shù)量的函數(shù)還是允許共享同一個(gè)名稱的)。
至此,我們就完成了PLOOC在一個(gè)工程中的部署。是不是特別簡(jiǎn)單?
也許文章到了一半我才問,已經(jīng)有點(diǎn)遲了——大家都熟悉基本的面向?qū)ο蟾拍畎?比如?br />
類(class)
私有成員(private member)
公共成員(public member)
保護(hù)成員(protected member)
構(gòu)造函數(shù)(constructor)
析構(gòu)函數(shù)(destructor)
類的方法(method)
……
如果不熟悉,還請(qǐng)找本C#或者C++的書略微學(xué)習(xí)一下為好。后面的內(nèi)容,我將假設(shè)你已經(jīng)對(duì)面向?qū)ο蟮幕鹃_發(fā)要素較為熟悉。
那么,我們?nèi)绾慰焖俚脑贑語言工程中構(gòu)建一個(gè)類呢?
【新建一個(gè)類從未如此簡(jiǎn)單】假設(shè)我們要?jiǎng)?chuàng)造一個(gè)新的類,叫做 my_class1
第一步:引入模板在工程管理器中,添加一個(gè)新的group,命名為 my_class1:
右鍵單擊 my_class1,并在彈出的菜單中選擇 "Add New Item to Group my_class1":
在彈出的對(duì)話框中選擇 User Code Template:
展開 Language Extension,可以看到有兩個(gè) PLOOC模板,分別對(duì)應(yīng):
基類和普通類(Base Class Template)派生類(Derived Class Template)
由于我們要?jiǎng)?chuàng)建的是一個(gè)普通類(未來也可以作為基類),因此選擇“Base Class Template”。單擊Location右邊的 "..." 按鈕,選擇一個(gè)保存代碼文件的路徑后,單擊“Add”。
此時(shí)我們可以看到,class_name.c 被添加到了 my_class1中,且MDK自動(dòng)在編輯器中為我們打開了兩個(gè)模板文件:class_name.h和class_name.c。
第二步:格式化在編輯器中打開或者選中 class_name.c。通過快捷鍵CTRL+H打開 替換窗口:
在Look in中選擇Current Document去掉Find Opitons屬性框中的 Match whold word前的勾選(這一步驟很重要)
接下來,依次:
將小寫的 替換為 my_class1將大寫的 替換為 MY_CLASS1
完成上述步驟后,保存class_name.c。打開class_name.h,重復(fù)上述過程,即:
將小寫的 替換為 my_class1將大寫的 替換為 MY_CLASS1
完成后保存 class_name.h.
第三步:加入工程編譯在工程管理器中展開 my_class1,并將其中的 class_name.c 刪除:
打開class_name.c 所在文件目錄:
找到我們剛剛編輯好的兩個(gè)文件 class_name.c 和 class_name.h:
用我們的類為這兩個(gè)文件命名:my_class1.c 和 my_class1.h
在MDK工程管理器中,將這兩個(gè)文件加入 my_class1 下:
如果此前你的工程就是可以正常編譯的話,在加入了上述文件后,應(yīng)該依然可以正常編譯:
第四步:如何設(shè)計(jì)你的類成員變量打開 my_class1.h,找到 def_class 所在的代碼片斷:
//!
ame class my_class1_t//! @{declare_class(my_class1_t)
def_class(my_class1_t,
public_member( //! )
private_member( //! ) protected_member( //! ))
end_def_class(my_class1_t) /* do not remove this for forward compatibility *///! @}
很容易注意到:
類所對(duì)應(yīng)的類型會(huì)自動(dòng)在尾部添加 "_t" 以表示這是一個(gè)自定義類型,當(dāng)然這不是強(qiáng)制的,當(dāng)你熟悉模板后,如果確實(shí)看它不順眼,可以改成任何自己喜歡的類型名稱。這里,由于我們的類叫做 my_class1,因此對(duì)應(yīng)的類型就是 my_class1_t。
declare_class(或者也可以寫成 dcl_class)用于類型的“前置聲明”,它的本質(zhì)就是
typedef struct my_class1_t my_class1_t;因此并沒有什么特別神秘的地方。
def_class用于定義類的成員。其中 public_member用于存放公共可見的成員變量;private_member用于存放私有成員;protected_member用于存放當(dāng)前類以及派生類可見的成員。這三者的順序任意,可以缺失,也可以存在多個(gè)——非常靈活。
第四步:如何設(shè)計(jì)構(gòu)造函數(shù)
找到 typedef struct my_class1_cfg_t 對(duì)應(yīng)的代碼塊:
typedef struct my_class1_cfg_t { //! put your configuration members here } my_class1_cfg_t;
可以看到,這是個(gè)平平無奇的結(jié)構(gòu)體。它用于向我們的構(gòu)造函數(shù)傳遞初始化類時(shí)所需的參數(shù)。在類的頭文件中,你很容易找到構(gòu)造函數(shù)的函數(shù)原型:
/*! \brief the constructor of the class: my_class1 */externmy_class1_t * my_class1_init(my_class1_t *ptObj, my_class1_cfg_t *ptCFG);
可以看到,其第一個(gè)參數(shù)是指向類實(shí)例的指針,而第二個(gè)參數(shù)就是我們的配置結(jié)構(gòu)體。在類的C源代碼文件中,可以找到構(gòu)造函數(shù)的實(shí)體:
#undef this#define this (*ptThis)/*! \brief the constructor of the class: my_class1 */my_class1_t * my_class1_init(my_class1_t *ptObj, my_class1_cfg_t *ptCFG){ /* initialise "this" (i.e. ptThis) to access class members */ class_internal(ptObj, ptThis, my_class1_t); ASSERT(NULL != ptObj && NULL != ptCFG);
return ptObj;}
此時(shí),在構(gòu)造函數(shù)中,我們可以通過 this.xxxx 的方式來訪問類的成員,以便根據(jù)配置結(jié)構(gòu)體中傳進(jìn)來的內(nèi)容對(duì)類進(jìn)行初始化。
也許你已經(jīng)注意到了,我們的模板中并沒有任何為類申請(qǐng)空間的代碼。這是有意為之。原因如下:
面向?qū)ο蟛⒎且欢ㄒ褂脛?dòng)態(tài)內(nèi)存分配,這是一種偏見我們只提供構(gòu)造函數(shù),而類的用戶可以自由的決定如何為類的實(shí)例分配存儲(chǔ)空間。由于我們創(chuàng)造的類(比如 my_class1_t)本質(zhì)上是一個(gè)完整的結(jié)構(gòu)體類型,因此可以由用戶像普通結(jié)構(gòu)體那樣:
進(jìn)行靜態(tài)分配:即定義靜態(tài)變量,或是全局變量
使用池分配:直接為目標(biāo)類構(gòu)建一個(gè)專用池,有效避免碎片化。進(jìn)行堆分配:使用普通的malloc()進(jìn)行分配,類的大小可以通過sizeof() 獲得,比如:
my_class1_cfg_t tCFG = { ...};my_class1_t *ptNewItem = my_class1_init( (my_class1_t *)malloc(sizeof(my_class1_t), &tCFG);if (NULL == ptNewItem) { printf("Failed to new my_class1_t \r
");}
...
free(ptNewItem);
當(dāng)然,如果你說我就是要那種形式主義,那你完全可以使用關(guān)鍵字__new_class:
__new_class(類型名稱>,構(gòu)造函數(shù)的參數(shù)>);__free_class(類型名稱>, 實(shí)例地址>)
比如:
my_class1_t *ptItem = __new_class(my_class, );if (NULL == ptItem) { printf("Failed to new my_class1_t \r
");}
...
__free_class(my_class, ptItem);
第五步:如何設(shè)計(jì)構(gòu)類的方法(method)
我們開篇說過,實(shí)踐面向?qū)ο笞钪匾氖枪δ埽切问街髁x。假設(shè)有一個(gè)類的方法叫做 method1,理想中,大家一定覺得如下的使用方式是最“正統(tǒng)”的:
my_class1_t *ptItem = new_class(my_class, );if (NULL == ptItem) { printf("Failed to new my_class1_t \r
");}
ptItem.method1();
free_class(my_class, ptItem);在C語言中,我們完全可以實(shí)現(xiàn)類似的效果——只要你在類的定義中加入函數(shù)指針就行了——其實(shí)很多OOC的模板都是這么做的(比如lw_oopc)。但你仔細(xì)思考一下,在類的結(jié)構(gòu)體中加入函數(shù)指針究竟有何利弊:
先來說好處:可以用“優(yōu)雅”的方式來完成方法的調(diào)用;
支持運(yùn)行時(shí)刻的多態(tài)(Polymorphism);
再來說缺點(diǎn):
在嵌入式應(yīng)用中,大部分類的方法都不需要多態(tài),更別說是運(yùn)行時(shí)刻的多態(tài)了;
函數(shù)指針會(huì)占用4個(gè)字節(jié);
通過函數(shù)指針來實(shí)現(xiàn)的間接調(diào)用,其效率低于普通的函數(shù)直接調(diào)用。
換句話說,對(duì)大部分類的大部分情況來說,我們都不需要考慮類的方法多態(tài)問題,就算有,很多時(shí)候也都是編譯時(shí)刻的靜態(tài)多態(tài)(plooc_example就展示了靜態(tài)多態(tài)的實(shí)現(xiàn)方式),那么在不考慮運(yùn)行時(shí)刻動(dòng)態(tài)多態(tài)的應(yīng)用場(chǎng)景下,直接用普通函數(shù)來實(shí)現(xiàn)類的方法就是務(wù)實(shí)的一個(gè)選擇了。
基于這種考慮,上述例子實(shí)際上應(yīng)該寫為:my_class1_t *ptItem = new_class(my_class, );if (NULL == ptItem) { printf("Failed to new my_class1_t \r
");}
my_class1_method1(ptItem,);
free_class(my_class, ptItem);這里,my_class1_method1() 是 my_class1.h 提供聲明、my_class1.c 提供實(shí)現(xiàn)的一個(gè)函數(shù)。前綴 my_class1_ 用于防止命名空間污染。
另外一個(gè)值得注意的細(xì)節(jié)是,OOPC中,任何類的方法,其函數(shù)的第一個(gè)參數(shù)一定是指向類實(shí)例的指針——也就是我們常說的 this 指針。以 my_class1_method1() 為例,它的形式為:
#undef this#define this (*ptThis)
void my_class1_method(my_class1_t *ptObj, ){ /* initialise "this" (i.e. ptThis) to access class members */ class_internal(ptObj, ptThis, my_class1_t); ... }這里,class_internal() 用于將 ptObj轉(zhuǎn)變成我們所需的 this指針(這里的ptThis),借助宏的幫助,我們就可以實(shí)現(xiàn) this.xxxx 這樣無成本的形式主義了。
第六步:如何設(shè)計(jì)類的接口(Interface)我們的模板還為每個(gè)類都提供了一個(gè)接口,并默認(rèn)將構(gòu)造和析構(gòu)函數(shù)都包含在內(nèi),比如,我們可以較為優(yōu)雅的對(duì)類進(jìn)行構(gòu)造和析構(gòu):
static my_class1_t s_tMyClass;...MY_CLASS.Init(&s_tMyClass, ...);...MY_CLASS.Depose(&s_tMyClass);
在 my_class1.h 中,我們可以找到這樣的結(jié)構(gòu)://!
ame interface i_my_class1_t//! @{def_interface(i_my_class1_t) my_class1_t * (*Init) (my_class1_t *ptObj, my_class1_cfg_t *ptCFG); void (*Depose) (my_class1_t *ptObj); /* other methods */
end_def_interface(i_my_class1_t) /*do not remove this for forward compatibility *///! @}
假設(shè)我們要加入一個(gè)新的方法,則只需要在 i_my_class1_t 的接口定義中添加對(duì)應(yīng)的函數(shù)指針即可,比如://!
ame interface i_my_class1_t//! @{def_interface(i_my_class1_t) my_class1_t * (*Init) (my_class1_t *ptObj, my_class1_cfg_t *ptCFG); void (*Depose) (my_class1_t *ptObj); /* other methods */ void (*Method1) (my_class1_t *ptObj, );end_def_interface(i_my_class1_t) /*do not remove this for forward compatibility *///! @}接下來,我們要在 my_class1.h 中添加對(duì)應(yīng)方法的函數(shù)聲明:
externvoid my_class1_method1(my_class1_t *ptObj, );這里,值得注意的是,習(xí)慣上函數(shù)的命名上與接口除大小寫歪,還有一個(gè)簡(jiǎn)單的對(duì)應(yīng)關(guān)系:即,所有的"."直接替換成"_",比如,使用上:
MY_CLASS1.Method1()就對(duì)應(yīng)為:
my_class1_method1()
與此同時(shí),我們需要在 my_class1.c 中添加 my_class1_method1() 函數(shù)的實(shí)體:void my_class1_method1(my_class1_t *ptObj, ){ class_internal(ptObj, ptThis, my_class1_t); ...}并找到名為 MY_CLASS1 的接口實(shí)例:
const i_my_class1_t MY_CLASS1 = { .Init = &my_class1_init, .Depose = &my_class1_depose, /* other methods */};在其中初始化我們的新方法(新函數(shù)指針) Method1:
const i_my_class1_t MY_CLASS1 = { .Init = &my_class1_init, .Depose = &my_class1_depose, /* other methods */ .Method1 = &my_class1_method1,};至此,我們就完成了類方法的添加和初始化。以后,在任何地方,都可以通過
類名大寫>.接口中方法名>()的形式來訪問類的操作函數(shù)了——這也算某種程度上的優(yōu)雅了吧。
第六步:如何設(shè)計(jì)派生類(Derived Class)派生類的創(chuàng)建在基本步驟上與普通類基本一致,除了在模板選擇階段使用對(duì)應(yīng)的模板外,還需要在“格式化”階段額外添加以下兩個(gè)替換步驟:
將 [B] 替換為 基類的大寫名稱;將 替換為基類的小寫名稱;
在類的定義階段,我們注意到:
//!
ame class _t//! @{declare_class(_t)
def_class(_t, which(implement(_t)) ...)
end_def_class(_t) /* do not remove this for forward compatibility *///! @}派生類在原有的類定義基礎(chǔ)上多出了的結(jié)構(gòu),以"," 與類的類型名隔開:which(implement(base_class_name>_t))這里,which() 其實(shí)是一個(gè)列表,它允許我們實(shí)現(xiàn)多重繼承。假設(shè)我們有多個(gè)基類,或是要繼承多個(gè)接口,則可以寫成如下的形式:which( implement(_t) implement(_t) implement(_t) implement(_t))需要注意的是,如果基類或是接口中存在名稱沖突(重名)的成員,則可以將 implement() 替換為 inherit() 來避免這種沖突。比如 _t 與 _t 都有一個(gè)叫做 wID 的成員,則可以通過將其中之一的implement() 替換為 inherit的方式在新的派生類中避免沖突:which( inherit(_t) implement(_t) implement(_t) implement(_t))
就像這里所展示的那樣,PLOOC支持多繼承,這也是 使用C語言來實(shí)現(xiàn)OO的魅力之一,具體方法,這里就不再贅述。
大家都知道,在面向?qū)ο笾,有一類成員只有當(dāng)前類和派生類能夠訪問——我們稱之為受保護(hù)成員(protected member)。在類的定義中,可以通過 protected_member() 將這些成員囊括起來,比如:
//!
ame class byte_queue_t//! @{declare_class(byte_queue_t)
def_class(byte_queue_t,
private_member( implement(mem_t) //! void *pTarget; //! ) protected_member( uint16_t hwHead; //! uint16_t hwTail; //! uint16_t hwCount; //! ))
end_def_class(byte_queue_t) /* do not remove this for forward compatibility *///! @}這里,hwHead、hwTail和hwCount 都只有當(dāng)前類和派生類能訪問。
對(duì)于那些只允許派生類訪問的方法(函數(shù))來說,我們一般會(huì)使用預(yù)編譯宏的形式將其有條件的保護(hù)起來:protected_method( extern mem_t byte_queue_buffer_get(byte_queue_t *ptObj);)這里,受到關(guān)鍵字 protected_method() 的保護(hù),函數(shù) byte_queue_buffer_get() 僅能夠允許類 byte_queue_t 自身極其派生類才能訪問了。
在我們前面創(chuàng)建的 my_class1.h 中我們也有一個(gè)類似的例子:protected_method( extern void my_class1_protected_method_example1(my_class1_t *ptObj);
extern void my_class1_protected_method_example2(my_class1_t *ptObj);)
函數(shù) my_class1_protected_method_example() 就是一個(gè)僅供 my_class1 極其派生類訪問的 受保護(hù)的方法。
在派生類中,如果要訪問基類的受保護(hù)成員,則可以借助 protected_internal() 的幫助,例如:
#undef this#define this (*ptThis)
#undef base#define base (*ptBase)
void enhanced_byte_queue_peek_reset(enhanced_byte_queue_t *ptObj){ /* initialise "this" (i.e. ptThis) to access class members */ class_internal(ptObj, ptThis, enhanced_byte_queue_t); /* initialise "base" (i.e. ptBase) to access protected members */ protected_internal(&this.use_as__byte_queue_t, ptBase, byte_queue_t); ASSERT(NULL != ptObj); /* ------------------atomicity sensitive start---------------- */ this.hwPeek = base.hwTail; this.hwPeekCount = base.hwCount; /* ------------------atomicity sensitive end---------------- */}這里,派生類借助 this.use_as__byte_queue_t 獲得了對(duì)基類的“引用”,并借助 protected_internal() 將其轉(zhuǎn)化為了名為 ptBase 的指針。在 base 宏的幫助下,我們得以通過 base.xxxx 來訪問基類的成員。在例子中,我們看到,base.hwTail 和 base.hwCount 正是前面所展示過的 byte_queue_t 的受保護(hù)成員。
【說在后面的話】
無論使用何種模板,OOPC來發(fā)的一個(gè)核心理念應(yīng)該是“務(wù)實(shí)”,即:以最小的成本(最好是零成本),占最大的便宜(來自O(shè)O所帶來的好處)。
此前,我曾經(jīng)在文章《真刀真槍模塊化(2.5)—— 君子協(xié)定》詳細(xì)介紹過PLOOC的原理和手動(dòng)部署技術(shù)。借助CMSIS-Pack和MDK中RTE的幫助,原本繁瑣的手動(dòng)部署和類的創(chuàng)建過程得到了空前的簡(jiǎn)化,使用OOPC進(jìn)行開發(fā)從未如此簡(jiǎn)單過——幾乎與直接使用C++相差無幾了。
不知不覺間,從2年前第一次將其公開算起,PLOOC已經(jīng)斬獲了三百多個(gè)Star——算是我倉庫中的明星工程了。從日志上來看,PLOOC相當(dāng)穩(wěn)定。距離我上一次“覺得其有必要更新”還是整整一年多前的事情,而加入CMSIS-Pack只是一件錦上添花的事情。
最后,感謝大家的支持——是你們的Star支撐著我一路對(duì)項(xiàng)目的持續(xù)更新。謝謝!
猜你喜歡:
WiFi6+藍(lán)牙+星閃,三合一開發(fā)板,真香!
Github上熱門 C 語言項(xiàng)目匯總!
嵌入式,可測(cè)試性軟件設(shè)計(jì)!
一些低功耗軟件設(shè)計(jì)的要點(diǎn)!
嵌入式 C 保護(hù)結(jié)構(gòu)體的方式
實(shí)用 | 10分鐘教你通過網(wǎng)頁點(diǎn)燈
談?wù)勄度胧杰浖募嫒菪裕?/strong> |
|